lienhua34
2014-10-07

在“进程控制三部曲”中,我们学习到了 fork 是三部曲的第一部,用于创建一个新进程。但是关于 fork 的更深入的一些的东西我们还没有涉及到,例如,fork 创建的新进程与调用进程之间的关系、父子进程的数据共享问题等。fork 是否可以无限制的调用?如果不行的话,最大限制是多少?另外,我们还将学习一个 fork 的变体 vfork。

1 fork 创建的新进程与调用进程之间的关系

UNIX 操作系统中的所有进程之间的关系呈现一个树形结构。除了进程 ID 为 0(swapper 进程)和 1(init 进程)的进程之外的其他进程,都会存在一个父进程。

fork 函数调用产生的新进程的父进程默认即为调用进程。fork 函数调用产生的父子进程各自的运行时间是不确定的。如果子进程先于父进程终止,这样没有什么问题。但,如果父进程先于子进程终止,那么子进程是不是就没有了父进程,进程树形结构就被破坏了?对于这个问题,UNIX 系统这么处理的:如果某个进程终止了,则将该进程的所有尚未结束的子进程的父进程设置为 init 进程(init 进程是绝不会终止的)。其操作过程大致为:在一个进程终止时,内核逐个检查所有活动进程(因为 UNIX 没有提供一个获取某个进程所有子进程的接口),如果是正在终止的进程的子进程,则将其父进程设置为 init 进程。

2 父子进程的数据共享问题

fork 函数创建的子进程会获得父进程的数据空间、堆和栈的副本。但是,大多数情况下,fork 之后都会紧接着调用 exec 执行新程序,从而覆盖了从父进程拷贝的这些副本,这就造成了内核做了很多无用功。

现在很多的实现都采用写时复制(Copy-On-Write,COW)技术。fork函数调用之后,父子进程共享这些区域,而且内核将这些区域的权限改为只读的。如果父、子进程中任何一个试图修改这些区域,则内核只为要修改的区域做一份拷贝给该进程。

下面我们来看一个共享数据的例子,

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int glob = ;
int
main(void)
{
int var;
pid_t pid;
var = ;
if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
var++;
glob++;
printf("child: glob=%d, var=%d\n", glob, var);
exit();
}
wait(NULL);
printf("parent: glob=%d, var=%d\n", glob, var);
exit();
}

该程序在 fork 之后的父进程等待子进程结束,而子进程将整型变量glob 和 var 都加了 1. 编译该程序,生成并执行 forkdemo. 从下面的运行结果,我们看到子进程修改的 glob 和 var 变量对父进程没有任何影响。

lienhua34:demo$ gcc -o forkdemo forkdemo.c
lienhua34:demo$ ./forkdemo
child: glob=, var=
parent: glob=, var=

虽说子进程享用的是父进程的数据副本,子进程的修改对父进程没有任何影响。但有个比较特殊的情况:文件 I/O。fork 会将父进程的所有打开文件描述符都复制到子进程。父子进程中相同的文件描述符则共享同一个文件表项(关于文件描述符和文件表项的关系请参考文档“内核 I/O 数据结构”)。下面我们看一个例子,

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int
main(void)
{
pid_t pid;
printf("before fork\n");
if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
printf("in child process\n");
exit();
}
wait(NULL);
printf("in parent process\n");
exit();
}

编译该程序,生成并执行文件 forkdemo,

lienhua34:demo$ gcc -o forkdemo forkdemo.c
lienhua34:demo$ ./forkdemo
before fork
in child process
in parent process
lienhua34:demo$ ./forkdemo > foo
lienhua34:demo$ cat foo
before fork
in child process
before fork
in parent process

在没有对标准输出重定向之前,运行 forkdemo 看不出啥问题。当重定向标准输出到一个文件(./forkdemo > foo)时,我们可以看到父进程打印的字符串在子进程打印的字符串之后。这是因为父子进程标准输出共享了同一个文件表项,也即共享了同一个文件偏移量。

另外,我们注意到在标准输出没有重定向时,字符串“before fork”只输出一次,但是在标准输出重定向到文件之后输出了两次。这是因为标准I/O 库函数 printf 在标准输出连接到终端设备时是行缓冲的,于是在 fork函数之后,缓冲区中的数据已经被冲洗了。而当标准输出重定向文件之后,printf 函数就变成了全缓冲了,在 fork 之前调用 printf 函数将字符串“before fork”写到缓冲区中,fork 时该字符串还在缓冲区中,于是便拷贝一份给子进程。当父子进程都调用 exit 函数之后,缓冲区中的数据都被冲洗到文件中,于是被出现了两份“before fork”。

3 fork 典型应用场景

fork 有两种典型的应用场景:

