【Linux系统】SIGCHLD 信号(选学了解)
SIGCHLD
信号
使用wait
和waitpid
函数可以有效地清理僵尸进程。父进程可以选择阻塞等待,直到子进程结束;或者采用非阻塞的方式,通过轮询检查是否有子进程需要被回收。
然而,无论是选择阻塞等待还是非阻塞的轮询方式,父进程与子进程之间都无法实现真正的异步执行,因为父进程仍需“分心”来管理子进程的状态。
当子进程终止时,会向父进程发送一个SIGCHLD
信号,这个信号的默认行为是被忽略。不过,父进程可以通过设置自定义的SIGCHLD
信号处理函数来改变这一行为。这样,父进程就可以专注于自己的任务,无需直接管理子进程的状态。一旦子进程终止,它会通知父进程,父进程只需要在其信号处理函数中调用wait
或waitpid
来清理子进程即可。
因此,我们能够通过自定义处理子进程发出的SIGCHLD
信号,在接收到该信号时利用waitpid
回收子进程资源。这种方法避免了主动等待子进程的结束,使得父子进程之间能够更加高效地异步执行。
演示代码如下:因为需要在函数 handler
中使用子进程 id,因此定义了一个全局变量 id
其实还有一种方法不用传id也不用定义全局变量:waitpid(-1, nullptr, 0);
-1 表示回收该父进程下的任意一个子进程
当
pid
参数为-1
时,waitpid
函数会等待任何一个子进程的状态变化。这意味着它会捕获任何已经终止的子进程,并回收其资源。这对于处理多个子进程的情况非常有用,因为父进程不需要知道具体是哪个子进程终止了。
#include<iostream> // 引入输入输出流库
#include<signal.h> // 引入信号处理库
#include<sys/wait.h> // 引入等待子进程状态改变的函数库
#include<sys/types.h> // 引入系统类型定义
#include<unistd.h> // 引入Unix标准函数库
pid_t id; // 定义全局变量id,用于存储子进程ID
// 定义信号处理函数
void handler(int signum)
{
waitpid(id, nullptr, 0); // 等待子进程结束,回收子进程资源
std::cout << "子进程退出, 我也退出了" << '\n'; // 输出子进程已退出的信息
// 当接收到信号时,调用raise给自己发送9号信号(SIGKILL),强制终止进程
raise(9);
}
int main()
{
id = fork(); // 创建子进程
if(id < 0)
{
perror("fork"); // 如果fork失败,输出错误信息
return 1; // 返回错误码1
}
// 子进程逻辑
if(id == 0)
{
std::cout << "I am 子 process" << '\n'; // 子进程输出标识信息
sleep(2); // 子进程暂停2秒
exit(0); // 子进程正常退出
}
// 父进程逻辑
else if (id > 0)
{
std::cout << "I am 父 process" << '\n'; // 父进程输出标识信息
signal(SIGCHLD, handler); // 设置SIGCHLD信号的处理函数为handler
int cnt = 0; // 初始化计数器
while(1)
{
sleep(1); // 每秒暂停1秒
std::cout << "cnt = " << cnt++ << '\n'; // 输出当前计数值
}
}
return 0; // 程序正常结束
}
运行结果如下:
问题一:如果同时多个子进程退出,是否会全部回收
但是这样通过信号回收子进程是有一定风险的!
因为信号是通过 pending
位图保存的,当一个父进程同时有多个子进程同时退出,同时发送 SIGCHLD
信号,则位图不能记录信号接收数量,就大概率会遗漏处理某些子进程,导致多个子进程僵尸的情况
验证如下:
#include <iostream> // 引入输入输出流库
#include <signal.h> // 引入信号处理库
#include <sys/wait.h> // 引入等待子进程状态改变的函数库
#include <sys/types.h> // 引入系统类型定义
#include <unistd.h> // 引入Unix标准函数库
// 定义信号处理函数
void handler(int signum)
{
pid_t id = waitpid(-1, nullptr, 0); // 等待任意一个子进程结束,回收其资源
std::cout << "回收子进程 id : " << id << '\n'; // 输出回收的子进程ID
}
int main()
{
pid_t id; // 定义变量id,用于存储子进程ID
// 循环创建15个子进程
for (int i = 1; i <= 15; ++i)
{
id = fork(); // 创建子进程
if (id < 0)
{
perror("fork"); // 如果fork失败,输出错误信息
return 1; // 返回错误码1
}
// 子进程逻辑
if (id == 0)
{
std::cout << "I am 子 process" << '\n'; // 子进程输出标识信息
sleep(2); // 子进程暂停2秒
exit(0); // 子进程正常退出
}
}
// 父进程逻辑
if (id > 0)
{
std::cout << "I am 父 process" << '\n'; // 父进程输出标识信息
signal(SIGCHLD, handler); // 设置SIGCHLD信号的处理函数为handler,当子进程结束时会触发此函数
int cnt = 0; // 初始化计数器
while (1)
{
sleep(1); // 每秒暂停1秒
std::cout << "cnt = " << cnt++ << '\n'; // 输出当前计数值
}
}
return 0; // 程序正常结束
}
运行结果如下:不少子进程没有被回收,而是变成了僵尸进程
解决办法:循环等待回收子进程,否则退出
演示代码如下:
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signum)
{
while (true)
{
pid_t id = waitpid(-1, nullptr, 0);
if(id > 0)
{
std::cout << "回收子进程 id : " << id << '\n';
}
else if(id < 0)
{
std::cout << "回收完毕, 暂时结束回收\n";
break;
}
}
}
int main()
{
pid_t id;
for (int i = 1; i <= 15; ++i)
{
id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
// 子进程
if (id == 0)
{
std::cout << "I am 子 process" << '\n';
sleep(2);
exit(0);
}
}
// 父进程
if (id > 0)
{
std::cout << "I am 父 process" << '\n';
signal(SIGCHLD, handler);
int cnt = 0;
while (1)
{
sleep(1);
std::cout << "cnt = " << cnt++ << '\n';
}
}
return 0;
}
运行结果如下:自己可以去查询,可以确定当前没有僵尸子进程
问题二:如果有子进程不退出,问题一中的循环wait,是否会退出循环
演示代码:
// 创建一个不退出的子进程
id = fork();
if (id == 0)
{
std::cout << "I am 不退出的子进程" << '\n';
sleep(6);
}
结果就是 循环没退出,因为 waitpid
是阻塞式等待,会等待子进程退出,因为该子进程不退出则循环不退出一直阻塞等待
因此需要换成非阻塞式等待,同时当 waitpid
的返回值为 0,说明当前没有退出的子进程,则此时可以主动退出循环
pid_t id = waitpid(-1, nullptr, WNOHANG);
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signum)
{
while (true)
{
pid_t id = waitpid(-1, nullptr, WNOHANG);
if (id > 0)
{
std::cout << "回收子进程 id : " << id << '\n';
}
else if(id == 0) // 表示没有子进程退出了(注意是没有退出的子进程了, 不是没有子进程)
{
std::cout << "暂时没有子进程退出\n";
break;
}
else if (id < 0) // 表示没有子进程了
{
std::cout << "waitpid error\n";
break;
}
}
}
int main()
{
pid_t id;
for (int i = 1; i <= 15; ++i)
{
id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
// 子进程
if (id == 0)
{
std::cout << "I am 子 process" << '\n';
sleep(2);
exit(0);
}
}
// 创建一个不退出的子进程
id = fork();
if (id == 0)
{
std::cout << "I am 不退出的子进程" << '\n';
sleep(6); // 时间长点, 模拟短时间内不退出
}
// 父进程
if (id > 0)
{
std::cout << "I am 父 process" << '\n';
signal(SIGCHLD, handler);
int cnt = 0;
while (1)
{
sleep(1);
std::cout << "cnt = " << cnt++ << '\n';
}
}
return 0;
}
运行结果如下
waitpid
系统调用的工作原理如下:
- 阻塞等待:当
waitpid
被调用时,如果当前没有符合条件的已退出子进程,内核会让父进程进入阻塞状态。这意味着父进程会被挂起,不再占用CPU时间,直到有子进程的状态发生变化(通常是退出)。- 非阻塞等待:如果
waitpid
调用时传递了WNOHANG
选项,内核会立即返回,即使没有子进程退出。在这种情况下,waitpid
不会阻塞父进程。- 状态变化通知:当一个子进程退出时,内核会检查该子进程的父进程是否正在等待子进程的状态变化。如果是,内核会唤醒父进程,使其从
waitpid
调用中返回,并传递子进程的退出状态。- 资源回收:父进程通过
waitpid
获得子进程的退出状态后,内核会释放子进程占用的资源,防止子进程变成僵尸进程。
意思是:父进程使用waitpid
系统调用时,若为阻塞等待,则OS将该父进程挂起(即阻塞),当目标子进程退出时,若该父进程正处于等待子进程退出的状态,则OS会传递子进程退出状态信息并使父进程退出阻塞状态(即使其从 waitpid
调用中返回)
子进程退出,OS是如何知道的,是因为OS需要轮询子进程的状态吗
当然不是OS轮询,前面讲解过 OS 就是一个躺在中断向量表上的一个代码块,OS的运行基本靠中断,因此进程退出也是通过中断通知OS,使其执行相应的后续”善后“工作
意思是子进程退出时,会向OS发送软件中断,此时进入内核态,执行该中断对应的中断处理例程:即更新子进程的 PCB,将子进程的状态标记为“已退出”(Zombie 状态),生成一个 SIGCHLD 信号并发送给父进程
子进程退出的详细过程
- 子进程调用
exit
或exit_group
系统调用:
- 子进程在调用
exit
或exit_group
系统调用时,会进入内核态。
- 不是子进程退出子进程发送的软件中断,而是子进程在调用 exit 或 exit_group 系统调用触发的软件中断
- 进入内核态:
- 当子进程调用
exit
或exit_group
时,控制权转移到内核,进入内核态。- 内核会执行相应的中断处理例程(中断服务程序)。
- 中断处理例程:
- 内核的中断处理例程会执行以下操作:
- 更新子进程的 PCB:内核会更新子进程的进程控制块(PCB),将子进程的状态标记为“已退出”(Zombie 状态)。
- 生成
SIGCHLD
信号:内核会生成一个SIGCHLD
信号并发送给父进程。- 父进程接收
SIGCHLD
信号:
- 父进程接收到
SIGCHLD
信号后,会调用预先注册的信号处理函数(如handler
)默认为忽略
主动忽略子进程的 SIGCHLD
Linux下,将SIGCHLD的处理动作置为SIG IGN,这样fork出来的子进程在终止时会自动清理掉
由于UNIX 的历史原因,要想不产⽣僵⼫进程还有另外⼀种办法:⽗进程调 ⽤sigaction将
SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不 会产⽣僵⼫进程,
也不会通知⽗进程。系统默认的忽略动作和⽤⼾⽤sigaction函数⾃定义的忽略 通常是没有区别的,但这
是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可 ⽤。
signal(SIGCHLD, SIG_IGN);
底层原理:
父进程未调用
waitpid
的情况
- 子进程退出:
- 子进程调用
exit
或exit_group
系统调用,进入内核态。- 内核更新子进程的 PCB,将其状态标记为“已退出”(Zombie 状态)。
- 内核生成
SIGCHLD
信号并发送给父进程。- 父进程处理
SIGCHLD
信号:
- 如果父进程注册了
SIGCHLD
信号处理函数(如handler
),内核会调用该处理函数。- 在信号处理函数中,父进程可以调用
waitpid
来获取子进程的退出状态并释放资源。- 父进程忽略
SIGCHLD
信号:
- 如果父进程将
SIGCHLD
信号的处理动作设置为SIG_IGN
,内核会自动回收子进程的资源,子进程不会变成僵尸进程。- 这意味着父进程不需要显式调用
wait
或waitpid
来回收子进程的资源。
问题:父进程忽略了该信号,内核如何知道父进程忽略了,然后进行的自动回收子进程的资源
内核记录信号处理动作:
内核会记录每个进程的信号处理动作。当父进程调用
signal
或sigaction
设置SIGCHLD
信号的处理动作时,内核会更新父进程的信号处理表。内核会记录
SIGCHLD
信号的处理动作为SIG_IGN
。子进程调用退出
- 内核生成
SIGCHLD
信号:
- 内核生成
SIGCHLD
信号并准备发送给父进程。- 内核会检查父进程的信号处理表,查看
SIGCHLD
信号的处理动作。- 检查信号处理动作:
- 如果父进程的信号处理动作是
SIG_IGN
,内核会知道父进程忽略了SIGCHLD
信号。- 内核会自动回收子进程的资源,子进程不会变成僵尸进程。
问题:系统对该信号的默认处理不就是忽略吗,为什么我们还要自己主动忽略
系统默认的忽略动作和⽤⼾⽤sigaction函数⾃定义的忽略 通常是没有区别的,但这
是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可 ⽤。
其实,因为位图本身一次只能记录一个进程退出信号,因此即使循环等待等操作,还是会有极小概率处理不了某些退出子进程