waitid 系统调用及示例

好的,我们来深入学习 waitid 系统调用

1. 函数介绍

在 Linux 系统中,当一个进程创建了子进程(使用 fork),父进程通常需要知道子进程何时结束(退出或被终止),以及它是如何结束的(正常退出码、被哪个信号杀死等)。这是进程管理和资源回收的重要环节。

waitid 系统调用就是用来让父进程(或具有适当权限的进程)等待一个或一组子进程的状态发生变化,并获取该变化的详细信息

你可以把它想象成一个“进程状态监听器”。父进程调用 waitid 后,它会挂起(阻塞),直到它感兴趣的子进程发生了指定类型的事件(比如退出、被信号终止、停止、继续等)。当事件发生时,waitid 会返回,并把详细信息(哪个子进程、如何结束的)填充到一个结构体中。

waitid 相比于老一些的 wait 和 waitpid,提供了更强大和灵活的功能

简单来说,waitid 就是让你用程序来“等待”并“获取”子进程的“死亡/停止/恢复”通知书,并且通知书上写得非常详细。

2. 函数原型

#include <sys/wait.h> // 包含 waitid 函数声明和相关常量

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

3. 功能

挂起调用进程,直到由 idtype 和 id 指定的一个或多个子进程的状态发生变化(变化类型由 options 指定)。当满足条件的子进程状态改变时,将详细的状态信息填充到 infop 指向的 siginfo_t 结构体中。

4. 参数详解

  • idtype:
    • idtype_t 类型。
    • 指定要等待的进程的类型。它决定了 id 参数的含义。常见的值有:
      • P_PID: 等待由 id 指定的特定进程 ID (PID) 的子进程。
      • P_PGID: 等待进程组 ID (PGID) 等于 id 的所有子进程。
      • P_ALL: 等待调用进程的所有子进程(此时 id 参数被忽略)。
  • id:
    • id_t 类型。
    • 其含义由 idtype 决定:
      • 如果 idtype 是 P_PID,则 id 是要等待的子进程的 PID。
      • 如果 idtype 是 P_PGID,则 id 是要等待的子进程组的 PGID。
      • 如果 idtype 是 P_ALL,则 id 被忽略(通常设为 0)。
  • infop:
    • siginfo_t * 类型。
    • 一个指向 siginfo_t 结构体的指针。当 waitid 成功返回时,该结构体会被内核填充为关于已改变状态的子进程的详细信息。
    • siginfo_t 结构体包含很多字段,关键的有:
      • si_pid: 导致状态改变的子进程的 PID。
      • si_status: 子进程的退出状态或导致其状态改变的信号编号。
      • si_code: 状态改变的原因代码,例如:
        • CLD_EXITED: 子进程通过 exit() 或从 main 返回正常退出。
        • CLD_KILLED: 子进程被信号杀死。
        • CLD_DUMPED: 子进程被信号杀死并产生了核心转储 (core dump)。
        • CLD_STOPPED: 子进程被信号(如 SIGSTOP)停止。
        • CLD_CONTINUED: 子进程从停止状态被 SIGCONT 信号恢复继续运行。
      • … 还有其他字段。
  • options:
    • int 类型。
    • 一个位掩码,用于指定要等待的状态变化类型以及调用的行为。可以是以下值的按位或 (|) 组合:
      • 状态类型 (必须至少指定一个):
        • WEXITED: 等待子进程正常退出(调用 exit() 或从 main 返回)。
        • WSTOPPED: 等待子进程被停止(通常是收到 SIGSTOPSIGTSTPSIGTTINSIGTTOU 信号)。
        • WCONTINUED: 等待被停止的子进程恢复运行(收到 SIGCONT 信号)。
      • 行为标志 (可选):
        • WNOHANG非阻塞。如果没有任何子进程的状态符合条件,waitid 立即返回 0,而不挂起调用进程。
        • WNOWAIT不收割。获取子进程状态信息,但不将其从内核的子进程表中删除。这意味着后续的 wait 调用仍可能获取到该子进程的信息。

5. 返回值

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 错误码 (errno)

  • ECHILD: 没有符合条件的子进程。例如,指定了一个不存在的 PID,或者使用 WNOHANG 时没有子进程处于可收割状态。
  • EINTR: 系统调用被信号中断。
  • EINVALidtype 或 options 参数无效。