• 创建一个新进程执行新的程序。即调用 fork 之后子进程立即调用 exec函数执行一个新程序,例如文档“进程控制三部曲”中的示例 2.

• 父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中比较常见:父进程等待客户端的服务请求,当接收到一个请求之后,父进程调用 fork,然后让子进程处理该请求,而父进程继续等待下一个服务请求。其代码框架如下所示:

void serve(int sockfd)
{
int clfd;
pid_t pid;
for (;;) {
clfd = accept(sockfd, NULL, NULL);
if (clfd < ) {
/* print error message */
continue;
}
if ((pid = fork()) < ) {
/* fork error */
continue;
} else if (pid == ) {
/* deal with clfd in child process */
close(clfd);
exit();
} else {
/* in parent process,
close the accepted socket "clfd",
then continues to listen next socket connection. */
}
}
}

4 fork 函数调用次数的最大限制是多少

每个实际用户 ID 具有一个在任何时刻的最大进程数。CHILD_MAX 规定了每个实际用户 ID 在任一时刻可具有的最大进程数。我们看下面一个例子,

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int
main(void)
{
pid_t pid;
int count;
printf("CHILD_MAX: %ld\n", sysconf(_SC_CHILD_MAX));
count = ;
for (;;) {
if ((pid = fork()) < ) {
printf("fork error: %s\n", strerror(errno));
break;
} else if (pid == ) {
sleep();
exit();
}
count++;
}
printf("count: %d\n", count);
exit();
}

编译该程序,生成并运行文件 forkdemo,

lienhua34:demo$ gcc -o forkdemo forkdemo.c
lienhua34:demo$ ./forkdemo
CHILD_MAX: 15969
fork error: Resource temporarily unavailable
count: 15737

从上面的运行结果可以看出我的系统规定了每个实际用户 ID 在任一时刻可具有的最大进程数为 15969。而在 for 循环中 fork 创建了 15737 个进程(包括调用进程本身)之后,fork 就因为没有可用资源而创建新进程失败。

5 fork 的变体vfork

vfork 函数是 fork 函数的一个变体,其调用序列和返回值与 fork 函数一致,不过两者的语义不同。维基百科上关于 vfork 的说明如下(参考fork(system_call))。

Vfork is a variant of fork with the same calling convention and much the same semantics; it originated in the 3BSD version of Unix,[citation needed] the first Unix to support virtual memory. It was standardized by POSIX, which permitted vfork to have exactly the same behavior as fork, but marked obsolescent in the 2004 edition,[4] and has disappeared from subsequent editions.

我们看到在 POSIX 2004 版本中已经将 vfork 函数注为过时的,而且在之后的版本中已经不再出现 vfork 函数了。但是,既然《APUE》中讲到了这个,那我们就来看一下 vfork 函数跟 fork 函数到底有什么区别吧。

vfork 函数和 fork 函数的区别有两点:

1. fork 会将父进程的地址空间拷贝给子进程;而 vfork 没有,子进程在父进程的地址空间中运行。

2. fork 无法确保父子进程的执行顺序;而 vfork 保证子进程先执行,父进程会一直阻塞直到子进程调用 exit 或 exec。(注:vfork 的这个特征可能会导致死锁,若子进程在调用 exit 或 exec 之前依赖于父进程的进一步动作,而父进程也正在等待子进程,于是出现了循环等待的问题。)

我们来对比一下 vfork 和 fork 在处理数据方面有什么不同,

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int glob = ;
int
main(void)
{
int var;
pid_t pid;
var = ;
if ((pid = vfork()) < ) {
printf("fork error: %s\n", strerror(errno));
exit(-);
} else if (pid == ) {
var++;
glob++;
printf("child: glob=%d, var=%d\n", glob, var);
exit();
}
printf("parent: glob=%d, var=%d\n", glob, var);
exit();
}

上面程序拷贝了上面 fork 函数处理共享数据的示例程序,将 fork 改成vfork,并且去掉了 wait(NULL) 语句。保存为 vforkdemo.c,编译该程序,生成并执行 vforkdemo 文件,

lienhua34:demo$ gcc -o vforkdemo vforkdemo.c
lienhua34:demo$ ./vforkdemo
child: glob=1, var=1
parent: glob=1, var=1

从上面的运行结果,我们看到 vfork 创建的子进程修改了 glob 和 var变量之后,父进程也看到了这个修改。

vfork 函数的出现原因可能是早期系统的 fork 没有实现写时复制技术,导致每次 fork 调用做了很多无用功(大多数情况下都是 fork 之后调用 exec执行新程序)且效率不高,于是便创造了 vfork 函数。而现在的实现基本都是采用写时复制技术,而且 vfork 函数使用不当还会出现死锁,于是 vfork函数也便没有了存在的必要性。

