「Linux 系统编程」信号的概念、生命周期与产生
1 什么是信号
1.1 信号的定义
信号(Signal) 是一种 异步通知机制,用于进程与内核之间或进程与进程之间的简单通信。其具有以下特性:
- 简单:信号本身不携带大量信息,仅表示某种事件的发生。
- 条件触发:只有在特定条件满足时才会发送。
- 异步处理:信号可以在进程执行的任何时刻到达,打断当前执行流程。
1.2 信号的特质
信号 常被形容为 “软件层面的中断”。其特点包括:
- 一旦信号产生,无论程序执行到何处,都必须 立即停止当前操作,转而去处理信号。
- 信号的处理完成后,程序再恢复执行之前的指令。
- 所有信号的产生和处理都是由 内核 完成的。
可以把 信号 类比于现实生活中的 中断 或 警报:
当你正在工作时,突然电话响了(信号产生),你会停下手中的工作(进程正常执行流程被中断),去接电话(执行信号处理函数),接完电话后再回来继续工作(恢复执行)。
1.3 信号的来源
-
内核:内核在检测到系统事件时,会向相关进程发送信号。
用户按下
Ctrl+C(产生SIGINT),除零操作(产生SIGFPE),子进程退出(向父进程发送SIGCHLD),进程访问非法内存(产生SIGSEGV)。 -
其他进程:一个进程可以通过系统调用
kill()向另一个进程(或进程组)发送信号。当然,发送者需要有相应的权限(通常是超级用户或目标进程的所有者)。 -
进程自身:进程可以通过
kill()或raise()系统调用给自己发送信号。
2 信号生命周期与处理
2.1 信号的生命周期
一个信号从产生到被处理,经历了以下几个步骤:
-
第 1 步:产生 (Generation)
信号旅程的起点。内核、其他进程或自身创建了一个信号事件。此时,信号只是一个“想法”,还没有真正送达目标进程。类比:手机收到新邮件的服务器推送通知(通知已生成,但还没到你手机上)。
-
第 2 步:判断阻塞状态 (Blocking?)
信号产生后,系统会检查目标进程是否 阻塞(屏蔽) 了该信号。是进程主动告诉内核:“我现在不想处理信号A,如果它来了,先帮我留着。”
-
第 3a 步:未被阻塞 -> 递送 (Delivery)
如果进程没有阻塞这个信号,内核会立即将信号递送给进程。这意味着内核会中断进程当前的正常执行流,强制它去处理这个信号。
类比:你的手机铃声大作,通知你立即查看邮件(推送被立即送达并提醒)。
-
第 3b 步:被阻塞 -> 未决 (Pending)
如果进程 阻塞 了这个信号,那么这个信号会保持在 “未决”(Pending) 状态。它被添加到进程的一个待处理信号列表中,但不会立即送达。
类比:你开启了手机的“勿扰模式”。新邮件通知来了,但手机不会响也不会亮屏,只会默默地出现在通知中心里(通知被静默保存,处于未决状态)。
重要特性:在阻塞期间,同种类型的未决信号通常只会被记录一次(除非使用实时信号)。即使用户按了十次
Ctrl+C,进程解除阻塞后也只会收到一个SIGINT。 -
第 4 步:解除阻塞 -> 递送
当进程后来通过系统调用解除了对该信号的阻塞时,内核会检查未决信号列表。如果发现该信号在列表中处于未决状态,就会立即将它递送给进程。
类比:你关闭了“勿扰模式”,手机立刻检查通知中心,并提醒你有未读邮件(递送之前未决的通知)。

