子进程信号处理

发布于:2025-07-25 ⋅ 阅读:(29) ⋅ 点赞:(0)

SIGCHLD 信号详解


一、信号定义与作用

SIGCHLD‌ 是 UNIX/Linux 系统中由内核向父进程发送的信号,用于通知子进程的状态变化(如终止、停止或恢复)‌。其主要作用包括:

  1. 回收子进程资源‌:避免子进程终止后成为僵尸进程(Zombie Process)占用系统资源‌。
  2. 监控子进程状态‌:父进程可响应子进程的异常退出、暂停或恢复事件‌。

二、触发条件

当子进程发生以下状态变化时,内核会向父进程发送 SIGCHLD 信号:

  1. 正常终止或异常退出‌(如调用 exit() 或被 SIGKILL 终止)‌。
  2. 被作业控制信号暂停‌(如 SIGSTOPSIGTSTP)‌。
  3. 由暂停状态恢复运行‌(收到 SIGCONT 信号)‌。

信号编号‌:17
默认行为‌:系统级忽略(不回收资源,可能导致僵尸进程)‌。


三、信号处理方式

1. ‌自定义信号处理函数

父进程需注册处理函数,通过 waitpid 回收子进程:

#include <signal.h>
#include <sys/wait.h> 

void handler(int sig) 
{
    int status;
    while (waitpid(-1, &status, WNOHANG) > 0); // 非阻塞回收所有终止子进程 
} 

int main() 
{
    signal(SIGCHLD, handler); // 注册处理函数 //
    ... 创建子进程 ... 
}
  • 关键点‌:
    • 使用 WNOHANG 标志循环调用 waitpid,防止多个子进程同时退出时信号丢失‌。
    • 必须在创建子进程前注册处理函数,避免竞争条件导致信号遗漏‌。
2. ‌显式忽略信号(自动回收)
signal(SIGCHLD, SIG_IGN); // 内核自动回收子进程资源
  • 注意‌:
    • 此为 Linux 特有行为,不符合 POSIX 标准,其他 UNIX 系统可能不支持‌。
    • 子进程停止时不再发送信号(BSD 系统行为差异)‌。

四、注意事项与风险

  1. 信号不排队问题
    多个子进程同时退出可能仅触发一次 SIGCHLD,处理函数需循环调用 waitpid 直至返回错误‌。
  2. 系统调用中断
    慢速系统调用(如 read)可能被信号中断,需手动重启或检查 EINTR 错误码‌。
  3. 高并发场景优化
    服务器中频繁的子进程退出可能影响性能,可通过忽略信号或批量回收策略优化‌。

五、典型应用场景

  1. 服务端进程监控
    Web 服务器父进程捕获子进程异常退出信号,实现自动重启机制‌。
  2. 交互式 Shell 作业控制
    Shell 通过 SIGCHLD 跟踪后台进程状态(如 jobs 命令)‌。

跨系统差异提示‌:

  • BSD 系统中设置 SA_NOCLDSTOP 标志后,子进程停止时不发送信号‌。
  • Python 等语言的信号处理遵循操作系统实现,需注意接口兼容性‌。

sigprocmask函数介绍

sigprocmask 是 UNIX/Linux 系统中用于管理进程信号屏蔽字的系统调用,通过控制信号的阻塞状态实现关键代码段的保护或信号同步。以下是其核心机制与应用详解:


一、函数原型与参数

#include <signal.h> 

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 参数说明
    • how:操作类型,可选值:
      • SIG_BLOCK:将 set 中的信号加入当前屏蔽集(阻塞)‌。
      • SIG_UNBLOCK:从当前屏蔽集中移除 set 中的信号(解除阻塞)‌。
      • SIG_SETMASK:直接替换当前屏蔽集为 set 指定的信号集‌。
    • set:指向待操作的信号集,若为 NULL 则不修改屏蔽集‌。
    • oldset:保存原屏蔽集,可为 NULL‌。
  • 返回值‌:成功返回 0,失败返回 -1 并设置 errno‌。