7. 相似函数或关联函数

  • wait: 最基础的等待子进程退出的函数。它等待任意一个子进程退出,并返回 PID 和状态码(需要使用宏如 WIFEXITEDWEXITSTATUS 等来解析)。pid_t wait(int *wstatus);
  • waitpidwait 的增强版。允许指定等待特定 PID 的子进程,或使用 WNOHANG 等选项。pid_t waitpid(pid_t pid, int *wstatus, int options);
  • wait3 / wait4: 更老的函数,功能与 waitpid 类似,但可以额外返回资源使用信息(struct rusage)。
  • siginfo_twaitid 使用的关键数据结构,包含详细的子进程状态信息。

8. 示例代码

下面的示例演示了如何使用 waitid 来等待不同类型的子进程事件。

#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>   // 包含 waitid, siginfo_t 等
#include <signal.h>     // 包含 kill, SIG* 常量
#include <string.h>
#include <errno.h>

// 辅助函数:打印 siginfo_t 中的信息
void print_siginfo(const siginfo_t *info) {
    printf("  Child PID: %d\n", info->si_pid);
    printf("  Signal/Exit Code: %d\n", info->si_status);
    printf("  Reason Code: ");
    switch (info->si_code) {
        case CLD_EXITED:
            printf("CLD_EXITED (Child called exit())\n");
            printf("    Exit Status: %d\n", info->si_status);
            break;
        case CLD_KILLED:
            printf("CLD_KILLED (Child was killed by signal)\n");
            printf("    Signal Number: %d\n", info->si_status);
            break;
        case CLD_DUMPED:
            printf("CLD_DUMPED (Child killed by signal and dumped core)\n");
            printf("    Signal Number: %d\n", info->si_status);
            break;
        case CLD_STOPPED:
            printf("CLD_STOPPED (Child was stopped by signal)\n");
            printf("    Stop Signal Number: %d\n", info->si_status);
            break;
        case CLD_CONTINUED:
            printf("CLD_CONTINUED (Child continued)\n");
            // si_status for CLD_CONTINUED is not defined to be meaningful
            break;
        default:
            printf("Unknown reason code: %d\n", info->si_code);
            break;
    }
}

