进程控制
- 进程终止
-
- 三种方法的比较
- 进程等待
-
- waitpid
- 程序替换
-
- 程序替换的原理
- exec家族函数
进程终止
在编写C语言程序时,main函数往往会return 0;但是为什么需要return 0呢,return的0被谁接收了呢?首先main函数的返回值是退出码,往往用0表示运行成功正常退出,!0表示失败,不同的退出码表示不同的失败原因,main函数的退出码是直接被OS接收的,用来让OS辨别该进程的执行情况,子进程的退出码则由父进程接收。 在Linux中我们使用命令echo $?可以显示出最近一个进程的退出码,C语言已经提供了一些内置的错误原因
使用函数strerror可以显示该错误码对应的错误信息
进程终止无外乎三种情况 1.代码跑完,结果对 2.代码跑完,结果不对 3.代码没跑完,进程异常 对于前两种情况的区分是由退出码来决定的,而如果代码出现异常,那么退出码本身是没有意义的。 退出码三种方法表示,
三种方法的比较
return VS exit return表示函数调用结束,main函数return表示进程退出,exit表示进程结束,任何地方调用都会导致进程退出 exit VS _exit exit是库函数会主动刷新缓冲区,而_exit是系统调用不会刷新缓冲区,所以exit底层是有_exit的,而由此我们可以知道输出缓冲区一定不在操作系统内部,因为倘若在,那么_exit也会刷新缓冲区,由此可以得知输出缓冲区是在库当中的,也就是库缓冲区 所以进程退出的最佳实践为exit
进程等待
为什么要等待进程? 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存 泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息,其中回收资源是父进程必须做的事,而获取子进程退出信息是可选的。 进程等待是让父进程通过等待的方式回收子进程的PCB与资源,如果需要就获取子进程的退出信息。
父进程调用wait函数表示等待任意一个子进程,如果子进程没有退出,父进程等待时就会阻塞,如果子进程退出,父进程wait时,wait就会返回,让系统自动解决子进程的僵尸问题,等待成功时返回子进程的pid。
waitpid
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:默认为0,表示阻塞等待
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该子进程的ID。
status参数,该参数是一个输出型参数,由操作系统填充,如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程,status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
正常终止的前八位为子进程的退出码,所以退出码的范围一般为0~255,而如果进程出现异常,前八位就没用了,后七位为信号编号,代表异常的信息。 通过status我们可以知道子进程的退出码为多少,如果子进程退出码为1,那么status则为0000000100000000,化为整形则为256,可以通过(status>>8)&0xFF来提取退出码,需要&0xFF的原因为消除高16位的影响,只考虑后8位。同样的如果出现异常,需要知道异常的原因,用status&0x7F来获取信号编号
我们可以发现信号是没有0的,因为信号为0代表进程是正常运行,没有出现异常。虽然status的后7位代表信号编号,但是由于操作系统本身的原因是只设置了1~64。 所以进程终止的三种情况,信号编号与退出码分别如下: 1.代码跑完,结果对,0,0 2.代码跑完,结果不对,0,!0 3.代码没跑完,进程异常,!0,无意义 由此我们终于明白了进程退出的整个流程: 当子进程退出时,子进程的exit_code与exit_signal会写入到子进程的PCB中,所以当子进程退出进入到僵尸状态时,OS不会释放其PCB,因为父进程会提取子进程的两个数据整合到status中,所以waitpid的本质也是获取子进程PCB内的属性数据,和getpid没区别,在调用完成后,也会让OS释放目标的PCB。 最终关心的也就只有两个数字,分别为退出码与信号,所以已经提供了两个宏来帮助判断,WIFEXIT(status)如果是正常终止则返回真,WEXITSTATUS(status)提取退出码。 在waitpid的参数中最后一个参数为option,意味选项,当选项为0时代表阻塞等待,还存在一个宏WNOHANG意味非阻塞等待,非阻塞等待不会因为条件没有就绪而阻塞,而是立即返回非阻塞,又被称为非阻塞轮询方案。 运行以下非阻塞等待代码:
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 10;
while(cnt—)
{
printf("I am child,%d\\n",cnt);
sleep(1);
}
exit(10);
}
pid_t rid = waitpid(–1,NULL,WNOHANG);
if(rid == id)printf("等待成功\\n");
else if(rid == 0)printf("子进程未结束\\n");
else printf("等待失败\\n");
return 0;
}
可以看到结果发现进程只有一个了,因为此时是非阻塞等待,父进程等待一次后发现子进程并未结束,所以父进程运行完之后的代码直接结束,此时子进程变成了孤儿进程,用ps命令查看可以发现此时该子进程的父进程变为了1,也就是bash,也可以发现此时ctrl c无法终止,因为孤儿进程此时与显示器无关了,只能用信号杀死或者等待其自然结束。所以非阻塞等待被称为非阻塞轮询方案,因为非阻塞情况下,父进程不可能只等待一次,需要多次循环等待。 与下方代码类似
而非阻塞轮询的意义就是不会卡主父进程,父进程可以在等待的过程中做其他事情,会更高效!
程序替换
fork() 之后,父子各自执行父进程代码的一部分如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!
程序替换的原理
子进程调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。调用exec函数进行替换时,子进程原本共享父进程的代码和数据,会发生写时拷贝,会从磁盘加载目标程序到物理内存中,至此子进程就有了一套独立的代码和数据,保持了进程之间的独立性。 以最简单的一个exec函数为例 int execl(const char *path, const char *arg, …),path为目标程序路径,arg为目标程序名称,…为可变参数,运行以下代码 
关联Linux的命令,就可以知道命令行解释器的运行原理,先利用fork创建子进程,将命令用exe系列加载到物理内存中,这时父进程也就是bash会waitpid进行等待子进程
exec家族函数
exec家族的名称如下表示 • l(list) : 表示参数采用列表 • v(vector) : 参数用数组 • p(path) : 有p自动搜索环境变量PATH • e(env) : 表示自己维护环境变量 例如execl函数就是用列表而不是数组表示选项;execlp函数执行指定的命令,需要让execlp自己在环境变量PATH中寻找指定的程序!所以其就不用带路径了直接带名字就行;对于execv函数是用数组来表示选项;对于execvp表示不带路径,用数组表示选项;对于execle则是可以自己传递环境变量表,当然也可以传递自带的environ,如果想在自带的environ中增加新的自己的环境变量,可以通过putenv函数在原先的环境变量表中增加新的。 以上的六种函数都是在man手册的第三页,也就是说是库函数而非系统调用,他们的底层都是有execve这个系统调用的,也就是说这六种函数在被调用的时候会去调用execve这个系统调用来完成任务
网硕互联帮助中心





评论前必须登录!
注册