一个现有进程可以调用 fork 函数来创建一个新进程:
#include // fork函数定义在该头文件
pid_t fork(void);
// fork函数被调用1次,返回2次
// 子进程 : 返回0
// 父进程 : 返回子进程ID
// 出错 : 返回-1
fork 函数有如下特点:
(1)fork 函数被调用1次,返回2次,2次返回的区别是:子进程返回0,父进程返回子进程的ID;
(2)子进程和父进程继续执行fork调用后的指令。子进程是父进程的副本,它获得了父进程 数据空间、堆和栈的副本。注意,这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部分,它们共享的是正文段。
(3)由于 fork 后经常跟随着 exec,所以很多实现并不执行一个父进程数据段、栈和堆的完全副本。作为替代,使用 写时复制 技术。这些区域由父进程和子进程 共享,而且内核将它们的访问权限设为 只读。如果父进程或子进程中的一个试图修改这些区域,则内核 只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的“一页”。
fork函数使用的一个例子:
#include
#include
#include
int var1 = 10;
char buf[] = "I am writting ! \n";
int main() {
pid_t pid;
int var2 = 20;
// write函数是不带缓冲的
// sizeof(buf)-1 是为了防止将终止符写入
if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1) {
fprintf(stderr, "write error !\n");
exit(-1);
}
printf("before fork\n");
if((pid = fork()) < 0) {
perror("fork error");
} else if(pid == 0) {
var1++;
var2++;
} else {
sleep(2);
}
printf("pid = %ld, var1 = %d, var2 = %d\n", long(pid), var1, var2);
exit(0);
}
运行结果:
$ ./a.out
I am writting !
before fork
pid = 0, var1 = 11, var2 = 21
pid = 136, var1 = 10, var2 = 20 // 父进程的变量没有被改动
$ ./a.out > b.out
$ cat b.out
I am writting !
before fork
pid = 0, var1 = 11, var2 = 21
before fork
pid = 138, var1 = 10, var2 = 20 // 父进程的变量没有被改动
【代码分析】
(1) write 函数
#include
ssize_t write(int fd, const void *buf, size_t nbytes);
// 从buf中读取 nbytes 个字节到文件 fd 中
// 若成功,返回已写的字节数
// 若失败,返回-1
write 函数是不带缓冲的,上例在fork之前调用了该函数,因此其数据写道标准输出一次;
(2)文件描述符
#include // 文件描述符定义在该头文件中
幻数0 : STDIN_FILENO // 标准输入
幻数1 : STDOUT_FILENO // 标准输出
幻数2 : STDERR_FILENO // 标准出错
(3)printf 是标准 I/O 库,标准 I/O 库是带缓冲的,并且:
A. 如果标准输出连接着 终端设备,则它是 行缓冲 的;
B. 其它情况下,它是 全缓冲 的。
当以 交互方式 运行该程序时(即第1次运行),标准输出缓冲区被换行符冲洗,因此只得到 printf 输出的行一次;
当 将标准输出重定向到一个文件 时(即第2次运行),在fork之前调用了一次 printf,但当调用 fork 时,由于此时是全缓冲,因此这些数据仍在缓冲区内,然后在将父进程数据空间复制到子进程中时,该缓冲区的数据也被复制到了子进程中,此时父进程和子进程各自有了带该行内容的缓冲区。则 exit 之前的第二个 printf 会 将其数据追加到已有缓冲区中。当进程终止时,其缓冲区中的内容被写到相应的文件中。
(4)父进程和子进程每个相同的打开描述符共享一个文件表项,因此,在重定向了父进程的标准输出时,子进程的标准输出也被重定向。
vfork 函数的 调用序列和返回值 与fork相同,但 二者的语义不同:
(1)vfork 函数用于创建一个新进程,该新进程的目的是 exec 一个新程序
vfork 和 fork 一样都创建一个子进程,但是它 并不将父进程的地址空间完全复制到子进程中,因为子进程会 **立即调用 exec (或 exit)**而不会引用该地址空间。要注意的是,子进程在调用 exec 或 exit 之前,它在父进程的空间运行,因此,如果子进程 修改数据(除了用于存放 vfork 返回值的变量)、进行函数调用、或者没有调用 exit 或 exec 就返回,则都可能带来未知的后果。
(2)vfork 保证子进程先行
vfork保证 子进程先行。在子进程调用 exec 或 exit 之后,父进程才可能被调度运行;当子进程调用这两个函数中的一个时,父进程会恢复运行(如果在调用这两个函数前,子进程依赖于父进程的进一步动作,则会导致 死锁)。
函数 vfork 的使用例子:
#include
#include
#include
int var1 = 10;
int main() {
pid_t pid;
int var2 = 20;
printf("before fork\n");
if((pid = vfork()) < 0) {
perror("fork error");
} else if(pid == 0) {
var1++;
var2++;
_exit(0); // _exit不执行冲洗操作
}
printf("pid = %ld, var1 = %d, var2 = %d\n", long(pid), var1, var2);
exit(0); // exit执行冲洗操作
}
执行结果:
$ ./a.out
before fork
pid = 148, var1 = 11, var2 = 21 // 父进程的变量被改了
【代码分析】
(1)因为子进程在父进程的地址空间中运行,因此子进程对变量的操作,会改变父进程中的变量值;
(2)子进程调用的是 _exit 而不是 exit。exit 会实现冲洗标准 I/O 流,因此如果子进程调用的是 exit,那么会出 现两种情况:
A. 如果 函数库采取的唯一操作就是冲洗 I/O 流,那么此时得到的输出结果与子进程调用 _exit 时得到的输出结果是一样的;
B. 如果 该实现也关闭了 I/O 流,那么表示标准输出 FILE 对象的相关存储区将被清0.因为子进程借用了父进程的地址空间,因此当父进程恢复运行并调用 printf 的时候,printf 会返回-1,即不会产生任何输出结果。