Linux系统:详解进程等待wait与waitpid解决僵尸进程

本节重点

  • 理解进程等待的相关概念
  • 掌握系统调用wait与waitpid的使用方法
  • 输出型status参数的存储结构
  • 阻塞等待与非阻塞等待

一、概念

进程等待是操作系统中父进程与子进程协作的核心机制,指父进程通过特定方式等待子进程终止并回收其资源的过程。这一机制的主要目的是避免子进程成为僵尸进程,从而释放系统资源并维护进程表的完整性。

二、进程等待的方法

2.1 wait与waitpid

2.1.1 wait

wait 是一个用于进程控制的系统调用,主要用于父进程等待其任意一个子进程终止,并回收子进程的资源(如进程表项)。

函数原型:

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

参数:

status :输出型参数,一个指向整数的指针,用于存储子进程的终止状态。如果不需要关心子进程的退出状态信息,可以传入NULL。

返回值:

成功时返回终止的子进程的PID,失败时返回-1并设置errno。

代码示例:

cpp 复制代码
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>

int main()
{
    //创建子进程:
    pid_t id=fork();
    if(id==0)
    {
        //child
        int i=5;
        while(i)
        {
            printf("我是一个子进程,PID->%d,PPID->%d\n",getpid(),getppid());
            sleep(1);
            i--;
        }
        exit(10);
    }
    else if(id>0)
    {
        //parent
        int ret=0;
        printf("我是一个父进程,PID->%d\n",getpid());
        int _ret=wait(&ret);
        if(_ret>0)
        {
            printf("wait success, child PID->%d,exitcode->%d\n",_ret,ret);
        }
        else
        {
            printf("wait fail,error->%d\n",errno);
        }
    }
    else
    {
        //id<0
        printf("fork fail\n");
    }
    return 0;
}

运行结果:

注意事项:

系统调用wait默认等待方式是阻塞等待,也就是说当父进程调用 wait 或waitpid 时,它会阻塞在调用wait的一段代码行,直到有一个子进程终止父进程解除阻塞状态后才会执行后续代码。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        printf("Child process: PID = %d\n", getpid());
        sleep(2); // 模拟子进程工作
        exit(0);
    } else if (pid > 0) {
        // 父进程
        int status;
        printf("Parent process: Waiting for child to terminate...\n");
        wait(&status); // 阻塞,直到子进程终止
        printf("Child process terminated with status: %d\n", WEXITSTATUS(status));
    } else {
        perror("fork failed");
    }
    return 0;
}

2.1.2 waitpid

waitpid 是 Unix/Linux 系统中用于等待子进程状态变化的系统调用,是 wait 的增强版本。它允许父进程更灵活地控制等待行为,可以指定等待特定的子进程或进程组,并支持阻塞和非阻塞模式。

函数原型:

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

参数解析:

**pid:**指定要等待的子进程

  • pid > 0:等待进程 ID 为 pid 的子进程。
  • pid == 0:等待与调用进程同组的任意子进程。
  • pid < -1:等待进程组 ID 为 |pid| 的任意子进程。
  • pid == -1:等待任意子进程(等效于 wait)。

status:指向一个整数用来存储子进程的退出状态

options:控制waitpid的行为,可以是以下标志的按位或:

  • 0:阻塞模式,等待子进程终止。
  • WNOHANG:非阻塞模式,如果没有子进程退出,立即返回。
  • WUNTRACED:报告已停止但未终止的子进程状态。
  • WCONTINUED:报告已继续运行的子进程状态。

返回值:

成功:

  • 返回被等待子进程的 PID。
  • 如果没有子进程满足条件且设置了 WNOHANG,返回 0

失败:

  • 返回 -1,并设置 errno 指示错误原因。

代码示例:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define N 3

int main() {
    pid_t pids[N];
    int i;

    // 创建多个子进程
    for (i = 0; i < N; i++) {
        pids[i] = fork();
        if (pids[i] == 0) {
            // 子进程
            printf("Child %d: PID = %d\n", i, getpid());
            sleep(i + 1);
            exit(i);
        }
    }

    // 父进程等待所有子进程终止
    for (i = 0; i < N; i++) {
        int status;
        pid_t result = waitpid(pids[i], &status, 0);
        if (result > 0) {
            if (WIFEXITED(status)) {
                printf("Child %d exited with status: %d\n", i, WEXITSTATUS(status));
            }
        } else {
            perror("waitpid failed");
        }
    }

    return 0;
}

运行结果:

cpp 复制代码
Child 0: PID = 12346
Child 1: PID = 12347
Child 2: PID = 12348
Child 0 exited with status: 0
Child 1 exited with status: 1
Child 2 exited with status: 2

注意事项:

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源获得子进程的退出信息。

如果调用wait/waitpid时,子进程存在且正常运行,则进程可能会阻塞。

如果不存在该子进程,则立即出错返回。

2.3 获取子进程的status

在Linux系统中,父进程通过wait/waitpid来等待和回收子进程的相关资源并通过status参数返回子进程的退出信息,该参数是一个输出型参数由操作系统填充。

如果传递NULL表示不关心子进程的退出信息。

2.3.1 status参数的存储结构

ststus 不能简单当作整型来看待,可以当作位图来看待,其中包含了子进程的退出码与终止信号,如下图:

其低 16 位用于存储子进程的退出状态信息。具体来说:

  • 低 7 位:表示导致子进程终止的信号编号(如果子进程被信号终止)。
  • 第 8 位:表示是否生成了核心转储文件(core dump)。
  • 高 8 位:表示子进程的退出码(如果子进程正常退出)。

注意:

