fork函数
以下内容节选自《Unix环境高级编程》 8.3
一个现有进程可以调用fork函数创建一个新进程。
由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但是返回两次。两次返回的唯一区别是子进程的返回值为0,而父进程的返回值则是新子进程的进程ID(注:可以理解为每个进程都是返回子进程ID,由于新子进程无子进程,所以返回值为0)。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数可以使一个进程获得所有子进程ID。
子进程和父进程继续执行fork调用之后的指令。子进程时父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这子进程所拥有的副本。父子进程不共享这些存储空间部分。父、子进程共享正文段。(关于正文段的讲解,参看博文《C程序的存储空间布局》)。
由于在fork之后经常跟随者exec,所以现在很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域有父子进程共享,而却内核将他们的访问权限改变为只读。如果父、子进程中的人一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一”页“。
某些平台提供firk函数的几种变体。比如linux 2.4.22提供了另一种新进程的创建函数——clone(2)系统调用。这是一种fork的泛型,它允许调用者控制哪些部分由父、子进程共享。
实例:演示fork函数,从中可以看到子进程对变量所作的改变并不影响父进程中该变量的值。
#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(bug)-1 != sizeof(buf) - 1)
err_sys("write error");
printf("before fork\n"); /* we dont flush stdout */
if((pid = fork()) < 0) {
err_sys("fork error");
} else if ( pid == 0) { /* child */
glob++; /* modify variables*/
} else {
sleep(2); /* parent */
}
print("pid = %d, glob = %d, var = %d\n, getpid(), glob, var");
exit(0);
}
如此执行此程序则得到:
$ ./a.out
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
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。取决于内核所使用的调度算法。如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。在上面的代码中,父进程使自己休眠2s,以使子进程先执行,但并不保证2s已经足够。
当写到标准输出时,我们将buf长度减去1作为输出字节数,这是为了避免将终止null字节写出。strlen计算不包含种终止null字节的字符串长度,而siezeof则计算包含终止null字节的缓冲区长度。两者直接的另一个差别是,使用strlen需要进行一次函数调用,而对于sizeof而言,因为缓冲区已用已知字符串进行了初始化,其长度是固定的,所以sizeof在编译时计算缓冲区长度。