2.2 信号的处理方式
当一个进程收到信号时,它可以选择以下 三种方式之一 来处理:
-
默认动作(Default Action):每个信号都有一个系统预设的默认行为。
常见的默认动作有:
-
Terminate:终止进程。(如SIGKILL,SIGTERM) -
Ignore:忽略信号。(如SIGCHLD的默认处理就是忽略,但父进程可以捕获它来做一些清理工作) -
Core:终止进程并生成核心转储文件(core dump),用于调试。(如SIGSEGV,SIGABRT) -
Stop:停止进程(暂停执行)。(如SIGSTOP,SIGTSTP) -
Continue:如果进程被停止了,让它继续运行。(如SIGCONT)
-
-
捕获(Catch):进程可以预先注册一个函数,称为信号处理函数(Signal Handler)。
当信号到来时,内核会中断进程当前的正常控制流,转而去执行这个注册好的处理函数。执行完毕后,进程再回到被中断的地方继续执行。
- 使用
signal()或sigaction()系统调用可以注册信号处理函数。
示例:
SIGINT(由 Ctrl+C 产生)的默认动作是终止进程。你可以捕获它,让它执行一个自定义的清理函数,然后再优雅地退出,而不是突然被杀死。 - 使用
-
忽略(Ignore):进程告诉内核,当这个信号到来时,直接丢弃它,什么都不做。
注意:
SIGKILL和SIGSTOP这两个信号不能被捕获、阻塞或忽略。这是为了给系统管理员一个最终极的手段来杀死或停止失控的进程。
2.3 信号屏蔽字与未决信号集
-
阻塞信号集(信号屏蔽字)
本质:一个位图(bitmap),用于记录哪些信号被屏蔽(阻塞)。
作用:被屏蔽的信号在解除屏蔽之前,即使产生也不会被递达,而是保持未决状态。
-
未决信号集
本质:也是一个位图,用于记录哪些信号已经产生但尚未被处理。
作用:内核通过该集合管理所有处于未决状态的信号。
注意:信号屏蔽字和未决信号集都是由内核维护的,用户可以通过
sigprocmask()等函数修改屏蔽字。
3 信号四要素与常规信号
3.1 信号四要素
使用命令 kill -l 可以查看系统中所有可用的信号(包括常规信号和实时信号)。

每个信号都有以下四个要素:
-
编号:信号的唯一标识符。
-
名称:如 SIGINT、SIGTERM 等。
-
对应事件:触发该信号的条件或事件。
-
默认处理动作:如终止、忽略、停止、继续等。
注意:在使用信号之前,必须明确其四要素,尤其是默认处理动作,以避免不可预期的行为。
3.2 常规信号
| 信号编号 | 信号名 | 默认行为 | 描述 |
|---|---|---|---|
| 1 | SIGHUP |
Terminate | 挂起。控制终端关闭或进程组领导终止时发送。也常用于让守护进程重新读取配置文件。 |
| 2 | SIGINT |
Terminate | 中断。来自键盘的中断,通常是用户按下了 Ctrl+C。 |
| 3 | SIGQUIT |
Core | 退出。来自键盘的退出,通常是用户按下了 Ctrl+\。会产生 core dump。 |
| 9 | SIGKILL |
Terminate | 杀死。立即无条件终止进程。此信号不能被捕获、阻塞或忽略。 |
| 11 | SIGSEGV |
Core | 段错误。进程进行了无效的内存访问(如访问不存在的内存,或写只读内存)。 |
| 13 | SIGPIPE |
Terminate | 管道破裂。向一个没有读端的管道写入数据。 |
| 14 | SIGALRM |
Terminate | 闹钟。由 alarm() 或 setitimer() 设置的定时器超时后产生。 |
| 15 | SIGTERM |
Terminate | 终止。这是 kill 命令默认发送的信号。它是一种要求进程优雅终止的请求。进程可以捕获它,完成清理工作后再退出。 |
| 17 | SIGCHLD |
Ignore | 子进程状态改变。当子进程停止、退出或被恢复时,内核会向父进程发送这个信号。 |
| 18 | SIGCONT |
Continue | 继续。让一个被停止的进程继续运行。 |
| 19 | SIGSTOP |
Stop | 停止。暂停进程的执行。此信号不能被捕获、阻塞或忽略。 |
| 20 | SIGTSTP |
Stop | 终端停止。来自终端的停止信号,通常是用户按下了 Ctrl+Z。 |
4 信号产生示例
4.1 kill 命令和系统调用
4.1.1 kill 系统调用函数
kill 函数 用于 发送信号给指定的进程。
kill 函数原型:
1 | |
参数说明:
pid:> 0:发送信号给 指定进程(进程ID为 pid)= 0:发送给 当前进程组 的所有进程< -1:取绝对值 |pid|,发送给 指定进程组 的所有成员= -1:发送给有权限的所有进程
signum:要发送的信号(如SIGKILL、SIGTERM)
返回值:
- 成功:返回
0 - 失败:返回
-1,并设置errno
4.1.2 kill 命令用法
1 | |
常用选项:
-9或-SIGKILL:强制杀死进程(立即无条件终止,此信号不能被捕获、阻塞或忽略。)-15或-SIGTERM:优雅终止进程(默认,可以捕获)
示例:
1 | |
4.1.3 kill 系统调用代码示例
- 子进程通过系统调用
kill杀死父进程
1 | |

