软中断信号,用于通知进程发生了异步事件,它是 Linux 系统响应某些条件而产生的一个事件
kill -l查看系统中支持的信号种类(具体信息自行查阅)

信号核心区别:非实时信号 - 实时信号,关键在于"是否排队"。
- 非实时信号 (信号值1-31),不可靠 (Unreliable),不支持排队,如果在进程处理前,您连续发送了 5 次相同的SIGHUP 信号,进程的 PCB(进程控制块)只会记录"收到了 SIGHUP"这个事实,而不会记录次数。进程最终只会处理 1 次SIGHUP 信号,剩下的 4 次被丢弃了。
- 实时信号 (信号 34-64),可靠 (Reliable),支持排队,如果在进程处理前,您连续发送了 5 次相同的实时信号(SIGRTMIN+1)。内核会将这 5 个信号全部放入队列中。进程最终会处理 5 次该信号,不会丢失。
Linux 信号如何产生和如何处理
1.信号可以由三种主要事件产生:
- 程序错误:这类错误通常由 CPU 硬件检测到,随后 Linux 内核会向导致此错误的进程发送一个相应的信号(如 SIGFPE - 浮点异常,SIGSEGV - 段错误)
- 外部事件:由终端驱动程序或内核根据外部变化来生成信号。用户在终端按下 Ctrl+C(产生 SIGINT 信号)、Ctrl+Z(产生 SIGTSTP 信号)。
- 显式请求:最直接的进程间通信方式。一个进程调用 kill() 函数,主动向另一个进程(或它自己)发送信号。
2.信号的同步与异步
- 同步信号 (Synchronous):由进程自身的执行动作引起(如"程序错误"或"显式请求"发给自己)。信号的产生与进程的执行是同步的。
- 异步信号 (Asynchronous):由进程外部的事件引起(如"外部事件"或另一个进程调用 kill())。进程无法预测信号何时会到达,只能被动接收。
3.进程如何处理信号
- 忽略信号 (Ignore):进程告诉内核,这个信号,可以直接丢弃。
- 捕获信号 (Capture):当信号到达时,内核会暂停进程的主程序,转而去执行这个自定义函数(信号处理函数"(Signal Handler))。
- 执行默认动作 (Default Action):如果进程既没有"忽略"也没有"捕获"这个信号,内核将执行该信号的系统默认动作。(终止 (Terminate),终止并转储 (Core Dump),忽略 (Ignore),暂停 (Stop),恢复 (Continue))
c
#include <stdio.h> // 包含 stdio.h 以便使用 printf 函数
#include <unistd.h> // 包含 unistd.h 以便使用 sleep 函数
#include <stdlib.h> // 包含 stdlib.h 以便使用 exit 函数
int main(void)
{
// 打印一条初始信息
printf("\nthis is an signal test function\n\n");
// (1) 进入一个无限循环
while (1) {
// (2) 循环打印提示信息
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"... \n");
// (3) 暂停 1 秒
sleep(1);
}
exit(0);
}
用户在终端上按下 Ctrl+C 组合键。这个动作被终端驱动程序捕获,并会向当前在前台运行的进程( signal_demo 程序)发送一个名为 SIGINT(SIGnal INTerrupt,中断信号)的信号。代码中,完全没有包含任何用于"捕获"或"忽略"SIGINT 信号的特殊代码,当 signal_demo 进程收到 SIGINT 信号时,它只能执行内核为 SIGINT 规定好的默认动作(终止进程 (Terminate the process))。
捕获信号相关 API 函数
我们使用信号只是通知进程而不是要杀死它,或者在杀死它前我们想进行某些收尾工作,这个时候就是需要我们去捕获这个信号,然后去处理它。
===============================================
signal() (已不推荐)用于为一个信号注册一个处理函数。
c
#include <stdio.h>
#include <unistd.h>
#include <signal.h> // 必须包含 signal.h
#include <stdlib.h> // 必须包含 stdlib.h
/**
* 自定义的信号处理函数
*/
void signal_handler(int sig)
{
printf("\n--- Signal Handler --- \n");
printf("Received signal: %d\n", sig);
// (4) 检查是否是 SIGINT 信号
if (sig == SIGINT) {
printf("OK, I get it. I will restore the default action.\n\n");
// 将 SIGINT 信号的处理恢复为默认 (SIG_DFL)默认处理方式(即终止进程)
signal(SIGINT, SIG_DFL);
}
}
int main(void)
{
printf("Program starting... (PID: %d)\n\n", getpid());
// (1) 注册信号处理函数
// 告诉内核: 当 SIGINT 信号 (Ctrl+C) 到来时,
// 不要执行默认动作, 而是去调用 signal_handler 函数
signal(SIGINT, signal_handler);
// (2) 无限循环, 等待信号
while (1) {
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"... \n");
sleep(1);
}
exit(0);
}
第一次按 Ctrl+C:内核查找发现 SIGINT 已经被注册,于是调用 signal_handler。程序打印出 "Received signal..." 等信息,然后执行 signal(SIGINT, SIG_DFL),将信号处理恢复为默认。程序不会退出,继续循环。
第二次按 Ctrl+C:内核再次收到 SIGINT,但此时它的处理方式已经是 SIG_DFL(默认)。因此,内核执行默认动作------终止进程。
===============================================
sigaction(推荐)
原因:signal() 在不同 Unix 系统上的行为不一致(即可移植性差)。sigaction() 是 POSIX 标准定义的现代函数,功能更强,行为更可靠。
就是对结构体进行sigaction填写,然后sigaction(SIGINT, &act, NULL);捕获相应终端后执行结构体内详细操作配置。
c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h> // 包含struct sigaction,以及所有相关的函数sigaction(), sigemptyset()
#include <sys/types.h>
#include <sys/wait.h>
/**
* 信号处理函数
*/
void signal_handler(int sig)
{
printf("\n--- Signal Handler --- \n");
printf("Received signal: %d\n", sig);
if (sig == SIGINT) {
printf("I have get SIGINT!\n\n");
printf("The signal handler will be reset to default automatically.\n\n");
}
}
int main(void)
{
// 定义一个 sigaction 结构体变量
struct sigaction act;
printf("this is sigaction function test demo! (PID: %d)\n\n", getpid());
// (2) 设置信号处理的回调函数
act.sa_handler = signal_handler;
// (3) 清空屏蔽信号集 (sa_mask)
// 告诉内核:在执行 signal_handler 期间,不要额外屏蔽其他信号。
sigemptyset(&act.sa_mask);
// (4) 设置关键的行为标志
// SA_RESETHAND 意思是:信号处理函数一旦被调用过一次,
// 就自动将该信号的处理方式恢复为默认。
act.sa_flags = SA_RESETHAND;
/*
1.SA_RESETHAND: "一次性"处理器。其作用与旧的 signal() 函数类似:信号处理函数一旦被调用过一次, 就自动将该信号的处理方式恢复为默认(SIG_DFL)。
2.SA_SIGINFO: "切换到扩展模式"。告诉内核请使用 act.sa_sigaction 作为处理函数,而不是 act.sa_handler。
3.SA_NOCLDSTOP: "不关心子进程暂停"。当用于 SIGCHLD 信号时,如果子进程只是被暂停(SIGSTOP)或继续(SIGCONT),不要通知父进程。
4.SA_NOCLDWAIT: "自动回收僵尸进程"。当用于 SIGCHLD 信号时,它告诉内核子进程结束后不要产生僵尸进程,父进程也不需要调用 wait() 来回收它们。
5.SA_NODEFER: "不要自动屏蔽自己"。允许在处理函数A执行期间,如果同一个信号再次到来,立即中断A,再次执行A(这称为"可重入"信号,不推荐新手使用)。
6.SA_RESTART: "自动重启系统调用"。一个非常有用的标志。如果进程正在执行一个"慢"系统调用(如 read 等待输入)时被信号中断,处理完信号后,内核会自动重新开始那个 read 调用,而不是让它失败。
*/
// (5) 正式注册信号
// 告诉内核: 请使用 act 结构体中定义的规则来处理 SIGINT 信号
sigaction(SIGINT, &act, NULL);
// 无限循环, 等待信号
while (1)
{
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"... \n\n");
sleep(1);
}
exit(0);
}
第一次 Ctrl+C 触发 signal_handler 并打印信息,第二次 Ctrl+C 终止进程。
sigaction结构体属于头文件signal.h
c
struct sigaction {
//1.信号处理函数指针(二选一)
void(*sa_handler)(int);
void(*sa_sigaction)(int, siginfo_t *, void *);
//2.信号掩码:在信号处理函数执行期间需要额外屏蔽的信号
sigset_t sa_mask;
//3.行为标志
int sa_flags;
//4.(已废弃,不应使用)
void(*sa_restorer)(void);
};
//行为标志
act.sa_flags = SA_RESETHAND;
/*
1.SA_RESETHAND: "一次性"处理器。其作用与旧的 signal() 函数类似:信号处理函数一旦被调用过一次, 就自动将该信号的处理方式恢复为默认(SIG_DFL)。
2.SA_SIGINFO: "切换到扩展模式"。告诉内核请使用 act.sa_sigaction 作为处理函数,而不是 act.sa_handler。
3.SA_NOCLDSTOP: "不关心子进程暂停"。当用于 SIGCHLD 信号时,如果子进程只是被暂停(SIGSTOP)或继续(SIGCONT),不要通知父进程。
4.SA_NOCLDWAIT: "自动回收僵尸进程"。当用于 SIGCHLD 信号时,它告诉内核子进程结束后不要产生僵尸进程,父进程也不需要调用 wait() 来回收它们。
5.SA_NODEFER: "不要自动屏蔽自己"。允许在处理函数A执行期间,如果同一个信号再次到来,立即中断A,再次执行A(这称为"可重入"信号,不推荐新手使用)。
6.SA_RESTART: "自动重启系统调用"。一个非常有用的标志。如果进程正在执行一个"慢"系统调用(如 read 等待输入)时被信号中断,处理完信号后,内核会自动重新开始那个 read 调用,而不是让它失败。
*/
| 标志 (Flag) | 默认行为 (当 sa_flags = 0) | 设置标志后的行为 |
|---|---|---|
| SA_RESETHAND | 处理函数永久有效 | 处理函数只执行一次,然后恢复默认 |
| SA_SIGINFO | 使用 sa_handler (简单, 1参数) | 使用 sa_sigaction (扩展) |
| SA_NOCLDSTOP | 子进程暂停/继续会发送 SIGCHLD | 子进程暂停/继续不会发送 SIGCHLD |
| SA_NOCLDWAIT | 子进程会变僵尸 (需wait) | 子进程自动回收 (不需wait) |
| SA_NODEFER | 处理期间,自动阻塞当前信号 | 处理期间,不阻塞当前信号 |
| SA_RESTART | 被中断的系统调用失败 (返回EINTR) | 被中断的系统调用自动重启 |
===============================================
Linux 如何在程序中主动发送信号
- 三种发送信号的API
- kill() (最常用):向任意进程(或进程组)发送任意信号。只能向自己拥有的进程发送信号(超级用户 root 除外,它可以向任何进程发送)。
- raise() (向自己发送):专用于向当前进程自己发送信号。不需要知道自己的PID,也永远不会有权限问题。等价于kill(getpid(), sig)。
- alarm() (定时发送):在 seconds 秒后,由内核向当前进程发送一个特定的 SIGALRM 信号。再次调用 alarm() 会覆盖上一个闹钟。调用 alarm(0) 会取消闹钟。
===================================
kill 命令与 kill() 函数
kill [信号] <PID>。用于从终端向指定PID的进程发送信号。如果不指定信号(如 kill 22142),默认发送的是 SIGTERM(终止信号,kill -9 (强制杀死)。
int kill(pid_t pid, int sig);pid 参数: (1) pid > 0: 发送给指定PID的进程; (2) pid = 0: 发送给当前进程组的所有进程; (3) pid = -1: 发送给除 init (PID 1) 外的所有进程(超级用户权限); (4) pid < -1: 发送给指定进程组(组ID为 pid 的绝对值)的所有进程。
kill() 与 raise()
c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int ret;
// (1) 创建子进程
if ((pid = fork()) < 0) {
printf("Fork error\n");
exit(1);
}
// (2) 子进程代码块
if (pid == 0) {
printf("Child(pid : %d) is waiting for any signal\n\n", getpid());
// (3) 子进程调用 raise() 向自己发送 SIGSTOP 信号,使自己暂停
raise(SIGSTOP);
exit(0);
}
// (4) 父进程代码块
else {
// 等待 1 秒,确保子进程已经运行并执行了 raise(SIGSTOP)
sleep(1);
// (5) 父进程使用 waitpid() 以非阻塞(WNOHANG)方式检查子进程状态
if ((waitpid(pid, NULL, WNOHANG)) == 0) {
// 返回 0 表示子进程仍然存在(没有终止),但可能已暂停
// (6) 父进程调用 kill() 向子进程发送 SIGKILL 信号 (信号 9),强制杀死它
if ((ret = kill(pid, SIGKILL)) == 0) {
printf("Parent kill %d\n\n",pid);
}
}
// (7) 父进程阻塞式等待,回收已确认被杀死的子进程(僵尸进程)
// 因为子进程已被SIGKILL,所以这里会立即返回
waitpid(pid, NULL, 0);
exit(0);
}
}
- 父进程创建子进程。
- 子进程运行,打印信息,然后调用 raise(SIGSTOP) 将自己暂停。
- 父进程 sleep(1) 确保子进程已暂停,然后 waitpid(...WNOHANG) 检查发现子进程还"活着"(返回0)。
- 父进程执行 kill(pid, SIGKILL),向已暂停的子进程发送强制杀死信号。
- 子进程被 SIGKILL 终止。
- 父进程调用 waitpid(...0) 回收子进程,程序结束。
===================================
alarm() 函数unsigned int alarm(unsigned int seconds);
c
int main()
{
printf("\nthis is an alarm test function\n\n");
// (1) 设置一个 5 秒后触发的闹钟,这个信号的默认处理动作是"终止进程"+
alarm(5);
// (2) 让主进程睡眠 20 秒
sleep(20);
// (3) 这一行永远不会被执行
printf("end!\n");
return 0;
}
//如果多个alarm,后面的会覆盖前面的,alarm(20)==>alarm(5) 5s后就会结束
- 程序启动,设置了一个 5 秒后发送 SIGALRM 信号的闹钟。
- 程序执行 sleep(20),主进程进入休眠状态,等待 20 秒。
- 在第 5 秒时,闹钟时间到。内核向该进程发送 SIGALRM 信号。
- 由于程序没有捕获 SIGALRM 信号,内核执行了该信号的默认处理动作------终止进程。
- 结果:程序在 sleep 到第 5 秒时就被强行终止了,因此 sleep(20) 没有睡满,最后那句 printf("end!\n")也永远不会被执行。终端会(根据系统不同)打印 "Alarm clock" (闹钟)字样。
alarm用处:
-
实现周期性任务(创建"闹钟") :可以在 SIGALRM 的信号处理函数中,再次调用 alarm()
来设置下一个闹钟。这样就可以实现一个每隔 N 秒钟执行一次的周期性任务(比如更新时钟、检查网络状态等)。
c#include <stdio.h> #include <unistd.h> #include <signal.h> // 必须包含 #include <stdlib.h> /** * (1) 这就是我们自定义的"闹钟"处理函数 * 当 SIGALRM 信号到来时,内核会执行这个函数 */ void alarm_handler(int sig) { // 打印信息,证明我们进来了 printf("叮! 闹钟响了! (收到的信号: %d)\n", sig); // (3) 关键一步:重新设置闹钟! // 告诉内核 2 秒后再提醒我一次 alarm(2); } int main() { printf("程序启动... (PID: %d)\n", getpid()); // (2) 捕获 SIGALRM 信号 // 告诉内核: "当 SIGALRM 信号到来时, 不要终止我, 去执行 alarm_handler" signal(SIGALRM, alarm_handler); // (3) 设置第一个闹钟 alarm(2); // (4) 主程序进入无限循环,等待闹钟来"中断"它 while (1) { // pause() 是一个特殊的系统调用 // 它会暂停程序,直到任意一个信号到来为止 // 这样可以避免 while(1) 空转导致 CPU 100% 占用 pause(); } // 程序永远不会执行到这里 return 0; }html程序启动... (PID: 12345) 叮! 闹钟响了! (收到的信号: 14) 叮! 闹钟响了! (收到的信号: 14) 叮! 闹钟响了! (收到的信号: 14) ... (每 2 秒打印一次) ... (您可以按 Ctrl+C 来终止它) -
为I/O操作设置"超时" (Timeout):可以在它之前设置一个闹钟,alarm(20) "一个任务"
alarm(0),规定时间内执行完就取消闹钟,这样避免了卡死操作。
csignal(SIGALRM, my_timeout_handler); // 1. 注册超时处理函数 alarm(10); // 2. 设置一个 10 秒的"死亡倒计时" bytes_read = read(fd, buffer, ...); // 3. 开始执行"慢"操作 alarm(0); // 4. (如果 read 很快完成了) 立即取消闹钟