工学1号馆

home

关于fork函数的深入剖析

Wu Yudong    November 13, 2016     Linux/Unix   596   

在《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

No comments yet.
To verify that you are human, please fill in "七"(required)