Linux系统是用C语言和汇编语言共同写出来的,系统调用都被封装好了在C语言的库里面。fork(), wait(), 和exec()都是系统调用,其中fork()被用于创建子进程;exec()被用于执行子进程;wait()被用于阻塞父进程以等待子进程。我们这一篇文章用来聊聊怎么用这些系统调用, 并通过这一篇文章来一览进程的运行机制。配合进程原理一起阅读更好。

fork()

fork()被用于创建一个子进程。这个函数返回0代表创建成功,返回负数代表创建失败。

来看一段代码

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

int main(){
int apple = 5; //5个苹果
pid_t pid;

pid = fork(); //创建子进程
if(pid < 0){ //如果没有创建成功
printf("Error: creating child process");
exit(EXIT_FAILURE); //退出
}

if(pid == 0){// 如果创建成功
//在这个if中的所有操作都是子进程的。
apple -= 4; //吃掉4个苹果
}else if(pid >= 0){ // 如果不在子进程中
printf("There are %d apples", apple); //输出苹果的数量
}
}

猜一下输出的结果是 1 还是 5。

当我们创建一个子进程的时候,这段代码,包括它当前的所有参数都被复制给了子进程。这样一来,子进程的操作无论怎么更改都是在子进程里生效。见下图
p1
答案是五个苹果, 猜对了嘛。

这些参数里面,唯一不同的是pid,子进程的pid是0,而父进程的pid没有赋值。我们不能说,子进程的作用域只在那个if语句中,在if外面就不是子进程了。其实不然,我们可以试一下下面的代码来证明子进程和父进程都拥有苹果,而且互不干扰。

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

int main(){
int apple = 5; //5个苹果
pid_t pid;

pid = fork(); //创建子进程
if(pid < 0){ //如果没有创建成功
printf("Error: creating child process");
exit(EXIT_FAILURE); //退出
}

if(pid == 0){// 如果创建成功
//在这个if中的所有操作都是子进程的。
apple -= 4; //吃掉4个苹果
}
printf("There are %d apples\n", apple); //输出苹果的数量
}

shell里面有两个输出

1
2
There are 5 apples
There are 1 apples

前一个是父进程的苹果,没有被吃掉,后一个是子进程的苹果,吃掉了四个。你看,即使是在if语句外面也是可以被执行的。if仅仅只是用于辅助判断哪些代码可以在子进程执行,哪些代码可以在父进程执行而已。接下来我们说exec()

#exec()

exec()是一系列函数的总称,主要的作用是执行某个文件。它们分别是execl(), execlp(), execle(), execv(), execvp(), execvpe()。我后面会补充一期文件系统的复习。现在只要假设exec()可以用来跑一个程序就好。

这么多函数,我应该用哪一个?别慌。以exec作为前缀,我们可以这样确定要用哪一个函数:

  • l代表输入的是参数,而v代表输入的是数组。
  • e表示需要设置环境,不带e表示不需要。
  • p表示输入的是可执行文件的名字,不带表示输入的是可执行文件的路径(函数会根据$PATH的路径来判断可执行文件的位置)。
    下面这段代码介绍了execlp()execvp()的区别。 程序会输出两段一样的结果,分别是子进程和父进程的运行结果。不同的是,父进程用execvp(), 根据上面的判断这是一个接受参数数组和可执行文件名的函数。你可以试着自己换不同的函数来尝试他们的效果。
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 <sys/types.h>
#include <sys/wait.h>

int main(){
char args1[] = "ls";
char args2[] = "-la";

char* command[] = {args1, args2, NULL};
pid_t pid1;

pid1 = fork();
if(pid1 < 0){
printf("Error: creating child process");
}

if(pid1 == 0){
//execlp 接收n个参数,其中第一个是可执行文件名,后面的都是可执行参数。
//最后一个参数必须为NULL。
execlp(args1, args1, args2, NULL);
}else{
wait(NULL);
//execvp 接收两个参数,一个是可执行文件名,一个是执行参数的数组。
//数组最后的元素必须为NULL。
execvp(args1, command);
}
}

NULL在函数中的作用是告诉函数参数到此为止。

#wait()
我们上面有用过wait()但是没有详细介绍过,它的用法也简单,就是搭配fork()使用。wait(NULL)的意思是等待子进程。如果没有用wait()主进程不会受到影响,但是子进程即使终止了资源也不会被回收,从而会变成僵尸进程(zombie process)^1wait()的返回值是子进程的ID。

如果你对第一次写 hello world 有印象,你一定记得代码最后的return 0;。课本上只会跟你说:“return 0代表这个函数的返回值是0”,但是你无法理解为什么要返回0,返回给谁。现在明白了父进程和子进程的概念,你就知道这个0是返回给父进程的, 是告诉父进程子进程的退出状态。

要接住这个返回值,我们要定义一个整型status。后面通过这个变量来确定子进程退出的状态。

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

int main(){
pid_t pid;
pid = fork();
//输入的是
if(pid < 0){
printf("Error: creating child process");
}

if(pid == 0){
return 0; //尝试换掉这个返回值看看输出的结果。
}else{
int status;
int id = wait(&status);
printf("%d\n", status);
printf("Child process return status is %d\n", WEXITSTATUS(status));
printf("Child process id is %d", id);
}
}

WEXITSTATUS是预设的函数,用来查看函数的返回值的。还有很多其他的函数可供使用, 详情点这里

这里有一个很大的问题,一定不要弄混淆了

  1. wait()的返回值不是子进程的返回值, 而是子进程的id。
  2. status不是子进程的返回值,而是进程的状态。

另外需要说明的是wait()函数并不仅仅是在等到子进程结束,而是在子进程状态转移的时候返回。根据文档的提示

A state change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal^2

进程在子进程终止,被信号暂停,以及被信号从新激活的时候都算是状态转移。

总结一下:

  1. 要创建子进程我们要用fork(),这个函数的返回值告诉我们子进程是否创建成功。
  2. 要在子进程中运行可执行文件要用exec(),这个函数有多个变种,用哪一个根据手头有的变量确定。
  3. wait()被父进程用于等待子进程的状态。