一个现有的进程可以调用 fork 函数创建一个新进程。
#include <uinstd.h>
pid_t fork(void);
// 返回值:
子进程返回 0;
父进程返回子进程 id;
若出错,返回 -1
由 fork 创建的新进程被称为子进程(child process)。fork 函数被调用一次,但返回两次。两次返回的区别在于,子进程的返回值是0,二父进程的返回值是新创建的进程的 ID。
将子进程的 ID 返回给父进程的理由是:因为一个进程的子进程可以有多个,且没有一个函数使一个进程可以获得其所有子进程的进程 ID。
fork 使子进程返回值 0 的理由是:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 来获取其父进程的进程 ID(进程 ID 0总是由内核交换进程使用,所以一个子进程的进程 ID 不可能为0).
子进程和父进程(均)继续执行 fork 调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程的:
- 数据空间(数据段)
- 堆和栈的副本
注意,这些是子进程自己所拥有的副本,不是共享关系。子进程和父进程共享正文段(text)。
由于在 fork 之后经常跟着 exec(表示执行),所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全副本(有点缓执行 lazy evaluation 的意思)。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程的任何一个试图这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
一个实例
#include "apue.h"
char buf[] = "a write to stdout!";
int globvar = 6;
int main(void){
int var = 0;
pid_t pid;
if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
err_sys("write error!");
if ((pid = fork()) < 0){
err_sys("fork errror!");
} else if (pid == 0) {
++globvar;
++var;
} else {
sleep(2);
}
printf("pid = %ld, globvar = %d, var = %d\n", (long)getpid(), globvar, var);
return 0;
}
输出为:
a write to stdout!
before fork
pid = 9344, glob = 7, var = 1
// 子进程的变量值改变了
pid = 9343, glob = 6, var = 0
// 父进程的变量值未发生改变
一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。上述程序中,父进程使自己休眠 2s,以此使子进程先执行。但并不保证 2s 已经足够。