全部博文(842)
分类: 系统运维
2012-05-14 16:45:56
一个存在的进程可以调用fork函数来创建一个新的进程
由fork函数创建的新进程被称为子进程。函数被调用一次却被返回两次。返回的唯一的区别是子进程的返回值是0,而父进程的返回值是新的子进程的ID。子 进程ID被返回给父进程的原因是一个进程可以有不只一个子进程,而没有函数允许一个进程获得它的子进程的ID。fork在子进程里返回0的原因是一个进程 只能有一个父进程,而子进程总是可以调用getppid来得到它的父进程ID。(进程ID 0被内核预留使用,所以它不可能是一个子进程的进程ID。)
子进程和父进程都持续执行fork调用之后的指令。子进程是父进程的一个复制品。例如,子进程得到父进程数据空间,堆和栈的拷贝。注意这对一个子进程来说是一个拷贝。父进程和子进程没有共享内存。父进程和子进程共享代码段。
当前实现不执行一个父进程数据、栈和堆的完全拷贝,因为fork后通常接着一个exec。相反,一个称为拷贝时写(copy-on-write,
COW)被使用。这些区域被父进程和子进程共享,而且它们的保护机制由内核改变为只读。如果任何一个进程尝试修改这些区域,内核就只拷贝这块内存,一般是
虚拟内存系统的一个页。
fork函数的变种由一些平台提供。本文讨论的4个平台都支持下节讨论的vfork的变体。
Linux2.4.22还通过clone系统调用支持新进程的创建。这上fork的衍生形式,允许调用者控制在父进程和子进程之间什么是共享的。
FreeBSD 5.2.1提供了rfork系统调用,它和Linux的clone系统调用相似。rfork调用由Plan 9操作系统继承。
Solaris
9提供了两个线程库:一个为了POSIX线程(pthreads),一个为了Solaris线程。fork的行为在这个两个库之间有所不同。对于
POSIX线程,fork创建一个只包含调用线程的进程,而对于Solaris线程,fork创建一个包含调用线程所在的进程所里所有线程的拷贝的进程。
为了提供和POSIX线程相似的语义,Solaris提供了fork1函数,它可以用来创建一个只复制调用线程的进程,而无视使用的线程库。线程在第11
章和第12章深入讨论。
下面的代码演示了fork函数的使用,展示了一个子进程里的变量的改变是如何不影响父进程里的变量的值的:
当我们向标准输出写入里,我们把buf的尺寸减一来避免写入终止的空字节。尽管strlen在计算字符串的长度时不包括这个终止的空字节,然而
sizeof计算缓冲的尺寸时会包括这个终止的空字节。另一个区别是使用strlen需要一个函数调用,而sizeof在编译期计算缓冲的长度,因为缓冲
由一个已知的字符串初始化,而它的尺寸是固定的。
注意上面代码里fork和I/O函数的交互。回想下第三章,write函数是没有缓冲的。因为write在fork之前调用,所以它的数据只写入标准输出
一次。然而,标准I/O函数是缓冲的。回想下5.12节,如果标准输出与一个终端设备连接,那么它是行缓冲的,不然,它是完全缓冲的。当我们交互式地运行
这个程序时,我们只得到prinf行的一份拷贝。在第二种情况,fork之前的printf被调用了一次,但是当fork被调用时这行仍保留在缓冲里。然
后这个缓冲在父进程的数据空间拷贝到子进程时,被拷贝到子进程里。父进程和子进程现在都有一个包含这行的缓冲。在exit之前的第二个printf,把它
的数据添加到已有的缓冲里。当进程终止时,它的缓冲的拷贝最终被冲洗了。
文件共享(File Sharing)
的前面的代码里,当我们重定向父进程的标准输出的时候,子进程的标准输出也被重定向了。事实上,fork的一个特性是所有被父进程打开的文件描述符都被复
制到子进程。我们说“复制”因为每个描述上都好像调用了dup函数。父进程和子进程为每个打开的描述符都共享一个文件表项。
考虑一个进程有三个不同的打开的文件,对应标准输入、标准输出和标准错误。当从fork返回时,两个进程的三个描述符会指向同样的文件表项。表项里有文件状态标志,当前文件偏移量和v-node指针。
父进程和子进程共享同样的文件偏移量是很重要的。考虑一个fork一个子进程的进程,它然后wait子进程的结束。假定两个进程都向标准输出写入,作为它
们的普通处理。如果父进程把它的标准输出重定向(通过外壳,也许),则当子进程写入到标准输出时,更新父进程的文件偏移量是必要的。在这种情况下,当父进
程在用wait等待它时,子进程可以向标准输出写入;当子进程完成时,父进程可以继续向标准输出写入,并知道它的输出会添加到子进程写的任何东西的后面。
如果父进程和子进程没有共享同样的文件偏移,这类交互将会更难完成而且需要父进程显式的行动。
如果父进程和子进程都向同一个描述符写入,在没有任何形式的同步下,比如让父进程wait子进程,它们的输出会混在一起(假设描述符在fork之前被打开。)尽管这是可能的,但它不是操作的普通模式。
在fork之后有两种处理描述符的普通情况:
1、父进程等待子进程完成。在这种情况下,父进程不用对它的描述符做任何事情。当子进程终止时,子进程写过或读过的任何共享的描述符都有相应地更新它们的偏移量。
2、父进程和子进程独立工作。这里,在fork之后,父进程关闭它不需要的描述符,而子进程做同样的事。这样,两者都不干涉另一个打开的描述符。这种情景通常是网络服务的情况。
除了打开的文件,父进程还有很多其它属性被子进程继承
1、真实用户ID、真实组ID、有效用户ID、有效组ID
2、补充组ID
3、进程组ID
4、会话ID
5、控制终端
6、设置用户ID和设置组ID
7、当前工作目录
8、根目录
9、文件模式创建掩码
10、信号掩码和配置
11、任何打开文件描述符的执行时关闭标志
12、环境
13、附加共享内存段
14、内存映射
15、资源限制
父进程和子进程间的区别有
1、fork的返回值
2、进程ID
3、父进程ID
4、子进程的tms_utime, tms_stime, tms_cutim和tms_cstime值被设为0
5、父进程设置的文件锁不被继承
6、子进程的pending alarm被清除
7、子进程的pending的信号集被设为空集
这些特性中有许多还没讨论过--我们会在随后各章讨论它们。
fork会失败的两个主要原因是a、系统已经有太多进程,这通常意味着有些其它错误;b、如果真实用户ID的进程总数超过了系统限制。回想下第二章CHILD_MAX指定了每个真实用户ID的最大进程数。
有两种fork的使用:
1、当一个进程想复制它自己以便父进程和子进程可以同一时间执行不同的代码段。这在网络服务很普遍--父进程等待从客户端的请求。当请求到达时,父进程调用fork并让子进程处理这个请求。父进程回去等待下一个服务请求的到达。
2、当一个进程想执行一个不同的程序时。这对shell很普遍。在这种情况下,子进程在从fork返回后执行一个exec(我们在8.10节讨论)。
一些操作系统合并第2步的操作--在fork后执行exec--成一个称为spawn的单个操作。UNIX系统把它们分开,因为有许多情况使用一个没有
exec的fork会很有用。还有,分开这两个操作允许子进程在fork和exec之前来改变进程的属性,比如I/O重定向,用户ID,信号配置,等等。
我们将在第15章看到很多例子。
SUS确实在高级实时选项组包含了spawn接口。但这些接口不被作为fork和exec的替代品。它们用来支持高效实现fork比较困难的系统,尤其是没有内存管理的硬件支持的系统。