目录
1.fork()
(1).概念
(2).fork的写时拷贝
(3).fork常规用法
2.进程终止
(1).进程退出场景/退出码
(2).进程常见退出方法
1).exit函数
2)._exit和exit的区别
3).进程退出后,OS层面做了什么呢?
3.进程等待
(1).概念
(2).为什么要让父进程等待
(3).wait
(4).waitpid
(5).获取子进程status
1.子进程的退出有两种情况
2.位运算获取子进程的退出状态(退出码)和终止信号
3.宏函数获取子进程的退出状态(退出码)和判断是否收到终止信号
Q:为什么我们echo可以查到各个进程的退出码?
理解waitpid调用的流程和status
4.第三个参数options-设置等待方式
1.阻塞等待和非阻塞等待
2.如何设计一个非阻塞等待的轮询机制
1.fork()
(1).概念
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构(pcb+地址空间mm_struct+页表)给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
(2).fork的写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
父进程和子进程的虚拟地址空间以及页表都是各自有一份的,但是对应数据虚拟地址映射到的物理地址是同一个。
默认所有的数据都是只读的,如果有子进程想要写某个数据(页表项100),会发生写时拷贝,就会发生缺页中断,先拷贝一份数据到新的物理地址空间,然后修改子进程页表的映射关系,让子进程对应数据的虚拟地址映射到新的物理地址,然后中断结束,让子进程来修改数据。
Q:如何保证进程独立性?(如何保证父子进程间的独立性)——写时拷贝
(3).fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
2.进程终止
(1).进程退出场景/退出码
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止(此时程序崩溃了,退出码也就变得没有意义了!我们echo出来也没有意义)
我们的main函数返回0就是表示代码运行完毕,结果正确。(这个返回值就是进程的退出码)
我们可以通过echo $?查看最近一次进程退出时的退出码
[zebra@VM-8-12-centos test6]$ echo $?
0
第二次第三次echo的时候输出的是前一次echo这个进程的退出码。实际上每个命令执行都是一个进程,都会有一个退出码,这个退出码就是main函数的返回值,每个进程都有一个main函数
不同的错误码对应不同的错误信息,当然我们也可以自己设计一种错误码和错误信息的映射。
(2).进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
1. 从main返回(代表进程退出,从函数返回也一样,叫做函数返回)
2. 调用exit
3. _exit
异常退出:
ctrl + c,信号终止
1).exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值(status有很多宏值,比如EXIT_SUCCESS)
- 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
e.g.:我们就算不加上\n,最后main return或者程序exit的时候也会把缓存区中的内容刷新到显示器上面。
printf("hello,zebra");
sleep(5);
exit(EXIT_SUCCESS);
return 0;
2)._exit和exit的区别
_exit则是强制终止进程,不会进行进程后续的收尾工作,比如刷新缓冲区。
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
在这里也变相说明了我们所谓的缓冲区是用户层面的缓冲区,不再操作系统层面
3).进程退出后,OS层面做了什么呢?
系统层面,少了一个进程: free PCB,free,mm_struct,free页表和各种映射关系,代码+数据申请的空间也要释放掉掉
3.进程等待
(1).概念
父进程执行fork之后,会创建子进程,子进程一般都是要帮助父进程完成某种任务,此时父进程需要通过wait/waitpid等待子进程退出。
(2).为什么要让父进程等待
1.通过获取子进程退出的信息,可以得到子进程的执行结果
2.既然父进程要拿到子进程的执行结果,也就需要保证父进程在子进程之后退出,使用wait可以保证时序问题,即让子进程先退出,父进程后退出。
3.子进程退出的时候会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放子进程占用的资源。
(3).wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
(4).waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
- pid:
Pid=-1,等待任意一个子进程。与wait等效。(如果这里给的pid和子进程对不上,会等待失败)
Pid>0.等待其进程ID与pid相等的子进程。
- status:(输出型参数,父进程拿到什么status结果,一定和子进程如何退出强相关)
为了确定是三种退出情况中的哪一种
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
说明:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回
(5).获取子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待(只看status低16比特位):
1.子进程的退出有两种情况
- 一种是正常终止(结果正确或者不正确)
- 一种是异常终止(进程因为异常问题,收到了某种信号)
所以我们可以通过判断是否收到终止信号(0-6,低7位)来判断进程是否是正常终止,如果是正常终止(低7位全是0),我们再去看退出状态(8-15,8个比特位)
2.位运算获取子进程的退出状态(退出码)和终止信号
如果我们要获取8-15这8位退出状态(退出码),可以让status右移8位,然后和000...000 1111 1111(也就是(status>>8)&0xFF)&一下(因为status是32位的,右移8位后,我们要把前面的24位置0)
如果我们要获取0-6这7位终止信号,可以让status&0x7F(我们只需要获取低7位就行)
如果正常退出,退出信号都是0;如果异常退出,退出信号就不是0,此时前面的退出状态也就没有意义
3.宏函数获取子进程的退出状态(退出码)和判断是否收到终止信号
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(判断进程是否收到终止信号)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
首先我们需要判断进程是否收到退出信号,如果收到了,那就是异常退出的;如果没有收到退出信号,那就是正常退出,退出码也就有效了
Q:为什么我们echo可以查到各个进程的退出码?
bash是命令行启动的所有进程的父进程。bash是通过wait方式得到子进程的退出结果,所以我们echo $?能够查到子进程的退出码
理解waitpid调用的流程和status
waitpid是一个系统调用接口,肯定是用户来调的。用户调用waitpid让父进程等待子进程退出并变成僵尸状态,然后从进程的pcb中获取exit_code和signal,组合起来放到status中,父进程中的status和用户层的status也就都发生了变化,用户就可以看到进程的退出码和终止信号了。(实际上这个status是一个指针,作为一个输出型参数,所以在操作系统内部修改status会直接影响用户层的status)
4.第三个参数options-设置等待方式
WNOHANG: 设置等待方式为非阻塞
若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
1.阻塞等待和非阻塞等待
首先都是等待的一种方式,都是父进程在等子进程退出(子进程退出是一种条件)
阻塞的本质:其实是进程的PCB被放入了等待队列,并将进程的状态改为S状态
返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度
- 阻塞等待:父进程调waitpid这个接口等待子进程完成时,把父进程放在等待队列里面,等到子进程退出后,再把它从等待队列里面拿出来。
- 非阻塞等待:父进程不会被放入等待队列,而是时不时地多次调用waitpid,直到子进程结束。
2.如何设计一个非阻塞等待的轮询机制
0:阻塞等待
WNOHANG: 设置等待方式为非阻塞
全称是which no hang,表示不会被hang住
我们进行非阻塞等待时,需要考虑三种情况,并进行基于非阻塞等待的轮询方案
每次循环调用waitpid,通过返回值判断子进程是否退出
情况一:这次查询子进程还没退出,父进程做自己的事情
情况二:这次查询子进程退出了
情况三:调用waitpid出错,返回错误信息
while(1){
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret == 0){
//子进程没有退出,但是waitpid等待是成功的,需要父进程重复进行等待
printf("Do father things!\n");
}
else if(ret > 0){
//子进程退出了,waitpid也成功了,获取到了对应的结果
printf("fahter wait: %d, success, status exit code: %d, status exit signal: %d\n", ret, (status>>8)&0xFF, status&0x7F);
break;
}
else{ //ret < 0
//等待失败
perror("waitpid");
break;
}
sleep(1); //每隔一秒进行一次轮询
}
阻塞就是调用waitpid,如果子进程没退出,就干等
非阻塞就是调用一次waitpid,然后继续执行后面的代码,需要自己设计一个基于非阻塞等待的轮询方案