- 父进程创建多个子进程,其中一个子进程杀死当前进程组的所有进程
1 | |

- 父进程创建多个子进程,并杀死一个指定的子进程。
1 | |

4.2 alarm 系统调用
4.2.1 alarm 函数介绍
alarm() 是一个用于设置实时(或“闹钟”)信号的系统调用。功能:让内核在指定的 秒数 后向调用它的进程发送一个 SIGALRM 信号。
函数原型:
1 | |
参数:
seconds:- 指定一个时间间隔(以 秒 为单位)。
- 经过
seconds秒后,内核将向进程发送SIGALRM信号。 - 如果
seconds为0,则 取消 之前设置的所有尚未触发的闹钟。
返回值:
- 返回 之前设置的闹钟剩余的秒数。
- 如果之前没有设置闹钟,则返回
0。
4.2.2 工作机制和特点
- 不可重入:一个进程在同一时间只能有一个活跃的
alarm。如果调用alarm()时,之前已经设置了一个尚未触发的闹钟,则 新的调用会覆盖旧的闹钟。
示例:先设置 alarm(10)(10秒后响),3秒后又设置 alarm(5)。那么原来的10秒闹钟会被取消,新的闹钟将在5秒后触发。并且,第二次调用 alarm(5) 会返回 7(因为第一个闹钟还剩7秒)。
- 信号交付:
SIGALRM的默认动作是终止进程。这意味着如果你只设置了alarm而没有处理这个信号,程序到时会被杀死。
因此,通常需要配合信号处理函数使用 signal() 来 捕获 SIGALRM 信号并执行自定义操作。
- 精度:
alarm()的精度是 秒级。对于需要更高精度(如微秒)的场景,可以使用setitimer()函数。
4.2.3 alarm 系统调用代码示例
以下示例中,设置了 3 秒的闹钟,然后进入一个等待循环。当 3 秒后收到 SIGALRM 信号时,信号处理函数会被调用,修改全局变量 alarm_fired 的值,从而打破主循环。
1 | |

4.3 setitimer() 系统调用
4.3.1 setitimer 函数介绍
1 | |
参数:
which:ITIMER_REAL:真实时间,到期发送SIGALRMITIMER_VIRTUAL:进程在用户态消耗的时间,到期发送SIGVTALRMITIMER_PROF:进程在内核态和用户态消耗的总时间,到期发送SIGPROF
new_value:设置的新定时器值old_value:返回之前的定时器设置(可为NULL)
结构体定义:
1 | |
可以理解为有2个定时器:
- 一个用于 第一个闹钟 什么时候触发打印
- 一个用于之后 间隔多少时间再次触发 闹钟(周期性触发)。
4.3.2 setitimer 系统调用代码示例
1 | |
