按 Ctrl+C 程序就退出了,这背后是操作系统给进程发了一个叫 SIGINT 的信号。信号是操作系统发给进程的软件中断,C 语言可以接管它们,写自定义处理函数。下面看信号的来龙去脉。
信号是什么
信号是操作系统发给进程的通知,比如:用户按了 Ctrl+C、别的进程发来消息、程序自己越界访问内存。每种信号都有个整数编号,比如 SIGINT 的值通常是 2。
默认行为
每个信号默认都有个处理动作,常见几类:
- 忽略(Ign):什么都不做直接返回。
- 终止(Term):把进程干掉。
- 继续/停止(Cont/Stop):恢复或挂起进程。
比如下面这个无限打印的程序,按 Ctrl+C 发 SIGINT,默认动作就是终止进程:
#include <stdio.h>
int main() {
while (1) {
printf("hello world\n");
}
return 0;
}输出会无限刷 hello world,直到 Ctrl+C 把它终止。
自定义信号处理函数
除了 SIGKILL 这类极少数,大多数信号都可以用自己的函数接管。处理函数必须返回 void,接收一个 int 参数(信号编号)。用 signal() 注册:
signal(type, signalHandler);一个经典的例子,捕获 Ctrl+C:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
/* 信号处理函数:打印信号编号 */
void signalHandler(int sig) {
printf("Caught signal: %d\n", sig);
exit(sig); /* 处理完退出 */
}
int main() {
signal(SIGINT, signalHandler); /* 接管 Ctrl+C */
while (1) {
printf("Hello World!\n");
/* 模拟工作,等一会 */
}
return 0;
}运行时程序一直打印 Hello World!,按 Ctrl+C:
Hello World!
Hello World!
...
Ctrl+C
Caught signal: 2原本该直接死掉的进程,被我们接管后走自定义逻辑,这就是优雅退出、清理资源的起点。
主动产生信号:raise()
不用等用户按键,程序自己也能发信号。raise() 给当前进程发信号:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void signalHandler(int sig) {
printf("Interrupt handled: %d\n", sig);
exit(sig);
}
int main() {
signal(SIGINT, signalHandler);
raise(SIGINT); /* 自己给自己发 SIGINT */
return 0;
}输出:
Interrupt handled: 2给别的进程发信号:kill()
kill() 可以把信号发给指定进程(用进程 ID):
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void handle_signal(int sig) {
printf("Received signal: %d\n", sig);
}
int main() {
signal(SIGINT, handle_signal);
pid_t pid = getpid(); /* 当前进程 ID */
kill(pid, SIGINT); /* 发给自己 */
return 0;
}输出:
Received signal: 2kill 名字吓人,其实只是发信号,信号内容是啥、进程怎么反应,由接收方决定。
常见信号速查
| 信号 | 含义 | 默认行为 |
|---|---|---|
SIGABRT | 程序异常中止(abort) | 终止 |
SIGFPE | 浮点异常(如除零、溢出) | 终止 |
SIGILL | 非法指令 | 终止 |
SIGINT | 中断(Ctrl+C) | 终止 |
SIGSEGV | 段错误(非法访问内存) | 终止 |
SIGTERM | 请求终止进程 | 终止 |
SIGKILL | 强制杀死进程 | 终止(不可捕获) |
SIGQUIT | 退出(会生成核心转储) | 终止 + core |
SIGCHLD | 子进程结束/停止 | 父进程处理 |
SIGCONT | 继续被停止的进程 | 恢复运行 |
SIGTSTP | 终端停止(Ctrl+Z) | 停止 |
信号机制的几个内部点
- 投递:操作系统把信号存进进程的待处理队列,进程准备好才处理,不会丢。
- 屏蔽:关键代码段可以屏蔽某些信号,防止被打断。
- 信号栈:信号一来,系统保存当前上下文、用专门的栈跑处理函数,跑完再恢复。
信号是 C 语言和操作系统之间的握手方式。学会捕获、处理、主动发送,写守护进程、优雅关闭、清理资源就都有了基础。