二、核心功能与原理

  1. 信号屏蔽机制
    • 每个进程拥有独立的信号屏蔽字(Signal Mask),决定哪些信号被临时阻塞‌。
    • 被阻塞的信号处于 ‌未决(pending)‌ 状态,直到解除阻塞后才会递送‌。
  2. 不可阻塞的信号
    • SIGKILL 和 SIGSTOP 无法被阻塞或忽略(强制终止/暂停进程)‌。

三、典型应用场景

  1. 保护临界区代码
    阻塞信号防止中断共享数据修改等关键操作‌67。
    sigset_t mask; 
    sigemptyset(&mask); 
    sigaddset(&mask, SIGINT); 
    sigprocmask(SIG_BLOCK, &mask, NULL); // 阻塞SIGINT 
    /* 临界区代码 */ 
    sigprocmask(SIG_UNBLOCK, &mask, NULL); // 恢复
  2. 多线程信号控制
    结合 pthread_sigmask 实现线程级信号屏蔽‌。
  3. 与 sigsuspend 配合
    临时修改屏蔽字并挂起进程,等待特定信号‌。

四、注意事项

  1. 信号丢失风险
    长时间阻塞可能导致非实时信号被丢弃‌。
  2. 异步信号安全
    避免在信号处理函数中调用非安全函数(如 printf)‌。
  3. 多线程环境
    sigprocmask 仅影响调用线程的屏蔽字,需使用 pthread_sigmask 控制进程级信号‌。

五、示例代码

以下代码演示阻塞 SIGINT 并恢复原屏蔽集:

#include <signal.h>
#include <stdio.h>

int main() {
    sigset_t new_mask, old_mask;
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGINT);

    // 阻塞SIGINT并保存原屏蔽集
    if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) == -1) {
        perror("sigprocmask");
        return 1;
    }

    printf("SIGINT blocked. Press Ctrl+C to test.\n");
    sleep(5); // 模拟关键操作

    // 恢复原屏蔽集
    sigprocmask(SIG_SETMASK, &old_mask, NULL);
    printf("SIGINT unblocked.\n");
    return 0;
}

逻辑流程‌:
初始屏蔽集 → SIG_BLOCK → 信号阻塞(pending)→ SIG_UNBLOCK/SIG_SETMASK → 恢复递送‌。

例:

static void
block_sigchild (void)
{
  sigset_t mask;
  int status;

  sigemptyset (&mask);
  sigaddset (&mask, SIGCHLD);

  if (sigprocmask (SIG_BLOCK, &mask, NULL) == -1)
    die_with_error ("sigprocmask");

  /* Reap any outstanding zombies that we may have inherited */
  while (waitpid (-1, &status, WNOHANG) > 0)
    ;
}

这段代码实现了对 SIGCHLD 信号的阻塞和僵尸进程清理功能,主要用于防止子进程状态变化干扰主程序执行。以下是分步解析:

  1. 信号阻塞部分

    • sigemptyset(&mask) 初始化空的信号集
    • sigaddset(&mask, SIGCHLD) 将 SIGCHLD 信号加入信号集
    • sigprocmask(SIG_BLOCK, &mask, NULL) 阻塞该信号,防止子进程退出中断主流程
    • 若阻塞失败调用 die_with_error 报错退出
  2. 僵尸进程清理部分

    • while (waitpid(-1, &status, WNOHANG) > 0) 循环非阻塞地回收所有已终止子进程
    • WNOHANG 参数确保没有僵尸进程时立即返回
    • 通过 status 参数丢弃子进程退出状态(未处理)

典型应用场景:

  • 在守护进程或服务端程序中,避免子进程退出信号干扰主事件循环
  • 防止未处理的 SIGCHLD 导致大量僵尸进程积累

注意:

  • 该实现会丢弃所有子进程退出状态,如需处理返回值需修改 waitpid 逻辑
  • 长期运行程序可能需要定期调用此函数清理新产生的僵尸进程

网站公告

今日签到

点亮在社区的每一天
去签到