%%
# 纲要
> 主干纲要、Hint/线索/路标
- Linux 信号机制
- 信号发送:`kill()` 与 `raise()`;
- 信号处理:`signal()` 与 `signaction()`
- 信号默认处理动作
- 信号屏蔽:信号屏蔽字
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
%%
# Linux 信号机制
Linux/UNIX 中的 "**信号**" 提供了一种 **进程间==异步通知==** 的机制,支持**一个进程向 "另一个进程或其自身" 发送信号**,信号即为 "**异步事件**"。
内核为每个进程维护了一个 **==信号集==**,用于记录当前进程的 **待处理信号/未决信号(pending signals)**。
当有信号被发送给进程时,信号的编号会被加入该 pending 集合中。
当且仅当进程从 **内核态==切换回用户态==** 时,**内核会检查当前进程是否有 =="未被屏蔽" 的未决信号==** 需要处理,如有则**在此时进行处理**[^4]。
> [!NOTE] "信号" 检查时机[^5]
> ![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-D8196BC79912F9ADE60BC95237C396FC.png|563]]
常见的时机有:
- **系统调用**返回用户态时;
- 进程从 **阻塞/休眠状态被唤醒**时(例如 `sleep()` 的唤醒)
> [!NOTE] 关于 sleep 的唤醒
>
> `sleep()` 系统调用会令进程从 "**用户态->内核态**",**在 "==内核态==" 下进入休眠状态**,并被标记为 "**可中断休眠**"。
>
> - 该状态下,当进程收到一个 "**未被屏蔽的未决信号**" 时,内核会**立即==唤醒==该进程**,使其变为 "**==就绪状态==**"(Runnable);
> - 当其**被调度器选中而运行**时,会切换 "**内核态->用户态**",此时内核检查发现 "**未被屏蔽的未决信号**",执行信号处理。
>
<br>
# 信号值
Linux 中的信号用 "**正整数**" 表示,均命名为 `SIG...`,以 "**宏**" 的形式定义在 **`<signal.h>` 头文件** 所引用的一些具体头文件中。
> [!NOTE] `man 7 signal` 可查看各信号的具体说明;`kill -l` 可查看信号值列表
>
> 参见[^3] [^2]
> ![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-396E3FBB5C36032863AF5BBD881BA73D.png|687]]
>
常用信号说明:
| 信号名 | 值 | 说明 | 默认处理行为 | 备注 |
| -------- | --- | --------------------------------------------------------------------------------------------------------- | ------- | --------- |
| SIGHUP | 1 | - 最初用于表示**终端挂断**(比如 ssh 断开),通知相关进程终端已断开。<br>- 现在通常用于**告知程序需要重新读取配置文件**。例如,许多守护进程会在接收到 `SIGHUP` 时**重载配置**。 | 终止 | |
| | | | | |
| SIGINT | 2 | **来自键盘的中断**——常用于通知进程终止当前操作并退出。 <br>(程序可捕获此信号并优雅地退出,执行刷出 I/O缓冲区等清理操作等) | | `ctrl+c` |
| SIGQUIT | 3 | **来自键盘的退出**——**通知进程退出 + 内核为进程生成 core dump 文件** | 终止+core | `ctrl+/` |
| | | | | |
| SIGTRAP | 5 | **跟踪的断点** | 终止+core | |
| SIGABORT | 6 | **异常结束**——来自 `abort` 的终止信号。 | 终止+core | |
| SIGKILL | 9 | **==强制终止==**(无法被忽略或捕获) | 终止 | |
| SIGTERM | 15 | **通知程序 "正常退出"**,是**标准的程序终止信号**,类似 `SIGINT`。 | | kill 命令默认 |
| | | | | |
| SIGSEGV | 11 | **段错误**(非法内存访问) | 终止+core | |
| | | | | |
| SIGCHLD | 17 | **子进程停止或终止**时,发送给其 "父进程" | 忽略 | |
| SIGCONT | 18 | 恢复被暂停的进程,继续执行(因收到 `SIGSTOP`、`SIGTSTP` 等信号而被暂停) | 终止 | |
| SIGSTOP | 19 | **==强制暂停==**(无法被忽略或捕获) | 暂停 | |
| SIGTSTP | 20 | **来自终端 tty 的暂停** | 暂停 | `ctrl+z` |
%%
#### 停止/暂停进程
> `SIGTSTP` 和 `SIGSTOP` 两个信号都用于 "停止(暂停)进程":
>
> **进程将仍然留在内存中,暂停执行直到收到 `SIGCONT` 信号**。
- `SIGTSTP(20)`:**请求停止(暂停)进程**。
- `SIGSTOP(19)`:**==强制停止(暂停)进程==**。 **不能被进程捕获、忽略或处理**。
- `SIGCONT(18)`:
#### 终止进程
- `SIGINT(2)` :
- `SIGQUIT(3)` :**请求停止(stop)进程**,并生成一个**核心转储文件**(core dump)。
- `SIGKILL(9)` :**==强制终止进程==**,该信号**==不能被进程捕获或忽略==**,进程无条件地立即终止。
- `SIGTERM(15)` : **==进程可捕获或忽略此信号==**,
> `SIGTERM` 信号用于请求程序的正常退出,是一种温和的终止方式,允许程序有机会进行适当的清理和关闭操作,例如关闭文件、释放资源、写入最终状态、进行其它清理等。
%%
<br><br><br>
# 信号发送
Linux/UNIX 中提供了两个系统调用,可用于发送信号[^1]:
![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-AA9A94877A172E3D96009894A17FF292.png|785]]
其中 `kill` 用于将信号发送给 "**指定进程或进程组**",而 `raise` 用于**向进程自身**发送信号,即 `raise(signo)` 等价于 `kill(getpid(), signo)`。
`kill` 的 pid 参数有四种情况:
- `pid > 0`:发送给**指定 PID 的进程**;
- `pid == 0`:发送给 "与发送进程**属于同一进程组**的所有进程";
- `pid < 0`:发送给 "**进程组 ID 等于 pid 绝对值**" 的所有进程。
- `pid == -1`:将信号发送给 **"发送进程==有权限向其发送==的所有进程"**。
> [!caution] `kill` 只能将信号发送给 "**与发送进程属于==同一所有者==**" 的进程。若发送进程 **==具有 root 权限==**,则可向任意进程发送信号。
> [!note] `abort()` 系统调用背后的实现即是通过 `raise()` 向进程自身发送 `SIGABRT` 信号
> [!info] shell 中 `kill` 命令背后即是执行 `kill()` 系统调用
> [!info] bash shell 中三个发送信号的快捷键
>
> - `Ctrl+c`:**中断进程**,发送 `SIGINT(2)` ,中断当前在 shell 前台运行的进程。
> - `Ctrl+z`:**暂停进程**,发送 `SIGTSTP(20)` ,暂停当前在 shell 前台运行的进程。
> - `Ctrl+\`:**中断 + 生成 coredump**,发送 `SIGQUIT(3)` 。
>
> 当在 shell 中以 "**后台模式**" 运行一个进程时(例如 `$ a.out &`),
> shell 会自动将该后台进程对上述 "**中断**" 与 "**暂停**" 的信号处理设置为 "**==忽略==**",从而使上述快捷键只作用于 "**前台进程**" 而不影响后台进程。
<br><br><br>
# 信号处理
当进程收到信号时,可采取三种方式之一进行处理[^4]:
1. **显式忽略该信号**;
2. **执行系统默认操作**:下列情况之一
3. **捕获信号 + 自定义处理**;
> [!NOTE] 系统默认操作取决于 "信号类型",为下列情况之一:
>
> - Terminate:终止进程;
> - Dump:终止进程 + 生成 core dump 文件
> - Ignore:忽略
> - Stop:暂停进程
> - Continue:暂停进程恢复运行
进程可通过 **`signal()` 或 `signaction()` 函数** 明确地向内核 "**==注册==其对于特定信号的处理方式**"(上述三种方式之一)。
对于特定信号,若进程未向内核声明处理方式,则将采取 "**系统默认动作**" 进行处理,即等价于声明上述第 2 点(`signal(..., SIG_DEF);`)
> [!caution] `SIGKILL(9)` 与 `SIGSTOP(19)` **无法被忽略 or 捕获**!
>
> 这两个信号为内核或 root 用户提供了 **强制终止/暂停进程** 的手段,故不会忽略和捕获(注册了信号处理函数也无效)。
>
> 此外,对于 `SIGSEGV` 等硬件异常相关的信号,尽管可被忽略,但其被忽略时是未定义行为。
> [!caution] 应当使用 `sigaction` 而非 `signal`!
>
> - `signal` 是早期接口,取决于实现(不可移植),不支持设置信号屏蔽字,非线程安全,可能丢失信号;
> - `sigaction` 提供了可靠 & 可移植 & 线程安全的接口,且支持设置信号屏蔽字。
>
> 尽管现代系统上,`signal` 背后实际上采用了 `sigaction` 进行实现,仍然不建议使用。
>
> `man 2 signal` 中有如下说明:
>
> ![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-4236CA7F926E71A0AE3EA85C2A069623.png|842]]
>
> [!NOTE] 进程在 `sleep` 过程中可被 "信号" 中断唤醒!
>
> `sleep()` 的进程状态是 `S` (**可中断休眠**),收到信号时将会被中断唤醒。
<br>
## signal 函数
![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-4432E36A7E9975358239D7E0FBED2785.png|771]]
> [!NOTE] signal 函数原型说明
>
> `void (*signal(int sigo, void (*func)(int))) (int);` 表明该函数:
>
> - 接收两个参数:`(int sigo, void (*func)(int))`,其中**第二项是一个函数指针**;
> - 返回一个函数指针:`void (*) (int)`;
>
> 等价于如下定义:
>
> ```cpp
> using sigFunc = void(int);
> sigFunc* signal(int, sigFunc *);
> ```
>
#### 使用示例
通过 `signal` 向内核**注册对特定信号处理方式**的示例如下:
```c
#include <signal.h> //提供signal()函数与信号宏常量
// 自定义信号处理程序
void handler(int signo) {
printf("Caught signal %d\n", signo);
}
int main() {
// 1.忽略指定信号;
signal(SIGINT, SIG_IGN);
// 2.按系统默认行为处理
signal(SIGINT, SIG_DFL);
// 3.捕获该信号并执行自定义处理
signal(SIGINT, handler); // `handler`为程序中的自定义函数, 其接收一个整型参数, 返回类型为void
}
```
> [!info] `SIG_IGN`、`SIG_DEF` 均是 `<signal.h>` 头文件中提供的 "**函数指针**"。
>
> ![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-553DBD2D56A62EBD25D9E85E9F3622D9.png|741]]
>
<br>
## sigaction 函数
![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-36B8311E08B7771BDD9DF5CF7113FE8A.png|741]]
该函数使用一个同名的结构体:`struct sigaction`,定义如下:
```c
struct sigaction {
void (*sa_handler)(int); // 自定义信号处理函数 or SIG_IGN or SIG_DFL
sigset_t sa_mask; // 处理期间需要屏蔽的信号
int sa_flags; // 信号处理时的行为标志,比如 SA_SIGINFO
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *); // 带更多信息的高级处理函数
};
```
> [!NOTE] 结构体中指定的 "**信号屏蔽集**" 仅在 "信号处理函数" 执行期间有效—— 执行之前添加屏蔽,执行结束后会 "**恢复为原先值**"
<br><br><br>
# 信号屏蔽
信号自 **产生后、送达进程前** 将一直处于 "**==挂起状态==**",称之为 "**未决的**(pending)"。
通常,系统会在**进程下次获得调度时**,将挂起信号**送达进程**。
每个进程都有一个 "**==信号屏蔽字==**"(signal mask),用以标识 "**当前进程==阻塞递达==的信号**"——**阻塞该信号送达进程**。
被标记为 "**阻塞**" 的信号,若进程对该信号的处理方式是 "**执行系统默认动作**" 或 "**捕获&自定义处理**",则 **==信号将一直挂起==**,直至进程 "**取消屏蔽**" 该信号 or 设置信号处理动作为 "**忽略该信号**"。被阻塞的信号,在阻塞解除后将会被送达进程。
> [!info] 内核默认阻塞 "**当前进程正在处理的==同一信号==**"
> [!info] "信号" 在被送达给进程之前,进程均可改变对该信号的处理方式。
> [!example] 使用场景
> 多线程场景下,某个线程进入临界区后,若不想被信号打断,则可**设置屏蔽字阻塞信号**,待执行完临界区代码后,再**解除阻塞,允许==信号抵达==**。
<br>
## 信号屏蔽字设置
Linux 提供了下列接口,用以操作 "**==信号集==**"(由 `sigset_t` 类型表示):
![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-39B598A6F529CF5C62211C2186BA8C49.png|715]]
同时,提供了 `sigprocmask` 函数来 "**获取 or 更改==信号屏蔽字==**"(以**信号集**为参数)
![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-2FE3AD68ADDB959196F6D01463F07DAA.png|736]]
> [!caution] `sigprocmask` 是非线程安全的。多线程场景下应使用 `pthread_sigmask`
同时,可通过 `sigpending()` 函数获取当前进程 "**被阻塞递送**" 的信号(**已产生,但由于被屏蔽而阻塞递送的信号**):
![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-B07921013FCCDCD75CF11540039A0CBF.png|652]]
#### 使用示例
```cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
// 屏蔽SIGINT信号
int main() {
sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT); // 添加SIGINT到屏蔽字中
// 设置当前屏蔽字: 阻塞SIGINT
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
printf("SIGINT is now blocked for 6 seconds. Try Ctrl+C now!\n");
sleep(6);
// 恢复原始屏蔽字,解除阻塞.
sigprocmask(SIG_SETMASK, &oldmask, NULL); // 如果此前已产生了SIGINT信号, 则这一步恢复时就将处理SIGINT而退出, 不会打印下一行
printf("SIGINT unblocked! Try Ctrl+C again now!\n");
while (1) {
sleep(1);
}
return 0;
}
```
<br><br><br>
%%
# 信号检测
![[_attachment/02-开发笔记/11-Linux/Linux 信号机制.assets/IMG-Linux 信号机制-806C9B3FCA5E2A30B0AAB71A706D5141.png|452]]
%%
<br><br><br>
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
# ♾️参考资料
- 《UNIX 环境高级编程》第 10 章信号
- 《Linux-UNIX 系统编程手册》第 20 章信号
- 《深入理解计算机系统》第 8.5 节
# Footnotes
[^1]: 《UNIX 环境高级编程》(P267)
[^2]: 《UNIX 环境高级编程》(P251)
[^3]: 《深入理解计算机系统》(P527)
[^4]: 《深入理解 Linux 内核》(P422)
[^5]: 《深入 Linux 内核架构》(P681)