写在前面
1. 本文内容对应《 UNIX 环境高级编程》 ( 第 2 版 ) 》第 8 章。
2. 总结了如何使用 fork 函数创建进程,以及父子进程间的一些关系。
3. 希望本文对您有所帮助,也欢迎您给我提意见和建议。
fork
一个现有进程可以调用 fork 函数创建一个新进程。 fork 函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是 0 ,而父进程的返回值则是新子进程的进程 ID 。原因在于,一个进程可以有多个子进程,并且没有一个函数可以获得所有子进程的进程 ID ;而一个进程只会有一个父进程,并且总能使用 getppid 获得其父进程的 ID 。如果系统中已经有太多的进程或者该实际用户 ID 的进程总数超过了系统限制( CHILD_MAX ), fork 函数将执行失败并返回- 1 。
写时复制
子进程和父进程继续执行 fork 调用之后的指令,但谁先执行是不确定的。父子进程共享正文段,但并不共享数据空间(包括环境变量和命令行参数,但命令行参数总是相同),堆和栈。子进程拥有自己的副本。由于在 fork 之后经常跟随 exec ,所有现在的很多实现并不执行一个父进程数据段,堆和栈的完全复制,而是使用写时复制( Copy-On-Write , COW )技术。这些区域由父子进程共享,而且内核将它们的访问权限修改为只读。如果父子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一页。与此不同, vfork 函数采用完全不复制的策略,在子进程执行 exec 之前,父子进程共享数据段和堆栈。
文件共享
fork 的 一个特性是父进程的所有打开文件描述符都被复制到子进程中。父子进程的每个相同的打开描述符共享一个文件表项(包括当前偏移量和文件状态标志,可参考文件 共享)。因此,如果父子进程写到同一个描述符文件,但又没有任何形式的同步,那么它们的输出就会相互混合。此外,当父进程在 fork 之前使用带缓冲区的标准 IO 的时候,子进程将复制该缓冲区。如果在 fork 之前没有冲刷该缓冲区,将出现重复输出。
在 fork 之后处理文件描述符有两种常见的情况:
l 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读写操作的任一共享描述符的文件偏移量已经执行了相应更新。
l 父子进程各自执行不同的程序段,并各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。‘
实验程序如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h>
int main(int argc, char *argv[]) { pid_t pid;
printf("Before fork./n"); if((pid = fork()) < 0) printf("fork error./n"); else if(pid == 0) { /*close(STDOUT_FILENO);*/ exit(0); } exit(0); } |
运行结果如下。由于重定向标准输出后,普通文件的默认缓冲类型是全缓冲,换行符‘ /n ’无法立即冲刷缓冲区,出现重复输出的现象。取消 close 注释,可避免重复输出。
pydeng@pydeng-laptop:~/apue.2e/mytest$ ./a.out > temp pydeng@pydeng-laptop:~/apue.2e/mytest$ cat temp Before fork. Before fork. |
父子进程间的区别
l fork 的返回值不同。
l 进程 ID 和父进程 ID 不同。
l 子进程 tms 结构(进程时间)中的各个字段均被置为 0 。
l 父进程设置的文件锁不会被子进程继承。
l 子进程的未处理的闹钟( alarm )被清除。
l 子进程的未处理信号集设置为空集。