Linux 信号 (Signals)
一、概念
信号是操作系统内核向用户空间进程发送的一种异步通知机制。它用于通知进程某个特定事件发生了。
1. 信号的生命周期
一个信号的处理遵循以下流程:
产生 (Generation): 由内核、其他进程或硬件事件(如
SIGSEGV
由 MMU 产生)创建。递送 (Delivery): 内核将信号传递给目标进程,并促使进程采取行动。
处理 (Handling): 目标进程收到信号后执行预定的操作。
2. 信号的来源
硬件异常: 由 CPU 检测到,内核将其转换为信号发给相关进程。
SIGSEGV
(Segmentation Fault): 非法内存访问。SIGFPE
(Floating-Point Exception): 算术错误,如除零。SIGILL
(Illegal Instruction): 执行了非法指令。
终端相关: 来自键盘的中断。
SIGINT
(Interrupt):Ctrl+C
,通常终止进程。SIGQUIT
(Quit):Ctrl+\
,终止并生成 core dump。SIGTSTP
(Terminal Stop):Ctrl+Z
,暂停进程。
软件事件:
SIGCHLD
: 子进程状态改变(停止、退出)。SIGPIPE
: 管道破裂(读端关闭后继续写)。SIGALRM
: 由alarm()
或setitimer()
设置的定时器超时。SIGUSR1
,SIGUSR2
: 用户自定义的信号。
其他进程: 使用
kill()
系统调用发送。
3. 信号的处理方式
进程对信号的处理有三种选择:
默认动作 (Default Action): 大多数信号的默认动作是终止进程。有些是忽略(
SIGCHLD
)或生成 core dump(SIGQUIT
)。忽略信号 (Ignore): 告诉内核无需处理该信号(
SIGKILL
和SIGSTOP
不能被忽略或捕获)。捕获信号 (Catch): 进程可以注册一个信号处理函数 (Signal Handler),当信号到来时,内核会中断进程的正常执行流,转而执行这个函数。
4. 重要系统调用和函数
signal()
: 简单的注册信号处理函数(已过时,可移植性差)。sigaction()
: 现代、标准且功能更强大的方法,用于注册信号处理函数,并能精确控制信号行为。kill()
: 向指定进程发送信号。alarm()
: 设置一个实时定时器,超时后产生SIGALRM
。pause()
: 挂起调用进程,直到任何信号到达。信号集操作函数:
sigemptyset()
,sigaddset()
,sigprocmask()
等,用于管理信号掩码,阻塞或解除阻塞特定信号。
5. 信号的特点和注意事项
异步性: 信号可能在进程执行的任何时刻到达。
可靠性: 标准信号(1-31)是不可靠的,相同信号在处理期间再次到来可能会丢失。实时信号(34-64)是可靠的,支持排队。
可重入性 (Reentrancy): 在信号处理函数中,只能调用异步信号安全 (async-signal-safe) 的函数(如
write()
,_exit()
)。严禁调用malloc()
,printf()
等非安全函数,否则可能导致死锁或未定义行为。全局变量和
volatile
: 信号处理函数和主程序之间通信的全局变量应声明为volatile
,防止编译器优化导致意外行为。
二、常见信号
信号可以分为以下几类:终止信号、中断/暂停信号、异常信号、作业控制信号和用户自定义信号。
1. 终止信号 (Termination Signals)
这些信号默认会导致进程终止。
信号名 | 值 | 默认动作 | 描述 & 触发场景 | 可否捕获/忽略 | 核心转储 |
---|---|---|---|---|---|
SIGHUP | 1 | Terminate | 挂起 (Hangup)。当控制终端关闭时发送给其关联的前台进程组。也常用于通知守护进程重新加载配置文件(如 nginx -s reload )。 | 是 | 否 |
SIGINT | 2 | Terminate | 中断 (Interrupt)。来自键盘的中断,通常由用户按下 Ctrl+C 产生。用于优雅地终止前台进程。 | 是 | 否 |
SIGQUIT | 3 | Core | 退出 (Quit)。来自键盘的退出,通常由用户按下 Ctrl+\ 产生。用于终止进程并生成核心转储 (core dump),便于后续调试。 | 是 | 是 |
SIGTERM | 15 | Terminate | 终止 (Terminate)。这是 kill 命令的默认信号。它要求进程正常终止,允许进程进行清理工作(关闭文件、释放资源等)。是优雅关闭进程的首选方式。 | 是 | 否 |
SIGKILL | 9 | Terminate | 杀死 (Kill)。立即、强制终止进程。该信号不能被捕获、阻塞或忽略。进程会立刻死亡,没有机会进行任何清理。是确保进程终止的“杀手锏”,但应作为最后手段使用。 | 否 | 否 |
使用场景总结:
想优雅地结束一个进程:先尝试
kill <PID>
或kill -TERM <PID>
(发送SIGTERM
)。进程不响应
SIGTERM
:尝试kill -INT <PID>
(发送SIGINT
)。需要强制结束一个“顽固”进程:使用
kill -9 <PID>
(发送SIGKILL
)。想让进程退出并留下调试文件:使用
kill -QUIT <PID>
(发送SIGQUIT
),需确保系统允许生成 core dump(ulimit -c unlimited
)。
2. 异常信号 (Exception Signals)
这些信号由 CPU 检测到异常后,由内核发送给进程。
信号名 | 值 | 默认动作 | 描述 & 触发场景 | 可否捕获/忽略 | 核心转储 |
---|---|---|---|---|---|
SIGILL | 4 | Core | 非法指令 (Illegal Instruction)。进程试图执行一条非法、格式错误、特权级别不对的CPU指令。通常由损坏的可执行文件或尝试执行数据段导致。 | 是 | 是 |
SIGTRAP | 5 | Core | 陷阱跟踪 (Trace Trap)。由断点指令或其他陷阱指令触发,主要用于调试器(如 gdb)实现单步执行和断点。 | 是 | 是 |
SIGABRT | 6 | Core | 中止 (Abort)。由进程自己调用 abort() 函数产生。表示进程检测到了致命错误并主动调用中止。C库中的 assert() 失败也会触发此信号。 | 是 | 是 |
SIGBUS | 7 | Core | 总线错误 (Bus Error)。无效的内存访问,但不同于 SIGSEGV 。例如,访问了物理地址不存在或违反了内存对齐限制的内存(如某些架构上要求4字节对齐,但你访问了一个奇地址)。 | 是 | 是 |
SIGFPE | 8 | Core | 浮点异常 (Floating-Point Exception)。发生了致命的算术运算错误,如除以零、溢出、非法操作(如对负数开平方)。 | 是 | 是 |
SIGSEGV | 11 | Core | 段错误 (Segmentation Fault)。最常见的异常信号。表示进程尝试访问未被分配给它的虚拟内存地址,或试图以不允许的方式(如写只读内存)访问内存。通常是指针错误(空指针、野指针、缓冲区溢出)的标志。 | 是 | 是 |
3. 作业控制信号 (Job Control Signals)
这些信号用于 Shell 管理前台和后台作业。
信号名 | 值 | 默认动作 | 描述 & 触发场景 | 可否捕获/忽略 |
---|---|---|---|---|
SIGSTOP | 17,19,23 | Stop | 停止 (Stop)。暂停进程的执行。该信号不能被捕获、阻塞或忽略。是 Ctrl+Z 的另一种实现。 | 否 |
SIGTSTP | 18,20,24 | Stop | 终端停止 (Terminal Stop)。来自终端的停止信号,通常由用户按下 Ctrl+Z 产生。请求进程暂停运行(转入后台),允许它稍后恢复(用 fg 或 bg 命令)。 | 是 |
SIGCONT | 19,18,25 | Continue | 继续 (Continue)。让一个被 SIGSTOP 或 SIGTSTP 暂停的进程恢复执行。即使被忽略,它也能唤醒进程。 | 是 |
4.I/O 相关信号
信号 | 核心用途 | 处理建议 |
---|---|---|
SIGPIPE | 处理连接断开后的写入错误 | 忽略它 (SIG_IGN ),在代码中检查 EPIPE 错误。 |
SIGIO | 异步通知文件描述符就绪 | 在某些设备驱动中有用,但在网络编程中优先选择 epoll 。 |
SIGURG | 处理带外数据 (OOB) | 为需要紧急命令的应用程序设置处理函数。 |
SIGCHLD | 回收子进程资源,避免僵尸进程 | 必须在使用 fork() 的服务器中设置处理函数,并使用 waitpid() 循环。 |
5. 其他重要信号
信号名 | 值 | 默认动作 | 描述 & 触发场景 | 可否捕获/忽略 |
---|---|---|---|---|
SIGCHLD | 17,20,18 | Ignore | 子进程状态改变 (Child Status Changed)。当子进程停止、恢复或终止时,内核会向其父进程发送此信号。父进程可以在此信号处理函数中调用 wait() 或 waitpid() 来回收子进程资源,防止僵尸进程的产生。 | 是 |
SIGPIPE | 13 | Terminate | 管道破裂 (Broken Pipe)。当进程向一个读端已关闭的管道、Socket 或 FIFO 写入数据时,内核会发送此信号。常见于网络编程中,对端关闭了连接,本方仍在写入。 | 是 |
SIGALRM | 14 | Terminate | 闹钟信号 (Alarm Clock)。由 alarm() 或 setitimer() 设置的定时器超时后触发。常用于实现超时机制。 | 是 |
SIGUSR1 | 10,30,16 | Terminate | 用户自定义信号 1 (User-Defined Signal 1)。 | 是 |
SIGUSR2 | 12,31,17 | Terminate | 用户自定义信号 2 (User-Defined Signal 2)。SIGUSR1 和 SIGUSR2 没有预定义的含义,完全由应用程序自行定义其行为。常用于进程间自定义通信,例如通知守护进程切换日志文件、报告状态等。 | 是 |
三、示例代码
1. SIGPIPE
- 必须处理的信号
场景:你的网络服务器客户端断开连接后,服务器试图回复一个“再见”消息,导致 send()
调用触发 SIGPIPE
,进程默认被终止。
解决方案:
最佳实践:忽略它。直接忽略
SIGPIPE
信号,让write()
或send()
函数返回错误码-1
并设置errno
为EPIPE
。这样你可以在代码中逻辑地处理该错误,而不是让进程突然死亡。c
// 在程序初始化时忽略 SIGPIPE #include <signal.h> signal(SIGPIPE, SIG_IGN);
之后,你的 I/O 调用会安全地失败:
c
ssize_t bytes_sent = send(socket_fd, buffer, len, MSG_NOSIGNAL); // 也可以使用 MSG_NOSIGNAL 标志避免产生信号 if (bytes_sent == -1) {if (errno == EPIPE) {// 对端已经关闭,安全地关闭本地的socket并清理资源close(socket_fd);}// 处理其他错误... }
2. SIGIO
/ SIGPOLL
- 异步 I/O 通知
场景:你想实现一个高性能的应用程序,在没有使用 select
/poll
/epoll
的情况下,希望内核在文件描述符就绪时主动通知你。
解决方案:
为信号安装一个处理函数。
使用
fcntl
设置文件描述符的所有者和异步标志。c
#include <unistd.h> #include <fcntl.h> #include <signal.h>void io_handler(int sig, siginfo_t *info, void *context) {int fd = info->si_fd; // 哪个fd就绪了?int band = info->si_band; // 什么事件?(POLLIN, POLLOUT, etc.)// ... 处理I/O ... }int main() {struct sigaction sa;sa.sa_sigaction = io_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_SIGINFO; // 为了获取详细的siginfo_tsigaction(SIGIO, &sa, NULL);int fd = open("/dev/some_device", O_RDONLY);// 设置当前进程为接收SIGIO信号的进程fcntl(fd, F_SETOWN, getpid());// 启用该fd的异步模式int flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | O_ASYNC);// ... 主循环可以做其他事,等待信号中断 ... }
注意:在现代高性能网络中,SIGIO
已经很少用于 socket,因为 epoll
的性能和可扩展性远优于信号驱动的 I/O 模型。但它仍然在某些设备驱动(如串口、自定义硬件)中非常有用。
3. SIGURG
- 处理带外数据
场景:需要通过网络发送一个紧急命令(例如,远程控制程序的“紧急停止”)。
解决方案:
捕获
SIGURG
信号。在信号处理函数中,使用
recv()
和MSG_OOB
标志来读取带外数据。c
// 发送端 send(sockfd, "!", 1, MSG_OOB);// 接收端 void urg_handler(int sig) {char oob_data;recv(sockfd, &oob_data, 1, MSG_OOB);// 处理紧急命令 } // ... 注册信号处理函数 ... signal(SIGURG, urg_handler); // 必须设置socket是它的进程所有者才能接收SIGURG fcntl(sockfd, F_SETOWN, getpid());
4. SIGCHLD
- 管理子进程
场景:一个并发服务器使用 fork()
为每个客户端创建一个新的子进程。子进程退出后,如果不回收会成为僵尸进程。
解决方案:
c
void child_handler(int sig) {int saved_errno = errno; // 保存errno,因为waitpid可能会修改它while (waitpid(-1, NULL, WNOHANG) > 0) { // 使用循环和WNOHANG回收所有已退出的子进程continue;}errno = saved_errno;
}int main() {// 设置SIGCHLD处理函数struct sigaction sa;sa.sa_handler = child_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // SA_NOCLDSTOP: 子进程停止时不发出信号sigaction(SIGCHLD, &sa, NULL);while(1) {// ... accept 连接 ...pid_t pid = fork();if (pid == 0) {// 子进程处理逻辑exit(0);}// 父进程继续循环}
}