理解fork函数

来源:互联网 发布:淘宝帐号永久冻结解封 编辑:程序博客网 时间:2024/06/09 01:26

*改(补充的内容来自实战Linux编程精髓第9章)*
fork函数原型: pid_t fork(void)
fork函数较难理解的是:一次调用,两次返回。
其实,fork()不止调用一次,它是在父进程调用一次,在子进程也调用一次,通过两次调用的不同返回值来判断是位于父进程,还是位于子进程。
我根据它的执行原理,粗略画了如下的图:
fork执行简略图
在父进程中,调用fork函数时,操作系统将父进程缓冲区已有的内容(即fork调用前的内容)复制到子进程缓冲区中,然后,fork函数返回子进程的pid,继续执行fork函数之后的语句;系统通过程序计数器(pc)指向子进程的fork函数,fork返回0,因为父子进程使用同一代码块,所以,子进程继续执行fork之后的语句,通过返回值的不同,判断是位于哪个进程,进而执行相应的任务。
注意:fork函数执行之后,父进程与子进程是同时交替进行的,并不是先执行完父进程再执行子进程。

(补充) 已打开的文件描述符共享。一个进程对一个文件描述符的动作会影响到该文件对另一个进程的状态。如图:
文件描述符共享
上图是内核的内部数据结构。关键的数据结构为文件表(file table)。其中的bookkeeping表示一个打开的文件,Offset表示在文件中当前的位置(读/写偏移量)。因此,进程修改文件内后,Offset会更新,下一个进程访问时,便从更新后的位置开始。
多个文件描述符可能指向同个打开的文件,这样会造成只有一个文件的所有文件描述符都关闭,该文件才被关闭。而且,这不只发生在进程间,甚至存在于同一个进程中。

如果父进程被kill或者退出, 则子进程会被指定一个新的父进程init。这时,新的父进程PID将会是1,即init的PID。这样的子进程被称为孤儿(orphan)进程。

若还不能理解见fork如何做到两次返回,可以用以下测试代码:

#include <unistd.h>#include <sys/types.h>  #include <stdio.h>void main ()  {          pid_t pid = 5;          printf("fork!");    // printf("fork!\n");     printf("pidbefore = %d ",pid);            pid=fork();      printf("pidafter = %d\n",pid);    printf("I am 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());  }

运行结果(我在结果前加了序号):

lin@lin-X555LI:~/learn$ ./forktest(1)fork!pidbefore = 5 pidafter = 99692)I am fork i am the parent process, my process id is 99683)fork!pidbefore = 5 pidafter = 04)I am fork i am the child process, my process id is 9969

我们可以看到,(1)和(3)的fork!pidbefore = 5是一样的,说明在父进程调用fork时,已将父进程缓冲区的内容复制到子进程缓冲区,而不是在子进程中重头开始执行得到的,子进程是从fork函数开始执行的;而(2)和(4)的先后也说明了是先执行完父进程的代码,再去执行子进程的代码。

特别地,当在fork调用前先执行的打印语句中有‘\n’,则运行该语句的打印内容不被复制到子进程缓冲区,运行结果如下(fork!\n):

fork!pidbefore = 5 pidafter = 9969I am fork i am the parent process, my process id is 9968pidbefore = 5 pidafter = 0I am fork i am the child process, my process id is 9969

这是为什么呢?
因为在标准输出是行缓冲的,当遇到换行符时,缓冲区的内容被冲洗掉了,所以,只有一次输出。详见fork&行缓冲&全缓冲

标准I/O库提供了三种类型的缓冲:
(1) 全缓冲:填满标准I/O缓冲区才实际进行I/O操作。
(2)行缓冲:在输入和输出中遇到换行符时,标准I/O库执行I/O操作,当流涉及终端时,通常使用行缓冲。
(3)不带缓冲:标准出错流stderr通常是不带缓冲的。

上面说的缓冲指的是应用层的缓冲,在进行实际的I/O操作时,相关的系统调用(read和write)其实在内核也有缓冲区的。

当直接执行./fork_sample时,由于标准输出时行缓冲的,所以遇到换行符’\n’后缓冲区被冲洗。

当将程序输出重定向到别的文件时,是标准输出是全缓冲的,fork之前printf的数据仍在缓冲区中,在fork时该缓冲区也被复制到子进程中,因此我们就会看到”before fork\n”输出了两次。