Linux系统是用C语言和汇编语言共同写出来的,系统调用都被封装好了在C语言的库里面。fork(), wait(), 和exec()都是系统调用,其中fork()被用于创建子进程;exec()被用于执行子进程;wait()被用于阻塞父进程以等待子进程。我们这一篇文章用来聊聊怎么用这些系统调用, 并通过这一篇文章来一览进程的运行机制。配合进程原理一起阅读更好。
fork()
fork()被用于创建一个子进程。这个函数返回0代表创建成功,返回负数代表创建失败。
来看一段代码
1 | #include <unistd.h> |
猜一下输出的结果是 1 还是 5。
当我们创建一个子进程的时候,这段代码,包括它当前的所有参数都被复制给了子进程。这样一来,子进程的操作无论怎么更改都是在子进程里生效。见下图
答案是五个苹果, 猜对了嘛。
这些参数里面,唯一不同的是pid,子进程的pid是0,而父进程的pid没有赋值。我们不能说,子进程的作用域只在那个if语句中,在if外面就不是子进程了。其实不然,我们可以试一下下面的代码来证明子进程和父进程都拥有苹果,而且互不干扰。
1 | #include <unistd.h> |
shell里面有两个输出
1 | There are 5 apples |
前一个是父进程的苹果,没有被吃掉,后一个是子进程的苹果,吃掉了四个。你看,即使是在if语句外面也是可以被执行的。if仅仅只是用于辅助判断哪些代码可以在子进程执行,哪些代码可以在父进程执行而已。接下来我们说exec()
exec()是一系列函数的总称,主要的作用是执行某个文件。它们分别是execl(), execlp(), execle(), execv(), execvp(), execvpe()。我后面会补充一期文件系统的复习。现在只要假设exec()可以用来跑一个程序就好。
这么多函数,我应该用哪一个?别慌。以exec作为前缀,我们可以这样确定要用哪一个函数:
l代表输入的是参数,而v代表输入的是数组。- 带
e表示需要设置环境,不带e表示不需要。 - 带
p表示输入的是可执行文件的名字,不带表示输入的是可执行文件的路径(函数会根据$PATH的路径来判断可执行文件的位置)。
下面这段代码介绍了execlp()和execvp()的区别。 程序会输出两段一样的结果,分别是子进程和父进程的运行结果。不同的是,父进程用execvp(), 根据上面的判断这是一个接受参数数组和可执行文件名的函数。你可以试着自己换不同的函数来尝试他们的效果。
1 | #include <unistd.h> |
NULL在函数中的作用是告诉函数参数到此为止。
#wait()
我们上面有用过wait()但是没有详细介绍过,它的用法也简单,就是搭配fork()使用。wait(NULL)的意思是等待子进程。如果没有用wait()主进程不会受到影响,但是子进程即使终止了资源也不会被回收,从而会变成僵尸进程(zombie process)^1。wait()的返回值是子进程的ID。
如果你对第一次写 hello world 有印象,你一定记得代码最后的return 0;。课本上只会跟你说:“return 0代表这个函数的返回值是0”,但是你无法理解为什么要返回0,返回给谁。现在明白了父进程和子进程的概念,你就知道这个0是返回给父进程的, 是告诉父进程子进程的退出状态。
要接住这个返回值,我们要定义一个整型status。后面通过这个变量来确定子进程退出的状态。
1 | #include <unistd.h> |
WEXITSTATUS是预设的函数,用来查看函数的返回值的。还有很多其他的函数可供使用, 详情点这里。
这里有一个很大的问题,一定不要弄混淆了
wait()的返回值不是子进程的返回值, 而是子进程的id。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
进程在子进程终止,被信号暂停,以及被信号从新激活的时候都算是状态转移。
总结一下:
- 要创建子进程我们要用
fork(),这个函数的返回值告诉我们子进程是否创建成功。 - 要在子进程中运行可执行文件要用
exec(),这个函数有多个变种,用哪一个根据手头有的变量确定。 wait()被父进程用于等待子进程的状态。