(done)

UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习的更多相关文章

  1. Linux学习笔记(六) 进程管理

    1.进程基础 当输入一个命令时,shell 会同时启动一个进程,这种任务与进程分离的方式是 Linux 系统上重要的概念 每个执行的任务都称为进程,在每个进程启动时,系统都会给它指定一个唯一的 ID, ...

  2. Linux内核学习笔记-2.进程管理

    原创文章,转载请注明:Linux内核学习笔记-2.进程管理) By Lucio.Yang 部分内容来自:Linux Kernel Development(Third Edition),Robert L ...

  3. UNIX环境高级编程学习笔记(十)为何 fork 函数会有两个不同的返回值【转】

    转自:http://blog.csdn.net/fool_duck/article/details/46917377 以下是基于 linux 0.11 内核的说明. 在init/main.c第138行 ...

  4. Linux System Programming 学习笔记(五) 进程管理

    1. 进程是unix系统中两个最重要的基础抽象之一(另一个是文件) A process is a running program A thread is the unit of activity in ...

  5. 进程管理之fork函数

    fork函数的定义 #include <unistd.h> #include <sys/types.h> pid_t fork(void); fork函数在父进程中返回子进程的 ...

  6. node.js在windows下的学习笔记(8)---进程管理Process

    process是一个全局内置对象,可以在代码中的任何位置访问此对象,这个对象代表我们的node.js代码宿主的操作系统进程对象. 使用process对象可以截获进程的异常.退出等事件,也可以获取进程的 ...

  7. UNIX基础知识--<<UNIX 环境编程>>读书笔记

    1 shell程序就是位于应用软件与系统调用之间的程序   每个用户登录系统,系统就会为用户分配shell (用户的登录的口令文件 在  /etc/passwd 2 ls filename  运行原理 ...

  8. Linux -- 进程管理之 fork() 函数

    一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间.然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同.相当于克隆了一个自己. Test1 f ...

  9. Linux学习笔记(五) 账号管理

    1.用户与组账号 用户账号:包括实际人员和逻辑性对象(例如应用程序执行特定工作的账号) 每一个用户账号包含一个唯一的用户 ID 和组 ID 标准用户是系统安装过程中自动创建的用户账号,其中除 root ...

随机推荐

  1. java中编码种类和区别

    为什么要编码 不知道大家有没有想过一个问题,那就是为什么要编码?我们能不能不编码?要回答这个问题必须要回到计算机是如何表示我们人类能够理解的符号的,这些符号也就是我们人类使用的语言.由于人类的语言有太 ...

  2. JAVA-JSP内置对象之session对象

    相关资料:<21天学通Java Web开发> session对象 1.session对象用来表示用户的会话状况,一般用于保存用户的各种信息.2.直到生命周期超时或者被认为释放掉为止. 方法 ...

  3. python parse xml using DOM

    demo: import xml.dom.minidom dom=xml.dom.minidom.parse('sample.xml')root = dom.documentElementcc=dom ...

  4. 大数据 Hive 简介

    第一部分:Hive简介 什么是Hive •Hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张数据库表,并提供类SQL查询功能. •本质是将SQL转换为MapReduce程序 ...

  5. spring @Bean注解解释

    解释:java config配置一个重要注解 @Bean明确地指示了一种方法,什么方法呢——产生一个bean的方法,并且交给Spring容器管理:从这   我们就明白了为啥@Bean是放在方法的注释上 ...

  6. OAuth 授权过程工作原理讲解

    转自:http://www.imooc.com/article/10931 在一个单位中,可能是存在多个不同的应用,比如学校会有财务的系统会有学生工作的系统,还有图书馆的系统等等,如果每个系统都用独立 ...

  7. C语言 · 3000米排名预测

    算法提高 3000米排名预测   时间限制:1.0s   内存限制:256.0MB      问题描述 3000米长跑时,围观党们兴高采烈地预测着最后的排名.因为他们来自不同的班,对所有运动员不一定都 ...

  8. [开发笔记]-实现winform半透明毛玻璃效果

    亲测win7下可用,win8下由于系统不支持Aero效果,所以效果不是半透明的. 代码: 博客园插入不了代码了..... public partial class Form1 : Form { int ...

  9. 【进阶修炼】——改善C#程序质量(2)

    16, 元素可变的情况下应避免用数组. 数组是定长的集合,可以考虑用ArrayList或List<T>集合.ArrayList元素是object类型,有装箱的开销,性能较低.另外Array ...

  10. Android训练课程(Android Training) - NFC基础

    NFC 基础 本文档介绍了在Android上的基本的NFC任务.它说明了如何发送和接收的NDEF消息(NDEF messages)的形式的表单里包含的NFC数据(NFC data),并介绍Androi ...