1、概述
系统调用fork允许一个进程(父进程)创建一个新进程(子进程)。通过fork,子进程几乎是父进程的复制版本,子进程获得父进程的栈、数据段、堆和执行文本段的拷贝。通常,调用fork产生子进程后,子进程随便会调用execve函数簇执行新的任务,随后执行exit相关函数退出。而父进程则通常会调用wait函数等待子进程终止。
库函数exit(status)终止一进程,将进程占用的资源归还内核,参数status为一整型变量,表示退出状态,父进程可使用wait获取该状态。库函数exit基于系统调用_exit,通常父子进程只有一个会调用exit退出,而另一个应使用_exit退出。
2、创建新进程
系统调用fork可以创建一新进程,几近于调用进程翻版。
#include <unistd.h>
pid_t fork(void);
父进程中返回子进程ID,失败返回-1;创建成功子进程返回0
fork调用完成后将存在两个进程,且每个进程都会从fork返回处继续执行。
程序可通过fork返回值来区分父子进程。在父进程中将返回新创建子进程的进程ID,而fork在子进程中则返回0。创建失败时返回-1,失败原因可能是进程数量超出限制。
调用fork时,通常会采用如下用法:
pid_t childPid;
switch (childPid = fork())
{
case -1:
/*Handle error*/
case 0:
/*Perform actions specific to child*/
default:
/*Perform actions specific to parent*/
}
需要注意的是:调用fork之后,CPU率先调度哪个进程是无法确定的,这可能会导致竞争条件,可能需要加一些同步手段来避免。
文件共享
调用fork后,子进程会获得父进程的所有文件描述符的副本,这些副本的创建方式类似于dup,即父子进程中对应的描述符指向相同的打开文件句柄。在文件IO一文中有提到,打开文件句柄包含有当前文件偏移量,即父子进程共享文件偏移量,如果子进程通过read、write、lseek修改了文件偏移量,父进程中也会观察到相应修改。这种机制有好有坏,若父子进程同时写入一个文件,这种机制确保二者不会覆盖彼此的输出,但并不能避免输出混杂在一起。如果不需要这种文件共享,可以采取两种办法:1、令父子进程使用不同的文件描述符;2、各自立即关闭不再使用的文件描述符。
控制进程的内存需求
通过将fork和wait组合使用,可以控制一个进程的内存需求。进程的内存需求受多种因素影响,比如调用函数,或者函数返回以及malloc和free调用对堆做出修改。若以如下方式调用fork和wait,将对函数func的调用置于括号中,由此可知,所有可能的变化都发生于子进程中,因此从对func调用开始,父进程的内存使用量保持不变。
pid_t childPid;
int status;
childPid = fork();
if(childPid == -1)
/*Handle error*/
if(childPid == 0)
exit(func(arg));
if(wait(&status) == -1)
/*Handle error*/
在func可能导致内存泄漏,或是引起堆内存过度碎片化而func又来源于第三方库,无法修改其源码时,这一用法具有实用性。
3、进程的终止:_exit和exit
通常,进程有正常终止和异常终止两种终止方式。异常终止由信号引起,该信号默认动作是终止进程。进程可调用_exit系统调用正常终止。
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void exit(int status);
程序通常不会调用_exit退出,而是使用库函数exit。它会在调用_exit退出进程前执行各种动作,包括:
- 调用退出处理程序(通过atexit或on_exit注册)
- 刷新stdio缓冲区
- 使用status值调用_exit
当程序从main函数中执行return返回或是执行到了main函数的末尾(即末尾没有return语句)时,等同于执行exit调用。
由于进程调用exit退出时会调用退出处理程序和刷新stdio缓冲区,当通过fork创建进程时,父子进程退出时都会调用相同的退出处理程序并刷新缓冲区,这会导致退出处理程序调用两次并且stdio重复输出。可以通过以下办法避免:
- 在fork之前显示调用fflush刷新缓冲区或者使用setvbuf等调用关闭stdio缓冲功能
- 子进程调用_exit而不是exit退出。即前面提到过的父子进程中应该只有一个进程调用exit退出,而另一个调用_exit退出,确保只有一个进程调用退出处理程序和刷新stdio缓冲区。