Linux中的fork()、exec()、exit()和wati()

前言

进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

进程控制是进程管理中最基本的功能。它用于创建一个新进程,终止一个已完成的进程,或者去终止一个因出现某事件而使其无法运行下去的进程,还可负责进程运行中的状态转换。

下面是对Linux中几个重要的进程控制函数进行解析。

fork()

功能

fork函数会在运行的程序中,创建一个新进程。

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

由fork 创建的新进程被称为子进程,而调用fork函数的进程称为父进程。子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。但值得注意的是,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。

函数原型及返回值

fork函数需要头文件unistd.h。

函数原型:

  • pid_t fork(void)
    

pid_t是进程描述符,实质就是一个int。fork被调用一次,会返回两次数值,且可能有三种不同的值:

  1. 在父进程中,fork返回新创建子进程的进程ID(通常为父进程PID+1)
  2. 在子进程中,fork返回0
  3. 如果出现错误,fork返回一个负值

代码演示

在Linux中新建一个fork.c文件,并键入如下代码,之后使用gcc -o fork fork.c 编译代码,生成可运行文件fork。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include< stdio.h > //因为博客代码渲染的问题,这里头文件名两边需要加空格,实际运行时需要去除,下面的头文件都是如此
#include< unistd.h >

int main()
{
pid_t pid;
int count = 0;
pid = fork(); //fork一个进程
if(pid == 0) { //pid为0,
printf("this is child process, pid is %d\n",getpid());//getpid返回当前进程PID
count+=2;
printf("count = %d\n",count);
} else if(pid > 0) {
printf("this is father process, pid is %d\n",getpid());
count++;
printf("count = %d\n",count);
} else {
fprintf(stderr,"ERROR:fork() failed!\n");
}
return 0;
}

下来在终端中运行fork,得到下图的结果。看样子像是程序中if语句的两条分支if(pid == 0)和else if(pid > 0)都得到了执行。但其实不是这样,出现这种运行结果的原因是因为,在main()函数调用了fork,创建了一个新的进程,这个进程称为原来进程的子进程,而且代码完全一样。

在父进程中,fork返回创建子进程的PID,PID大于0;在子进程中,fork返回0。根据返回值的不同,有不同的执行结果。

而且子进程与原来的进程并发执行,谁先谁后没有规律,由操作系统调度决定。这次是父进程在前,下一次也可能是子进程在前。

exec()

功能

exec函数是操作系统的功能,它以新的进程去代替原来的进程。

由于未创建新进程,进程标识符(PID)不会更改,但进程的计算机代码、数据、堆和堆栈将被新程序的代码、数据、堆和堆栈替换。

函数原型及返回值

exec函数需要头文件unistd.h

函数原型:

  • int execl(char const *path, char const *arg0, …);
  • int execv(char const *path, char const *argv[]);
  • int execle(char const *path, char const *arg0, …, char const *envp[]);
  • int execlp(char const *file, char const *arg0, …);
  • int execve(char const *path, char const *argv[], char const *envp[]);
  • int execvp(char const *file, char const *argv[]);

在上面的函数原型中,exec之后的l、v、p和e分别代表不同的操作能力,如下表:

后缀 操作能力
l 接收以逗号分割的命令行参数列表,列表以NULL指针作为结束标志
v 接收一个字符串数组的指针,数组以NULL为结束
p 使用PATH环境变量查找要执行文件参数中命名的文件夹
e 可通过参数传递传递环境变量

exec如果成功执行,不会返回任何内容。而如果exec出错,则会返回-1,并将失败原因记录在errno中。

errno的几种值如下:

  1. E2BIG,参数列表超过系统限制。
  2. EACCES,指定的文件具有锁或共享冲突。
  3. ENOENT,找不到文件或路径名称。
  4. ENOMEM,没有足够的内存可用于执行新的进程映像。

代码演示

