信号基础:信号的概念、常见信号与 signal() 函数
信号,说白了就是操作系统给进程发的一种“软中断”。
我刚开始学 Linux 编程时,总觉得信号这东西挺玄乎。后来在嵌入式项目中调试一个串口程序,发现按 Ctrl+C 杀不掉进程,才真正体会到——不懂信号,你连怎么优雅地退出程序都做不到。
今天我们就来聊聊信号的基础知识。包括信号是什么、有哪些常见信号、以及怎么用 signal() 函数去处理它们。
信号是什么?
信号是一种异步事件通知机制。内核或者某个进程,可以向另一个进程发送信号。收到信号的进程,可以选择忽略、执行默认动作、或者调用自定义的处理函数。
你想想看,进程正在跑着,突然收到一个信号,就像你正在写代码,突然有人拍你肩膀——你得决定是继续写、还是回头看一眼、还是直接关机走人。
核心要点:信号是异步的。进程不知道信号什么时候来,只能提前注册好处理函数。
常见信号一览
Linux 系统定义了数十种信号。我挑几个最常用的,也是面试和项目中经常遇到的,给你列出来:
| 信号名 | 编号 | 默认动作 | 说明 |
|---|---|---|---|
| SIGINT | 2 | 终止进程 | 终端中断,通常由 Ctrl+C 触发 |
| SIGKILL | 9 | 终止进程 | 强制杀死,不能被捕获或忽略 |
| SIGSEGV | 11 | 终止进程(core dump) | 段错误,非法内存访问 |
| SIGTERM | 15 | 终止进程 | 软件终止信号,可被捕获 |
| SIGUSR1 | 10 | 终止进程 | 用户自定义信号1 |
| SIGCHLD | 17 | 忽略 | 子进程状态改变时发送给父进程 |
这里我要特别提一下 SIGKILL 和 SIGSTOP(编号 19)。这两个信号,你没法用 signal() 去捕获或忽略。为什么?因为系统要保证管理员总有办法杀掉一个失控的进程。我曾经在项目里遇到一个死循环的守护进程,普通信号发过去没反应,最后就是靠 kill -9 解决的。
注意:不要试图捕获 SIGKILL 和 SIGSTOP。这是内核的底线,你绕不过去。
signal() 函数的使用
处理信号,最基础的函数就是 signal()。它的原型很简单:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
嗯,这个声明看着有点绕。我习惯把它拆开理解:
signum:你要处理的信号编号或宏名handler:一个函数指针,指向你的信号处理函数- 返回值:之前的信号处理函数指针
说白了,就是告诉内核:“嘿,当这个信号来了,你帮我调用这个函数。”
一个简单的例子
我们写一个程序,捕获 SIGINT(Ctrl+C),然后打印一条消息,而不是直接退出:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("\n收到 SIGINT 信号!按 Ctrl+C 也没用,我偏不退出。\n");
}
int main() {
// 注册信号处理函数
signal(SIGINT, sigint_handler);
printf("程序运行中... 按 Ctrl+C 试试看。\n");
// 让程序一直跑着
while (1) {
sleep(1);
}
return 0;
}
运行这个程序,你按 Ctrl+C,它不会退出,而是打印一条消息。嗯,这里要注意:printf() 在信号处理函数中其实不是完全安全的,但为了演示,我们先用着。
个人建议:信号处理函数里尽量做简单的事。比如设置一个全局标志位,然后在主循环里检查这个标志位。我在项目中吃过亏,在信号处理函数里调用了 malloc(),结果死锁了。信号处理函数不是普通函数,它可能打断任何代码的执行。
信号处理函数的注意事项
用 signal() 有几个坑,我帮你列出来:
- 可重入性:信号处理函数必须是可重入的。不要调用非可重入函数,比如
malloc()、printf()、fopen()等。 - 全局变量:如果要在信号处理函数和主程序之间共享变量,记得用
volatile sig_atomic_t类型。这个类型保证读写是原子的。 - 信号屏蔽:在处理一个信号时,同类型的信号会被自动屏蔽,防止嵌套。但其他信号仍可能打断当前处理函数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1; // 只做一件事:设置标志位
}
int main() {
signal(SIGINT, handler);
while (!flag) {
// 主循环做自己的事
sleep(1);
}
printf("收到信号,准备退出。\n");
return 0;
}
这个写法就安全多了。信号处理函数只设置一个标志位,主循环去检查它。我在实际项目中,处理 SIGTERM 和 SIGINT 时,基本都是这个模式。
信号与系统接口的关系
信号不是孤立存在的。它和很多系统调用有交互。比如:
pause():让进程挂起,直到收到一个信号alarm():定时发送 SIGALRM 信号kill():向指定进程发送信号raise():向自己发送信号
我记得有一次调试一个网络程序,客户端连上后一直没反应。后来发现是服务端调用了 pause() 等待信号,但信号处理函数没注册好,导致进程永远挂在那。嗯,这种问题排查起来挺费劲的。
SVG 知识结构图
下面这张图,帮你理清信号相关的核心概念:
总结
信号是 Linux 编程中绕不开的基础知识。你不需要记住所有信号,但 SIGINT、SIGKILL、SIGSEGV、SIGTERM 这几个,必须烂熟于心。
signal() 函数虽然简单,但用起来有讲究。记住:信号处理函数里别干重活,设个标志位就够了。如果你想要更精细的控制,后面可以学 sigaction(),它比 signal() 更强大、更可靠。
嗯,今天就聊到这里。信号这东西,多写几个 demo 就熟了。
公众号:蓝海资料掘金营,微信 deep3321