当前位置: 首页 > article >正文

Linux系统中处理子进程的终止问题

1. 理解子进程终止的机制

在Unix/Linux系统中,当子进程终止时,会向父进程发送一个SIGCHLD信号。父进程需要捕捉这个信号,并通过调用wait()waitpid()等函数来回收子进程的资源。这一过程被称为“回收僵尸进程”。

如果父进程没有及时调用wait()或相关函数,子进程将会成为僵尸进程,占用系统资源,直到父进程终止或调用相应的等待函数。

2. 使用wait()waitpid()函数

  • wait():使父进程阻塞,直到任一子进程终止。它会返回终止子进程的PID,并存储子进程的退出状态。
  • waitpid():提供更精细的控制,可以等待特定的子进程或采用非阻塞方式。
示例代码:阻塞等待子进程终止
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {
        std::cerr << "Fork failed!" << std::endl;
        return 1;
    }
    else if (pid == 0) {
        // 子进程执行的代码
        std::cout << "子进程PID:" << getpid() << " 启动。" << std::endl;
        sleep(2);  // 模拟子进程工作
        std::cout << "子进程PID:" << getpid() << " 结束。" << std::endl;
        return 42;  // 子进程以状态42退出
    }
    else {
        // 父进程执行的代码
        std::cout << "父进程PID:" << getpid() << " 等待子进程结束。" << std::endl;
        int status;
        pid_t terminated_pid = wait(&status);  // 阻塞等待任一子进程结束

        if (terminated_pid > 0) {
            if (WIFEXITED(status)) {
                std::cout << "子进程PID:" << terminated_pid 
                          << " 以状态 " << WEXITSTATUS(status) << " 退出。" << std::endl;
            }
            else if (WIFSIGNALED(status)) {
                std::cout << "子进程PID:" << terminated_pid 
                          << " 被信号 " << WTERMSIG(status) << " 终止。" << std::endl;
            }
        }
        else {
            std::cerr << "等待子进程失败。" << std::endl;
        }
    }

    return 0;
}
输出示例:
父进程PID:12345 等待子进程结束。
子进程PID:12346 启动。
子进程PID:12346 结束。
子进程PID:12346 以状态 42 退出。
代码解释:
  1. 父进程调用fork()创建子进程。
  2. 子进程执行自己的任务后,以状态42退出。
  3. 父进程调用wait()阻塞等待子进程结束,并获取子进程的退出状态。
  4. 父进程通过宏WIFEXITEDWEXITSTATUS判断子进程是否正常退出及其退出状态。

3. 使用SIGCHLD信号处理异步回收

为了避免父进程被阻塞,可以通过信号处理函数异步处理子进程的终止。这在需要父进程继续执行其他任务时非常有用。

示例代码:使用SIGCHLD处理子进程终止
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <cstring>

// 信号处理函数
void sigchld_handler(int signum) {
    // 循环回收所有已终止的子进程
    while (true) {
        int status;
        pid_t pid = waitpid(-1, &status, WNOHANG);
        if (pid <= 0) {
            break;
        }

        if (WIFEXITED(status)) {
            std::cout << "[Signal Handler] 子进程PID:" << pid 
                      << " 以状态 " << WEXITSTATUS(status) << " 退出。" << std::endl;
        }
        else if (WIFSIGNALED(status)) {
            std::cout << "[Signal Handler] 子进程PID:" << pid 
                      << " 被信号 " << WTERMSIG(status) << " 终止。" << std::endl;
        }
    }
}

int main() {
    // 注册SIGCHLD信号处理函数
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        std::cerr << "无法注册SIGCHLD处理器:" << strerror(errno) << std::endl;
        return 1;
    }

    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {
        std::cerr << "Fork failed!" << std::endl;
        return 1;
    }
    else if (pid == 0) {
        // 子进程执行的代码
        std::cout << "子进程PID:" << getpid() << " 启动。" << std::endl;
        sleep(2);  // 模拟子进程工作
        std::cout << "子进程PID:" << getpid() << " 结束。" << std::endl;
        return 24;  // 子进程以状态24退出
    }
    else {
        // 父进程执行的其他任务
        std::cout << "父进程PID:" << getpid() << " 正在执行其他任务。" << std::endl;
        // 模拟父进程执行其他任务
        for (int i = 0; i < 5; ++i) {
            std::cout << "父进程执行中:" << i + 1 << std::endl;
            sleep(1);
        }
        // 父进程结束前确保所有子进程已被回收
        // 可以调用wait(NULL)或者让信号处理器完成回收
    }

    return 0;
}
输出示例:
父进程PID:12345 正在执行其他任务。
子进程PID:12346 启动。
父进程执行中:1
父进程执行中:2
子进程PID:12346 结束。
[Signal Handler] 子进程PID:12346 以状态 24 退出。
父进程执行中:3
父进程执行中:4
父进程执行中:5
代码解释:
  1. 注册SIGCHLD信号处理器
    • 使用sigaction结构体注册sigchld_handler函数作为SIGCHLD信号的处理器。
    • SA_RESTART标志用于在信号处理后自动重启被中断的系统调用。
    • SA_NOCLDSTOP标志表示当子进程停止或继续时,父进程不接收SIGCHLD信号。
  2. 创建子进程
    • 子进程执行自己的任务并以状态24退出。
  3. 父进程执行其他任务
    • 父进程在等待子进程结束的同时,继续执行其他任务,不会被阻塞。
  4. 信号处理函数sigchld_handler
    • 当子进程终止时,SIGCHLD信号会被触发,sigchld_handler函数会被调用。
    • 在函数内部,使用waitpidWNOHANG选项非阻塞地回收所有已终止的子进程,防止僵尸进程的产生。