新建一个exec.c文件,并键入如下代码,之后使用gcc编译代码,生成可运行文件exec。代码主要功能是使用以上6种不同exec函数来执行“ls -a”这条命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include< stdio.h >  
#include< stdlib.h >
#include< unistd.h >
#include< string.h >

int main()
{
//以NULL结尾的字符串数组的指针,适合包含v的exec函数参数
char *arg[] = {"ls", "-a", NULL};
/**
* 创建子进程并调用函数execl
* execl 中希望接收以逗号分隔的参数列表,并以NULL指针为结束标志
*/
if( fork() == 0 )
{
// in clild
printf( "1------------execl------------\n" );
if( execl( "/bin/ls", "ls","-a", NULL ) == -1 )
{
printf( "execl error " );
exit(1);
}
}
/**
*创建子进程并调用函数execv
*execv中希望接收一个以NULL结尾的字符串数组的指针
*/
if( fork() == 0 )
{
// in child
printf("2------------execv------------\n");
if( execv( "/bin/ls",arg) < 0)
{
printf("execv error ");
exit(1);
}
}
/**
*创建子进程并调用 execlp
*execlp中
*l希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
*p是一个以NULL结尾的字符串数组指针,函数可以DOS的PATH变量查找子程序文件
*/
if( fork() == 0 )
{
// in clhild
printf("3------------execlp------------\n");
if( execlp( "ls", "ls", "-a", NULL ) < 0 )
{
printf( "execlp error " );
exit(1);
}
}
/**
*创建子里程并调用execvp
*v 望接收到一个以NULL结尾的字符串数组的指针
*p 是一个以NULL结尾的字符串数组指针,函数可以在PATH变量查找子程序文件
*/
if( fork() == 0 )
{
printf("4------------execvp------------\n");
if( execvp( "ls", arg ) < 0 )
{
printf( "execvp error " );
exit( 1 );
}
}
/**
*创建子进程并调用execle
*l 希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
*e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
*/
if( fork() == 0 )
{
printf("5------------execle------------\n");
if( execle("/bin/ls", "ls", "-a", NULL, NULL) == -1 )
{
printf("execle error ");
exit(1);
}
}
/**
*创建子进程并调用execve
* v 希望接收到一个以NULL结尾的字符串数组的指针
* e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
*/
if( fork() == 0 )
{
printf("6------------execve-----------\n");
if( execve( "/bin/ls", arg, NULL ) == 0)
{
printf("execve error ");
exit(1);
}
}
return 0;
}

在终端中运行exec,得到下图的结果。可以看到顺序非常混乱,并没有按照我们程序中那样的顺序输出,这里也就体现了前面fork的特性,子进程与原来的进程并发执行,谁先谁后并没有规律,由操作系统调度决定。

再看所有命令的执行结果,都是“ls -a”的结果。从代码中,我们可以看到exec几种不同函数的不同使用方法,但同一条命令的执行结果都是一样的。

exit()

功能

exit函数将会终止所调用它的进程,但在终止之前会检查文件的打开情况,把缓冲区的内容写回文件。

_exit函数同样是终止所调用它的进程,但它不会检查文件的打开情况,而是直接使进程停止运行,并清除进程所使用的内存空间和内核中的各种数据结构。

exit函数和_exit函数最终都要把控制权交给内核。

函数原型及返回值

exit函数需要头文件stdlib.h。

函数原型:

  • void exit(int status)

_exit函数需要头文件unistd.h。

函数原型:

  • void _exit(int status)

以上两个函数原型均无返回值。status是返回给父进程的状态值,父进程可通过wait系统调用获得。

代码演示

printf函数采用缓冲I/O的方式,该函数在遇到“/n”换行符时自动的从缓冲区中将记录读出。其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。

下面的两个例子比较了这两个函数的区别。

1
2
3
4
5
6
7
8
9
10
11
#include< unistd.h >  
#include< stdio.h >
#include< stdlib.h >

int main()
{
printf("first line\n"); //遇到\n会刷新缓冲区,也就是把内容输出到屏幕上
printf("second line");
exit(0);
return 0;
}

