在《Unix进程标志与fork函数 》中,总结了一下fork函数的使用,有理论也有实践,但随着学习的深入,发现我对它的理解还是不够深入,本文将深入剖析一下该函数。
本文地址:http://wuyudong.com/2016/11/13/2995.html,转载请注明出处。
这里先举个例子:
/* * fork1.c * Created on: 2016-11-11 * Author: wuyudong */ #include <unistd.h> #include <stdio.h> int main () { pid_t fpid; //fpid表示fork函数返回的值 int count = 0; if ((fpid = fork()) < 0) { //从这里开始,子进程和父进程共享后面的 //fork()产生一个子进程, 返回值为0,即fpid = 0 //父进程的fpid=子进程ID printf("error in fork!"); } else if (fpid == 0) { printf("son's fpid is %d\n", fpid); printf("I am the child process, my process id is %d\n", getpid()); printf("I am son!\n"); count++; } else { printf("father's fpid is %d\n", fpid); printf("I am the parent process, my process id is %d\n", getpid()); printf("I am father!\n"); count++; } printf("result of count is: %d\n",count); return 0; }
运行结果如下:
wu@ubuntu:~/opt/Cproject$ gcc -g fork1.c -o fork1
wu@ubuntu:~/opt/Cproject$ ./fork1
father’s fpid is 30331
I am the parent process, my process id is 30330
I am father!
result of count is: 1
son’s fpid is 0
I am the child process, my process id is 30331
I am son!
result of count is: 1
在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,将要执行的下一条语句都是if ((fpid = fork()) < 0)后面的代码,执行完fork后,进程1的变量为count=0,fpid != 0(父进程)。进程2的变量为count=0,fpid=0(子进程),这里通过fpid来识别和操作父子进程的。
子进程与父进程继续执行fork之后的指令,子进程是父进程的副本(子进程获得父进程的数据空间、堆和栈的副本)。注意,这是子进程所拥有的副本,子进程与父进程并不共享这些存储空间部分。父进程与子进程共享正文段。
相关概念可以看看这篇文章《C程序的存储空间布局 》
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
上面的程序两个进程虽然共享执行后面的代码段,但是由于fid不同,在执行ifelse分支的时候出现不同
再来看一个例子:
/* * fork2.c * Created on: 2016-11-11 * Author: wuyudong */ #include <unistd.h> #include <stdio.h> int main(void) { int i = 0; printf("i son/pa ppid pid fpid\n"); //ppid指当前进程的父进程pid //pid指当前进程的pid, //fpid指fork返回给当前进程的值 for (i = 0; i < 2; i++) { pid_t fpid = fork(); if (fpid == 0) printf("%d child %4d %4d %4d\n", i, getppid(), getpid(), fpid); else printf("%d parent %4d %4d %4d\n", i, getppid(), getpid(), fpid); } return 0; }
运行结果如下:
wu@ubuntu:~/opt/Cproject$ gcc -g fork2.c -o fork2
wu@ubuntu:~/opt/Cproject$ ./fork2
i son/pa ppid pid fpid
0 parent 2567 2652 2653
0 child 2652 2653 0
1 parent 2652 2653 2654
1 child 1920 2654 0
1 parent 2567 2652 2655
1 child 1920 2655 0
下面来分析一下代码产生的结果:
第一步:在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是p2652和p2653(用pxxxx表示进程ID为xxxx的进程)。可以看到父进程p2652的父进程是p2567,子进程p2653的父进程正好是p2652。我们用一个链表来表示这个关系:p2567->p2652->p2653
第一次fork后,p2652(父进程)的变量为i=0,fpid=2653(fork函数在父进程中返向子进程id),代码内容为:
第二步:这里是子进程p2653先运行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p2654,对于此时的子进程p2653由于执行fork后,其fpid的值等于其子进程的pid(2654),这样在执行if语句的时候打印输出parent,而其子进程pid(2654)由于其fpid=0,所以打印child。这时候子进程p2653结束,其子进程p2654被init收养,其ppid本应该是1,但是由于在ubuntu系统中使用Upstart作为默认init系统,可以查一下
wu@ubuntu:~$ ps aux | grep init
root 1 0.3 0.3 4480 3548 ? Ss 18:36 0:05 /sbin/init
wu 1920 0.0 0.3 6140 3692 ? Ss 18:37 0:00 init –user
wu 3429 0.0 0.1 4692 2036 pts/7 S+ 19:01 0:00 grep –color=auto init
可以看到,这里pid=1920的进程收养那些父进程结束的子进程,这也解释了子进程pid2654的ppid为1920。接着父进程p2652执行fork,生成一个子进程p2655,这时候p2652的fpid=2655,打印parent,执行完后退出,这时候其子进程p2655,其fpid=0,所以打印child,由于它的父进程退出,所以由init进程领养,故ppid=1920
当i=0时,父进程先执行还是子进程先执行是没有固定的先后顺序的,如果将上面的程序再运行一次,出现下面的结果:
wu@ubuntu:~/opt/Cproject$ ./fork2
i son/pa ppid pid fpid
0 parent 2567 2657 2658
1 parent 2567 2657 2659
1 child 1920 2659 0
0 child 1920 2658 0
1 parent 1920 2658 2660
1 child 1920 2660 0
我们再来看一份代码:
/* * fork3.c * Created on: 2016-11-11 * Author: wuyudong */ #include <unistd.h> #include <stdio.h> int main(void) { int i; for (i = 0; i < 3; i++) { pid_t fpid = fork(); if (fpid == 0) printf("son\n"); else printf("father\n"); } return 0; }
运行结果:
wu@ubuntu:~/opt/Cproject$ gcc -g fork3.c -o fork3
wu@ubuntu:~/opt/Cproject$ ./fork3
father
father
father
son
father
son
son
father
father
son
son
son
father
son
简单分析如下:
for | i=0 | i=1 | i=2 |
father | father | father | |
son | |||
son | father | ||
son | |||
son | father | father | |
son | |||
son | father | ||
son |
其中每一行分别代表一个进程的运行打印结果。
总结一下规律,对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)=2(2N-1)次,创建的子进程数为1+2+4+……+2N-1=2N-1个。
如果想测一下一个程序中到底创建了几个子进程,最好的方法就是调用printf函数打印该进程的pid,也即调用printf(“%d/n”, getpid()); 或者通过printf(“+\n”); 来判断产生了几个进程。有人想通过调用printf(“+”); 来统计创建了几个进程,这是不正确的,看下面的代码:
/* * fork4.c * Created on: 2016-11-11 * Author: wuyudong */ #include <unistd.h> #include <stdio.h> int main() { pid_t fpid; //fpid表示fork函数返回的值 //printf("fork!"); printf("fork!\n"); fpid = fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) printf("I am the child process, my process id is %d\n", getpid()); else printf("I am the parent process, my process id is %d\n", getpid()); return 0; }
运行结果:
wu@ubuntu:~/opt/Cproject$ gcc -g fork4.c -o fork4
wu@ubuntu:~/opt/Cproject$ ./fork4
fork!
I am the parent process, my process id is 3881
I am the child process, my process id is 3882
如果把语句printf(“fork!\n”); 注释掉,执行printf(“fork!”); 则新的程序的执行结果是:
wu@ubuntu:~/opt/Cproject$ gcc -g fork4.c -o fork4
wu@ubuntu:~/opt/Cproject$ ./fork4
fork!I am the parent process, my process id is 3898
fork!I am the child process, my process id is 3899
程序的唯一的区别就在于一个\n回车符号,为什么结果会相差这么大呢?
这就跟printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上。但是只要看到有\n 则会立即刷新stdout,因此就马上能够打印了。
运行了printf(“fork!”)后,“fork!”仅仅被放到了缓冲里,程序运行到fork时缓冲里面的“fork!” 被子进程复制过去了。因此在子进程度stdout缓冲里面就也有了“fork!” 。所以你最终看到的会是“fork! ” 被printf了2次!!!!
而运行printf(“fork! \n”)后,“fork!”被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork! 内容。因此你看到的结果会是fork! 被printf了1次!!!! 所以说printf(“+”); 不能正确地反应进程的数量。
关于fork的深入剖析,这里先告一段落,随着研究的深入还将展开进一步的讨论……
参考资料:
http://blog.csdn.net/jason314/article/details/5640969
http://blog.csdn.net/lifan1314521/article/details/43033275
Comments