当进程正常终止时,低7位和第8位都为0,高8位存储子进程的退出码,可以通过位运算(ststus>>8)&0×FF或通过系统提供的宏来获取。

当进程异常终止或被信号所终止时,退出状态无意义故高8位都为0,终止信号存储在低7位中可以通过位运算status&0×7F或通过宏来获取。

2.3.2 用于解析ststus的宏

以下宏定义在<sys/wait.h>文件中,用于解析返回后的status参数。

子进程是否正常退出:

cpp 复制代码
WIFEXITED(status)
  • 如果子进程正常退出(例如调用了 exit( ) 或 return ),则返回非零值(真)。
  • 否则返回0(假)。

获取子进程的退出码:

cpp 复制代码
WEXITSTATUS(status)
  • 如果 WEXITSTATUS(status) 返回真,则可以使用此宏获取子进程的退出码。
  • 退出码是子进程调用 exit(code) 或 return code 时指定的值。

检查子进程是否被信号终止(异常退出):

cpp 复制代码
WIFSIGNALED(status)

如果子进程被信号终止返回非零,否则返回0。

获取导致子进程终止的信号编号:

cpp 复制代码
WTERMSIG(status)

如果WTERMSIG(ststus)返回真,则可以使用此宏获取导致子进程终止的信号编号。

2.4 阻塞等待与非阻塞等待

cpp 复制代码
pid_t waitpid(pid_t pid, int *status, int options);

系统调用waitpid的第三个参数options用来设置阻塞等待与非阻塞等待,默认为0表示阻塞等待,当传递宏WNHANG时表示非阻塞等待。

阻塞等待:

当waitpid处于阻塞等待时进程会阻塞于调用waitpid的代码段处(停止执行),直到等待的进程终止,waitpid才会返回继续执行后续代码。

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
int main()
{
    pid_t id=fork();
    int status;
    if(id==0)
    {
        //child
        printf("我是一个子进程,我的PID是%d,父进程PID%d\n",getpid(),getppid());
        sleep(10);
        exit(0);
    }
    else if(id>0)
    {
        //parent
        pid_t ret=waitpid(id,&status,0);
        if(ret>0)
        {
            printf("wait success!\n");
        }
        else
        {
            printf("wait fail!\n");
        }
        printf("这是父进程的后续代码\n");
    }
    else
    {
        perror("fork fail!");
    }
    return 0;
}

运行结果:

非阻塞等待:

当waitpid处于非阻塞状态时,进程不会停止执行自己的代码,但waitpid会不断查询相应进程的状态信息直到被等待进程终止,期间waitpid的返回值一直是0。

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>

//函数指针类型:
typedef void (*func_t)();
#define NUM 5
func_t handers[NUM+1];

void DownLoad()
{
    printf("我是一个下载的任务\n");
}
void Flush()
{
    printf("我是一个刷新的任务\n");
}
void Log()
{
    printf("我是一个记录日志的任务\n");
} 

//注册
void registerhanders(func_t h[],func_t f)
{
    int i=0;
    for(;i<NUM;i++)
    {
        if(h[i]==NULL)
        {
            break;
        }
    }
    if(i==NUM)
    {
        return;
    }
    h[i]=f;
    h[i+1]=NULL;
}
int main()
{
    //注册函数:
    registerhanders(handers,DownLoad);
    registerhanders(handers,Flush);
    registerhanders(handers,Log);


    pid_t id=fork();
    int status;
    if(id==0)
    {
        //child
        printf("我是一个子进程,我的PID是%d,父进程PID为%d\n",getpid(),getppid());
        sleep(5);
    }
    else if(id>0)
    {
        //parent
        while(1)
        {
            int ret=waitpid(id,&status,WNOHANG);
            if(ret==0)
            {
                printf("轮询结束,子进程未退出!\n");
            }
            else if(ret>0)
            {
                printf("轮询结束,子进程退出!\n");
                break;
            }
            else
            {
                //ret<0
                perror("waitpid fail!\n");
                break;
            }
            sleep(1);
            //printf("这是父进程代码!\n");
            for(int i=0;i<NUM;i++)
            {
                if(handers[i]!=NULL)
                handers[i]();
            }
       }
    }
    else
    {
        //id<0
        //fork fail!
        perror("fork fail!");
    }
    return 0;
}

如上述所示就是一段非阻塞进程等待的代码,父进程除了一次次查询子进程的进程状态之外也在执行自己的三个函数任务(下载任务,刷新任务,记录日志的任务),当子进程退出后父进程会立即回收子进程资源,接着整段程序结束。

运行结果:

相关推荐
涛ing41 分钟前
【Linux “less“ 命令详解】
linux·运维·c语言·c++·人工智能·vscode·bash
黑金IT2 小时前
如何在 Electron 应用中安全地进行主进程与渲染器进程通信
服务器·安全·electron
林木木木木木木木木木3 小时前
【随身WiFi】随身WiFi Debian系统优化教程
linux·运维·debian·随身wifi
杨浦老苏3 小时前
Docker应用端口查看器docker-port-viewer
运维·docker·群晖
临观_3 小时前
打靶日记 zico2: 1
linux·网络安全
AI大模型顾潇3 小时前
[特殊字符] AI 大模型的 Prompt Engineering 原理:从基础到源码实践
运维·人工智能·spring·自然语言处理·自动化·大模型·prompt
痆古酊旳琲伤3 小时前
Linux驱动开发1 - Platform设备
linux·驱动开发
周Echo周3 小时前
16、堆基础知识点和priority_queue的模拟实现
java·linux·c语言·开发语言·c++·后端·算法
柳如烟@4 小时前
基于GTID的主从复制
运维·mysql·gdit
谢斯4 小时前
【jenkins】首次配置jenkins
运维·jenkins