马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
信号是什么
在 Linux 里,进程之间可以通过信号(signal)通信,用来传递"某件事发生了"这类异步通知。信号是一种很轻量的通信机制——不传数据,只传一个编号,告诉对方"该处理一下了"。
信号有三个特点值得先记住:
- 异步:信号什么时候来,接收方提前不知道。接收方在执行自己的任务,信号可能随时插进来。
- 不阻塞主流程:进程收到信号后不会"暂停手上的活"去等待,信号通常被记下来,等合适的时机再处理。
- 有默认行为:每个信号都有约定好的默认动作,比如终止进程、终止并生成 core、忽略等。
用 kill -l 可以看到系统支持的全部信号:
kill -l
规律是:编号 1–31 是普通信号(非实时信号),34–64 是实时信号(RT signal)。其中没有 0,32 和 33 也不被用作普通信号编号。信号既可以用数字写,也可以用名字写(比如 -9 和 -KILL 等价),本质上名字就是宏定义。
进程是怎么"看待"信号的
很多教材会写这样一段话,理解它其实就理解了信号的整个生命周期:
- 信号没来的时候,进程就已经知道来了该怎么办。进程可以提前用
signal() 或 sigaction() 注册处理函数,也可以保持默认行为。
- 进程能识别信号。系统给每个信号定义了编号和默认动作,比如 SIGINT 默认终止、SIGKILL 默认立即终止且不可捕获。
- 信号来了如果正忙,得先存着。这就是信号的异步性:进程正在跑关键逻辑,来不及处理时内核先把信号挂起,等程序执行到可以中断的点再处理。
- 信号到了不一定要立刻处理,可以挑时间。处理时机由内核和用户态程序共同决定,这也是异步事件编程的灵活性所在。
- 信号的产生无法精确预料,所以必然是异步发送的。
换句话说:信号由其他进程、用户操作或内核事件产生,接收方在信号到达前可能一直在忙自己的事,处理靠的是注册好的回调。
信号的产生方式
方式一:通过 signal() 注册处理函数
signal() 是最常用的注册接口,原型如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数与含义:
signum:信号编号,既可以是 SIGINT 这类宏,也可以是数字。
handler:处理函数指针,原型是 void handler(int signo);也可以是两个特殊值 SIG_IGN(忽略该信号)和 SIG_DFL(恢复默认行为)。
返回值:返回该信号此前注册的处理函数(一个函数指针);此前没有注册过则返回 SIG_DFL。
三种注册形态:
- 传自定义函数指针 → 收到信号时调用它。
- 传
SIG_IGN → 收到信号直接丢弃,不做任何处理。
- 传
SIG_DFL → 交回系统默认动作,通常是终止进程。
注意一点:注册动作本身不会立刻执行处理函数,只有真正收到对应信号时才会触发。注册了 SIGINT 的处理函数而进程一辈子没收到 SIGINT,那个函数永远不会被调用。
完整的表述就是:先定义一个处理函数(比如专门处理 SIGINT 的函数),再用 signal() 把它注册到 SIGINT 上;之后进程收到 SIGINT,内核就转去执行这个函数。
方式二:kill 命令 / kill() 函数
kill 这个名字容易误导,它不一定"杀进程",本质是给进程发信号。基本语法:
kill [选项] <pid>
目标进程 ID 用 ps、pgrep 等命令查到。常见写法:
kill 1234 # 默认发 SIGTERM(15)
kill -9 1234 # SIGKILL,不可捕获、不可忽略
kill -19 1234 # SIGSTOP,暂停进程
kill -TERM 1234 # 用信号名也行
方式三:终端按键
键盘组合键由终端驱动程序翻译成信号:
- Ctrl + C → 内核向当前前台进程组发送 SIGINT(编号 2),通常导致进程终止,效果类似主动退出。
- Ctrl + \ → 发送 SIGQUIT(编号 3),请求进程退出;与 SIGINT 不同,SIGQUIT 在 core dump 开启的情况下会生成 core 文件。
一条完整的链路就是:按键 → 操作系统解释成某种信号 → 发给目标进程 → 进程收到 → 走处理逻辑。
方式四:程序异常与内核事件
除了人为发送,还有几类来源:访问非法内存触发 SIGSEGV(段错误)、浮点异常触发 SIGFPE、子进程退出给父进程发 SIGCHLD、定时器到点发 SIGALRM、管道读端全关闭写端收到 SIGPIPE。这些在系统编程里非常常见。
Term 与 Core:信号默认动作的区别
信号的默认动作有几种,最常被问到的是 Term 和 Core 这两个:
- Term(Terminate):直接终止进程,不做 core dump。SIGTERM(15)属于这一类——这也是
kill 不带参数的默认信号。它"温和"的地方在于进程有机会在退出前清理资源、落盘、关闭连接。
- Core(Core dump):终止进程,同时把进程当时的内存映像写成一个 core 文件,供后续调试。SIGQUIT(3)、SIGABRT(6)、SIGSEGV(11)都落在这个类别。
所以 kill -QUIT 和 kill -TERM 看起来都是"让它退出",但前者会留下 core 文件,后者不会。调试线上崩溃时,core 文件往往才是线索来源。
不过现在默认基本拿不到 core 文件,原因通常是:
ulimit -c 为 0(最常见),用 ulimit -c unlimited 打开即可。
- 内核参数
kernel.core_pattern 指向了某个程序(如 systemd-coredump、apport)接管,core 被转存到别处而非当前目录。
- 生产容器里通常把 core 关掉以省空间。
用 cat /proc/sys/kernel/core_pattern 和 ulimit -c 两条命令就能确认当前环境是否留 core。
哪些信号抓不住
有两个例外必须记住:
- SIGKILL(9):既不能捕获也不能忽略,进程一定死。正因如此,它成了"强制结束"的兜底手段,也意味着进程没有清理的机会。
- SIGSTOP(19):同样不可捕获、不可忽略,会把进程完全暂停(不是终止)。
所以正确的停止姿势是:先 kill(SIGTERM)给进程优雅退出的机会,超时还没退,再上 kill -9。运维里"服务 kill 不掉"的九成情况,都是直接跳过了 SIGTERM 那一步。
一个最小示例
下面的程序注册了对 SIGINT 的处理,收到信号时打印一行提示而不是直接退出:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
printf("收到信号 %d,正在清理...\n", signum);
}
int main()
{
signal(SIGINT, handler);
printf("pid=%d,试试 Ctrl+C\n", getpid());
while (1) {
sleep(1);
}
return 0;
}
运行后按 Ctrl+C,进程打印提示但不会退出;再发一次真正的终止信号(或用 kill -9)才会结束。
实际生产中更推荐 sigaction() 而不是 signal():它的语义更明确,而且能保证信号处理函数在执行期间不会自己被同一个信号打断(配合 sigset_t 屏蔽字使用),是POSIX 推荐的做法。
小结
信号是 Linux 进程间最轻量的异步通信方式:编号 1–31 为普通信号、34–64 为实时信号;产生来源包括键盘组合键、kill 命令、别的进程 kill() 调用以及内核异常事件;处理靠 signal() / sigaction() 注册回调;默认动作里 Term 是直接终止,Core 会额外留下 core 文件。
最实用的三条经验:优雅退出用 SIGTERM、进程卡死才用 SIGKILL、怀疑段错误时先确认 ulimit -c 与 core_pattern 有没有打开,否则 core 文件根本不会生成。
(本文由 qidao123.com企服评测-应用市场 编辑提示整理:示例基于 Linux/GNU C 环境,编译可用 gcc main.c -o main,信号编号以 x86_64 Linux 的 1–31 普通信号为准。) |