Linux signal是操作系统内核用来向进程通知异步事件的一种机制。每个信号用一个整数编号表示,内核、其他进程或者进程自身都可以通过系统调用发送信号。信号不携带复杂数据,只表达“某类事情发生了”,接收方进程可以按照默认动作处理,也可以自定义处理函数,或者选择暂时阻塞。
信号在Linux中承担的核心作用
信号最直观的用途是进程控制与通信。比如我们在终端按下Ctrl+C,内核会向前台进程组发送SIGINT,默认动作是终止进程。这种机制让用户不需要知道进程内部状态,就能从外部干预程序运行。后台守护进程也常借助SIGTERM做优雅退出,收到后释放资源再自行结束,而不是直接被SIGKILL强杀。
除了人为控制,信号也是内核报告硬件或软件异常的主要渠道。当程序执行了非法内存访问,CPU触发页错误,内核会向该进程发送SIGSEGV;执行了除以零的操作,会收到SIGFPE。这类信号本质上是在告诉进程:你刚才的行为操作系统无法继续容忍,需要立刻处理或终止。借助信号,错误从内核态以异步方式传递到用户态,避免了轮询检测的开销。
常见信号分类与默认动作
Linux信号大致分为标准信号和实时信号。标准信号编号从1到31,例如SIGHUP(终端挂断)、SIGQUIT(退出并core dump)、SIGUSR1和SIGUSR2(用户自定义)。实时信号从SIGRTMIN开始,支持排队,不会像标准信号那样丢失。下面列出几个高频信号的用途:
| 信号名 | 编号 | 默认动作 | 典型场景 |
|---|---|---|---|
| SIGINT | 2 | 终止 | 终端Ctrl+C中断 |
| SIGTERM | 15 | 终止 | 系统关机或管理服务停止 |
| SIGKILL | 9 | 终止且不可捕获 | 强制杀进程 |
| SIGSEGV | 11 | 终止并core dump | 非法内存访问 |
注意SIGKILL和SIGSTOP这两个信号既不能被捕获也不能被忽略,这是内核为保证系统可管理性留下的底线。其余大部分信号都允许进程通过signal()或sigaction()修改处理方式。
如何用代码注册信号处理函数
早期接口signal()使用简单但行为在不同Unix系统有差异,现代程序更推荐sigaction()。它允许明确指定处理函数、信号掩码以及是否重启被中断的系统调用。下面示例用C语言捕获SIGINT,打印提示后正常退出:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void handle_sigint(int sig) {
// 收到SIGINT时执行,不推荐使用printf等不可重入函数,这里仅作演示
write(STDOUT_FILENO, "received SIGINT, exitingn", 24);
exit(0);
}
int main() {
struct sigaction sa;
sa.sa_handler = handle_sigint;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
// 注册SIGINT的处理函数
sigaction(SIGINT, &sa, NULL);
while (1) {
sleep(1);
}
return 0;
}
上面的代码把SIGINT绑定到自定义函数,终端按Ctrl+C就不再直接杀掉进程,而是先输出一行信息再退出。实际服务中,处理函数里通常只设置一个原子变量标志,主循环检测到后做清理,这样避开在信号处理函数里调用非异步信号安全函数的风险。
如果程序需要临时屏蔽某些信号,可以使用sigprocmask()。例如临界区修改全局结构时,先阻塞SIGUSR1,处理完再解除,避免数据竞争。信号阻塞和忽略不同:阻塞是暂时不递送,解除后若发生过仍会到达;忽略则是永久丢弃。
信号与多线程环境注意事项
在多线程程序中,信号递送规则更复杂。默认情况下,信号可以发给进程内的任意线程,具体由内核选择。通常做法是在主线程创建时,用pthread_sigmask屏蔽所有信号,再单独起一个信号处理线程,通过sigwait()同步等待信号,从而避免异步信号处理函数带来的可重入难题。
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void* sig_thread(void* arg) {
sigset_t* set = (sigset_t*)arg;
int sig;
// 同步等待信号,不会异步打断其他线程
while (sigwait(set, &sig) == 0) {
if (sig == SIGTERM) break;
}
return NULL;
}
int main() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGTERM);
pthread_sigmask(SIG_BLOCK, &set, NULL);
pthread_t tid;
pthread_create(&tid, NULL, sig_thread, &set);
pthread_join(tid, NULL);
return 0;
}
这种模式下,信号像普通事件一样被一个专用线程顺序处理,既安全又容易推理。很多网络服务框架底层就采用类似模型,把SIGTERM、SIGHUP统一交给管理线程,业务线程专注读写逻辑。
信号机制的局限与替代方案
信号只能携带编号,无法附带结构化数据,且标准信号不排队,连续发送多次可能只收到一次。对于需要可靠、带负载的进程间通信,应当使用管道、消息队列、共享内存或者socket。信号更适合做控制面通知,而不是数据面传输。理解这一点,才能在架构设计时把信号用在刀刃上,而不是试图用它替代所有IPC方式。
另外,在容器化和编排系统里,进程常作为PID 1运行,此时bash等init行为差异会导致信号转发问题。比如Docker中前台进程若不是直接接收SIGTERM,就需要用tini之类的小工具做信号透传。把握这些实践细节,才能让基于信号的重启、缩容指令真正生效。