信号(Signal)是 Linux 系统中进程间通信的一种机制,用于通知进程发生了某个事件。当进程收到信号后,系统会按照特定的流程处理该信号。本文档详细说明 Linux 进程收到信号后的完整处理流程。
信号的基本概念
什么是信号
信号是 Linux 系统中一种异步通知机制,用于:
- 通知进程发生了某个事件
- 请求进程执行某个操作
- 中断进程的正常执行流程
信号的分类
按行为分类
| 类型 |
说明 |
示例 |
| 终止信号 |
导致进程终止 |
SIGTERM, SIGKILL, SIGQUIT |
| 忽略信号 |
进程可以忽略 |
SIGCHLD(默认忽略) |
| 停止信号 |
暂停进程执行 |
SIGSTOP, SIGTSTP |
| 继续信号 |
恢复暂停的进程 |
SIGCONT |
| 错误信号 |
由硬件异常产生 |
SIGSEGV, SIGFPE, SIGILL |
按可处理性分类
| 类型 |
说明 |
示例 |
| 可捕获信号 |
可以注册处理函数 |
SIGTERM, SIGINT, SIGHUP |
| 不可捕获信号 |
无法注册处理函数 |
SIGKILL, SIGSTOP |
常用信号列表
| 信号编号 |
信号名 |
默认行为 |
说明 |
| 1 |
SIGHUP |
终止 |
挂起信号,终端关闭时发送 |
| 2 |
SIGINT |
终止 |
中断信号(Ctrl+C) |
| 3 |
SIGQUIT |
终止+核心转储 |
退出信号(Ctrl+\) |
| 9 |
SIGKILL |
终止 |
强制终止,不可捕获 |
| 11 |
SIGSEGV |
终止+核心转储 |
段错误 |
| 13 |
SIGPIPE |
终止 |
管道破裂 |
| 14 |
SIGALRM |
终止 |
定时器信号 |
| 15 |
SIGTERM |
终止 |
终止信号(可捕获) |
| 17 |
SIGCHLD |
忽略 |
子进程状态改变 |
| 18 |
SIGCONT |
继续 |
继续执行 |
| 19 |
SIGSTOP |
停止 |
停止信号,不可捕获 |
| 20 |
SIGTSTP |
停止 |
终端停止信号(Ctrl+Z) |
信号处理的完整流程
流程图概览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 发送信号 (kill/signal) ↓ 内核检查权限 ↓ 信号投递到目标进程 ↓ 检查信号掩码(是否被阻塞) ↓ 信号被阻塞? → 是 → 加入待处理信号集(pending) ↓ 否 检查信号处理方式 ↓ ┌─────────────────┬─────────────────┬─────────────────┐ │ 默认处理 │ 忽略信号 │ 自定义处理函数 │ │ (SIG_DFL) │ (SIG_IGN) │ (用户函数) │ └─────────────────┴─────────────────┴─────────────────┘ ↓ ↓ ↓ 执行默认动作 直接返回 切换到用户空间 (终止/停止等) 执行处理函数
|
详细处理步骤
1. 信号发送阶段
1 2 3 4 5 6
| ➜ kill -TERM 1234
raise(SIGTERM); kill(getpid(), SIGTERM);
|
内核操作:
- 检查发送者权限(能否向目标进程发送信号)
- 验证目标进程是否存在
- 将信号加入目标进程的信号队列
2. 信号投递阶段
内核在以下时机检查并投递信号:
- 从系统调用返回用户空间时
- 从中断/异常处理返回用户空间时
- 进程从睡眠状态被唤醒时
3. 信号处理阶段
3.1 检查信号掩码(Signal Mask)
1 2 3 4 5
| sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigprocmask(SIG_BLOCK, &mask, NULL);
|
如果信号被阻塞:
- 信号被加入待处理信号集(pending set)
- 进程继续执行,不处理该信号
- 直到信号被解除阻塞后才处理
如果信号未被阻塞:
3.2 检查信号处理方式
每个信号有三种可能的处理方式:
1 2 3 4 5 6 7 8 9 10 11
| signal(SIGTERM, SIG_DFL);
signal(SIGTERM, SIG_IGN);
void handler(int sig) { } signal(SIGTERM, handler);
|
3.3 执行相应的处理
默认处理(SIG_DFL):
- 内核直接执行默认动作
- 如:SIGTERM → 终止进程,SIGSTOP → 停止进程
忽略信号(SIG_IGN):
自定义处理函数:
- 内核切换到用户空间
- 执行用户注册的信号处理函数
- 处理函数执行完毕后返回
4. 信号处理函数执行
1 2 3 4 5 6 7 8
| void signal_handler(int signo) { printf("Received signal %d\n", signo); }
|
注意事项:
- 信号处理函数应该是可重入的(reentrant)
- 避免在信号处理函数中使用不可重入的函数(如 malloc, printf)
- 信号处理函数执行时,可能被其他信号中断
5. 从信号处理返回
处理函数执行完毕后:
- 恢复进程的执行上下文
- 继续执行被信号中断的代码
- 或执行 sigreturn 系统调用返回内核
内核层面的处理
进程的信号相关数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct task_struct { struct sigpending pending; sigset_t blocked; struct sigaction sigaction[64]; };
struct sigpending { struct list_head list; sigset_t signal; };
struct sigaction { __sighandler_t sa_handler; sigset_t sa_mask; unsigned long sa_flags; void (*sa_restorer)(void); };
|
内核信号处理流程
1. 信号发送(send_signal)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int send_signal(int sig, struct task_struct *t, int group) { if (!check_permission(sig, t)) return -EPERM; struct sigqueue *q = sigqueue_alloc(); q->info.si_signo = sig; sigaddset(&t->pending.signal, sig); list_add_tail(&q->list, &t->pending.list); if (t->state & TASK_INTERRUPTIBLE) wake_up_process(t); return 0; }
|
2. 信号投递(deliver_signal)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| void do_signal(struct pt_regs *regs) { struct sigpending *pending; struct sigaction *sa; int signr; pending = ¤t->pending; signr = dequeue_signal(current, ¤t->blocked, &info); if (signr == 0) return; sa = ¤t->sigaction[signr]; if (sa->sa_handler == SIG_IGN) { return; } else if (sa->sa_handler == SIG_DFL) { do_default_action(signr); } else { handle_signal(signr, sa, regs); } }
|
3. 用户空间处理函数调用
1 2 3 4 5 6 7 8
| void handle_signal(int sig, struct sigaction *sa, struct pt_regs *regs) { }
|
信号掩码和阻塞
信号掩码的作用
信号掩码(Signal Mask)用于控制哪些信号被阻塞(blocked),被阻塞的信号不会立即处理,而是加入待处理信号集。
信号掩码操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <signal.h>
sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigprocmask(SIG_BLOCK, &mask, NULL);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
sigprocmask(SIG_SETMASK, &mask, NULL);
|
临时阻塞信号
1 2 3 4 5 6 7 8 9 10 11 12 13
| sigset_t oldmask, newmask; sigemptyset(&newmask); sigaddset(&newmask, SIGINT);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
critical_section();
sigprocmask(SIG_SETMASK, &oldmask, NULL);
|
实际代码示例
示例1:基本的信号处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <stdio.h> #include <signal.h> #include <unistd.h>
void sig_handler(int signo) { if (signo == SIGINT) { printf("Received SIGINT (Ctrl+C)\n"); } else if (signo == SIGTERM) { printf("Received SIGTERM\n"); } }
int main() { if (signal(SIGINT, sig_handler) == SIG_ERR) { perror("signal"); return 1; } if (signal(SIGTERM, sig_handler) == SIG_ERR) { perror("signal"); return 1; } printf("Process PID: %d\n", getpid()); printf("Waiting for signal...\n"); while (1) { pause(); } return 0; }
|
编译运行:
1 2 3
| gcc -o signal_demo signal_demo.c ./signal_demo
|
示例2:使用 sigaction(推荐方式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <stdio.h> #include <signal.h> #include <unistd.h> #include <string.h>
void sig_handler(int signo, siginfo_t *info, void *context) { printf("Received signal %d from PID %d\n", signo, info->si_pid); }
int main() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = sig_handler; sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGTERM); if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); return 1; } printf("Process PID: %d\n", getpid()); printf("Press Ctrl+C to send SIGINT\n"); while (1) { pause(); } return 0; }
|
示例3:信号掩码的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include <stdio.h> #include <signal.h> #include <unistd.h>
void sigint_handler(int sig) { printf("SIGINT handler called\n"); }
void sigterm_handler(int sig) { printf("SIGTERM handler called\n"); }
int main() { signal(SIGINT, sigint_handler); signal(SIGTERM, sigterm_handler); sigset_t mask, oldmask; sigemptyset(&mask); sigaddset(&mask, SIGINT); printf("Blocking SIGINT for 5 seconds...\n"); sigprocmask(SIG_BLOCK, &mask, &oldmask); sleep(5); printf("Unblocking SIGINT...\n"); sigset_t pending; sigpending(&pending); if (sigismember(&pending, SIGINT)) { printf("SIGINT was pending and will be delivered now\n"); } sigprocmask(SIG_SETMASK, &oldmask, NULL); printf("SIGINT is now unblocked\n"); while (1) { pause(); } return 0; }
|
示例4:处理多个信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <stdio.h> #include <signal.h> #include <unistd.h>
volatile sig_atomic_t flag = 0;
void handler(int sig) { if (sig == SIGUSR1) { flag = 1; printf("Received SIGUSR1, setting flag\n"); } else if (sig == SIGUSR2) { flag = 2; printf("Received SIGUSR2, setting flag\n"); } }
int main() { signal(SIGUSR1, handler); signal(SIGUSR2, handler); printf("Process PID: %d\n", getpid()); printf("Send SIGUSR1 or SIGUSR2 to this process\n"); while (1) { pause(); if (flag == 1) { printf("Processing SIGUSR1 action\n"); flag = 0; } else if (flag == 2) { printf("Processing SIGUSR2 action\n"); flag = 0; } } return 0; }
|
信号处理的时机
信号投递的时机
信号在以下时机被检查和投递:
从系统调用返回
从中断/异常返回
进程被唤醒时
显式检查信号
1 2
| sigset_t pending; sigpending(&pending);
|
信号处理的原子性
- 信号处理函数执行期间,相同的信号默认会被阻塞
- 可以使用
sa_mask 指定在处理函数执行时阻塞其他信号
- 这确保了信号处理的原子性
特殊信号的处理
SIGKILL 和 SIGSTOP
这两个信号的特殊之处:
- 无法被捕获:不能注册处理函数
- 无法被阻塞:不能通过信号掩码阻塞
- 无法被忽略:必须执行默认动作
1 2 3 4
| signal(SIGKILL, handler); signal(SIGSTOP, handler); sigprocmask(SIG_BLOCK, &mask);
|
SIGCHLD 信号
子进程状态改变时发送给父进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <stdio.h> #include <signal.h> #include <sys/wait.h> #include <unistd.h>
void sigchld_handler(int sig) { int status; pid_t pid; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { printf("Child %d terminated\n", pid); } }
int main() { signal(SIGCHLD, sigchld_handler); pid_t pid = fork(); if (pid == 0) { sleep(2); exit(0); } printf("Parent waiting for child...\n"); while (1) { pause(); } return 0; }
|
实时信号(RT Signals)
Linux 支持实时信号(SIGRTMIN 到 SIGRTMAX):
1 2 3 4 5 6 7 8
|
union sigval value; value.sival_int = 123; sigqueue(pid, SIGRTMIN, value);
|
常见问题和注意事项
1. 信号处理函数应该是可重入的
1 2 3 4 5 6 7 8 9 10 11 12
| void handler(int sig) { printf("Signal received\n"); malloc(100); }
volatile sig_atomic_t flag = 0;
void handler(int sig) { flag = 1; }
|
2. 信号可能丢失
标准信号(1-31)不支持排队,如果信号在处理期间再次到达,可能会丢失:
1 2 3 4 5 6
| sigset_t mask; sigaddset(&mask, SIGINT); sigprocmask(SIG_BLOCK, &mask, NULL);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
|
3. 信号处理函数的执行时间
信号处理函数应该尽可能简短,避免长时间阻塞:
1 2 3 4 5 6 7 8 9 10
| void handler(int sig) { sleep(10); complex_operation(); }
void handler(int sig) { flag = 1; }
|
4. 信号处理中的系统调用
如果信号处理函数中断了阻塞的系统调用,系统调用可能返回 EINTR:
1 2 3 4 5 6 7 8
| while (1) { n = read(fd, buf, size); if (n == -1 && errno == EINTR) { continue; } break; }
|
5. 信号处理函数中的异步安全函数
信号处理函数中只能使用异步安全(async-signal-safe)的函数:
异步安全的函数:
write()
read()(某些情况下)
_exit()
sigaction()
kill()
- 等等
不安全的函数:
printf(), malloc(), free()
- 大部分标准库函数
调试信号处理
使用 strace 跟踪信号
1 2 3 4 5
| strace -e trace=signal -p <PID>
strace -e signal -p <PID>
|
使用 gdb 调试
1 2 3 4 5 6
| (gdb) handle SIGINT stop print (gdb) handle SIGTERM stop print
(gdb) info signals
|
查看进程的信号状态
1 2 3 4 5
| cat /proc/<PID>/status | grep Sig
cat /proc/<PID>/status
|
总结
Linux 进程收到信号的处理流程可以概括为:
- 信号发送:通过 kill、raise 等函数发送信号
- 权限检查:内核检查发送者是否有权限
- 信号投递:在适当的时机(系统调用返回、中断返回等)投递信号
- 掩码检查:检查信号是否被阻塞
- 处理方式:根据信号的处理方式(默认/忽略/自定义)执行相应操作
- 用户处理:如果是自定义处理函数,切换到用户空间执行
- 返回恢复:处理完成后恢复进程执行
理解这个流程对于编写健壮的 Linux 程序非常重要,特别是在处理信号、实现优雅关闭、处理异常情况等方面。
参考文献