Sunday不上发条在这里祝大家新的一年,bug 退散,需求减半,代码一次跑通,薪资节节攀升!🎉🎉🎉🎉
一、进程终止
即正在执行的程序停止执行,操作系统进行系统资源释放(进程申请的相关内核数据结构和代码数据)。
进程是用来完成某个任务的,所以结束时无非三种情况:
**•**代码运行完毕,结果正确
**•**代码运行完毕,结果不正确
**•**代码异常终止
1、退出码
我们在以前写main函数时,总在最后返回一个0,这个0其实就是退出码。0就表示我们的程序运行完毕,结果正确;结果不正确就可能返回其他非0 的退出码。那怎么查看这些退出码呢?
退出码:进程终止时返回给操作系统一个整数(0~133),用来标识进程的终止状态。
我们可以借助strerror函数,strerror 是 C 标准库中的核心函数(定义在<string.h>头文件),核心作用是将系统的「错误码(errno)」转换为人类可读的字符串描述。


2、常见退出方法
2.1 正常终止
💦 main函数返回
我们可以通过echo &? 查看进程退出码。
💦**_exit**参数:status 定义了进程的终止状态,父进程通过wait来获取该值。
• 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现 返回值是255。
💦exit
📢 那么_exit 和 exit 有什么区别呢?_exit 属于系统调用(内核态底层),而exit 属于标准库中的函数。
exit 在底层调用了 _exit, 但在此之前还进行了执行用户定义的清理函数和刷新缓冲区,关闭流等操作。
💤我们可以通过一个实验验证一下:在printf时我们先不用换行刷新缓冲区
所以说,缓冲区一定不在系统内部,而是库缓冲区,C语言提供。
2.2 异常退出
进程未完成预期功能,因外部信号或内部错误被迫中断,终止状态为「错误」。
💦 ctrl + c,信号终止
收到外部信号:
SIGINT(信号 2):用户在终端按下Ctrl+C,终止前台运行的进程
SIGKILL(信号 9):强制终止进程,无法被捕获或忽略(命令:kill -9 进程PID),用于终止无响应的进程。
程序内部运行错误:进程执行过程中出现无法恢复的逻辑错误,触发内核终止信号,例如:
除零错误(
int a = 1 / 0;);栈溢出(递归调用无终止条件,耗尽栈空间);
自定义类型的非法操作(如未初始化的指针调用成员函数)。
**注意:**当进程发生异常退出,此时退出码无意义。
二、进程等待
1、进程等待的必要性
前面我们在进程状态中提到,当子进程结束,父进程不去获取子进程的退出信息,那么此时该子进程就会变成僵尸进程,操作系统依然需要维护相关的数据结构,就会造成内存泄漏。进程等待的一个重要作用就是回收子进程资源,防止内存泄漏,同时,获取子进程的退出信息。
bashwhile :;do ps axj|head -1 && ps axj|grep ./test|grep -v test.c;sleep 1; done
2、进程等待的方式
2.1 wait 方法
等待任意一个退出的子进程,并返回子进程pid。如果子进程一直不退出,父进程就会一直阻塞在wait 处。