int main() {
    pid_t pid1, pid2, pid3;
    siginfo_t info;

    printf("--- Demonstrating waitid ---\n");
    printf("Parent PID: %d\n", getpid());

    // 1. 创建一个会正常退出的子进程
    pid1 = fork();
    if (pid1 == 0) {
        // --- Child 1 ---
        printf("[Child 1, PID %d] Running for 3 seconds then exiting with status 42.\n", getpid());
        sleep(3);
        exit(42);
    }

    // 2. 创建一个会被信号杀死的子进程
    pid2 = fork();
    if (pid2 == 0) {
        // --- Child 2 ---
        printf("[Child 2, PID %d] Running for 5 seconds then will be killed by SIGTERM.\n", getpid());
        sleep(5);
        // 这行不会执行到
        exit(0);
    }

    // 3. 创建一个会停止和恢复的子进程
    pid3 = fork();
    if (pid3 == 0) {
        // --- Child 3 ---
        printf("[Child 3, PID %d] Running, then will stop and continue.\n", getpid());
        printf("[Child 3] Entering loop, press Ctrl+Z in another terminal to stop me (if I'm foreground).\n");
        printf("[Child 3] Or, the parent will send SIGSTOP and SIGCONT.\n");
        int counter = 0;
        while (counter < 10) {
            printf("[Child 3] Working... %d\n", counter++);
            sleep(1);
        }
        printf("[Child 3] Finished normally.\n");
        exit(100);
    }

    // --- Parent Process ---
    printf("[Parent] Created children: PID1=%d, PID2=%d, PID3=%d\n", pid1, pid2, pid3);

    // 稍等一下,让子进程启动
    sleep(1);

    // 4. 向 Child 3 发送 SIGSTOP 使其停止
    printf("\n[Parent] Sending SIGSTOP to Child 3 (PID %d)...\n", pid3);
    if (kill(pid3, SIGSTOP) == -1) {
        perror("[Parent] kill SIGSTOP");
    }

    // 等待 Child 3 停止
    printf("[Parent] Waiting for Child 3 to stop using waitid(WSTOPPED)...\n");
    memset(&info, 0, sizeof(info)); // 清零结构体
    if (waitid(P_PID, pid3, &info, WSTOPPED) == -1) {
        perror("[Parent] waitid for stop");
    } else {
        printf("[Parent] Detected Child 3 stopped:\n");
        print_siginfo(&info);
    }

    // 5. 等待 Child 1 正常退出
    printf("\n[Parent] Waiting for Child 1 to exit using waitid(WEXITED)...\n");
    memset(&info, 0, sizeof(info));
    if (waitid(P_PID, pid1, &info, WEXITED) == -1) {
        perror("[Parent] waitid for exit pid1");
    } else {
        printf("[Parent] Detected Child 1 exited:\n");
        print_siginfo(&info);
    }

    // 6. 向 Child 2 发送 SIGTERM 使其终止
    printf("\n[Parent] Sending SIGTERM to Child 2 (PID %d)...\n", pid2);
    if (kill(pid2, SIGTERM) == -1) {
        perror("[Parent] kill SIGTERM");
    }

    // 等待 Child 2 被杀死
    printf("[Parent] Waiting for Child 2 to be killed using waitid(WEXITED)...\n");
    memset(&info, 0, sizeof(info));
    if (waitid(P_PID, pid2, &info, WEXITED) == -1) {
        perror("[Parent] waitid for exit pid2");
    } else {
        printf("[Parent] Detected Child 2 killed/exited:\n");
        print_siginfo(&info);
    }

    // 7. 向 Child 3 发送 SIGCONT 使其恢复
    printf("\n[Parent] Sending SIGCONT to Child 3 (PID %d)...\n", pid3);
    if (kill(pid3, SIGCONT) == -1) {
        perror("[Parent] kill SIGCONT");
    }

    // 等待 Child 3 恢复运行 (这个可能不会立即发生,取决于子进程何时真正恢复)
    // 更常见的是等待它最终退出
    printf("[Parent] Waiting for Child 3 to continue and then exit using waitid(WCONTINUED | WEXITED)...\n");
    printf("[Parent] (WCONTINUED detection might be unreliable, waiting for exit instead)\n");
    memset(&info, 0, sizeof(info));
    // 通常我们只等待最终的退出
    if (waitid(P_PID, pid3, &info, WEXITED) == -1) {
        perror("[Parent] waitid for exit pid3");
    } else {
        printf("[Parent] Detected Child 3 exited:\n");
        print_siginfo(&info);
    }

    // 8. 演示 WNOHANG (非阻塞)
    printf("\n[Parent] Demonstrating WNOHANG...\n");
    printf("[Parent] Calling waitid(P_ALL, 0, info, WEXITED | WNOHANG)...\n");
    memset(&info, 0, sizeof(info));
    int result = waitid(P_ALL, 0, &info, WEXITED | WNOHANG);
    if (result == -1) {
        perror("[Parent] waitid WNOHANG");
    } else if (result == 0) {
        // 如果返回 0,表示成功调用,但没有符合条件的子进程状态改变
        // 因为我们已经等待了所有子进程退出,所以这里应该没有更多可收割的
        printf("[Parent] WNOHANG returned 0: No children available to wait for.\n");
    }

    printf("\n[Parent] All children have been waited for. Parent exiting.\n");

    printf("\n--- Summary ---\n");
    printf("1. waitid(idtype, id, infop, options) waits for child process state changes.\n");
    printf("2. idtype/id let you specify which child/children to wait for (PID, PGID, ALL).\n");
    printf("3. options specify what events to wait for (WEXITED, WSTOPPED, WCONTINUED).\n");
    printf("4. WNOHANG makes it non-blocking. WNOWAIT gets status without reaping.\n");
    printf("5. infop (siginfo_t*) provides detailed information about the event.\n");
    printf("6. It's more flexible and informative than wait/waitpid.\n");

    return 0;
}

9. 编译和运行

# 假设代码保存在 waitid_example.c 中
gcc -o waitid_example waitid_example.c

# 运行程序
./waitid_example

10. 预期输出