3. 避免僵尸进程的策略

  • 及时调用wait()waitpid():确保父进程在子进程终止后,立即回收其资源。
  • 使用信号处理器:如上文所示,通过注册SIGCHLD信号处理器,可以在子进程终止时自动回收资源,而不需要父进程主动等待。
  • 设置SIGCHLDSIG_IGN:在一些系统上,可以通过将SIGCHLD信号的处理方式设置为忽略,从而自动回收子进程资源。这种方法不适用于所有情况,需谨慎使用。
示例代码:设置SIGCHLD为忽略
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>

int main() {
    // 设置SIGCHLD为忽略
    signal(SIGCHLD, SIG_IGN);

    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {
        std::cerr << "Fork failed!" << std::endl;
        return 1;
    }
    else if (pid == 0) {
        // 子进程执行的代码
        std::cout << "子进程PID:" << getpid() << " 启动。" << std::endl;
        sleep(2);  // 模拟子进程工作
        std::cout << "子进程PID:" << getpid() << " 结束。" << std::endl;
        return 0;
    }
    else {
        // 父进程执行其他任务
        std::cout << "父进程PID:" << getpid() << " 正在执行其他任务。" << std::endl;
        sleep(5);  // 父进程等待子进程结束
    }

    return 0;
}
注意事项:
  • 这种方法依赖于系统对SIGCHLD的具体实现,不保证在所有Unix/Linux系统中都有效。
  • 尽管简便,但可能无法获取子进程的退出状态,限制了错误处理和日志记录的能力。

4. 处理多个子进程的终止

当父进程创建多个子进程时,需要确保所有子进程的终止都被正确处理,以避免僵尸进程。可以在SIGCHLD处理函数中使用循环调用waitpid,直到所有终止的子进程都被回收。

示例代码:处理多个子进程
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <vector>
#include <cstring>

// 信号处理函数
void sigchld_handler(int signum) {
    // 循环回收所有已终止的子进程
    while (true) {
        int status;
        pid_t pid = waitpid(-1, &status, WNOHANG);
        if (pid <= 0) {
            break;
        }

        if (WIFEXITED(status)) {
            std::cout << "[Signal Handler] 子进程PID:" << pid 
                      << " 以状态 " << WEXITSTATUS(status) << " 退出。" << std::endl;
        }
        else if (WIFSIGNALED(status)) {
            std::cout << "[Signal Handler] 子进程PID:" << pid 
                      << " 被信号 " << WTERMSIG(status) << " 终止。" << std::endl;
        }
    }
}

int main() {
    // 注册SIGCHLD信号处理函数
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        std::cerr << "无法注册SIGCHLD处理器:" << strerror(errno) << std::endl;
        return 1;
    }

    std::vector<pid_t> child_pids;

    // 创建多个子进程
    for (int i = 0; i < 3; ++i) {
        pid_t pid = fork();
        if (pid < 0) {
            std::cerr << "Fork failed!" << std::endl;
            return 1;
        }
        else if (pid == 0) {
            // 子进程执行的代码
            std::cout << "子进程PID:" << getpid() << " 启动。" << std::endl;
            sleep(2 + i);  // 模拟不同的工作时间
            std::cout << "子进程PID:" << getpid() << " 结束。" << std::endl;
            return i;
        }
        else {
            // 父进程记录子进程PID
            child_pids.push_back(pid);
        }
    }

    // 父进程执行其他任务
    std::cout << "父进程PID:" << getpid() << " 正在执行其他任务。" << std::endl;
    sleep(6);  // 等待所有子进程结束

    // 由于SIGCHLD处理器已经回收了子进程,父进程无需再次调用wait

    return 0;
}
输出示例:
父进程PID:12345 正在执行其他任务。
子进程PID:12346 启动。
子进程PID:12347 启动。
子进程PID:12348 启动。
子进程PID:12346 结束。
[Signal Handler] 子进程PID:12346 以状态 0 退出。
子进程PID:12347 结束。
[Signal Handler] 子进程PID:12347 以状态 1 退出。
子进程PID:12348 结束。
[Signal Handler] 子进程PID:12348 以状态 2 退出。
代码解释:
  1. 创建多个子进程:循环调用fork()创建三个子进程,每个子进程有不同的工作时间。
  2. 信号处理函数sigchld_handler会被多次调用,以回收每个子进程的资源。
  3. 父进程执行其他任务:父进程在子进程运行期间继续执行其他任务,不会被阻塞。
  4. 无需显式等待:由于信号处理器已经负责回收子进程,父进程无需再次调用wait()waitpid()

