%% # 纲要 > 主干纲要、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)