一个现有进程能够调用fork函数建立一个新进程。算法
#include <unistd.h> pid_t fork( void ); 返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1
由fork建立的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。两次返回的惟一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给父进程的理由是:由于一个进程的子进程能够有多个,而且没有一个函数使一个进程能够得到其全部子进程的进程ID。fork使子进程获得返回值0的理由是:一个进程只会有一个父进程,因此子进程老是能够调用getppid以得到其父进程的进程ID(进程ID 0老是由内核交换进程使用,因此一个子进程的进程ID不可能为0)。shell
子进程和父进程继续执行fork调用以后的指令。子进程是父进程的副本。例如,子进程得到父进程的数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段(text,代码段)。编程
因为在fork以后常常跟随着exec,因此如今的不少实现并不执行一个父进程数据段、栈和堆的彻底复制。做为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父、子进程共享,并且内核将它们的访问权限改变为只读的。若是父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制做一个副本,一般是虚拟存储器系统中的一“页”。网络
Linux 2.4.22提供了另外一种新进程建立函数——clone(2)系统调用。这是一种fork的泛型,它容许调用者控制哪些部分由父、子进程共享。函数
程序清单8-1中的程序演示了fork函数,从中能够看到子进程对变量所做的改变并不影响父进程中该变量的值。学习
程序清单8-1 fork函数示例spa
[root@localhost apue]# cat prog8-1.c #include "apue.h" int glob = 6; /* external variable in initialized data */ char buf[] = "a write to stdout\n"; int main(void) { int var; /* automatic variable on the stack */ pid_t pid; var = 88; if(write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) -1) err_sys("write error"); printf("before fork\n"); /* we don't flush stdout */ if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid == 0) /* child */ { glob++; /* modify variables */ var++; } else { sleep(2); /* parent */ } printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var); exit(0); }
若是执行此程序则获得:操作系统
[root@localhost apue]# ./prog8-1 a write to stdout before fork pid = 13367, glob = 7, var = 89 子进程的变量值改变了 pid = 13366, glob = 6, var = 88 父进程的变量值没有改变 [root@localhost apue]# ./prog8-1 > tmp.out [root@localhost apue]# cat tmp.out a write to stdout before fork pid = 13369, glob = 7, var = 89 before fork pid = 13368, glob = 6, var = 88
通常来讲,在fork以后是父进程先执行仍是子进程先执行是不肯定的。这取决于内核所使用的调度算法。若是要求父、子进程之间相互同步,则要求某种形式的进程间通讯。.net
当写到标准输出时,咱们将buf长度减去1做为输出字节数,这是为了不将终止null字节写出。strlen计算不包括终止null字节的字符串长度,而sizeof则计算包括终止null字节的缓冲区长度。二者之间的另外一个差异是,使用strlen需进行一次函数调用,而对于sizeof而言,由于缓冲区已用已知字符串进行了初始化,其长度是固定的,因此sizeof在编译时计算缓冲区长度。3d
注意程序清单8-1中fork与I/O函数之间的交互关系。write函数是不带缓冲的。由于在fork以前调用write,因此其数据写到标准输出一次。可是标准I/O库是带缓冲的(这里用到了标准I/O库的printf函数)。若是标准输出连到终端设备,则它是行缓冲的,不然它是全缓冲的。当以交互方式运行该程序时(此时是行缓冲的),只获得该printf输出的行一次,其缘由是标准输出缓冲区在fork以前已由换行符冲洗。可是当将标准输出重定向到一个文件时(此时是全缓冲的),却获得printf输出行两次。其缘由是,在fork以前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中(咱们没有用fflush冲洗缓冲区),而后在将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程中。因而那时父、子进程各自有了带该行内容的标准I/O缓冲区。(子进程复制父进程缓冲区对程序的影响实例解析可参考:http://blog.csdn.net/lollipop_jin/article/details/8774057)在exit以前的第二个printf将其数据添加到现有的缓冲区中。当每一个进程终止时,最终会冲洗其缓冲区中的副本。
对程序清单8-1需注意的另外一点是:在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的全部打开文件描述符都被复制到子进程中。父、子进程的每一个相同的打开描述符共享一个文件表项。
考虑下述状况,一个进程具备三个不一样的打开文件,它们是标准输入、标准输出和标准出错。在从fork返回时,咱们有了如图8-1所示的结构。
这种共享文件的方式使父、子进程对同一文件使用了一个文件偏移量。若是父、子进程写到同一描述符文件,但又没有任何形式的同步(例如使父进程等待子进程),那么它们的输出就会相互混合(假定全部的描述符是在fork以前打开的)。
在fork以后处理文件描述符有两种常见的状况:
(1)父进程等待子进程完成。在这种状况下,父进程无需对其描述符作任何处理。当子进程终止后,它曾进行过读、写操做的任一共享描述符的文件偏移量已执行了相应的更新。
(2)父、子进程各自执行不一样的程序段。在这种状况下,在fork以后,父、子进程各自关闭它们不须要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程中常用的。
除了打开文件以外,父进程的不少其余属性也由子进程继承(能够理解为共享),包括:
父、子进程之间的区别是:
使fork失败的两个主要缘由是:系统中已经有了太多的进程(一般意味着某个方面出了问题),或者该实际用户ID的进程总数超过了系统限制(CHILD_MAX)。
fork有下面两种用法:
(1)一个父进程但愿复制本身,使父、子进程同时执行不一样的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
(2)一个进程要执行一个不一样的程序。这对shell是常见的状况。在这种状况下,子进程从fork返回后当即调用exec。
某些操做系统将(2)中的两个操做(fork以后执行exec)组合成一个,并称其为spawn。UNIX将这两个操做分开,由于在不少场合须要单独使用fork,其后并不跟随exec。另外,将这两个操做分开,使得子进程在fork和exec之间能够更改本身的属性。例如I/O重定向、用户ID、信号安排等。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅做我的学习记录所用。关于本书可参考:http://www.apuebook.com/。