使用gcc编译之后,运行exit。看到两行的内容都输出了。

这个代码内容与上面一样,只是改为调用_exit()函数。

1
2
3
4
5
6
7
8
9
10
11
#include< unistd.h >  
#include< stdio.h >
#include< stdlib.h >

int main()
{
printf("first line\n"); //遇到\n会刷新缓冲区
printf("second line");
_exit(0);
return 0;
}

编译后运行,发现只输出了一行,第二行并没有输出,说明_exit()函数在终止进程的时候,没有检查缓冲区,丢失了缓冲区的数据。

wait()

功能

wait函数会使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接收到了一个指定的信号为止。如果该父进程没有子进程或者它的子进程已经结束,则wait()函数就会立即返回。

waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程(它可以指定需要等待终止的子进程),它还有若干选项,如可提供一个非阻塞版本的 wait()功能,也能支持作业控制。

实际上,wait()函数只是 waitpid()函数的一个特例,在Linux 内部实现 wait()函数时直接调用的就是waitpid()函数。

函数原型及返回值

wait函数和waitpid函数需要头文件sys/types.h和sys/wait.h。

函数原型:

  • pid_t wait(int *status)
    
  • pid_t waitpid(pid_t pid, int *status, int options)
    

参数:

  • status:子进程退出时的状态。详细如下表:
status宏名 说明
WIFEXITED 进程通过调用_exit()或exit()正常退出,该宏值非0
WIFSIGNALED 子进程因得到的信号没有被捕捉导致退出,该宏值非0
WIFSTOPPED 子进程没有终止但停止了,并可重新执行时,改宏值非0。仅当waitpid()调用中使用了WUNTRACED选项出现
WEXITSTATUS 如果WIFEXITED返回非0,该宏返回由子进程调用_exit()或exit()时设置的调用参数status值
WTERMSIG 如果WIFSIGNALED返回非0,该宏返回导致子进程退出的信号值
WSTOPSIG 如果WIFSTOPPED返回非0,该宏返回导致子进程停止的信号值
  • pid:进程描述符。详细如下表:
pid取值 说明
pid>0 等待进程号为pid的子进程结束
pid=0 等待组ID等于调用组进程ID的子进程结束
pid=-1 等待任一子进程结束,等价于调用wait()
pid<-1 等待组ID等于PID的绝对值的任一子进程结束
  • options:根据不同情况,来指定waitpid()的操作
options取值 说明
WHOHANG 若pid指定的子进程没结束,则waitpid()不阻塞,立即返回,返回值为0
WUNTRACED 为实现某种操作,由pid指定的任一进程已被暂停,且其状态自暂停以来没有报告过,则返回其状态
0 同wait(),阻塞父进程,等待子进程的退出

代码演示

这个代码测试wait()函数,先用fork()新建一个子进程,然后让子进程暂停5s。接下来对原有的父进程使用wait()函数,wait()函数会使得父进程阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include< unistd.h >  
#include< stdio.h >
#include< stdlib.h >
#include< sys/wait.h >
#include< sys/types.h >

int main()
{
pid_t pc,pr;
pc = fork(); //创建子进程
if(pc < 0){
printf("Fork Error\n");
}
else if(pc == 0){ //子进程
printf("I am child progress.I am going sleep!\n");
sleep(5);
printf("I am child progress.I am going exit!\n");
exit(0);
}
else { //父进程
pr = wait(NULL);
if(pr < 0){
printf("Error\n");
}
printf("I am father progress.I get child exit code:%d\n", pr); //获得退出代号
}
return 0;
}

运行编译后的wait,看到父进程被阻塞,直到子进程执行完毕,返回子进程退出代号。wait()函数一个比较大的应用是可以用来写守护进程的代码,直到被守护的进程结束,守护进程继续执行自己的代码。

