在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核做如下的事情:
进程 = 内核数据结构 + 代码和数据。 创建一个进程,必须要有内核数据结构(pcb) 、代码和数据。对于内核数据结构而言,子进程继承了父进程的绝大部分内容;而对于代码和数据,如果子进程没有作任何修改,那么它们共用一份代码和数据。但是,如果父进程 或者 子进程(假设该进程为 proc ),想要修改某个数据 x(假设其物理地址是 A),那么就会发生 “写时拷贝” ,将数据拷贝到物理地址 B 的地方,然后修改数据,并且更改 proc 的页表映射关系,让 x 的虚拟地址映射到 物理地址 B 处。(详情可见 【Linux】进程地址空间)
写时拷贝本质上是一种按需申请资源的策略。由于操作系统不允许任何的资源浪费,所以,操作系统不会为子进程独立开辟一块物理内存来存放代码和数据,而是和父进程共用。当我们对数据不作修改的时候,就直接运行;要修改数据的时候,就进行写时拷贝。
但是,只有数据会发生写时拷贝吗?代码会不会发生写时拷贝?目前而言,确实只有数据会被修改,因为代码是不可变的,编译之后生成的可执行文件,无法去修改里面的代码。(但是,代码会发生替换,具体下一篇blog会涉及)
如下,执行下列代码。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main( void )
6 {
7 pid_t pid;
8 printf("Before: pid is %d\n", getpid());
9 if ( (pid=fork()) == -1 )
10 {
11 perror("fork()");
12 exit(1);
13 }
14 printf("After:pid is %d, fork return %d\n", getpid(), pid);
15 sleep(1);
16 return 0;
17 }
其结果如下图所示。这里看到了三行输出,一行before,两行after。进程5497先打印before消息,然后它又打印after。另一个after消息有5498打印的。注意到进程5498没有打印before,这是因为,fork 之前的代码,由当前进程(父进程)独立执行。fork之后的代码,由父子进程(当前进程变成父进程)这两个执行流分别执行。
值得注意的是,fork之后,谁先执行是完全由调度器的调度算法决定的。
由上图我们也可以了解到 fork 函数的返回值。在子进程中,fork 函数返回0 ,在父进程中,fork 函数返回子进程的pid。
fork函数的用法主要用以下两种,本文主要讨论第一种情况,对其进行分析。
有时,fork 函数也会调用失败,其失败的原因一般分为两种:系统中有太多的进程;实际用户的进程数超过了限制。
一个进程终止了,有两种情况:
在我们写 C 语言代码的时候, main 函数总是要 return 0; 这个返回的 0 ,叫做退出码
。退出码用来标识进程执行结果的正确与否。如果退出码为 0,结果正确;退出码为 非0,结果不正确。结果不正确的时候,退出码是 非0,那么就可以是 1、2、3、4、…… 等等,由于要知道结果不正确的原因,所以用不同的退出码标识不同的错误。
所以,退出码为0,进程执行结果正确;退出码 非0,进程执行结果不正确,可以根据退出码知道错误原因。
下图是下列代码的运行结果。对于这段程序,是想要实现 1 到 100 的加法,我们并没有打印任何信息来标识结果是否正确。但是,却可以根据退出码,来知道是不是正确的。如下, echo $? 的指令,是打印上一个进程的退出码,打印出来是 11 ,根据程序源代码来看,很明显 ret 不等于5050。
这就是,退出码的作用,我们可以用退出码来查看进程执行结果的正确与否。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main( void )
6 {
7 int ret=0,i;
8 for(i=1;i<100;i++){
9 ret += i;
10 }
11
12 if(ret == 5050) return 0;
13 else return 11;
14 }
当然,如果 退出码 不是0,也就是说 进程执行结果不正确,我们可以根据退出码来判断错误原因。这可以用 strerror 函数来演示,这是C语言中
1 #include<stdio.h>
2 #include<string.h>
3
4 int main( void )
5 {
6 int i;
7 for(i=0;i<=20;i++){
8 printf("%d: %s\n",i,strerror(i));
9 }
10
11 return 0;
12 }
但是,这上面的退出码和错误信息的对应关系,只不过是C语言定义的,其他环境下不一定支持这种对应关系。
如下图,在 Linux 环境下,我杀掉一个不存在的进程,它报错 “No such process”。然后查看上一个进程(kill -9 21345)的退出码,是 1 。在C语言的标准下,“No such process” 应该是3号退出码。
至此,我们可以了解到的信息是,进程退出有两种状态,一种是正常执行完退出,另一种是进程崩溃。对于前者, 结果正确的情况下,我们不用去关心;结果不正确的情况下,我们可以通过退出码来判断错误原因。
对于操作系统而言,一个进程退出了,就要释放该进程的 内核数据结构、代码和数据(如果有独立的)。
那么如何实现进程退出呢?
如下演示,用 C 语言的库函数 exit 会冲刷缓冲区,将缓冲区内的数据打印出来。但是系统调用 _exit 会直接结束进程,不会刷新缓冲区。
通过这个例子,我们可以猜测, exit 内部是调用了 _exit 的。只是 exit 先进行了缓冲区的冲刷,然后再调用 _exit 这个系统调用。
子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼” 的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息(退出码 / 信号)。
所以,进程等待就是通过系统调用,获取子进程的退出码或者退出信号的方式,顺便解决内存问题。
进程等待可以通过两个系统调用,wait 和 waitpid 。
头文件:
#include
#include
函数:
pid_t wait(int* status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
通过下列代码来验证 wait 的作用。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main( void )
8 {
9
10 pid_t id=fork();
11 if(id == 0) // 子进程
12 {
13 int cnt=5;
14 while(cnt){
15 printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
16 cnt--;
17 sleep(1);
18 }
19 exit(0);
20 }
21 // 父进程
22 sleep(10);
23 pid_t ret=wait(NULL);
24 printf("我是父进程,我醒了,我的pid是: %d ,ret: %d \n",getpid(),ret);
25 sleep(5);
26 return 0;
27 }
如下,父子进程两个执行流同时执行,但是父进程先睡 10 s,在子进程执行 5s 后,父进程还在 sleep,此时子进程已经退出,但是父进程没有回收他的退出信息,于是子进程变成僵尸状态,该状态持续 5s 。父进程醒来后,回收了子进程的退出信息,于是子进程彻底退出。
这里我们并没有关心子进程的退出信息,于是在 wait 函数的参数填写的是 NULL。
如果想要获取退出结果,最好使用 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:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
简单理解一下,这个系统调用的返回值 >0 ,等待成功;返回值 =-1,等待失败。
第一个参数 pid > 0 ,表示指定要等待的进程, pid = -1 ,表示等待任意一个子进程。
至于第二个参数 status ,不能简单地看作一个整数,要把它看作位图,等待结束之后,status 存储的就是进程的退出信息(传的指针,所以可以改变实参)。
如下,status 的低八位表示终止信号,高八位表示退出状态(退出码)。status 是 int 类型的变量,有 32 位比特位,对于高 16 位,这里并不关心,只使用低16 位。
core dump 如果为0,表示没有收到信号,正常退出。那么此时就要关心其退出码,根据退出码判断结果是否正确,如果结果错误,错误原因是什么。
如下代码运行结果,确实可以看出 status 并不是简单地存储数据。它实际上就是一个位图结构。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main( void )
8 {
9
10 pid_t id=fork();
11 if(id == 0) // 子进程
12 {
13 int cnt=5;
14 while(cnt){
15 printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
16 cnt--;
17 sleep(1);
18 }
19 exit(122);
20 }
21 // 父进程
22 int status=0;
23 pid_t ret=waitpid(id,&status,0);
24 printf("我是父进程,我的pid是: %d ,ret: %d ,status: %d \n",getpid(),ret,status);
25 return 0;
26 }
需要像下面一样才可以。status 右移八位,然后按位与 0xff ,也就是二进制的 11111111 ,得到退出码。 status 按位与0x7f ,也就是 01111111,得到退出信号。结果和我们上面所讲的 status 位图结构是一样的。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main( void )
8 {
9
10 pid_t id=fork();
11 if(id == 0) // 子进程
12 {
13 int cnt=5;
14 while(cnt){
15 printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
16 cnt--;
17 sleep(1);
18 }
19 exit(122);
20 }
21 // 父进程
22 int status=0;
23 pid_t ret=waitpid(id,&status,0);
24 printf("我是父进程,我的pid是: %d ,ret: %d ,退出码: %d ,信号: %d \n",getpid(),ret,(status>>8)&0xff,status&0x7f);
25 return 0;
26 }
如下图,修改一下子进程的代码,使其异常退出,其信号变成了 8 ,说明是异常退出,此时退出码为0。
但是,一般而言我们不会使用上面的方法(按位与)来得到退出码和退出信号,而是使用宏。
WIFEXITED( ); 如果子进程正常退出,返回值为真。
WEXITSTATUS( ); 得到子进程的退出码。
如下代码,在父进程等待成功之后,分情况输出。
如果子进程正常退出,那么输出其退出码。
如果子进程异常退出,那么输出信号。信号不可以用 WIFEXITED( ); 这个宏来得到,这个宏只是让我们知道子进程是否正常退出,所以得到信号要用按位与的方法。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main( void )
8 {
9
10 pid_t id=fork();
11 if(id == 0) // 子进程
12 {
13 int cnt=5;
14 while(cnt){
15 printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
16 cnt--;
17 sleep(1);
18 }
19 exit(122);
20 }
21 // 父进程
22 while(1)
23 {
24 int status=0;
25 pid_t ret=waitpid(id,&status,WNOHANG);
26 if(ret == -1){
27 printf("wait error\n");
28 exit(-1);
29 }
30 else if(ret == 0){
31 printf("子进程正常执行,还未结束,我先执行这里代码!\n");
32 sleep(1);
33 continue;
34 }
35 else{ // 等待成功
36 if(WIFEXITED(status)){
37 printf("wait sucess,child exit code: %d \n",WEXITSTATUS(status));
38 }
39 else{
40 printf("wait sucess,child exit signal: %d \n",status & 0x7f);
41 }
42 break;
43 }
44 }
45 return 0;
46 }
在子进程没有退出的时候,父进程只能一直在调用 waitpid 的地方等待,这叫做阻塞等待。
如下, 通过代码 和 运行情况可以看出,父进程确实在 waitpid 的地方等待,这就是阻塞等待,父进程没有干任何事情,只是在等待子进程退出。
如果不想进行阻塞等待,让父进程在等待子进程的同时,可以执行其他代码,那么可以将 waitpid 的第三个参数设置为 WNOHANG 。如下是 man 手册中,waitpid 系统调用的返回值说明。
如果使用 WNOHANG 参数,当子进程还没有结束的时候,如果等待出错,返回 -1;等待正常,返回0。
这种方式叫非阻塞轮询
,父进程会隔一段时间观察一下子进程的状态,得到返回值,如果返回值是 -1,说明子进程异常执行;如果返回值是0,说明子进程正在正常执行,还未结束;如果返回值大于0,那么得到的返回值就是子进程的 pid,子进程结束。
我们可以通过下列代码来观察非阻塞轮询。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main( void )
8 {
9
10 pid_t id=fork();
11 if(id == 0) // 子进程
12 {
13 int cnt=5;
14 while(cnt){
15 printf("我是子进程,我的pid是: %d ,cnt: %d \n",getpid(),cnt);
16 cnt--;
17 sleep(1);
18 }
19 exit(122);
20 }
21 // 父进程
22 while(1)
23 {
24 int status=0;
25 pid_t ret=waitpid(id,&status,WNOHANG);
26 if(ret == -1){ // 子进程异常执行
27 printf("wait error\n");
28 exit(-1);
29 }
30 else if(ret == 0){ // 子进程正常
31 printf("子进程正常执行,还未结束,我先执行这里代码!\n");
32 sleep(1);
33 continue;
34 }
35 else{ // 子进程执行结束
36 printf("我是父进程,我的pid是: %d ,ret: %d ,退出码: %d ,信号: %d \n",getpid(),ret,(status>>8)&0xff,status&0x7f);
37 break;
38 }
39 }
40 return 0;
41 }
代码运行结果如下,确实父进程在执行其他的代码。