文章目录
- [1. 进程创建](#1. 进程创建)
-
- [1.1 fork](#1.1 fork)
- [1.2 写时拷贝](#1.2 写时拷贝)
- 2.进程终止
-
- [2.1 退出码](#2.1 退出码)
- [2.2 进程如何返回退出码](#2.2 进程如何返回退出码)
- [3. 进程等待](#3. 进程等待)
-
- [3.1 wait](#3.1 wait)
-
- [3.1.1 阻塞等待](#3.1.1 阻塞等待)
- [3.1.2 退出码与退出信号](#3.1.2 退出码与退出信号)
- [3.2 waitpid](#3.2 waitpid)
1. 进程创建
1.1 fork
我们可以使用fork函数来创建子进程,创建子进程后,父子进程之间就是两个执行流,可以分别完成不同的任务,也可以完成一个任务的不同模块。
1.2 写时拷贝
fork创建的子进程和父进程之间,数据和代码是共享的,但如果子进程要进行写入,就要发生写时拷贝。现在,我们来具体讲一讲子进程的写时拷贝。
子进程进行写入时,无法对虚拟地址空间写入,所以会由操作系统通过页表查找映射关系,如果不存在相应的映射关系,即出现错误,会由操作系统终止该进程;如果存在相应的映射关系,就会发生写时拷贝。
实质上,对于父进程中一个可读可写的数据而言,一旦创建子进程后,操作系统会将这个数据的权限更改为只读,此时一旦有任意一方(父进程或子进程)要对这个数据进行写入,就会发生写时拷贝,父进程和子进程发生数据分离,此时这个数据的权限又会变为可读可写。
现在,我们来思考一个问题:
为什么创建子进程后,不立刻进行数据分离呢?因为创建子进程后,父进程或子进程不一定会对数据修改,此时共享数据即可,没有另开内存空间的必要;而进行数据修改时,有需要,再开额外的内存空间。这种做法提高了内存内存空间的使用率,是一种惰性申请。
2.进程终止
什么是进程终止?进程创建时,由操作系统在内存中申请相关的资源;进程终止时,由操作系统释放内存中相关的资源。
那么,我们再来思考一个问题:一个进程终止,会有几种情况?
进程,本质上是为了完成相应任务而创建的,在此基础上,我们可以对进程终止时的情况作出分类:
- 进程正常运行完,执行结果正确
- 进程正常运行完,执行结果错误
- 进程未正常运行完,异常终止
PS:我们在进程终止这个章节中,主要了解进程正常运行完的两种情况,关于进程异常终止的情况,我们在进程等待章节中会做介绍。
2.1 退出码
什么是退出码?我们可以回忆一下,main函数中最后的return 0以及曾经用过的exit(0)函数中的数字0 ,这些数字就是退出码。简而言之,退出码是用来标记一个进程退出时,结果是否正确的一个数字。退出码为0,表示任务执行成功;退出码为非0,表示任务执行失败或错误。
为什么要有退出码呢?父进程创建了子进程,此时,子进程任务执行结束,父进程要不要知道子进程任务完成的情况呢?毫无疑问,肯定是需要的。那父进程又怎样了解其子进程的任务完成情况呢?通过子进程的退出码。
所以,子进程的退出码最终会交给父进程,子进程task_struct结构内部有相应的变量存储退出码,父进程中也有相应的变量用以存储其子进程的退出码。
在Linux中,我们可以使用echo $?
这个命令,来查看最近一个执行完的进程的退出码。
将退出码改为1,继续查看。
此时,所查看到的退出码就变为1了。
在Linux中,对于一部分非0的退出码有相应的定义,即不同的退出码,会表示不同的任务执行失败的原因,我们可以使用strerror
这个函数来查看。strerror
这个函数,形参给相应的退出码,会返回对应这个退出码的一个字符串。
看到上图中错误码2对应的错误信息,我们会想到,平时使用ls
命令时,如果想要列出不存在的目录或文件,就会出现这样的信息。显然,这本质上就是该命令所对应进程的错误码,以字符串形式显示出的结果。

错误码2对应的恰好就是:No such file or directory
。
2.2 进程如何返回退出码
我们介绍三种进程返回退出码的方式。
main函数中return返回退出码
exit返回退出码:exit函数是C语言库中提供的一个函数调用,exit的形参即为相应的退出码。
_exit()返回退出码:注意区分_exit与exit,前者是操作系统提供的调用接口,后者是C语言提供的函数。_exit()与exit()用法相同。
接着,我们对这三种进程返回退出码的方式做一个区分。
return 和 exit 有何区别?
return
实质上表示的是函数调用结束,用来返回函数的返回值,所以,对于普通函数而言,return就是普通函数的返回;但对于main函数而言,return 就表示进程结束并返回相应退出码。
而exit
,本身就是一个用于进程退出,返回退出码的一个函数,所以无论使用在哪个地方,都能使进程退出。
因此,我们可以这样理解,exit 等同于main函数中的return。
exit和_exit有何区别?
这两个函数,在结束进程,返回相应退出码上没有什么区别。最大的区别在于, exit
函数退出进程时,会刷新输出缓冲区,而 _exit
这个系统调用并不会刷新输出缓冲区。
以下代码运行结果可作为依据:
没有任何结果,因为hello C++ 是被写入到输出缓冲区中,而_exit
退出进程时,不刷新输出缓冲区,因此没有任何显示。
再来看exit
.
此时有输出结果,说明输出缓冲区被刷新。
同时,上文提及 exit() 等同于main函数中的return,因此main函数中的 return 也会刷新输出缓冲区。
3. 进程等待
什么是进程等待呢?
进程等待,指的是父进程等待子进程结束,接收子进程的退出信息,并由操作系统释放子进程相关资源的过程。
进程等待有其存在的必要性。父进程一般情况下,都要去接收子进程的退出信息,但这一点并不是任何情况下都需要的;可子进程结束后,相应资源要进行释放,这却是必须的------因此,父进程必须要进行进程等待,否则子进程会变成僵尸进程,不会接收任何信号,无法被杀死,因而相关资源就无法被操作系统释放,这就导致了内存泄漏。
那么,我们改如何进行进程等待呢?以下介绍两种常用的进程等待方式。
3.1 wait
wait:是Linux中提供的系统调用接口,用于进程等待,且等待的是任意一个子进程,即哪个子进程先结束,就处理哪个子进程。---int wait(int* status)
我们暂且先不管wait
的形参到底是什么意思,反正是一个指针,我们先传一个空指针,然后用wait
来解决子进程的僵尸问题。

在上述代码中,父进程在子进程结束后约5s左右才会调用wait
,因此在这5s期间,子进程会进入僵尸状态。而当父进程成功调用wait
后,子进程就会解除僵尸状态,进入死亡,由操作系统释放相关资源。
我们可以看到,通过进程等待,成功解决了僵尸进程的问题。
接下来,我们来介绍wait
相关的另外两个知识点。
3.1.1 阻塞等待
其一, wait
进程等待是阻塞等待,也就是说父进程执行到wait
之后,如果子进程没有结束,会卡住,即一直等待子进程结束后,再继续往下执行。我们可以来看下面的代码示例:
可以看到父进程在wait处卡住了,即阻塞等待,一直等到子进程推出后,才继续往下执行,输出waiting success。
3.1.2 退出码与退出信号
另外我们要讲的一点是关于wait
的形参status
和返回值。
wait
的返回值有两个,如果等待成功,就返回子进程的pid;如果等待失败,等待失败是指等待一个不属于父进程或根本不存在的进程,实质上就是去等待一个不在父进程所管理的子进程链表中的进程(wait本质上就是到子进程链表中去循环检测有没有一个子进程退出了),此时就返回-1。
那么status
又是什么东西?首先,我们要知道,传一个指针进去,是为了从wait中获取某些值,即status
本质上是一个输出型参数。那么,wait
这个系统调用,想给我们传递什么值呢?
我们可能会想到,父进程等待子进程,会需要子进程的退出码,所以status
会记录子进程的退出码。但实际上,status
不仅仅会记录退出码,它还会记录退出信号。
那退出信号又是什么呢?
前面我们讲过,一个子进程结束,可能有三种情况:正常退出,结果正确;正常退出,结果错误;异常退出(没有退出码)。
而我们现在有退出码来表示结果的错误与否,显然,我们还需要一个东西来表示是异常退出还是正常退出,这个东西就是退出信号。
我们可以使用kill -l
命令,来查看Linux中的不同信号。
一个进程会异常退出,肯定是因为该进程进行了某些非法操作,因此被操作系统使用信号杀死,此时该进程的退出信号就记录了该进程是因为进行了什么非法操作而被操作系统终止的。
既然,int* status
指向一个整型,而这个整型变量既表示退出码,又表示退出信息,这怎么做到呢?
Linux中,是这样处理的。一个整型变量有32位,高16位不用,用低16位。而低16位中的,高八位用以记录退出码信息,低八位中的最高位用以存储core dump(此处暂不做介绍),剩余的7位则用以存储退出信号。
我们来看代码示例:

上述代码写的是子进程正常退出,并且退出码为100的情况。我们来看执行结果:
status
确实记录了退出码和退出信号两种信息,其中退出信号为0时,即表示正常退出。
接下来,我们再来看一个异常退出的情况。
此时,退出信号就为8,那么这表示什么非法操作呢?表示的是浮点数处理错误,既float process error。

但是,我们发现,每次通过status拿出退出码和退出信号,都要我们自己进行额外的位操作,这太麻烦了,因此我们可以用提供好的宏进行简化。
WIFEXITED 这个宏可用于处理status,获取退出码;WEXITSTATUS 这个宏可用于处理status,得到进程是否正常退出,0表示异常退出,1表示正常退出。这两个宏函数本质上也是在进行我们之前代码中所用的位操作,并不是什么高深的东西。
3.2 waitpid
waitpid也是一个用于进程等待的系统调用接口,不过waitpid与wait有一些区别。
waitpid:int waitpid(pid_t pid,int* status,int options)
我们可以看到,相比于wait,waitpid多了两个参数,一个是pid,另一个是options。
pid:这个参数用于指定子进程的pid,也就是说,waitpid可以进行特定子进程的等待。如果这个值给-1,那么就与wait相同,进行任意子进程的等待。
options:这个参数用于选择waitpid是阻塞等待,还是非阻塞等待。前面讲过,wait是默认阻塞等待,而waitpid可以进行选择------如果这个参数为0,则表示阻塞等待;如果参数为宏WNOHANG,那么就表示非阻塞等待。
那么,什么叫非阻塞等待呢?
非阻塞等待,也就是说在waitpid调用时,去查看子进程是否退出,如论子进程是否退出,waitpid都会返回------当子进程还在执行时,waitpid返回0;当子进程退出时,waitpid返回相应子进程的pid(当然,如果waitpid失败,失败原因与wait相同,则返回-1)。
由于非阻塞等待中,一次waitpid调用,只会等待一次,因此若使用非阻塞等待,需要将其放入一个循环中,不断重复waitpid调用,进行等待,即非阻塞轮询等待。
非阻塞轮询等待有什么好处呢?阻塞等待时,父进程需要等待子进程退出,才能继续执行后面的代码;而非阻塞轮询等待中,当一次调用waitpid,发现子进程未结束后,可以不用立刻再次调用waitpid,而是经过一段时间后再调用,而在这段时间内,父进程就可以去执行其它功能,这样就大大提高了进行运行的效率。
我们来看下面这个非阻塞轮询等待的示例代码:
此时,这种非阻塞轮询等待,最终输出的结果如下所示:
显然,此时的父进程非阻塞等待,在前后两次的waitpid调用之间,完成了两次打印工作。