--- Demonstrating waitid ---
Parent PID: 12345
[Child 1, PID 12346] Running for 3 seconds then exiting with status 42.
[Child 2, PID 12347] Running for 5 seconds then will be killed by SIGTERM.
[Child 3, PID 12348] Running, then will stop and continue.
[Child 3] Entering loop, press Ctrl+Z in another terminal to stop me (if I'm foreground).
[Child 3] Or, the parent will send SIGSTOP and SIGCONT.
[Parent] Created children: PID1=12346, PID2=12347, PID3=12348

[Parent] Sending SIGSTOP to Child 3 (PID 12348)...
[Parent] Waiting for Child 3 to stop using waitid(WSTOPPED)...
[Child 3] Working... 0
[Child 3] Working... 1
[Parent] Detected Child 3 stopped:
  Child PID: 12348
  Signal/Exit Code: 19
  Reason Code: CLD_STOPPED (Child was stopped by signal)
    Stop Signal Number: 19

[Parent] Waiting for Child 1 to exit using waitid(WEXITED)...
[Child 1] Running for 3 seconds then exiting with status 42.
[Parent] Detected Child 1 exited:
  Child PID: 12346
  Signal/Exit Code: 42
  Reason Code: CLD_EXITED (Child called exit())
    Exit Status: 42

[Parent] Sending SIGTERM to Child 2 (PID 12347)...
[Parent] Waiting for Child 2 to be killed using waitid(WEXITED)...
[Child 2] Running for 5 seconds then will be killed by SIGTERM.
[Parent] Detected Child 2 killed/exited:
  Child PID: 12347
  Signal/Exit Code: 15
  Reason Code: CLD_KILLED (Child was killed by signal)
    Signal Number: 15

[Parent] Sending SIGCONT to Child 3 (PID 12348)...
[Parent] Waiting for Child 3 to continue and then exit using waitid(WCONTINUED | WEXITED)...
[Parent] (WCONTINUED detection might be unreliable, waiting for exit instead)
[Child 3] Working... 2
[Child 3] Working... 3
[Child 3] Working... 4
[Child 3] Working... 5
[Child 3] Working... 6
[Child 3] Working... 7
[Child 3] Working... 8
[Child 3] Working... 9
[Child 3] Finished normally.
[Parent] Detected Child 3 exited:
  Child PID: 12348
  Signal/Exit Code: 100
  Reason Code: CLD_EXITED (Child called exit())
    Exit Status: 100

[Parent] Demonstrating WNOHANG...
[Parent] Calling waitid(P_ALL, 0, info, WEXITED | WNOHANG)...
[Parent] WNOHANG returned 0: No children available to wait for.

[Parent] All children have been waited for. Parent exiting.

--- Summary ---
1. waitid(idtype, id, infop, options) waits for child process state changes.
2. idtype/id let you specify which child/children to wait for (PID, PGID, ALL).
3. options specify what events to wait for (WEXITED, WSTOPPED, WCONTINUED).
4. WNOHANG makes it non-blocking. WNOWAIT gets status without reaping.
5. infop (siginfo_t*) provides detailed information about the event.
6. It's more flexible and informative than wait/waitpid.

11. 总结

waitid 是一个功能强大且信息丰富的系统调用,用于等待子进程状态变化。

  • 核心优势
    • 灵活性高:可以精确指定等待哪个进程/进程组,以及等待哪种类型的事件(退出、停止、恢复)。
    • 信息详细:通过 siginfo_t 结构体返回非常详细的子进程状态信息,比 wait/waitpid 的 wstatus 整数更易于理解和使用。
    • 功能完整:支持停止/恢复事件的等待(WSTOPPEDWCONTINUED)。
  • 参数idtype/id 定义范围,options 定义事件类型和行为,infop 接收结果。
  • 使用场景
    • 需要精确控制等待哪个子进程。
    • 需要区分子进程是正常退出、被信号杀死还是停止/恢复。
    • 编写复杂的进程管理器或守护进程。
  • 与 wait/waitpid 的关系
    • wait(&status) 基本等价于 waitpid(-1, &status, 0)
    • waitpid(pid, &status, options) 功能是 waitid 的子集。
    • waitid 提供了 waitpid 所没有的 WSTOPPED/WCONTINUED 等选项(除非使用非标准扩展),以及更详细的信息返回方式。
此条目发表在未分类分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注