前言
🧊之前讲操作系统和进程的时候有进行引入,如果还不会通过系统调用来创建子进程的话可以看看【Linux】操作系统与进程的概念。
🧊本篇文章主要内容为进程终止、进程等待和进程替换三个部分。
进程终止
情况分类
🧊进程终止,即进程的结束,怎么才能算进程的结束呢?一般分为两种情况:
- 正常执行结束
- 程序崩溃
正常结束
🧊其中进程正常执行完了还分作结果正确和结果不正确两种情况,主要体现在主函数的退出码中。
🧊在 main 函数中,return 的数字就是退出码,若是正常结束后结果正确的话退出码就为 0,否则退出码就是非 0。
🧊而当结果出错时,我们便想要了解原因,而退出码便可以是 1、2、3、4... 用于表示不同的原因,具体的数值由用户自己决定。
查询退出码
echo $?
🧊退出码有他自己的环境变量 '?' ,可以使用 echo $? 打印退出码,下面我们写一个程序来示范一下。
int main()
{
int sum = 0;
for(int i = 1;i<=5;i++)
{
sum+=i;
}
if(sum == 15)
{
return 0;
}
else
{
return 233;
}
}
🧊之后,我们将原程序修改一下,使最后的结果出错。进程的退出码就变成了我们原先设定的 233 了。
🧊但是当我们第二次查询退出码时,退出码就发生改变了,这是怎么回事呢?
🧊这是因为 ? 这个环境变量只会保留最近一次的退出码,而 echo 这个命令本质上也是一个进程,它也有退出码,所以第二次查询到的就是上次 echo 结束之后的退出码。
程序崩溃
🧊在程序运行的途中进程会遇到各种程序崩溃的情况,比如除 0,或对空指针的解引用。
🧊而崩溃的本质就是: 进程因为某些原因,导致进程收到了来自操作系统的信号,进而结束进程。
int main()
{
int i = 5;
i/=0;
return 0;
}
🧊这里我们故意在程序中除 0,便引发了程序的崩溃。
如何理解进程退出
🧊那我们该如何理解进程退出呢?
🧊我们都知道,进程是以一个 PCB 的形式被操作系统管理,而 OS 内少了一个进程,就代表 OS 要释放进程对应的内核数据结构以及相对应的代码和数据。
如何进程退出
return
*🧊只有 main 函数 return 才代表进程的退出,其他函数使用 return 仅代表函数返回。*
🧊实际上,进程执行的本质就是 main 函数流执行。
exit
🧊可以使用 exit 这个函数结束进程,其中函数的参数就是进程的退出码。同时,在代码的任意地方调用该函数都会直接让进程退出。
🧊如以下代码,在函数内部调用 exit 成功并以退出码为 10 的情况退出进程。
void add()
{
exit(10);
}
int main()
{
add();
return 0;
}
_exit
🧊不仅如此,其实还有一个函数叫做 _exit,也是用于进程退出的。
对比
🧊在不同场景使用之后,我们便能够发现两个 exit 函数之间的区别。
🧊其中,exit 在进程退出前会刷新缓冲区,而 _exit 并不会,这是由于_exit 本质是系统调用。exit 的内部也是调用 _exit 进行进程退出的。可以这样简略地形容:
exit(int code)
{
//刷新缓冲区
...
_exit(code);
}
进程等待
为什么要进行进程等待
🧊之前我们说过,子进程退出后若父进程为读取其退出信息则会进入僵尸状态造成内存泄漏,为了避免这个情况则需要:
🧊父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
🧊其中,回收子进程是必须要做的,获取子进程执行的结果不是必须要做的。
🧊是否获取需要由用户自己判断。
等待函数
wait
🧊通过 man 手册查询,我们可以看到 wait 函数的详细信息,该函数会随机等待一个子进程,等待成功返回子进程的 pid 否则返回-1。
🧊其中的 status 是输出型参数,若传一个空指针就相当于不获取该进程的退出码。
🧊虽然它是一个int类型,但我们不能把它当作一个完整的整数,而应该将其看作位图,我们获取的退出信息就被存放在这个位图之中。
🧊我们知道,进程结束后的退出信息是这样组成的:
🧊放在位图之中,一个整型在 Linux 中一个整型是 4 字节,便有 32 个比特位,其中前十六位不使用,次低八位表示退出状态,最后七位表示终止信号。
🧊我们直接写一串代码看看实际中如何使用这个函数。
🧊首先,创建一个子进程,暂停三秒后退出,退出码为 11。而父进程则是等待子进程,分别输出次低八位和最后七位的数据。
int main()
{
pid_t pid = fork();
if (!pid) // 子进程
{
sleep(3);
exit(11);
}
// 父进程
int status;
wait(&status);
printf("%d\n%d\n", (status >> 8) & 0xFF, status & 0x7F);
return 0;
}
🧊运行程序后,等待了三秒,我们便拿到了子进程的退出码,正是设置的 11,因为是正常结束,因此并不会收到信号。** **
** 🧊之后,我们再在子进程中故意除 0,引起程序崩溃再看看程序运行结果。**
int main()
{
pid_t pid = fork();
if (!pid) // 子进程
{
int i = 5/0;
sleep(3);
exit(11);
}
// 父进程
int status;
wait(&status);
printf("%d\n%d\n", (status >> 8) & 0xFF, status & 0x7F);
return 0;
}
**🧊由此便可以看到,子进程收到了 8 号信号从而引发进程结束。 **
waitpid
🧊还有一个接口叫做 waitpid,可以通过传入的pid指定等待一个进程。若 pid 为 -1 则代表
🧊剩下两个参数,一个就是退出信息,另一个则是选项。正常使用的时候直接传 0 即可。
int main()
{
pid_t pid = fork();
if (!pid) // 子进程
{
sleep(3);
exit(11);
}
// 父进程
int status;
waitpid(pid, &status, 0);
printf("%d\n%d\n", (status >> 8) & 0xFF, status & 0x7F);
return 0;
}
非阻塞轮询
🧊相信仔细观察的话,我们可以得知在等待子进程的过程之中,父进程其实是处于阻塞状态的。
🧊🧊若我们想让父进程在等待的时候还能干点自己的事情又该怎么做呢?只需要将 waitpid 的选项设置成 WNOHANG 即可。
int main()
{
pid_t pid = fork();
if (!pid) // 子进程
{
int time = 10;
for (int i = 1; i <= time; i++)
{
printf("I am child process : %d\n", i);
sleep(1);
}
exit(11);
}
// 父进程
while (1)
{
int status;
int ret = waitpid(pid, &status, WNOHANG); //选项设置为WNOHANG
if (ret == 0) //还在等待的情况
{
printf("子进程还没有结束运行,我再干点其他事\n");
sleep(1);
continue; //回溯
}
else if (ret < 0) //等待失败的情况
{
printf("等待失败");
exit(1); //进程退出
}
else //等待成功的情况
{
printf("等待成功, %d %d\n", (status >> 8) & 0xFF, status & 0x7F);
break;
}
}
return 0;
}
通过宏查询信息
🧊库中有几个宏可以帮助我们直接获取 status 之中的退出信息,分别是:
- WIFEXITED 用于查看子进程的退出情况
- WEXITSTATUS 当上面那个宏非0才能使用,可以获取子进程的退出码
- WTERMSIG 获取导致子进程终止的信号的编号。
🧊因此我们上面的代码可以改写成这样:
int main()
{
pid_t pid = fork();
if (!pid) // 子进程
{
int time = 10;
for (int i = 1; i <= time; i++)
{
printf("I am child process : %d,pid: %d\n", i,getpid());
sleep(1);
}
return 111;
}
// 父进程
while (1)
{
int status;
int ret = waitpid(pid, &status, WNOHANG);
if (ret == 0)
{
printf("子进程还没有结束运行,我再干点其他事\n");
sleep(1);
continue;
}
else if (ret < 0)
{
printf("等待失败");
exit(1);
}
else
{
if (WIFEXITED(status))
{
printf("等待成功, 退出码为: %d\n", WEXITSTATUS(status));
}
else
{
printf("子进程异常退出, 收到%d号信号\n", WTERMSIG(status));
}
break;
}
}
return 0;
}
🧊可以看到,我们一样达到了原先的结果。
父进程如何获取子进程的退出信息
🧊我们都知道,每一个进程运行时,操作系统都会为进程维护一个 PCB,记录其相关的数据。
- 而进程结束后,操作系统会维护该子进程的PCB,填入对应的退出信息。
- 父进程通过 waitpid/wait 等系统调用接口获取相关信息。
- 子进程还没有退出的时候,父进程只能一直阻塞等待。
- 父进程不在运行状态,就不在运行队列,而在阻塞队列中,可以理解成PCB中有指向父进程的指针,子进程结束后父进程重新运行。
程序替换
什么是程序替换
🧊之前我们建立子进程的时候,让子进程运行我们局部的代码。那我们如果想要让它运行一个新的程序呢?
🧊我们可以通过程序替换让我们当前进程运行一个新的进程,我们可以运行观察一下以下代码。
int main()
{
printf("begin....\n");
execl("/bin/ls","ls","-l",NULL);
printf("end....\n");
return 0;
}
🧊可以看到,程序结束后只输出 begin,之后运行了 ls -l 的命令,之后并没有再输出 end。
🧊其实,执行程序替换后,新的代码和数据就被加载了,后续的代码就属于老代码了,不会被执行。
🧊本质上就是,将当前进程的数据段和代码段用一个新进程的进行替换。
相关函数
🧊下面介绍一下关于进程替换的相关函数。
execl
🧊第一个就是在上面示例代码中使用的 execl,其中它的参数分别是可执行程序的路径以及运行该程序时所带的参数。
🧊值得注意的是,参数列表后带 ... 的代表的是可变参数列表,可以使用尽可能多的参数,传参结束后用 NULL 结尾。
🧊还是以上面示例代码为例,第一个参数我传入了 ls 命令的地址,之后添加了 ls 和 -l 两个参数,最后以 NULL 结尾,因此最后替换后的程序就是命令 ls -l。即在命令行之中如何使用,在这里便一样如何使用。
execv
🧊而第二个函数则是支持我们用一个指针数组来描述我们的命令。指针数组中要以NULL结尾。
🧊于是,我们上个程序便可以写成这样,最终现象还是一样的。
int main()
{
printf("begin....\n");
char* const argv[] = {
"ls",
"-l",
NULL
};
execv("/bin/ls",argv);
printf("end....\n");
return 0;
}
execlp
🧊使用这个函数可以直接传程序的名称,之后其会在环境变量之中查找,就不需要我们传路径去找程序了。
🧊同样对之前的程序进行改造。
int main()
{
printf("begin....\n");
execlp("ls","ls","-l",NULL);
printf("end....\n");
return 0;
}
execve
🧊去手册查询,我们会发现只有这个函数没有跟其他函数放在一起,其实是因为该函数才是系统调用的接口。其他函数都是对这个函数的封装,只不过根据不同的需求,设计出了不同的函数。
**🧊前两个参数我们都相当熟悉了,重点讲讲最后一个。 **
🧊之前我们都讲过环境变量,而这个最后一个变量就是环境变量表,即我们可以传入一个环境变量表作为替换后程序的环境变量。
🧊例如,在这个程序之中我们新增加一个环境变量,之后利用程序替换打印其环境变量表并观察。
int main()
{
printf("begin....\n");
putenv("hello=you can see me"); //新增一个环境变量
char*const argv[] = {"env"}; //构建传入的参数列表
execve("/bin/env",argv,environ); //程序替换
printf("end....\n"); //end不会被打印
return 0;
}
🧊由于环境变量表过大,就截了一小部分,可以看到程序替换后还能看到新增的环境变量。
共性
**🧊其实不止这几个函数,总共有七个函数,但是只要搞懂其中的特性,使用起来就非常简单。 **
🧊根据函数的名字,我们便可以得到相对应的特性
- l:使用函数参数传指令
- v:使用数组传指令
- p:只需函数名就能替换
- **e:可以传入环境变量 **
🧊这些程序替换的函数,只有在失败的情况下才会返回 -1。
🧊由于进程之间是具有独立性的 ,因此程序替换只影响调用的进程,上面也讲过了其本质就是替换掉原先的数据段和代码段,因此在发生替换的时候就会发生写时拷贝,确保进程之间相互独立。
🧊好了,今天 进程控制的相关内容 到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。
版权归原作者 LinAlpaca 所有, 如有侵权,请联系我们删除。