winston 发表于 2012-2-6 14:26:02

linux fork函数的精辟解说


此文原文来源于一个blog,文章的名称为:linux fork函数的精辟解说
原文地址:http://blog.chinaunix.net/space.php?uid=12461657&do=blog&id=3062996
感觉这篇文章不错,在此分享下来,在原文的基础上增加了自己的一些理解和说明。
开始演示:
# cat fork.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

main()
{
    pid_t pid;
    pid=fork();
    if (pid < 0)
      printf("Error in fork!");
    else if (pid == 0)
      printf("I am the child process. The process id is %d\n",getpid());
    else
      printf("I am the parent process. The process id is %d\n",getpid());
}
# gcc fork.c -o fork
# ./fork
I am the child process. The process id is 6260
I am the parent process. The process id is 6259
先说什么是进程
一个进程,就是一个可执行程序的一次执行过程中的一个状态。
打个比方      我们把一个大学数学老师比作一个可执行程序,老师就是一个人,相当于一份源码,这个数学老师每个学期可能要教多个班,假设他教1班和2班,时间是一个学期,那么他从开学到期末教这两个班这个过程就是两个进程,两个进程的周期都是一个学期。

      在稍微理解了进程的概念之后,我们说在正在运行的计算机中,不管是Linux或者是Windows系统都运行有很多进程,虽然我们的系统中进程比较多,但是对于单核的CPU而言,每一时刻只能有一个进程占用CPU,其他的进程就可能处在等待执行、就绪、结束等状态(状态名称可能随不同操作系统而相异)。

      那么要使我们的操作系统能够正常执行,操作系统对这些进程的管理,典型的情况,是通过内存中的进程表完成的(这里的进程表不是源码的意思)。进程表中的每一个表项,记录的是当前操作系统中一个进程的情况,比如执行到哪里,下一步要执行什么命令等。

      一个称为“程序计数器(program counter, pc)”的寄存器,指出当前占用CPU的进程要执行的下一条指令的位置。当分给某个进程的CPU时间已经用完,操作系统将该进程相关的寄存器的值,保存到该进程在进程表中对应的表项里面;然后把将要接替掉当前进程的那个进程的上下文从内存的进程表中读入,并更新相应的寄存器。这个过程称为“上下文交换(process context switch)”,实际的上下文交换需要涉及到更多的数据,那和fork无关,不再多说,主要要记住程序寄存器pc指出程序当前已经执行到哪里,是进程上下文的重要内容,换出 CPU的进程要保存这个寄存器的值,换入CPU的进程,也要根据进程表中保存的本进程执行上下文信息,更新这个寄存器)。

好了,有这些概念打底,可以说fork了。当你的程序执行到下面的语句:
pid=fork();
操作系统会进行如下的大概过程:
创建一个新的子进程,而这个子进程是父进程的副本,接下来这两个进行就由操作系统调度,直到程序执行结束。

      那么这个过程,我们可以对其进行更加详细的分析。在执行fork以后,操作系统复制一份当前执行的进程的数据,包括进程的数据空间、堆和栈等,并且在进程表中相应为它建立一个新的表项。上下文也是原进程(父进程)的拷贝。但是父、子进程共享正文段,也就是CPU执行的机器指令部分,这个可共享的,在存储器中只需要一个副本,而且这个副本通常是只读的。

那在什么时候父子进程中就分道扬镳呢?
      从上面的实验结果我们看出那两条打印出来的语句不是在同一个进程里面的,因为在同一个进程里面不可能存在,不通结果的getpid(),事实说明是两个不同进程返回的结果,那么在执行pid=fork()以后,开始出现父、子进程,既然是两个进程,那么接下来谁先被调度,也就是说执行pid=fork()以后,在单核CPU下,只会有一个进程被调度,假设是我们的父进程占用CPU时间,父进程继续执行,操作系统对fork的实现,使这个调用在父进程中返回刚刚创建的子进程的pid(一个正整数),所以下面的if语句中pid<0, pid==0的两个分支都不会执行。所以输出I am the parent process....。

      子进程在之后的某个时候得到调度,它的上下文被换入。我们上面分析过,在子进行创建的时候也会复制父进程的上下文,所以子进程不会从头开始执行,而是从pid=fork()开始执行,基于操作系统对fork的实现,使得子进程中fork调用返回0。所以在这个进程(中pid=0。这个进程继续执行的过程中,if语句中 pid<0不满足,但是pid==0是true。所以输出I am the child process...。

我们下面来看一个和我在上面分析的一个结论似乎存在矛盾的现象

# cat fork.c      
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

main()
{
    pid_t pid;
    printf("fork!");
    pid=fork();
    if (pid < 0)
      printf("Error in fork!\n");
    else if (pid == 0)
                printf("I am the child process. The process id is %d\n",getpid());
      else
                printf("I am the parent process. The process id is %d\n",getpid());
}
# gcc fork.c -o fork
# ./fork            
fork!I am the child process. The process id is 7378
fork!I am the parent process. The process id is 7377
#
       这里我添加了printf("fork!")这一行,执行了以后我们发现,“fork!”打印了两次,我们上面不是说,fork以后的子进程的上下文不是和父进程一样吗,也就是说子进程不会从头开始执行,应该从fork执行,那么fork!的出现不是有矛盾吗?我们再来看看下面的现象

# cat fork.c      
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

main()
{
    pid_t pid;
    printf("fork!\n");
    pid=fork();
    if (pid < 0)
      printf("Error in fork!\n");
    else if (pid == 0)
                printf("I am the child process. The process id is %d\n",getpid());
      else
                printf("I am the parent process. The process id is %d\n",getpid());
}
# gcc fork.c -o fork
# ./fork            
fork!
I am the child process. The process id is 7458
I am the parent process. The process id is 7457
#


这里我在printf("fork!")这一行的fork!后面添加了一个换行符,变成printf("fork!\n")执行以后发现只打印一个fork!这个到底是什么原因呢?

      主要的区别是因为有了一个\n 回车符号,说起真正的原因,这和printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上,但是只要看到有\n 则会立即刷新stdout,因此就马上能够打印了。
      还没有执行到fork()的时候,原进程运行了printf("fork!") 后,fork!仅仅被放到了缓冲里,执行了fork,这时候子进程复制一份父进程的数据,包括这个stdout缓冲,在子进程度stdout缓冲里面就也有了fork!。执行到后面这两个缓冲都打印到屏幕上,所以出现两次,并不是printf执行两次。而运行 printf("fork!\n")后, fork!被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork!内容,因此你看到的结果会是fork!被printf了1次!!!
最后,分析有不恰当之处望指出!!!

作者:imfinger 发表于2012-2-5 23:29:43 原文链接

页: [1]
查看完整版本: linux fork函数的精辟解说