fock 的意思是复制进程, 就是把当前的程序再加载一次, 不同之处在,加载后,所有的状态和当前进程是一样的(包括变量)。 fock 不象线程需提供一个函数做为入口, fock后,新进程的入口就在 fock的下一条语句。
一个现存进程调用f o r k函数是U N I X内核创建一个新进程的唯一方法(这并不适用于前节提
及的交换进程、i n i t进程和页精灵进程。这些进程是由内核作为自举过程的一部分以特殊方式
创建的)。
#i nclude <sys/types.h>
#i nclude <unistd.h>
pid_t fork(void);
返回:子进程中为0,父进程中为子进程I D,出错为-1
由f o r k创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返
回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程 I D。将子进程I D返回
给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以
获得其所有子进程的进程I D。f o r k使子进程得到返回值0的理由是:一个进程只会有一个父进
程,所以子进程总是可以调用g e t p p i d以获得其父进程的进程I D (进程ID 0总是由交换进程使用,
所以一个子进程的进程I D不可能为0 )。
子进程和父进程继续执行f o r k之后的指令。子进程是父进程的复制品。例如,子进程获得
父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这
些存储空间部分。如果正文段是只读的,则父、子进程共享正文段(见7 . 6节)。
现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在 f o r k之后经常跟随着
e x e c。作为替代,使用了在写时复制( C o p y - O n - Write, COW)的技术。这些区域由父、子进程共
享,而且内核将它们的存取许可权改变为只读的。如果有进程试图修改这些区域,则内核为有
关部分,典型的是虚存系统中的“页”,做一个拷贝。B a c h〔1 9 8 6〕的9 . 2节和L e ff l e r等〔1 9 8 9〕
的5 . 7节对这种特征做了更详细的说明。
实例
程序8 - 1例示了f o r k函数。如果执行此程序则得到:
$ a . o u t
a write to stdout
before fork
pid = 430, glob = 7, var = 89 子进程的变量值改变了
pid = 429, glob = 6, var = 88 父进程的变量值没有改变
$ a.out > temp.out
$ cat temp.out
a write to stdout
before fork
pid = 432, glob = 7, var = 89
before fork
pid = 431, glob = 6, var = 88
一般来说,在f o r k之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的
调度算法。如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。在程序 8 - 1中,父进程使自己睡眠2秒钟,以此使子进程先执行。但并不保证 2秒钟已经足够,在8 . 8节说明竟
争条件时,还将谈及这一问题及其他类型的同步方法。在 1 0 . 6节中,在f o r k之后将用信号使父、子进程同步。
注意,程序8 - 1中f o r k与I / O函数之间的关系。回忆第3章中所述,w r i t e函数是不带缓存的。
因为在f o r k之前调用w r i t e,所以其数据写到标准输出一次。但是,标准 I / O库是带缓存的。回
忆一下5 . 1 2节,如果标准输出连到终端设备,则它是行缓存的,否则它是全缓存的。当以交互
方式运行该程序时,只得到p r i n t f输出的行一次,其原因是标准输出缓存由新行符刷新。但是
当将标准输出重新定向到一个文件时,却得到p r i n t f输出行两次。其原因是,在f o r k之前调用了
p r i n t f一次,但当调用f o r k时,该行数据仍在缓存中,然后在父进程数据空间复制到子进程中时,
该缓存数据也被复制到子进程中。于是那时父、子进程各自有了带该行内容的缓存。在 e x i t之
前的第二个p r i n t f将其数据添加到现存的缓存中。当每个进程终止时,其缓存中的内容被写到
相应文件中。
程序8-1 fork函数实例
文件共享
对程序8 - 1需注意的另一点是:在重新定向父进程的标准输出时,子进程的标准输出也被
重新定向。实际上,f o r k的一个特性是所有由父进程打开的描述符都被复制到子进程中。父、
子进程每个相同的打开描述符共享一个文件表项(见图3 - 3 )。
考虑下述情况,一个进程打开了三个不同文件,它们是:标准输入、标准输出和标准出错。
在从f o r k返回时,我们有了如图8 - 1中所示的安排。
这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。考虑下述情况:一
个进程f o r k了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父、子进程
都向标准输出执行写操作。如果父进程使其标准输出重新定向 (很可能是由s h e l l实现的),那么
子进程写到该标准输出时,它将更新与父进程共享的该文件的位移量。在我们所考虑的例子中,
当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出上,
并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共享同一文件位移量,这种
形式的交互就很难实现。
如果父、子进程写到同一描述符文件,但又没有任何形式的同步(例如使父进程等待子进
程),那么它们的输出就会相互混合(假定所用的描述符是在 f o r k之前打开的)。虽然这种情况
是可能发生的(见程序8 - 1),但这并不是常用的操作方式。
在f o r k之后处理文件描述符有两种常见的情况:
(1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进
程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量已做了相应更新。
(2) 父、子进程各自执行不同的程序段。在这种情况下,在f o r k之后,父、子进程各自关闭
它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中
经常使用的。
除了打开文件之外,很多父进程的其他性质也由子进程继承:
* 实际用户I D、实际组I D、有效用户I D、有效组I D。
* 添加组I D。
* 进程组I D。
* 对话期I D。
* 控制终端。
* 设置-用户- I D标志和设置-组- I D标志。
* 当前工作目录。
* 根目录。
* 文件方式创建屏蔽字。
* 信号屏蔽和排列。
* 对任一打开文件描述符的在执行时关闭标志。
* 环境。
* 连接的共享存储段。
* 资源限制。
父、子进程之间的区别是:
* fork的返回值。
* 进程I D。
* 不同的父进程I D。
* 子进程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e设置为0。
* 父进程设置的锁,子进程不继承。
* 子进程的未决告警被清除。
* 子进程的未决信号集设置为空集。
其中很多特性至今尚末讨论过,我们将在以后几章中对它们进行说明。
使f o r k失败的两个主要原因是:( a )系统中已经有了太多的进程(通常意味着某个方面出了问
题),或者( b )该实际用户I D的进程总数超过了系统限制。回忆表2 - 7,其中C H I L D _ M A X规定了
每个实际用户I D在任一时刻可具有的最大进程数。
f o r k有两种用法:
(1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程
中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用 f o r k,使子进
程处理此请求。父进程则继续等待下一个服务请求。
(2) 一个进程要执行一个不同的程序。这对 s h e l l是常见的情况。在这种情况下,子进程在
从f o r k返回后立即调用e x e c (我们将在8 . 9节说明e x e c )。
某些操作系统将( 2 )中的两个操作( f o r k之后执行e x e c )组合成一个,并称其为s p a w n。U N I X
将这两个操作分开,因为在很多场合需要单独使用 f o r k,其后并不跟随e x e c。另外,将这两个
操作分开,使得子进程在f o r k和e x e c之间可以更改自己的属性。例如I / O重新定向、用户I D、信
号排列等。在第1 4章中有很多这方面的例子。
自己试验得结论:
#i nclude <unistd.h>
#i nclude <stdio.h>
int main()
{
int pid;
int pid2;
//printf("%d ",getppid());
printf("=============/n");
printf("LLLLLLLLLLLLL");
if((pid=fork())==0){
printf("This is the child process:%d/n",pid);
}
else
{
if((pid2=fork())==0)
printf("This is another child process:%d/n ",pid2);
else
printf("This is the parent process:%d %d/n",pid,pid2);
}
printf("--------/n");
return 1;
}
输出结果为
=============
LLLLLLLLLLLLLThis is the child process:0
--------
LLLLLLLLLLLLLThis is another child process:0
--------
LLLLLLLLLLLLLThis is the parent process:22180 22181
--------
#i nclude <unistd.h>
#i nclude <stdio.h>
int main()
{
int pid;
int pid2;
//printf("%d ",getppid());
//printf("=============/n");
printf("LLLLLLLLLLLLL");
if((pid=fork())==0){
printf("This is the child process:%d/n",pid);
}
else
{
if((pid2=fork())==0)
printf("This is another child process:%d/n ",pid2);
else
printf("This is the parent process:%d %d/n",pid,pid2);
}
printf("--------/n");
return 1;
}
输出结果为:
LLLLLLLLLLLLLThis is the child process:0
--------
LLLLLLLLLLLLLThis is another child process:0
--------
LLLLLLLLLLLLLThis is the parent process:22212 22213
--------
原因是
子进程同时复制父进程的缓存内容 printf()进行行缓存 在exit 之后才将数据写回标准输出文件中
对fork函数的体会 进程的创建
创建一个进程的系统调用很简单.我们只要调用fork函数就可以了.
#i nclude <unistd.h>
pid_t fork();
当一个进程调用了fork以后,系统会创建一个子进程.这个子进程和父进程不同的地方只有他的进程ID和父进程ID,其他的都是一样.就象符进程克隆(clone)自己一样.当然创建两个一模一样的进程是没有意义的.为了区分父进程和子进程,我们必须跟踪fork的返回值. 当fork掉用失败的时候(内存不足或者是用户的最大进程数已到)fork返回-1,否则fork的返回值有重要的作用.对于父进程fork返回子进程的ID,而对于fork子进程返回0.我们就是根据这个返回值来区分父子进程的. 父进程为什么要创建子进程呢?前面我们已经说过了Linux是一个多用户操作系统,在同一时间会有许多的用户在争夺系统的资源.有时进程为了早一点完成任务就创建子进程来争夺资源. 一旦子进程被创建,父子进程一起从fork处继续执行,相互竞争系统的资源.有时候我们希望子进程继续执行,而父进程阻塞,直到子进程完成任务.这个时候我们可以调用wait或者waitpid系统调用