第二十五章:信号处理
筑基中期涉及内核源码:
一
林小源开始研究信号处理器的注册和执行。
用户态程序通过 系统调用来注册信号处理器。处理器是一个函数指针——当信号到达时,内核会把进程的执行流重定向到这个函数。处理器执行完毕后,进程通过 系统调用返回到之前被中断的位置。
/*
* sigaction 结构体:
* sa_handler — 信号处理函数(或 SIG_IGN/SIG_DFL)
* sa_flags — 标志位
* sa_mask — 处理器执行期间额外阻塞的信号
*/
#define SIG_DFL ((void (*)(int))0)
#define SIG_IGN ((void (*)(int))1)
#define SA_RESTART 0x10000000
#define SA_SIGINFO 0x00000004
struct sigaction {
void (*sa_handler)(int);
unsigned long sa_flags;
unsigned long sa_mask;
};
struct signal_table {
struct sigaction actions[32]; /* 每个信号一个处理器 */
unsigned long blocked; /* 阻塞的信号掩码 */
unsigned long pending; /* 待处理的信号 */
};
void sigterm_handler(int sig) {
printf(" [处理器] 收到 SIGTERM,优雅退出\n");
}
void sigchld_handler(int sig) {
printf(" [处理器] 收到 SIGCHLD,回收子进程\n");
}
struct signal_table sigtable;
memset(&sigtable, 0, sizeof(sigtable));
printf("=== 信号处理器注册 ===\n\n");
/* 注册 SIGTERM 处理器 */
sigtable.actions[15].sa_handler = sigterm_handler;
sigtable.actions[15].sa_flags = SA_RESTART;
printf("注册 SIGTERM 处理器: sigterm_handler\n");
printf(" SA_RESTART: 被信号中断的系统调用自动重启\n\n");
/* 注册 SIGCHLD 处理器 */
sigtable.actions[17].sa_handler = sigchld_handler;
sigtable.actions[17].sa_flags = SA_SIGINFO;
printf("注册 SIGCHLD 处理器: sigchld_handler\n");
printf(" SA_SIGINFO: 传递详细的 siginfo_t 信息\n\n");
/* 模拟信号处理 */
printf("--- 模拟信号到达 ---\n");
int sig = 15; /* SIGTERM */
if (sigtable.actions[sig].sa_handler != SIG_DFL &&
sigtable.actions[sig].sa_handler != SIG_IGN) {
sigtable.actions[sig].sa_handler(sig);
}
printf("\n--- 信号处理流程 ---\n");
printf("1. 进程从内核态返回用户态\n");
printf("2. 检查 pending 信号\n");
printf("3. 如果有处理器 → 调用处理器\n");
printf("4. 处理器执行完毕 → sigreturn()\n");
printf("5. 返回到之前被中断的位置\n");#include <stdio.h>
#include <string.h>
/*
* sigaction 结构体:
* sa_handler — 信号处理函数(或 SIG_IGN/SIG_DFL)
* sa_flags — 标志位
* sa_mask — 处理器执行期间额外阻塞的信号
*/
#define SIG_DFL ((void (*)(int))0)
#define SIG_IGN ((void (*)(int))1)
#define SA_RESTART 0x10000000
#define SA_SIGINFO 0x00000004
struct sigaction {
void (*sa_handler)(int);
unsigned long sa_flags;
unsigned long sa_mask;
};
struct signal_table {
struct sigaction actions[32]; /* 每个信号一个处理器 */
unsigned long blocked; /* 阻塞的信号掩码 */
unsigned long pending; /* 待处理的信号 */
};
void sigterm_handler(int sig) {
printf(" [处理器] 收到 SIGTERM,优雅退出\n");
}
void sigchld_handler(int sig) {
printf(" [处理器] 收到 SIGCHLD,回收子进程\n");
}
int main() {
struct signal_table sigtable;
memset(&sigtable, 0, sizeof(sigtable));
printf("=== 信号处理器注册 ===\n\n");
/* 注册 SIGTERM 处理器 */
sigtable.actions[15].sa_handler = sigterm_handler;
sigtable.actions[15].sa_flags = SA_RESTART;
printf("注册 SIGTERM 处理器: sigterm_handler\n");
printf(" SA_RESTART: 被信号中断的系统调用自动重启\n\n");
/* 注册 SIGCHLD 处理器 */
sigtable.actions[17].sa_handler = sigchld_handler;
sigtable.actions[17].sa_flags = SA_SIGINFO;
printf("注册 SIGCHLD 处理器: sigchld_handler\n");
printf(" SA_SIGINFO: 传递详细的 siginfo_t 信息\n\n");
/* 模拟信号处理 */
printf("--- 模拟信号到达 ---\n");
int sig = 15; /* SIGTERM */
if (sigtable.actions[sig].sa_handler != SIG_DFL &&
sigtable.actions[sig].sa_handler != SIG_IGN) {
sigtable.actions[sig].sa_handler(sig);
}
printf("\n--- 信号处理流程 ---\n");
printf("1. 进程从内核态返回用户态\n");
printf("2. 检查 pending 信号\n");
printf("3. 如果有处理器 → 调用处理器\n");
printf("4. 处理器执行完毕 → sigreturn()\n");
printf("5. 返回到之前被中断的位置\n");
return 0;
}林小源望着信号处理器的注册流程,脑海中浮现出一幅画面:每个进程都有一个信号表,表中有 32 个槽位(对应 32 种信号),每个槽位可以放一个函数指针。当信号到达时,内核查表找到对应的函数,然后把进程的执行流"劫持"到那个函数。
"劫持?"剑侍不知何时又出现了,听到林小源的话,纠正道,"不是劫持,是重定向。内核修改进程的用户态栈,把返回地址指向信号处理器,把处理器的参数设好,然后返回用户态。进程一回到用户态,就发现自己在信号处理器里了。处理器执行完后,调用 ,内核恢复原来的栈和寄存器,进程继续执行之前被中断的地方。"
"听起来很复杂。"
"确实复杂。"剑侍说,"但这是必要的复杂——信号处理器必须在用户态执行,不能在内核态。而且处理器执行完毕后,必须能无缝地回到之前的执行位置——就像什么都没发生过一样。"
林小源在信号处理器的执行机制中看到了一种"上下文切换"的精妙——不是进程间的上下文切换,而是同一个进程内部的上下文切换。内核保存当前的执行状态,重定向到信号处理器,处理器执行完后恢复状态继续执行。
二
林小源在研究信号处理器的过程中,发现了一个微妙的问题。
如果信号处理器正在执行时,又来了一个新的信号怎么办?
答案是:取决于信号掩码。 的 字段定义了处理器执行期间额外阻塞的信号。如果新来的信号在 中,它会被推迟到处理器执行完毕后再处理。
剑侍解释道:"默认情况下,处理器正在处理 时,新的 会被阻塞——内核会自动把正在处理的信号加入掩码。这防止了信号处理器被自己中断——你想想,如果处理器正在处理 ,又来了一个 ,处理器被中断,重新进入同一个处理器——这就是递归,可能会导致栈溢出。"
"那 呢?"林小源问。
" 告诉内核不要自动阻塞正在处理的信号。"剑侍说,"这在某些特殊场景下有用——比如你需要在处理器中处理同一个信号的多次到达。但大多数情况下,你不需要它。"
林小源又问:"那 呢?"
"好问题。"剑侍的眼睛亮了起来," 解决了一个很实际的问题。假设你的进程正在 read() 一个文件,突然来了一个信号。如果没有 ,read() 会返回 -EINTR——'被信号中断'。你的代码必须检查这个错误,然后重新调用 read()。这很烦人。有了 ,内核会自动重启被中断的系统调用——你的代码根本不需要知道有信号发生过。"
"所以 是一个便利性标志?"
"可以这么说。但不是所有系统调用都支持自动重启——、、 等就不支持。所以你不能完全依赖它。"
信号处理是一门精细的学问。每一个标志、每一个掩码位、每一个默认行为,都有它的道理。
三
林小源在观察信号处理的过程中,注意到了一个有趣的现象。
shell 小妹注册了 (Ctrl+C)的处理器。当用户按下 Ctrl+C 时,内核向 shell 小妹发送 。shell 小妹的处理器不是直接退出——它检查当前是否有前台进程在运行。如果有,它把 转发给前台进程;如果没有,它打印一个新的提示符。
林小源把这个观察告诉剑侍。剑侍说:"这就是 shell 的信号转发模式。shell 是进程组的领导者——它管理着一组前台进程。当用户按 Ctrl+C 时,信号不是发给某个具体的进程,而是发给前台进程组。shell 收到信号后,决定怎么处理——通常是转发给前台进程。"
"所以 Ctrl+C 能中断正在运行的命令,是因为 shell 把 SIGINT 转发给了那个命令?"
"对。"剑侍说,"命令退出后,shell 回收它,然后打印新的提示符。整个流程:用户按 Ctrl+C → 内核发送 SIGINT → shell 的处理器转发给前台进程 → 前台进程退出 → shell 回收 → 新的提示符。"
林小源感叹道:"shell 小妹比她看起来要聪明。"
"她当然聪明。"剑侍说,"她不懂内核的深处,但她把信号处理玩得炉火纯青。每一个 shell 脚本、每一个管道、每一个重定向,背后都有信号处理的影子。她是 Unix 世界中最精巧的用户态程序之一。"
林小源望着 shell 小妹忙碌的身影,心中升起一股敬意。她不懂内核的深处,但她的每一次信号处理都是对 Unix 哲学的完美诠释。
道藏笔记
内核启示
信号处理器是用户态程序处理信号的方式。
系统调用注册信号处理器,结构体里三个关键字段各管一摊: 放处理函数(也可以是 或 ), 放标志位(、、 等), 指定处理器执行期间额外阻塞哪些信号。
整个信号处理流程是这样的:进程从内核态返回用户态时,内核先检查有没有 pending 信号,如果有注册的处理器就调用它,处理器执行完后调 ,内核恢复原来的栈和寄存器,进程接着干之前被中断的事。
标志让被信号中断的系统调用自动重启——这避免了 错误的处理。
信号处理是进程与外界交互的窗口。
信号之试
用户按下 Ctrl+C 时,本章讲到 shell 通常会向前台进程送出哪一个信号?