目录
[2.1 什么是进程终止](#2.1 什么是进程终止)
[2.2 进程退出的场景(原因)](#2.2 进程退出的场景(原因))
[2.3 进程退出码](#2.3 进程退出码)
[2.4 错误码errno](#2.4 错误码errno)
[2.5 进程常见的退出方法](#2.5 进程常见的退出方法)
[3.1 进程等待的概念](#3.1 进程等待的概念)
[3.2 僵尸进程为什么要被回收](#3.2 僵尸进程为什么要被回收)
[3.3 wait和waitpid](#3.3 wait和waitpid)
[3.4 等待失败](#3.4 等待失败)
[3.4 进程等待的原理](#3.4 进程等待的原理)
[3.5 非阻塞轮询](#3.5 非阻塞轮询)
1.进程创建
在Linux系统中,我们一般用系统调用接口fork创建进程。
当前进程调用fork()创建一个子进程,当前进程则是该子进程的父进程。一个父进程可以有多个子进程,但一个子进程在同一时间段只能有一个父进程(父进程可以改变)。
更详细的信息请查看:浅析fork()和底层实现 - tp_16b - 博客园 (cnblogs.com)
注意:当系统中有太多的进程或实际用户的进程数超过了限制 ,fork调用可能会失败,这时需要释放进程资源。
2.进程终止(退出)
2.1 什么是进程终止
结束一个进程的生命可以分为两个步骤:进程退出和进程销毁。进程退出主要是释放进程的资源,使进程成为僵死(
TASK_ZOMBIE
)状态;进程销毁主要是通过父进程,释放僵死进程的各种信息。当前进程被设为
TASK_ZOMBIE
僵死状态后会向其父进程发生SIGCHLD信号,父进程收到SIGCHLD信号后,才会销毁僵死状态的子进程,。父进程通过调用
wait()
或waitpid()
函数等待SIGCHLD处理僵死进程进程退出和销毁过程分析 - 66Ring's Blog
终止进程的结果如下:
- 进程中的任何剩余线程都标记为终止。
- 将释放进程分配的任何资源。
- 所有内核对象都已关闭。
- 进程代码将从内存中删除。
- 设置进程退出代码。
- 进程对象已发出信号。
2.2 进程退出的场景(原因)
根据程序是否运行完毕,我们可以把进程划分为运行完毕正常退出和运行异常非正常退出。
对于正常退出的进程,我们又可以根据程序运行的结果,划分为结果正确和结果不正确。
那么我们是如何判断退出的结果正不正确呢?以及不正确有哪些原因呢?
2.3 进程退出码
任何进程退出时,都会留下退出码,操作系统根据退出码可以知道进程是否正常运行。退出码是0到N的整数(不同的操作系统N可能不同),通常0表示正常退出,其他数字表示不同的错误。
在Linux系统中,我们可以用"?"获取上一个进程的退出码。我们编写一个程序,main函数的返回值设置为3,![](https://i-blog.csdnimg.cn/direct/e3063e2b50de41cdbe6d86e628e4a01a.png)我们执行该源文件生成的可执行文件,![](https://i-blog.csdnimg.cn/direct/1a8b469aced34ec08ed88ddf0bcef24e.png)可以用"echo"将"?"的内容打印到终端。退出码是一个整数,不同的整数对应着不同的退出信息,我们可以用strerror打印退出码对于的退出信息。
CentOS7系统中一共有134个退出码。
我们用ls列出一个当前目录下不存在的文件yls610,发现终端显示报错,报错信息刚好是退出码2对应的信息,我们用"echo $?"输出上一个进程返回的退出码也是2,
再"echo $?"输出,结果是0,这是为什么呢?因为上一个"echo $?"是success退出,虽然echo是内建命令,不创建进程,但它还是会调用接口。
2.4 错误码errno
C语言标准库errno.h中有个宏errno,该宏定义为一个int
类型的左值, 存储最近一次函数调用产生的错误码,也就是最近一次返回的错误码。
在纯C语言的环境下,我们可以利用这个errno和strerror,用strerror读取错误码对应的错误信息,并将错误码作为进程的退出码,让父进程也知道错误信息。我们编写一个程序,验证一下,
我们讨论了程序运行完毕的情况。但是对于一个进程,我们一般先判断程序是否异常,再判断程序运行的结果是否正确,因为程序出现了异常,程序就无法正常运行,就难以运行完毕(不会产生错误码和退出码),这时我们又该怎么办呢?(两点:为什么发生异常,以及是什么样的异常)
我们知道,C语言中,我们不能对野指针解引用,如果访问野指针,可能抛异常,这是因为访问野指针,本质上是对虚拟地址的访问,但这个虚拟地址在页表中并没有建立映射关系,或者建立了映射关系,但权限为只读;C语言中我们不能对0整除,整除时会触发浮点异常,状态寄存器会发出对应的信号。
而这些异常问题,都是通过发出信号告诉操作系统使进程提前退出。
如何证明异常是发送信号使进程退出的呢?
我们之前学习了用"kill -9"指令杀死一个进程,"kill -9"就相当于一个信号,信号8SIGFPE是"Floating point exception",浮点数异常;信号11SIGSEGV是"Segmentation fault",段错误,我们可以用信号8和信号11使一个进程退出。执行该文件生成的可执行文件,在root权限下用kill -8杀死该进程。
用kill -11杀死进程,
2.5 进程常见的退出方法
正常终止
从main函数返回
调用库函数exit
在main函数中,exit中的参数status表示退出码;在进程中调用exit,表示退出该进程。
系统接口_exit
参数:status 定义了进程的终止状态,父进程通过wait来获取该值。
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255,
exit和_exit都可以终止进程,他俩有什么区别呢?
其实exit最后会调用_exit, 但在调用_exit之前,还做了其他工作:
执行用户通过 atexit或on_exit定义的清理函数。
关闭所有打开的流,所有的缓存数据均被写入
调用_exit
由于_exit不刷新缓冲区,我们可以得出一个结论,缓冲区不在内核区,在用户区,因为操作系统不做浪费资源的事。
3.进程等待
3.1 进程等待的概念
狭义的进程等待是指,当子进程退出,进入僵尸状态,父进程会对子进程的状态进程检测并回收,如果子进程没有进入僵尸状态,父进程会等待子进程进入僵尸状态再回收。
3.2 僵尸进程为什么要被回收
基本概念:
**僵尸进程:**一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。僵尸进程无法被"kill -9"。
背景:
父进程创建子进程,目的就是让子进程执行自己创建的任务,任务的执行情况最终需要父进程知道。
unix提供了一种机制保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。
冲突:
但这样就导致了问题,如果父进程不调用wait / waitpid的话, 那么保留子进程的那段信息就不会释放,一直不释放,就会造成内存泄漏。 进程ID - 维基百科,自由的百科全书 (wikipedia.org)
所以,为什么要有进程等待?
一是因为父进程要识别子进程执行任务的最终情况;
二是因为不进行"进程等待"(调用系统接口wait或waitpid回收子进程)会造成"内存泄漏"。
3.3 wait和waitpid
wait
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关系可设置为NULL。
我们编写一个程序,
执行,
可以看到,子进程15580打印五次后退出,变为僵尸进程,最终被父进程调用wait成功回收。
waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0,等待其进程ID与pid相等的子进程
- status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
那么wait和waitpid是如何获取子进程的退出信息呢?
我们是通过输出型参数status获得进程退出的结果。
输入型参数是指这个参数的值已知,由函数外传给函数里使用.输入型参数是指这个参数的值未知,要通过函数传出来。
我们编写一个可执行文件,来检测一下,
对于这个问题,我们必须认识一下status,
参数:status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。非空,操作系统会将子进程的退出信息用该参数反馈给父进程。
由于status返回的是进程退出信息,根据进程退出的场景,我们可以将退出信息分类两类,一类是异常退出,一类是运行完毕后退出。
由于进程中异常在先,一般程序执行中出现了异常,就不会运行完毕,而status是一个整形,有32位,采用位图的算法思想,不同的bit位代表不同的退出原因,其中后低7位表示进程异常的信息,低7位全为0,表示进程没有异常,第低8位是core dump标志;次低8位(15-8)表示程序正常执行完毕返回的错误码,如果没有出现异常(低8位全为0),且次低8位全为0,表示程序运行结果正确,上面子进程的错误码(退出码)为1,status的后16位为0000 0001 0000 0000,转换为十进制就是256。具体细节如下图(只研究status低16比特位)
核心转储 - 维基百科,自由的百科全书 (wikipedia.org)
关于上面的程序,有些同学可能有疑惑,为什么要用wait的参数status呢?我们直接将status定义成全局变量不行吗?exit返回全局变量不可行吗?
不可行,因为进程具有独立性,进程之间的数据、代码、PCB实例、内存空间等都是相对独立的,进程数据之间的传递一般都要通过调用系统接口实现。
我们将上面代码中第二个if语句改为:
重新生成可执行文件并执行,
退出码就是"1"了。
我们编写一个"对空指针"解引用,看看打印结果,
在子进程中整除0,
宏WIFEXITED、WEXITSTATUS
如果用"status&0x7F"表示是否异常,"(status>>8)&0x7F"表示错误码,一些用户可能看不懂,所以标准库中定义了两个宏WIFEXITED、WEXITSTATUS,分别代表这两个运算,
我们用这两个宏代替移位运算,
执行,
结果无误。
3.4 等待失败
什么时候父进程会等待失败呢?(waitpid返回值为-1)
父进程等待到的进程不是它想等待的进程,父进程的儿子是张三等来的却是李四,父进程就会等待失败。我们看下执行结果,
3.4 进程等待的原理
父进程通过且必须通过操作系统管理的调用接口wait或waitpid获取子进程的PCB实例中的成员exit_code和exit_signal。
上面我们演示的"进程等待",全是阻塞式等待,父进程等等待期间,只能单一地等待子进程退出变为僵尸进程,不能进行或参与其他任何操作。这种等待效率低,"我等的时候啥事都不能干,手机都不能刷",但实现简单。还有一种等待方式,就是隔一段时间询问一次子进程是否退出了,没退出父进程就去干自己的事,不会一直等待子进程退出。这种等待方式叫做非阻塞轮询。
3.5 非阻塞轮询
waitpid函数的第三个参数option,设置为0就表示"阻塞等待",设置为"WNOHANG"就表示非阻塞等待。
夯住(Hang)是指程序仍在运行,卡在某个方法调用上,没有返回也没有异常抛出;卡住时间从几秒到几小时不等。
子进程和父进程在交叉打印。如果我们把WNOHANG改为0,就是阻塞等待, 这样父进程会在子进程运行完才会继续运行,
4.进程的程序替换
4.1 程序替换的概念
我们之前所创建的子进程,基本上都是继承了父进程的代码和数据。
当我们想要创建一个子进程去执行其他的任务,去完成独立于父进程的代码和数据的任务时,该怎么办呢?
我们只要替换子进程的代码和数据即可。
那么如何替换进程的代码和数据呢?
只要将新的程序和数据从磁盘(或其他存储介质)中读取到内存中,然后用它来替换当前正在运行的进程的代码和数据。
4.2 程序替换的原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
4.3 程序替换实例
我们可以看到,执行源文件生成的程序,就相当于执行指令"ls"。
4.4 程序替换接口
4.4.1 execl
l表示链表的意思,其中参数path表示要执行文件的绝对路径或相对路径,
4.4.2 execlp
p就表示环境变量PATH,execlp中第一个参数file表示要执行文件的文件名,这里不用写路径是因为execlp会自动到环境变量PATH中去寻找该文件所在路径,找到路径了,还要确定要执行的文件名,第二个"ls"就表示要执行的文件名。
实例演示:
4.4.3 execv
v表示vector,
path表示可执行文件的路径,argv是一个字符串指针数组,该数组第一个参数是要执行文件的名称,最后一个参数必须是NULL,中间是程序执行选项。
实例演示:
注意:
ls是一个可执行文件,文件中有main函数,main函数有参数,谁传给它的参数?
答案是execv函数,execv会将argv数组中除了第一个参数外都传递给ls的main函数。
在命令行中运行程序,所创建的进程都是bash的子进程,这些进程都是通过调用exec系列的函数执行的。
4.4.4 execvp
同理,execvp会在环境变量PATH中寻找参数file(可以写文件名,也可以写文件所在的绝对路径),
实例演示:
4.4.5 execle
在学习execle之前,我们先了解两个知识点,
1.用make生成多个文件:
2.在c程序中调用cpp程序
Linux系统中C++可执行文件的后缀可以写成" .cc "或" .cxx "。我们编写mycommand.c,在其main函数中通过execl调用otherExe.cpp生成的可执行程序otherExe,
我们用C语言编写的程序去调用CPP编写的程序,同样,也可以用这两个语言编写的程序去调用其他语言编写的可执行程序。