bash
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0) // 子进程
{
int cnt = 5;
while(cnt--)
{
printf("我是一个子进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
exit(0);
}
sleep(10);
pid_t rid = wait(NULL);
if(rid > 0) printf("等待成功,%d\n",rid);
return 0;
}

2.2 waitpid 方法
wait 的升级版,可以用来等待指定进程,这也是为什么fork 创建子进程时要给父进程返回子进程pid的原因。


参数pid 传 -1时等待任意进程,即此时与wait 完全相同。即wait(NULL) 与 waitpid(-1, NULL, 0) 作用完全一样。
利用 waitpid 进行阻塞等待指定的子进程:
bash
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0) // 子进程
{
int cnt = 5;
while(cnt--)
{
printf("我是一个子进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
exit(0);
}
sleep(10);
pid_t rid = waitpid(id,NULL,0);
if(rid > 0) printf("等待成功,%d\n",rid);
return 0;
}

waitpid参数解析:

•*status:
子进程退状态,是一个输出型参数。这不就是我们前面提到的进程终止相关的信息吗。
bash
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<errno.h>
int main()
{
pid_t id = fork();
if(id == 0) // 子进程
{
int cnt = 3;
while(cnt)
{
printf("我是一个子进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) printf("等待成功,rid = %d, status = %d\n",rid, status);
else printf("等待失败,%d : %s\n",errno, strerror(errno));
return 0;
}

❄️子进程明明是exit(1) 退出的,即退出码应该是1,但怎么打印出了256呢?

操作系统中用一个整数的低16个比特位(0~15)来存储进程终止的相关信息,高16位不用。低16个比特位又分为0~6记录终止信号(即进程异常退出信息),第7位为core dump标志(先不谈)以及8~15记录退出状态(即正常终止信息)。
查看终止信号:
bashkill -l
而我们的进程正常结束,所以0~7位均为0,第八位为1,即100000000,转化为10进制即256。
那我们对status 右移8不就是1了嘛,通过 (status >> 8) & 0xFF。就可以获得0~15个比特位。

❄️上面我们的程序正常终止,那进程异常终止呢?
此时子进程会把自己的异常终止信号放在低7个比特位,通过 status & 0x7F 就可以获得status的低7个比特位。
当我们在另外一个窗口杀掉子进程:kill -9 25130
可以看到我们拿到的status 的低7个比特位 即为9,而9 号信号就是我们在杀掉子进程时传递给子进程的终止信号,这也从侧面反映了进程异常终止其实就是收到了信号。
🔊我们还可以继续实验:在代码中写一个除零的操作
子进程异常终止,父进程等待成功,status 的低 7个比特位的值为8,对应8号信号(算术运算错误:除零错误)。
补充:
其实系统提供了两个宏来提取退出信息(底层也是位操作):
WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程 是否是正常退出)
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程 的退出码)
• option:
(1)阻塞模式 (options = 0):父进程调用 **waitpid()**后,若指定子进程未终止 / 未进入僵尸状态,父进程会被挂起(阻塞),直到子进程终止后才继续执行;
例如:scanf(),cin 等等,只要不进行输入,进程就一直会等待用户输入,阻塞。
当我们执行:sleep 100,此时输入指令无响应。
因为sleep 100 就是bash 进程的一个子进程,执行sleep 100,此时bash阻塞等待,所以,输入其他指令无响应。
(2)非阻塞模式 (options = WNOHANG):父进程调用 **waitpid()**后,无论指定子进程是否终止,都会立即返回,不会阻塞等待。
• 返回值大于0:表示等待成功;
• 返回值等于0:表示调用结束,子进程还没有退出;
• 返回值小于0:表示等待失败。
bash
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
while(1)
{
printf("我是一个子进程,我的pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
// 父进程
while(1) // 非阻塞轮询(循环)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG);
if(rid > 0)
{
printf("等待成功,rid = %d, exit code: %d, exit signal: %d\n",rid, WEXITSTATUS(status), WIFEXITED(status));
break; // 等待成功,结束
}
else if(rid == 0)
{
printf("本轮调用结束,子进程还在运行\n");
sleep(1);
// 执行任务...
}
else
{
printf("等待失败,%d : %s\n",errno, strerror(errno));
break; // 等待失败,结束
}
}
return 0;
}

对于非阻塞调用,由于父进程并不知道子进程什么时候能退出,所以父进程需要一直去查看子进程退出信息(循环调用waitpid),即非阻塞轮询。同时,非阻塞调用父进程并不会一直阻塞等待,而是非阻塞轮询,所以父进程在等待过程中可以周期性地做一些自己的事情,这就提高了效率。
++注意:++进程异常终止时,退出码无意义!!!就相当于你考试作弊被抓住,此时考试成绩就是无意义的。
3、进程等待怎么做到的?
首先说明一点:父进程没办法直接拿到子进程的信息。
子进程退出后,变成僵尸进程,此时子进程的PCB(task_struct)仍被操作系统维护,而子进程的退出信息存储在子进程的 task_struct结构体对象 中,父进程无法直接拿到子进程退出信息。所以通过 waitpid 这样的系统调用接口间接获得。

前面提到的getpid(),getppid()也是这个原理。
😄 创作不易,你的点赞和关注都是对我莫大的鼓励,再次感谢您的观看😘



