5. 使用prctl设置子进程终止时的行为

在某些情况下,可以通过prctl系统调用设置子进程终止时的行为。例如,可以设置PR_SET_CHILD_SUBREAPER,使得特定的进程成为“子进程的收割者”,用于复杂的进程管理。

示例代码:设置PR_SET_CHILD_SUBREAPER
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <cstring>
#include <sys/prctl.h>

int main() {
    // 将当前进程设置为子进程的收割者
    if (prctl(PR_SET_CHILD_SUBREAPER, 1) == -1) {
        std::cerr << "prctl failed: " << strerror(errno) << std::endl;
        return 1;
    }

    pid_t pid = fork();
    if (pid < 0) {
        std::cerr << "Fork failed!" << std::endl;
        return 1;
    }
    else if (pid == 0) {
        // 子进程执行的代码
        std::cout << "子进程PID:" << getpid() << " 启动。" << std::endl;
        sleep(2);
        std::cout << "子进程PID:" << getpid() << " 结束。" << std::endl;
        return 0;
    }
    else {
        // 父进程作为子进程的收割者,等待子进程结束
        std::cout << "父进程设置为子进程的收割者,等待子进程结束。" << std::endl;
        int status;
        pid_t terminated_pid = wait(&status);
        if (terminated_pid > 0) {
            if (WIFEXITED(status)) {
                std::cout << "子进程PID:" << terminated_pid 
                          << " 以状态 " << WEXITSTATUS(status) << " 退出。" << std::endl;
            }
        }
    }

    return 0;
}
代码解释:
  • prctl(PR_SET_CHILD_SUBREAPER, 1):将当前进程设置为子进程的收割者,使其能够回收其子孙进程的资源。
  • 这种设置在需要复杂的子进程管理,或在容器化环境中非常有用。

6. 总结

在Unix/Linux系统中,父进程通过调用wait()waitpid()来处理子进程的终止。这可以是同步的阻塞等待,也可以通过信号处理器异步处理。关键在于确保父进程及时回收子进程资源,避免僵尸进程的产生。此外,针对复杂场景,可以使用更多高级的功能,如prctl设置等。


http://www.kler.cn/a/519958.html

相关文章:

  • 正点原子Linux 移植USB Wifi模块MT7601U驱动(上)
  • Vue.js组件开发-实现多个文件附件压缩下载
  • 使用Python和Qt6创建GUI应用程序--前言
  • Go语言开发项目文件规范
  • 电力场效应晶体管(电力 MOSFET),全控型器件
  • MongoDB 备份与恢复综述
  • LabVIEW橡胶动态特性测试系统
  • Python数据分析-Python语法基础,IPython和Jupyter-Notebooks(二)
  • MySQL回表详解:原理、优化与实践
  • 双指针专题算法:替换数字、链表相交、环形链表ii
  • 基于微信小程序的校园二手交易市场的设计与实现(LW+源码+讲解)
  • 大模型GUI系列论文阅读 DAY4续:《Large Language Model Agent for Fake News Detection》
  • 《边界感知的分而治之方法:基于扩散模型的无监督阴影去除解决方案》学习笔记
  • Effective C++ 规则47: 请使用 Traits Class 表现类型信息
  • Ubuntu24.04初始化MySQL报错 error while loading shared libraries libaio.so.1
  • 【Rust自学】15.3. Deref trait Pt.2:隐式解引用转化与可变性
  • 【Leetcode】--- 接雨水
  • 分布式机器学习中【拓扑】与【通信】的区别和联系
  • CodeForces 611:New Year and Domino ← 二维前缀和
  • 单链表OJ篇
  • docker日志保留策略设置
  • Avalonia系列文章之再试牛刀
  • 【数据结构】时间复杂度空间复杂度
  • 用python实现接口下单
  • 用Ollama跑DeepSeek R1
  • 【Eigen教程】矩阵、数组和向量类(二)