下面的代码用来测试waitpid()函数,仍是fork新建一个子进程,只不过同时在父进程中不断循环检测子进程的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include< unistd.h >  
#include< stdio.h >
#include< stdlib.h >
#include< sys/wait.h >
#include< sys/types.h >

int main()
{
pid_t pc,pr;
pc = fork();//创建子进程
if(pc < 0){
printf("Fork Error\n");
}
else if(pc == 0){ //子进程
printf("I am child progress.I am going sleep!\n");
sleep(5);
printf("I am child progress.I am going exit!\n");
exit(0);
}
else{ //父进程
do{ //循环检测子进程是否退出
pr = waitpid(pc, NULL, WNOHANG); //设置options,使父进程不阻塞
if(pr == 0){ //子进程未退出,则父进程打印信息并暂停1s
printf("I am father progress.The child process has not exited\n");
sleep(1);
}
}while(pr == 0);

if(pr == pc){ //发现子进程退出,打印相应情况
printf("I am father progress.I get child process exit code:%d\n", pr);
}
else{
printf("Error\n");
}
}
return 0;
}

运行编译后的waitpid,看到父进程通过创建fork创建了一个子进程,子进程运行之后,开始暂停。在子进程暂停的这几秒间,因为设置了waitpid()函数的参数,父进程没有阻塞,反而能一直检测子进程的状态。

这里的waitpid()可以用来一直检测子进程状态,防止僵尸进程的出现。

下面的例子用来说明一下宏名的作用,status的宏名就是根据status的值,来返回不同的值,一些宏名和值的定义在上面的表也给过了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include< stdio.h >  
#include< stdlib.h >
#include< string.h >
#include< unistd.h >
#include< sys/wait.h >
#include< sys/types.h >

void out_status(int status){ //根据不同status值,调用不同的宏
if(WIFEXITED(status)){ //正常终止
printf("normal exit: %d\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)){ //异常终止
printf("abnormal term: %d\n", WTERMSIG(status));
}
else if(WIFSTOPPED(status)){ //终止前暂停或者等待过
printf("stopped sig: %d\n", WSTOPSIG(status)); //kill -19 测试结果
}
else{
printf("unknow sig\n");
}
}

int main(){
int status;
pid_t pid;
if((pid = fork()) < 0){
printf("fork error");
exit(1);
}
else if(pid == 0){
printf("pid: %d, ppid: %d\n", getpid(), getppid());
exit(3); //子进程终止运行
}
wait(&status); //父进程阻塞,等待子进程结束并回收
out_status(status); //调用函数
printf("--------------------------\n");

if((pid = fork()) < 0){
printf("fork error");
exit(1);
}
else if(pid == 0){
printf("pid: %d, ppid: %d\n", getpid(), getppid());
int i = 3, j = 0;
int k = i / j; //异常测试
printf("k: %d\n", k);
}
wait(&status);
out_status(status); //调用函数
printf("--------------------------\n");

if((pid = fork()) < 0){
printf("fork error");
exit(1);
}
else if(pid == 0){
printf("pid: %d, ppid: %d\n", getpid(), getppid());
pause(); //暂停测试
}

do{ //暂停测试需要用waitpid来捕获暂停的信号,并返回
pid = waitpid(pid, &status, WNOHANG | WUNTRACED);
if(pid == 0){
sleep(1);
}
}while(pid == 0);
out_status(status);

return 0;
}

编译之后,运行hong。

第一个进程停止是子进程调用exit()的正常停止,并且status的宏返回子进程调用exit()时,设置的“3”。

第二个进程停止是子进程中出现了分母为零的错误,导致进程停止。status的宏返回导致子进程退出的信号值“8”,对应信号SIGFPE,意思是“浮点异常”。

第三个进程停止是通过kill命令给进程发送停止信号“19”,来让子进程停止。这里status的宏返回导致子进程停止的信号值“19”.

查找进程并发送终止信号

参考:

[1] Linux C 中的fork

[2] Linux exec函数族

[3] Linux wait函数解析