Linux 进程控制:进程终止与等待・waitpid 选项参数与状态解析(告别僵尸进程)

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、进程等待的必要性

前面我们在进程状态中提到,当子进程结束,父进程不去获取子进程的退出信息,那么此时该子进程就会变成僵尸进程,操作系统依然需要维护相关的数据结构,就会造成内存泄漏。进程等待的一个重要作用就是回收子进程资源,防止内存泄漏,同时,获取子进程的退出信息

bash 复制代码
while :;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记录退出状态(即正常终止信息)

查看终止信号:

bash 复制代码
kill -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()也是这个原理。

😄 创作不易,你的点赞和关注都是对我莫大的鼓励,再次感谢您的观看😘

相关推荐
2501_930799242 小时前
vllm部署时的nginx 配置
运维·nginx·vllm
linux修理工2 小时前
ubuntu 2204 tsinghua
linux·运维·ubuntu
琥珀.2 小时前
查看linux下java服务进程是否正常
java·linux·运维
oMcLin2 小时前
Ubuntu 22.04 无法安装依赖包:解决 apt‑get 错误“Could not resolve”
linux·运维·ubuntu
哈乐2 小时前
信息系统项目管理师(第1章~第5章)
运维
❀͜͡傀儡师2 小时前
docker安装spug运维管理平台
运维·docker·容器
QyynerBoomer2 小时前
Linux进程创建详解
linux·运维·服务器
航Hang*2 小时前
第1章:初识Linux系统——第12节:总复习①
linux·笔记·学习·考试复习