文章目录
个人主页:https://blog.csdn.net/weixin_51609435?type=blog
进程相关:
这一节我们聊聊linux下进程有关的内容,什么是孤儿进程,什么是僵尸进程?
了解这节内容之前,我们应该知道
什么是进程替换
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。
在Linux中使用exec函数组主要有以下两种情况
- 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 函数族让自己重生。
- 如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生
既然进程的替换需要用到exec族函数,下面我们就来看看他的族函数:
exec函数会取代执行它的进程, 也就是说, 一旦exec函数执行成功, 它就不会返回了, 进程结束. 但是如果exec函数执行失败, 它会返回失败的信息, 而且进程继续执行后面的代码!
通常exec会放在fork() 函数的子进程部分, 来替代子进程执行啦, 执行成功后子程序就会消失, 但是执行失败的话, 必须用exit()函数来让子进程退出!
exec函数都有下面这些:
(1)execl和execv 这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成(l其实就是list的缩写),execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。
————————————————
(2)execlp和execvp 这两个函数在上面2个基础上加了p,较上面2个来说,区别是:上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会首先去找file,如果找到则执行执行,如果没找到则会去环境变量PATH所指定的目录下去找,如果找到则执行如果没找到则报错)
————————————————
(3)execle和execve 这两个函数较基本exec来说加了e,函数的参数列表中也多了一个字符串数组envp形参,e就是environment环境变量的意思,和基本版本的exec的区别就是:执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序
exec族函数中最常用的有两个 execl() 和 execlp(),这两个函数是对其他 4 个函数做了进一步的封装,下面介绍一下。
execl()
该函数可用于执行任意一个可执行程序,函数需要通过指定的文件路径才能找到这个可执行程序。
参数:
path: 要启动的可执行程序的路径,推荐使用绝对路径
arg: ps aux 查看进程的时候,启动的进程的名字,可以随意指定,一般和要启动的可执行程序名相同
... : 要执行的命令需要的参数,可以写多个,最后以 NULL 结尾,表示参数指定完了。
返回值:如果这个函数执行成功,没有返回值,如果执行失败,返回 -1
execlp()
该函数常用于执行已经设置了环境变量的可执行程序,函数中的 path,也是说这个函数会自动搜索系统的环境变量 PATH,因此使用这个函数执行可执行程序不需要指定路径,只需要指定出名字即可。
参数:
file: 可执行程序的名字
在环境变量 PATH 中,可执行程序可以不加路径
没有在环境变量 PATH 中,可执行程序需要指定绝对路径
arg: ps aux 查看进程的时候,启动的进程的名字,可以随意指定,一般和要启动的可执行程序名相同
... : 要执行的命令需要的参数,可以写多个,最后以 NULL 结尾,表示参数指定完了。
返回值:如果这个函数执行成功,没有返回值,如果执行失败,返回 -1
关于 exec 族函数,我们一般不会在进程中直接调用,如果直接调用这个进程的代码区代码被替换也就不能按照原来的流程工作了。我们一般在调用这些函数的时候都会先创建一个子进程,在子进程中调用 exec 族函数,子进程的用户区数据被替换掉开始执行新的程序中的代码逻辑,但是父进程不受任何影响仍然可以继续正常工作。
我演示一段代码讲一下execl () 或者 execlp () 函数的使用方法:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 创建子进程
pid_t pid = fork();
// 在子进程中执行磁盘上的可执行程序
if(pid == 0)
{
// 磁盘上的可执行程序 /bin/ps
#if 1
execl("/bin/ps", "title", "aux", NULL);
// 也可以这么写
// execl("/bin/ps", "title", "a", "u", "x", NULL);
#else
execlp("ps", "title", "aux", NULL);
// 也可以这么写
// execl("ps", "title", "a", "u", "x", NULL);
#endif
// 如果成功当前子进程的代码区别 ps中的代码区代码替换
// 下面的所有代码都不会执行
// 如果函数调用失败了,才会继续执行下面的代码
perror("execl");
printf("++++++++++++++++++++++++\n");
printf("++++++++++++++++++++++++\n");
printf("++++++++++++++++++++++++\n");
printf("++++++++++++++++++++++++\n");
printf("++++++++++++++++++++++++\n");
printf("++++++++++++++++++++++++\n");
}
else if(pid > 0)
{
printf("我是父进程.....\n");
}
return 0;
}
下面我们来聊聊什么是孤儿进程
在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称之为孤儿进程(跟现实是一样的)。
操作系统是非常关爱运行的每一个进程的,当检测到某一个进程变成了孤儿进程,这时候系统中就会有一个固定的进程领养这个孤儿进程(有干爹了)。如果使用 Linux 没有桌面终端,这个领养孤儿进程的进程就是 init 进程(PID=1),如果有桌面终端,这个领养孤儿进程就是桌面进程。
那么问题来了,系统为什么要领养这个孤儿进程呢?在子进程退出的时候, 进程中的用户区可以自己释放, 但是进程内核区的pcb资源自己无法释放,必须要由父进程来释放子进程的pcb资源,孤儿进程被领养之后,这件事儿干爹就可以代劳了,这样可以避免系统资源的浪费。
下面这段代码就可以得到一个孤儿进程:
输出结果:
父进程向退出, 子进程变成孤儿进程, 子进程被1452号进程回收(不同linux系统中回收孤儿进程的进程号不一样)
僵尸进程
在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行,子进程先与父进程结束,子进程无法释放自己的 PCB 资源,需要父进程来做这个件事儿,但是如果父进程也不管,这时候子进程就变成了僵尸进程。
官方一点的说法是:
当子进程先于父进程结束,父进程没有获取子进程的退出码,此时子进程变成僵死进程;
注意:
僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡了,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。 僵尸进程就相当于是一副已经腐烂只剩下骨头的尸体。
僵尸进程的出现是由于这个已死亡的进程的父进程不作为造成的。
运行下面的代码就可以得到一个僵尸进程了:
父进程永远不会停止,而子进程已经结束
ps查看进程信息会发现这五个僵尸子进程defunct
如何去处理僵尸进程?
为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收,回收方式有两种,一种是阻塞方式 wait(),一种是非阻塞方式 waitpid()。
(1)父进程调用wait()方法获取子进程的退出码;
(2)父进程先结束(子进程会变成孤儿进程,孤儿进程会被收养);
其实两种处理僵死进程的方法的本质是一样的,都调用了wait方法;
但是两种方法都有区别:就是父进程调用wait会阻塞,等子进程执行完之后,父进程才会去执行;
关于wait函数
这是个阻塞函数,如果没有子进程退出,函数会一直阻塞等待,当检测到子进程退出了,该函数阻塞解除回收子进程资源。这个函数被调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。
函数原型如下:
**#include <sys/wait.h>**
**pid_t wait(int *status)**
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数:传出参数,通过传递出的信息判断回收的进程是怎么退出的,如果不需要该信息可以指定为 NULL。取出整形变量中的数据需要使用一些宏函数,具体操作方式如下:
- WIFEXITED(status): 返回 1, 进程是正常退出的
- WEXITSTATUS(status):得到进程退出时候的状态码,相当于 return 后边的数值,或者 exit () 函数的参数
- WIFSIGNALED(status): 返回 1, 进程是被信号杀死了
- WTERMSIG(status): 获得进程是被哪个信号杀死的,会得到信号的编号
返回值:
- 成功:返回被回收的子进程的进程 ID
- 失败: -1
- 没有子进程资源可以回收了,函数的阻塞会自动解除,返回 - 1
- 回收子进程资源的时候出现了异常
在主进程中调用wait来解决上僵尸进程代码:
运行结果如下:
waitpid函数
waitpid () 函数可以看做是 wait () 函数的升级版,通过该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。
该函数函数原型如下:
#include <sys/wait.h>
// 这个函数可以设置阻塞, 也可以设置为非阻塞
// 这个函数可以指定回收哪些子进程的资源
pid_t waitpid(pid_t pid, int *status, int options);
参数:
pid:
- -1:回收所有的子进程资源,和 wait () 是一样的,无差别回收,并不是一次性就可以回收多个,也是需要循环回收的
- 大于0:指定回收某一个进程的资源 ,pid 是要回收的子进程的进程 ID
- 0:回收当前进程组的所有子进程 ID
- 小于 -1:pid 的绝对值代表进程组 ID,表示要回收这个进程组的所有子进程资源
status: NULL, 和 wait 的参数是一样的
options: 控制函数是阻塞还是非阻塞
- 0: 函数是行为是阻塞的 ==> 和 wait 一样
- WNOHANG: 函数是行为是非阻塞的
返回值:
- 如果函数是非阻塞的,并且子进程还在运行,返回 0
- 成功:得到子进程的进程 ID
- 失败: -1
- 没有子进程资源可以回收了,函数如果是阻塞的,阻塞会解除,直接返回 - 1
- 回收子进程资源的时候出现了异常
下面代码演示了如何通过 waitpid() 非阻塞回收多个子进程资源:
运行结果:
麻烦这次一定三连!!!