Linux-进程控制
进程创建
进程的创建:
- 命令行启动命令(程序、指令……)
- 通过程序自身,fork出来的子进程
fork函数
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
语法:
#include<unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
注:
- 当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方
- fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
- 所有fork出来的子进程,都是以父进程为模板,数据代码继承父进程
- 创建子进程的本质是系统多了一个进程,多了一套进程相关的数据结构(以父进程为模板)
注:
- 上端代码中的pid是变量,是父进程创建的,是父进程运行fork中的代码的
- pid中有两个不同的值,是因为fork() return返回的时候本质是把返回值写入变量,必定会写时拷贝(注:这里的写时拷贝并不是立刻进行写时拷贝,只是写时拷贝机制(策略),只用当父子进行数据写入或者数据修改时,才进行写时拷贝),一个变量名内容是不同的本质是父子页表映射数据到了不同的内存区域
- pid变量里有两个不同的值,可以从而让父子进入不同的业务逻辑
- fork函数只是创建子进程的过程,创建父子进程的依旧是操作系统
补: fork创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
写时拷贝
父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
注:
- 写时拷贝的实现过程时,需要OS参与完成,OS时进程、内存的管理者 总结:
- 写时拷贝可以保证父子进程的独立性
- 写时拷贝的意义:
- 并不是所有数据父子进程都会写入,不需要写入修改的数据(只读)拷贝是没有意义的,因此写时拷贝是可以避免内存和系统资源的浪费
- fork时,创建数据结构,如果还要将数据拷贝一份,fork函数的效率就会降低,因此写时拷贝可以提高fork的效率
- fork本身就是向系统索要更多的资源,如果再拷贝数据的话,索要的资源就会变多,创建子进程就很容易失败,因此写时拷贝可以提高fork创建子进程的成功率
补充:
- Fork后子进程保留了父进程的:
- 环境变量
- 当前工作目录
- 不会保留 父进程的文件锁,pending alarms和pending signals
- 不会保留 进程号
- fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样
- fork创建子进程就是在内核中通过调用clone实现
进程终止
注:
- main函数中return 0 中的0代表程序的退出码,代表程序退出,结果是否运行正确
- 这个退出码默认是给系统看的确认进程执行结果是否正确(0代表运行成功,!0代表运行结果有问题)
- 用户想得知退出码,需要用到echo $? 来查看
- echo $? 是用来查看最近一次执行的程序的退出码
进程退出场景
进程退出情况:
- 代码跑完,结果正确,退出码0
- 代码跑完,结果不正确,逻辑问题,但是没有导致程序崩溃,退出码!0
- 代码没有运行完毕,程序崩溃,退出码没有意义
注:
- 退出码,可以是人为定义的,也可以是使用系统的错误码list
- 当程序运行失败的时候,往往最关系的是失败的原因(计算机擅长处理整数类型的数据,将int转化为string(错误码描述))
- 子进程的退出情况是父进程所关心的
总结:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
进程常见退出方法
- 正常终止(可以通过 echo $? 查看进程退出码):
- 从main返回
- 调用exit
- _exit
- 异常退出:
- ctrl + c,信号终止
注:
- 子进程运行,父进程可以关心,也可以不关心子进程的运行结果
- 进程非正常退出:野指针、/0、越界访问……,退出码是无意义的
_exit函数&&exit函数&&return退出
注:main函数return,非main函数的return不是终止进程,而是结束进程
注:任何函数exit都表示直接终止进程
注:
- exit:在退出的时候,会进行后续资源处理,包括刷新缓冲区
- _exit:在退出时,不会进行后续资源储量,直接终止进程
结论:
- 在调用exit之前:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
- return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
- void _exit(int status):
- 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
- 虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
总结:
- 站在OS角度,进程终止的核心思想是:归还资源
- 释放(释放不是真的把数据结构对象销毁,而是设置为不用的状态,然后保存起来,如果不用的对象多了就有一个数据结构池(这种规则在Linux中叫Slab分派器))曾经为了管理进程所维护的所有的数据结构对象
- 池可以提高申请空间的时间,提高用户效率
- 释放(不是代码和数据清空,而是把内存设置为无效就可以了)程序代码和数据占用的内存空间
- 取消曾经该进程的连接关系
进程等待
进程等待必要性:
- 回收僵尸进程,解决内存泄漏
- 需要获取子进程的运行结束状态(有些父进程不需要子进程的运行结束状态)
- 尽量父进程要晚于子进程退出,这样可以规范化进行资源回收(编码角度)
注:进程一旦变成僵尸状态用kill -9 也无能为力,因为没有办法杀死一个已经死去的进程。
进程等待的方法
wait方法
#include<sys/types.h>#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
注:wait方法是等待任意一个子进程,当子进程退出时,wait就可以退出并返回该子进程的pid
循环创建进程以及循环等待:
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>intmain(){int i=0;while(i<5){
pid_t id=fork();if(id<0){perror("fork");return1;}if(id==0){int count=5;while(count){printf("child is runing:%d,ppid:%d,pid:%d\n",count--,getppid(),getpid());sleep(1);}printf("child quit.....\n");exit(0);}
i++;}for(i=0;i<5;i++){printf("father is waiting...\n");sleep(10);
pid_t ret=wait(NULL);printf("father is wait done,ret:%d\n",ret);sleep(3);printf("father quit...\n");}return0;}
注:一般而言,使用fork函数后需要让父进程进行等待
waitpid方法
#include<sys/types.h>#include<sys/wait.h>
pid_ t waitpid(pid_t pid,int*status,int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
注:
- 这里等待指定进程的的工作是由父进程来完成的
- pid_t pid可以用fork的返回值(父进程返回的是子进程的pid)
- 等待的本质是管理(代码层面的管理)的一种方式
- waitpid的中的参数options可以设置成0(阻塞式等待)
注:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
补:
- waitpid默认阻塞等待任意一个或指定子进程退出,当options被设置为WNOHANG则函数非阻塞,且当没有子进程退出时,waitpid返回0
- 进程等待:等待子进程退出(绝对不是退出指定子进程),获取子进程返回值,释放子进程资源,避免出现僵尸进程
获取子进程status
pid_t wait(int*status)
pid_ t waitpid(pid_t pid,int*status,int options)
其中的status是输出型参数
注:
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待(这里只研究status低16比特 位,status一共有32位 )
退出状态
注:
- 全局变量不可以通过设置全局变量,来告知父进程,子进程的退出码,因为当父子进程中的任意一个进程的数据进行写入或者修改时会进行写时拷贝(当子进程中的数据将修改时,不会影响父进程的数据因此是绝对不行的)
- waitpid/wait拿到status的值是从OS中的PCB拿到的,因为子进程要退出时要将退出码交给子进程的PCB,waitpid/wait拿到的值是通过task_struct拿到的
终止信号
终止信号(异常):
注:
- 正常终止是没有收到任何退出信号
- 只有正常退出退出状态才有显示,否则异常退出退出状态都为0(退出状态是没有意义的)
- 正常退出终止信号为0;异常退出终止信号不为0
- 一般进程提前终止,本质是该进程收到了操作系统发送的信号
- 如果该进程运行时,受到kill -9 【pid】的命令时,该进程终止且终止信号为9
完整的进程等待:
或者:
注:系统提供了一堆的宏(函数),可以用来判断退出码、退出状态
进程的等待方式
阻塞式等待
当父进程调用waitpid函数时,父进程等待子进程,直到子进程运行结束(退出),在这期间父进程一直等(什么事情也不做)——阻塞式等待
pid_t ret=waitpid(id,&status,0);//阻塞式等待
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>intmain(){
pid_t id =fork();if(id==0){int count=3;while(count){printf("child is running:%d,ppid:%d,pid:%d\n",count--,getppid(),getpid());sleep(1);}printf("child quit....\n");exit(0);}int status=0;
pid_t ret=waitpid(id,&status,0);if(ret>0){printf("wait success!\n");if(WIFEXITED(status)){printf("normal quit!\n");printf("quit code:%d\n",WEXITSTATUS(status));}else{printf("process quit error!\n");}}elseif(ret==0){printf("no process to wait\n");}else{printf("wait failed\n");return1;}return0;}
注:
- 在C语言/C++等这类编程语言中,所带调用的函数全部都是阻塞函数(调用->执行(调用方都在等待什么事情都没做)->返回->结束)
- 在C语言/C++等这类编程语言中,所带调用的函数全部都是阻塞函数,之所以是阻塞调用:执行的都是单执行流,并且单执行流简单
- waitpid的中的参数options可以设置成0(默认是阻塞式等待)
非阻塞式等待
当父进程调用waitpid函数时,父进程每隔一段时间会询问子进程的状态,如果父进程等待失败,父进程会往复的等待,直到父进程等待子进程退出并返回该子进程的pid为止,在等待失败到重新询问子进程的期间里,父进程会做一些其他事情——非阻塞式等待
pid_t ret =waitpid(id,&status,WNOHANG);//非阻塞式等待
注:
- 父进程多次询问子进程的状态的方式——基于非阻塞函数的轮询检测方案
- 上述的失败是指并不是真的失败了,仅仅是对方的状态没有达到预期,下次再检测
- 失败分为两种:
- 并不是真的失败了,仅仅是对方的状态没有达到预期
- 真正的失败
- 非阻塞等待的本质是检测子进程的状态(通过多次检测,来检查子进程的状态)
- 非阻塞式等待在自然界多(这种方式比较高效)
补:
- 计算机资源在即将被吃完的时候,计算机会卡住,服务器hang住了——宕机
- 进程中的“等” : 将当前进程放入到等待队列(进程“等”时是不可能将进程放在CPU中的),并将进程状态设置为非R状态,当满足唤醒条件时,唤醒进程将进程从等待队列放回到运行队列,并将进程的状态设置为R状态(这些操作都是OS负责完成的)
- wait、waitpid是系统函数,它们都是通过操作系统来调的,因此当进程等待或进程调度时都是由OS完成的
- 进程PCB(task_struct)中存有退出码、退出信号(wait、waitpid系统函数,它们是通过PCB从而得到子进程的status的)……
进程程序替换
创建子进程的目的:
- 执行父进程的部分代码
- 执行其他程序的代码(需要进行程序替换)
进程程序替换——当父进程创建出子进程让子进程执行其他程序的代码,这时该程序会从磁盘中将代码和数据直接加载到物理内存中,将子进程的页表由指向父进程的代码和数据指向到该程序的代码和数据
注:
- 程序(代码和数据的集合)本质是文件
- 操作系统是通过写时拷贝机制的方式将程序代码和数据直接加载到物理空间上交给子进程
- 在进程程序替换的时候,没有创建新的子进程,原因是该进程相关的内核数据结构没受影响(该进程的pid没变)
- 进程的程序替换是不改变进程内核的数据结构的,而只会修改部分的页表和数据,将新程序的代码和数据加载到内存,重新构建映射关系,和父进程彻底脱离
替换原理:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
替换函数
intexecl(constchar*path,constchar*arg,...);
注:
- 进程的程序替换是通过exec*函数来完成替换的
- exec*函数,不用考虑返回值,只要返回一定是这个函数调用失败了
补:
- 当软件运行时,一定要先将软件加载到内存中,软件从磁盘中加载到内存一定是通过exec*特殊函数(加载器)来完成的
intexeclp(constchar*file,constchar*arg,...);
注:一般环境变量PATH,都是一些系统命令才能在PATH中找到,或者还可以把自己的命令导入到PATH中
Linux下的环境变量与命令行参数
intexecv(constchar*path,char*const argv[]);
intexecvp(constchar*file,char*const argv[]);
intexecle(constchar*path,constchar*arg,...,char*const envp[]);
intexecve(constchar*filename,char*const argv[],char*const envp[]);
补:
- 上述代码中的myenv自定义的环境变量(相当于是局部环境变量),不能被mycmd可执行程序继承(Linux下的继承与编程语言下的继承是两码事),因此想要在在mycmd中打印出myenv就必须使用execle、execve、execvpe
- 用Makefile编译多个源文件(过程如下):
intexecvpe(constchar*file,char*const argv[],char*const envp[]);
注:
- 使用execvpe是需要注意自己编译的可执行程序是无法在系统默认路径下找到的需要将可执行程序所在路径放到默认路径下即可
- execve是其余六个函数的底层实现,只不过是改变了其余函数的调用方式(参数)
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
命名理解
这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。换句话说execve是其余六个函数的底层实现,只不过是改变了其余函数的调用方式(参数)
exec*函数之间的关系:
总结
- 什么是程序替换:通过exec*让特定进城去加载磁盘中的其它进程,以达到运行的目的,期间不创建新的进程
- 为什么要程序替换:
- 子进程执行父进程部分代码
- 子进程自身执行新的程序的需求
- 如何程序替换:通过exec*进行程序替换
- exec*只要返回就说明出错了
补充:
- 程序替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序(加载另一个程序在内存中,更新页表信息,初始化虚拟地址空间)
简易的shell
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(waitpid)
注:
- 解析命令需要分为内置命令(chdir)和非内置命令(fork及execvp)
- 内置命令不需要创建子进程,而是在shell内的一个函数调用来使用的
代码如下:
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/types.h>#include<sys/wait.h>#include<unistd.h>#defineNUM128#defineSIZE32char command_line[NUM];char*command_parse[SIZE];intmain(){while(1){memset(command_line,'\0',sizeof(command_line));printf("[lc@myhost my_shell]$ ");fflush(stdout);//数据读取if(fgets(command_line,NUM-1,stdin)){
command_line[strlen(command_line)-1]='\0';//字符串(命令行数据分析)int index=0;
command_parse[index]=strtok(command_line," ");while(1){
index++;
command_parse[index]=strtok(NULL," ");if(command_parse[index]==NULL){break;}}}//判断命令//内置命令//非内置命令//第三方命令//执行内置命令if(strcmp(command_parse[0],"cd")==0&&chdir(command_parse[1])==0){continue;//shell内的一个函数调用:内置命令}//执行非内置命令if(fork()==0){//子进程execvp(command_parse[0],command_parse);exit(1);}int status=0;
pid_t ret=waitpid(-1,&status,0);if(ret>0&&WIFEXITED(status)){printf("exit code:%d\n",WEXITSTATUS(status));}}return0;}
注:
- 上段代码中cd命令的实现:不能创建子进程执行cd,这样是不会改变父进程(mini_shell)的所在路径的;也不能让父进程执行cd命令这样会导致光执行cd命令而不能执行cd命令以下的代码 ,因此像这样的命令需要用系统接口来完成命令的执行。eg:chdir
- 内置命令:并没有通过创建子进程来实现,而是通过用一些系统接口(chdir……)实现的就是内置命令
- 删除使用ctrl+backspace
版权归原作者 The August 所有, 如有侵权,请联系我们删除。