第10章:信号(Signals)
一、导读
10.1 本章学习目标
第10章从内核角度讲解信号的实现机制。信号是UNIX中最古老的进程间通信方式,用于通知进程发生了异步事件。从用户按下Ctrl+C发送SIGINT,到子进程退出时父进程收到SIGCHLD,再到定时器到期触发SIGALRM——信号无处不在。理解信号的内核实现,对于编写健壮的系统程序、理解进程间通信、以及诊断程序行为都至关重要。
本章的核心学习目标包括:
- 理解信号在内核中的表示:task_struct中的信号相关字段
- 掌握信号发送的内核流程:从kill系统调用到信号加入pending集合
- 理解信号检查和处理时机:内核何时检查待处理信号
- 掌握信号处理函数的栈帧构造:内核如何跳转到用户空间的信号处理函数
- 了解实时信号与普通信号的区别:排队机制和可靠性差异
- 理解信号阻塞和排队的实现:sigprocmask和信号队列
10.2 为什么本章如此重要
信号是进程间通信的基础机制,也是很多系统行为的底层支撑。进程退出、定时器、I/O事件、错误通知等,都通过信号机制传递。不理解信号的实现,就无法理解为什么某些系统调用会被中断(EINTR),为什么多线程程序中的信号处理如此复杂,为什么某些程序在退出前需要做清理工作。
从安全角度看,信号也是一个潜在的攻击面。信号处理函数运行在用户空间,但由内核触发,如果处理不当可能导致竞态条件、资源泄漏等问题。从性能角度看,频繁的发送和处理信号会带来额外的开销。
10.3 核心问题引导
在阅读本章时,请带着以下问题思考:
当执行kill -9 PID时,内核如何将SIGKILL信号传递给目标进程?
为什么SIGKILL和SIGSTOP不能被捕获或忽略?内核如何特殊处理这两个信号?
信号处理函数是如何被执行的?内核如何构造特殊的栈帧跳转到用户空间?
为什么普通信号不排队?如果发送多个相同的信号,为什么只处理一次?
sigreturn系统调用的作用是什么?为什么信号处理函数返回时需要它?
多线程程序中,信号是如何在线程之间分配的?
二、核心概念详解
2.1 信号的内核表示
每个进程在内核中的task_struct包含信号相关的信息:
/* 信号相关的数据结构 */
/* include/linux/sched.h */
struct task_struct {
/* ... */
/* 信号处理 */
struct signal_struct *signal; /* 线程组共享的信号信息 */
struct sighand_struct *sighand; /* 信号处理函数数组 */
sigset_t blocked; /* 被阻塞的信号集 */
sigset_t real_blocked; /* 临时阻塞集 */
sigset_t saved_sigmask; /* 保存的信号掩码 */
struct sigpending pending; /* 线程私有的待处理信号 */
/* ... */
};
/* 线程组共享的信号信息 */
struct signal_struct {
atomic_t sigcnt; /* 引用计数 */
atomic_t live; /* 线程数 */
int nr_threads; /* 线程总数 */
/* 进程组的信号 */
struct sigpending shared_pending; /* 线程组共享的待处理信号 */
/* POSIX 定时器 */
struct list_head posix_timers;
/* 进程统计 */
cputime_t utime;
cputime_t stime;
/* 退出相关 */
int exit_code;
int exit_signal;
/* ... */
};
/* 信号处理函数数组 */
struct sighand_struct {
atomic_t count; /* 引用计数 */
struct k_sigaction action[_NSIG]; /* 每个信号的处理函数 */
spinlock_t siglock; /* 保护锁 */
};
/* 信号动作 */
struct k_sigaction {
__sighandler_t sa_handler; /* 信号处理函数 */
unsigned long sa_flags; /* 标志 */
__sigrestore_t sa_restorer; /* 恢复函数 */
sigset_t sa_mask; /* 执行处理函数时额外阻塞的信号 */
};
/* 待处理信号集 */
struct sigpending {
struct list_head list; /* sigqueue 链表 */
sigset_t signal; /* 信号位图 */
};# 查看进程的信号信息
cat /proc/self/status | grep Sig
# SigQ: 0/63497 # 信号队列中当前/最大信号数
# SigPnd: 0000000000000000 # 待处理信号(位图)
# SigBlk: 0000000000000000 # 阻塞的信号
# SigIgn: 0000000000000000 # 忽略的信号
# SigCgt: 0000000000000000 # 捕获的信号2.2 信号发送
信号发送通过kill系统调用实现。内核将信号添加到目标进程的pending集合中。
/* kill 系统调用 */
/* kernel/signal.c */
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) {
struct siginfo info;
/* 填充 siginfo */
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER; /* 来自用户空间 */
info.si_pid = task_tgid_vnr(current); /* 发送者PID */
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}
/* 信号发送的核心流程 */
static int kill_something_info(int sig, struct siginfo *info, pid_t pid) {
if (pid > 0) {
/* 发送给特定进程 */
struct task_struct *p = find_task_by_vnr(pid);
if (!p)
return -ESRCH;
return send_signal(sig, info, p, PIDTYPE_PID);
}
/* pid == 0: 发送给同进程组的所有进程 */
/* pid == -1: 发送给所有有权限发送的进程 */
/* pid < -1: 发送给进程组 |pid| 的所有进程 */
}
/* 发送信号到进程 */
static int send_signal(int sig, struct siginfo *info,
struct task_struct *t, enum pid_type type) {
struct sigpending *pending;
struct sigqueue *q;
/* 确定使用哪个 pending 集合 */
/* 对于线程组共享的信号,使用 shared_pending */
/* 对于线程私有的信号,使用 pending */
pending = (type == PIDTYPE_PID) ? &t->pending : &t->signal->shared_pending;
/* 检查信号是否被忽略 */
if (sig_ignored(t, sig))
return 0;
/* 分配 sigqueue 结构 */
q = __sigqueue_alloc(sig, t->signal, GFP_ATOMIC, 0);
if (q) {
list_add_tail(&q->list, &pending->list);
*siginfo_from_sigqueue(q, info);
}
/* 在信号位图中标记 */
sigaddset(&pending->signal, sig);
/* 唤醒目标进程(如果它在睡眠) */
signal_wake_up(t, sig == SIGKILL);
return 0;
}2.3 信号检查和传递
内核在从内核态返回用户态之前检查待处理信号。如果有未被阻塞的信号,内核会传递信号。
/* 信号检查(内核返回用户态前) */
/* arch/x86/kernel/signal.c */
void do_notify_resume(struct pt_regs *regs, unsigned long thread_info_flags) {
/* 检查是否有待处理信号 */
if (thread_info_flags & _TIF_SIGPENDING)
do_signal(regs);
/* 检查是否需要通知跟踪器 */
if (thread_info_flags & _TIF_NOTIFY_RESUME) {
clear_thread_flag(TIF_NOTIFY_RESUME);
tracehook_notify_resume(regs);
}
}
/* 信号传递的核心函数 */
static void do_signal(struct pt_regs *regs) {
struct ksignal ksig;
bool stepping;
/* 获取下一个要处理的信号 */
if (get_signal(&ksig)) {
/* 有信号需要处理 */
/* 检查是否是停止/继续信号 */
if (ksig->sig == SIGSTOP || ksig->sig == SIGTSTP ||
ksig->sig == SIGTTIN || ksig->sig == SIGTTOU) {
/* 停止进程 */
do_signal_stop(ksig->sig);
return;
}
/* 检查信号动作 */
switch (ksig->ka.sa.sa_handler) {
case SIG_DFL:
/* 默认动作 */
switch (sig_default_action(ksig->sig)) {
case ACTION_TERM:
/* 终止进程 */
do_exit(ksig->sig | 0x80); /* core dump */
break;
case ACTION_STOP:
/* 停止进程 */
do_signal_stop(ksig->sig);
break;
case ACTION_CONT:
/* 继续进程 */
break;
case ACTION_IGN:
case ACTION_NONE:
break;
}
break;
case SIG_IGN:
/* 忽略信号 */
break;
default:
/* 用户定义的信号处理函数 */
/* 构造特殊的用户态栈帧 */
handle_signal(&ksig, regs);
break;
}
}
}2.4 信号处理函数的执行
当信号的动作是用户定义的处理函数时,内核需要构造特殊的用户态栈帧,使进程跳转到信号处理函数执行。
/* 信号处理函数的栈帧构造(x86-64,简化) */
/* arch/x86/kernel/signal.c */
static int handle_signal(struct ksignal *ksig, struct pt_regs *regs) {
sigset_t *oldset = sigmask_to_save();
struct pt_regs *regs_copy;
int ret;
/* 1. 在用户栈上分配信号帧 */
/* 信号帧包含:
* - 保存的寄存器上下文
* - 信号编号
* - siginfo 结构
* - ucontext 结构
* - 信号掩码
*/
/* 2. 设置信号处理函数的参数 */
/* rdi = 信号编号 */
/* rsi = siginfo 指针 */
/* rdx = ucontext 指针 */
/* 3. 修改返回地址为信号处理函数 */
regs->ip = (unsigned long)ksig->ka.sa.sa_handler;
/* 4. 设置信号帧的返回地址为 sigreturn trampoline */
/* 信号处理函数执行完后,ret 指令跳转到 sigreturn trampoline */
/* sigreturn trampoline 调用 sigreturn 系统调用 */
/* 5. 更新信号掩码 */
sigact_restore_blocked(oldset, &ksig->ka);
/* 6. 清除 TIF_SIGPENDING 标志 */
clear_thread_flag(TIF_SIGPENDING);
return 0;
}
/*
* 信号处理的完整流程:
*
* 1. 内核检测到待处理信号
* 2. 在用户栈上构造信号帧(保存当前上下文)
* 3. 修改 RIP 为信号处理函数地址
* 4. 返回用户态 → CPU 执行信号处理函数
* 5. 信号处理函数执行 ret → 跳转到 sigreturn trampoline
* 6. trampoline 调用 sigreturn 系统调用
* 7. 内核恢复保存的上下文
* 8. 继续执行原来的代码
*//* sigreturn 系统调用 */
/* 从信号处理函数返回 */
SYSCALL_DEFINE0(sigreturn) {
struct pt_regs *regs = current_pt_regs();
struct sigframe __user *frame;
sigset_t set;
/* 获取信号帧地址 */
frame = (struct sigframe __user *)(regs->sp - 8);
/* 验证帧地址 */
if (!access_ok(frame, sizeof(*frame)))
goto badframe;
/* 从信号帧恢复信号掩码 */
if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set)))
goto badframe;
set_current_blocked(&set);
/* 恢复寄存器上下文 */
if (restore_sigcontext(regs, &frame->uc.uc_mcontext))
goto badframe;
/* 恢复栈指针 */
/* 返回到原来的执行点 */
return regs->ax;
badframe:
force_sig(SIGSEGV);
return 0;
}2.5 实时信号
Linux支持两种信号:普通信号(1-31)和实时信号(SIGRTMIN-SIGRTMAX,通常34-64)。
/* 普通信号 vs 实时信号 */
/*
* 普通信号(1-31):
* - 不排队:多个相同信号只保留一个
* - 不保证传递顺序
* - 不携带额外信息(除了信号编号)
* - 例如:SIGINT, SIGTERM, SIGKILL
*
* 实时信号(SIGRTMIN-SIGRTMAX):
* - 排队:多个相同信号都会保留
* - 按发送顺序传递
* - 可以携带额外数据(sigval)
* - 更可靠
* - 例如:SIGRTMIN, SIGRTMIN+1, ...
*/
/* 实时信号的发送 */
#include <signal.h>
#include <stdio.h>
int main() {
sigval_t value;
value.sival_int = 42;
/* 发送实时信号,携带数据 */
sigqueue(getpid(), SIGRTMIN, value);
return 0;
}
/* 实时信号的处理 */
void rt_handler(int sig, siginfo_t *info, void *context) {
printf("Received signal %d, data = %d\n",
sig, info->si_value.sival_int);
}
int main() {
struct sigaction sa;
sa.sa_sigaction = rt_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO; /* 必须设置,以获取 siginfo */
sigaction(SIGRTMIN, &sa, NULL);
/* 发送多个相同的实时信号 */
for (int i = 0; i < 5; i++) {
sigval_t val;
val.sival_int = i;
sigqueue(getpid(), SIGRTMIN, val);
}
/* 输出:
* Received signal 34, data = 0
* Received signal 34, data = 1
* Received signal 34, data = 2
* Received signal 34, data = 3
* Received signal 34, data = 4
* 所有5个信号都被接收!
*/
return 0;
}2.6 信号的阻塞
进程可以阻塞某些信号,被阻塞的信号不会立即传递,而是留在pending集合中,直到解除阻塞。
/* 信号阻塞的内核实现 */
/* kernel/signal.c */
/* sigprocmask 系统调用 */
SYSCALL_DEFINE3(sigprocmask, int, how, old_sigset_t, nset,
old_sigset_t __user *, oset) {
sigset_t old_set;
int error;
/* 保存旧的信号掩码 */
if (oset) {
old_set = current->blocked;
/* 复制到用户空间... */
}
if (nset) {
sigset_t new_set;
/* 从用户空间复制... */
spin_lock_irq(¤t->sighand->siglock);
switch (how) {
case SIG_BLOCK:
/* 阻塞指定信号 */
sigorsets(¤t->blocked, ¤t->blocked, &new_set);
break;
case SIG_UNBLOCK:
/* 解除阻塞 */
signotset(&new_set, &new_set, NULL);
sigandsets(¤t->blocked, ¤t->blocked, &new_set);
break;
case SIG_SETMASK:
/* 设置信号掩码 */
current->blocked = new_set;
break;
default:
error = -EINVAL;
}
/* 检查是否有待处理的信号需要传递 */
recalc_sigpending();
spin_unlock_irq(¤t->sighand->siglock);
}
return error;
}
/*
* 信号阻塞的效果:
*
* 1. 信号被阻塞时:
* - 信号仍然被添加到 pending 集合
* - 但不会被传递(不执行处理函数)
* - 信号在 pending 中等待
*
* 2. 解除阻塞时:
* - 内核检查 pending 集合
* - 如果有未阻塞的待处理信号,立即传递
*
* 3. 特殊情况:
* - SIGKILL 和 SIGSTOP 不能被阻塞
* - 内核在 sigprocmask 中自动清除这两个信号
*//* 用户空间使用信号阻塞 */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main() {
sigset_t set, old_set, pending;
/* 阻塞 SIGINT (Ctrl+C) */
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, &old_set);
printf("SIGINT is blocked. Press Ctrl+C...\n");
sleep(5);
/* 检查 pending 信号 */
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("SIGINT is pending!\n");
}
/* 解除阻塞 */
printf("Unblocking SIGINT...\n");
sigprocmask(SIG_SETMASK, &old_set, NULL);
/* 此时 SIGINT 会被立即传递 */
printf("Done.\n");
return 0;
}三、实战案例
3.1 观察信号的排队行为
/* signal_queue.c - 测试信号的排队行为 */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
static volatile int sigint_count = 0;
static volatile int sigrt_count = 0;
void sigint_handler(int sig) {
sigint_count++;
printf("SIGINT received (count: %d)\n", sigint_count);
}
void sigrt_handler(int sig, siginfo_t *info, void *context) {
sigrt_count++;
printf("SIGRTMIN received (count: %d, data: %d)\n",
sigrt_count, info->si_value.sival_int);
}
int main() {
struct sigaction sa;
/* 设置 SIGINT 处理函数 */
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
/* 设置 SIGRTMIN 处理函数 */
sa.sa_sigaction = sigrt_handler;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGRTMIN, &sa, NULL);
/* 阻塞信号 */
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGRTMIN);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("PID: %d\n", getpid());
printf("Sending signals while blocked...\n");
/* 发送多个 SIGINT(不排队) */
for (int i = 0; i < 5; i++) {
kill(getpid(), SIGINT);
}
/* 发送多个 SIGRTMIN(排队) */
for (int i = 0; i < 5; i++) {
sigval_t val;
val.sival_int = i * 10;
sigqueue(getpid(), SIGRTMIN, val);
}
printf("Unblocking signals...\n");
/* 解除阻塞 */
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("\nFinal counts:\n");
printf("SIGINT: %d (expected: 1, not queued)\n", sigint_count);
printf("SIGRTMIN: %d (expected: 5, queued)\n", sigrt_count);
return 0;
}运行结果:
PID: 12345
Sending signals while blocked...
Unblocking signals...
SIGINT received (count: 1)
SIGRTMIN received (count: 1, data: 0)
SIGRTMIN received (count: 2, data: 10)
SIGRTMIN received (count: 3, data: 20)
SIGRTMIN received (count: 4, data: 30)
SIGRTMIN received (count: 5, data: 40)
Final counts:
SIGINT: 1 (expected: 1, not queued)
SIGRTMIN: 5 (expected: 5, queued)3.2 信号与系统调用的交互
/* signal_syscall.c - 信号中断系统调用 */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void handler(int sig) {
printf("Caught signal %d (%s)\n", sig, strsignal(sig));
}
int main() {
struct sigaction sa;
/* 设置 SIGINT 处理函数,不使用 SA_RESTART */
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* 不使用 SA_RESTART */
sigaction(SIGINT, &sa, NULL);
char buf[100];
printf("Reading from stdin (press Ctrl+C to interrupt)...\n");
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
if (n < 0) {
if (errno == EINTR) {
printf("read() was interrupted by signal! (EINTR)\n");
printf("This is because SA_RESTART was not set.\n");
} else {
perror("read");
}
} else {
printf("Read %zd bytes\n", n);
}
return 0;
}3.3 使用 /proc 分析信号状态
# 查看进程的信号信息
cat /proc/self/status | grep -i sig
# SigQ: 0/63497
# SigPnd: 0000000000000000
# SigBlk: 0000000000000000
# SigIgn: 0000000000000004 # SIGINT 被忽略?
# SigCgt: 0000000000000000
# 解码信号位图
python3 -c "
mask = 0x0000000000000004
for i in range(1, 65):
if mask & (1 << (i-1)):
print(f'Signal {i} is set')
"
# Signal 3 is set (SIGQUIT)
# 查看信号定义
kill -l
# 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL ...四、本章小结
本章从内核角度深入讲解了信号的实现机制。信号在内核中通过task_struct的pending集合和blocked掩码管理。信号发送将信号添加到目标进程的pending集合,内核在返回用户态前检查pending集合,传递未被阻塞的信号。
信号处理函数的执行需要内核构造特殊的用户态栈帧:保存当前上下文,修改指令指针跳转到处理函数,处理函数返回后通过sigreturn系统调用恢复原始上下文。普通信号不排队(多个相同信号只保留一个),实时信号支持排队和携带数据。
信号的阻塞机制允许进程临时延迟信号的传递,被阻塞的信号留在pending集合中,直到解除阻塞后立即传递。SIGKILL和SIGSTOP不能被阻塞、捕获或忽略,这是内核的强制规定。
五、思考与练习
思考题
为什么SIGKILL不能被捕获或忽略?如果允许捕获SIGKILL,会带来什么问题?
信号处理函数为什么应该是"可重入安全"的?哪些函数是信号安全的?
在多线程程序中,信号是如何在线程之间分配的?线程组共享的信号和线程私有的信号有什么区别?
为什么信号处理函数中不能调用printf?应该使用什么替代?
编程练习
编写一个程序,验证普通信号不排队的行为:阻塞SIGINT,发送多个SIGINT,然后解除阻塞,观察只处理一次。
编写一个程序,使用实时信号(SIGRTMIN)实现简单的进程间消息传递,验证实时信号的排队和数据携带功能。
编写一个程序,演示信号中断系统调用的行为(EINTR),然后使用SA_RESTART标志重新测试,对比两种行为。