今天看unp时看见了并发,可惜对fork()忘的一干二净,只怪读apue时没有做好笔记,后悔死了..重新翻了资料,记下备忘.

要理解fork(),首先需要懂得c程序和进程的概念:

c程序组成部分如下:

  • Text segment,正文段,由CPU执行的及其指令部分,可共享,通常是只读.
  • Initialized data segment, 初始化数据段,包含程序中需要明确赋初值的变量.
  • Uninitialized data segment, 非初始化数据段,对应于初始化数据段,又称bss段(block started by symbol,由符号开始的段),程序执行前,由内核初始化为0或空指针.
  • Stack,栈,存储自动变量和每次函数调用时所需保存的信息,程序递归时回用到栈保存返回地址和调用者的环境信息.
  • Heap, 堆,通常在堆中进行动态存储分配,位于非初始化数据段和栈之间.

典型的存储器安排如图所示:

至于进程,它主要包含三个元素:

  • 一个可以执行的程序;
  • 和该进程相关联的全部数据(包括变量,内存空间,缓冲区等等);
  • 程序的执行上下文(execution context).

操作系统对进程的管理,典型的情况,是通过进程表完成的.进程表中的每一个表项,记录的是当前操作系统中一个进程的情况.当分给某个进程的 CPU时间已经用完,操作系统将该进程相关的寄存器的值,保存到该进程在进程表中对应的表项里面;把将要接替这个进程占用 CPU的那个进程的上下文,从进程表中读出,并更新相应的寄存器,这个过程称为上下文交换(process context switch).

然后是fork(),由fork创建的新进程称为子进程(child process),操作系统在进程表中相应为子进程建立一个新的表项.fork函数有个特性,"一次调用,二次返回".它在调用进程(即父进程(parent process))中返回一次,返回值是子进程的ID号;在子进程又返回一次,返回值为0.因此,我们可以通过返回值判断当前进程是父进程还是子进程.

父进程调用fork之前打开的所有描述子在fork返回之后由子进程分享,也就是说,子进程是父进程的副本,它获得父进程的数据空间,堆和栈的拷贝.注意,这里是拷贝,而不是分享,父进程与子进程分享的是正文段.(见前图)

fork()的两个典型用法为:

  1. 一个进程创建自身的一个拷贝,这样每个拷贝可以在另外一个拷贝在执行其他任务的同时处理各自的操作,这在网络编程中是很常见的.
  2. 一个进程想要执行另外一个程序.因为创建一个新进程的唯一方法是调用fork,所以我们可以用fork创建一个拷贝,然后在子进程中调用exec把自身替换成新的程序.

下面是一个很简单的fork()示例:

#include <stdio.h>
#include <unistd.h>
int   main(void)
{
    fork();
    printf("P1\n");
    fork();
    printf("P2\n");
}

在继续往下之前,不妨猜猜输出结果是怎样子的..ctrl+a可见一个可能的结果和解释:

P1 //主进程(记为p1)执行printf

P1 //p1子进程(记为p10)执行printf

P2 //p1执行printf

P2 //p10执行printf

P2 //p1子进程(记为p11)执行printf

P2 //p10子进程(记为p100)执行printf

注意在fork之后,由父进程先执行还是子进程先执行是不确定的,这取决于内核使用的调度算法..

现在再看一个例子:

#include <unistd.h>
#include <sys/types.h>
int main(void)
{
    pid_t pid;
    printf("fork!\n");
    pid=fork();
    if (pid < 0)
        printf("error in fork!");
    else if (pid == 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());
}

输出结果很诡异,为:

fork!
i am the child process, my process id is 4286
i am the parent process, my process id is 4285

为什么这里fork!只输出一次呢?为了对比,可以将printf("fork!\n");里面的\n去掉,这时候程序倒是输出两次fork!了..解释如下,其实这与printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里,并没有实际显示到屏幕上.因此fork()后,子进程得到这份拷贝.而当printf()中含有\n时,printf将刷新stdout,因此子进程无法得到这份拷贝,fork!也就理所当然地只输出一次罢了.