UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习
在“进程控制三部曲”中,我们学习到了 fork 是三部曲的第一部,用于创建一个新进程。但是关于 fork 的更深入的一些的东西我们还没有涉及到,例如,fork 创建的新进程与调用进程之间的关系、父子进程的数据共享问题等。fork 是否可以无限制的调用?如果不行的话,最大限制是多少?另外,我们还将学习一个 fork 的变体 vfork。 1 fork 创建的新进程与调用进程之间的关系UNIX 操作系统中的所有进程之间的关系呈现一个树形结构。除了进程 ID 为 0(swapper 进程)和 1(init 进程)的进程之外的其他进程,都会存在一个父进程。 fork 函数调用产生的新进程的父进程默认即为调用进程。fork 函数调用产生的父子进程各自的运行时间是不确定的。如果子进程先于父进程终止,这样没有什么问题。但,如果父进程先于子进程终止,那么子进程是不是就没有了父进程,进程树形结构就被破坏了?对于这个问题,UNIX 系统这么处理的:如果某个进程终止了,则将该进程的所有尚未结束的子进程的父进程设置为 init 进程(init 进程是绝不会终止的)。其操作过程大致为:在一个进程终止时,内核逐个检查所有活动进程(因为 UNIX 没有提供一个获取某个进程所有子进程的接口),如果是正在终止的进程的子进程,则将其父进程设置为 init 进程。 2 父子进程的数据共享问题fork 函数创建的子进程会获得父进程的数据空间、堆和栈的副本。但是,大多数情况下,fork 之后都会紧接着调用 exec 执行新程序,从而覆盖了从父进程拷贝的这些副本,这就造成了内核做了很多无用功。 现在很多的实现都采用写时复制(Copy-On-Write,COW)技术。fork函数调用之后,父子进程共享这些区域,而且内核将这些区域的权限改为只读的。如果父、子进程中任何一个试图修改这些区域,则内核只为要修改的区域做一份拷贝给该进程。 下面我们来看一个共享数据的例子, #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> int glob = 0; int main(void) { int var; pid_t pid; var = 0; if ((pid = fork()) < 0) { printf("fork error: %sn",strerror(errno)); exit(-1); } else if (pid == 0) { var++; glob++; printf(child: glob=%d,var=%dn",glob,var); exit(0); } wait(NULL); printf(parent: glob=%d,255); line-height:1.5!important">var); exit(0); } 该程序在 fork 之后的父进程等待子进程结束,而子进程将整型变量glob 和 var 都加了 1. 编译该程序,生成并执行 forkdemo. 从下面的运行结果,我们看到子进程修改的 glob 和 var 变量对父进程没有任何影响。 lienhua34:demo$ gcc -o forkdemo forkdemo.c lienhua34:demo$ ./forkdemo child: glob=1,var=1 parent: glob=0,128); line-height:1.5!important">0 虽说子进程享用的是父进程的数据副本,子进程的修改对父进程没有任何影响。但有个比较特殊的情况:文件 I/O。fork 会将父进程的所有打开文件描述符都复制到子进程。父子进程中相同的文件描述符则共享同一个文件表项(关于文件描述符和文件表项的关系请参考文档“内核 I/O 数据结构”)。下面我们看一个例子,255); line-height:1.5!important">void) { pid_t pid; printf(before forkn"); in child processn"); exit(in parent processn"); exit( 编译该程序,生成并执行文件 forkdemo,209); border:none!important"> lienhua34:demo$ gcc -o forkdemo forkdemo.c lienhua34:demo$ ./forkdemo before fork in child process in parent process lienhua34:demo$ ./forkdemo > foo lienhua34:demo$ cat foo before fork in child process before fork in parent process 4 fork 函数调用次数的最大限制是多少 每个实际用户 ID 具有一个在任何时刻的最大进程数。CHILD_MAX 规定了每个实际用户 ID 在任一时刻可具有的最大进程数。我们看下面一个例子,255); line-height:1.5!important">void)
{
pid_t pid;
int count;
printf(CHILD_MAX: %ldn1;
for (;;) {
0) {
printf(break;
} 0) {
sleep(3);
exit(0);
}
count++;
}
printf(count: %dn
从上面的运行结果可以看出我的系统规定了每个实际用户 ID 在任一时刻可具有的最大进程数为 15969。而在 for 循环中 fork 创建了 15737 个进程(包括调用进程本身)之后,fork 就因为没有可用资源而创建新进程失败。 vfork 函数是 fork 函数的一个变体,其调用序列和返回值与 fork 函数一致,不过两者的语义不同。维基百科上关于 vfork 的说明如下(参考fork(system_call))。 Vfork is a variant of fork with the same calling convention and much the same semantics; it originated in the 3BSD version of Unix,[citation needed] the first Unix to support virtual memory. It was standardized by POSIX,which permitted vfork to have exactly the same behavior as fork,but marked obsolescent in the 2004 edition,[4] and has disappeared from subsequent editions. 我们看到在 POSIX 2004 版本中已经将 vfork 函数注为过时的,而且在之后的版本中已经不再出现 vfork 函数了。但是,既然《APUE》中讲到了这个,那我们就来看一下 vfork 函数跟 fork 函数到底有什么区别吧。 vfork 函数和 fork 函数的区别有两点: 1. fork 会将父进程的地址空间拷贝给子进程;而 vfork 没有,子进程在父进程的地址空间中运行。 2. fork 无法确保父子进程的执行顺序;而 vfork 保证子进程先执行,父进程会一直阻塞直到子进程调用 exit 或 exec。(注:vfork 的这个特征可能会导致死锁,若子进程在调用 exit 或 exec 之前依赖于父进程的进一步动作,而父进程也正在等待子进程,于是出现了循环等待的问题。) 我们来对比一下 vfork 和 fork 在处理数据方面有什么不同,255); line-height:1.5!important">if 从上面的运行结果,我们看到 vfork 创建的子进程修改了 glob 和 var变量之后,父进程也看到了这个修改。 vfork 函数的出现原因可能是早期系统的 fork 没有实现写时复制技术,导致每次 fork 调用做了很多无用功(大多数情况下都是 fork 之后调用 exec执行新程序)且效率不高,于是便创造了 vfork 函数。而现在的实现基本都是采用写时复制技术,而且 vfork 函数使用不当还会出现死锁,于是 vfork函数也便没有了存在的必要性。 (done) (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |