Linux进程信号(二):深入理解信号阻塞、未决、sigaction、中断机制、可重入函数与SIGCHLD

🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 | 《C语言基础》 | 《数据结构》 | 《机器学习导论》 | 《前端基础》 | 《python基础》 | 《C++从入门到入土》 | 《Linux的学习之旅》 ✨ 数据即知识,压缩即智能
文章目录
- Linux进程信号(二):深入理解信号阻塞、未决、sigaction、中断机制、可重入函数与SIGCHLD
- 一、重新建立信号处理的整体模型
-
- [1.1 信号处理分为三个阶段](#1.1 信号处理分为三个阶段)
- [1.2 信号本质上属于进程控制机制](#1.2 信号本质上属于进程控制机制)
- 二、三个核心概念:Pending、Block与Delivery
-
- [2.1 什么是信号递达?](#2.1 什么是信号递达?)
- [2.2 什么是未决信号?](#2.2 什么是未决信号?)
- [2.3 什么是信号阻塞?](#2.3 什么是信号阻塞?)
- [2.4 阻塞与忽略有什么区别?](#2.4 阻塞与忽略有什么区别?)
- [2.5 普通信号多次产生会怎样?](#2.5 普通信号多次产生会怎样?)
- 三、Linux内核怎样保存信号状态?
-
- [3.1 为什么可以使用位图思想?](#3.1 为什么可以使用位图思想?)
- [3.2 内核中的三类信息](#3.2 内核中的三类信息)
- [3.3 为什么不能直接操作内核中的Block位图?](#3.3 为什么不能直接操作内核中的Block位图?)
- 四、sigset_t:信号集
-
- [4.1 什么是sigset_t?](#4.1 什么是sigset_t?)
- [4.2 为什么不直接使用一个整数?](#4.2 为什么不直接使用一个整数?)
- 五、信号集的五个基本操作函数
-
- [5.1 函数原型](#5.1 函数原型)
- [5.2 sigemptyset()](#5.2 sigemptyset())
- [5.3 sigfillset()](#5.3 sigfillset())
- [5.4 sigaddset()](#5.4 sigaddset())
- [5.5 sigdelset()](#5.5 sigdelset())
- [5.6 sigismember()](#5.6 sigismember())
- [5.7 一个简单实验](#5.7 一个简单实验)
- 六、sigprocmask():真正修改信号屏蔽字
-
- [6.1 函数原型](#6.1 函数原型)
- [6.2 how的三个常见取值](#6.2 how的三个常见取值)
- [6.3 怎样阻塞SIGINT?](#6.3 怎样阻塞SIGINT?)
- [6.4 怎样恢复原来的信号屏蔽字?](#6.4 怎样恢复原来的信号屏蔽字?)
- 七、sigpending():获取未决信号集合
-
- [7.1 函数原型](#7.1 函数原型)
- [7.2 实验目标](#7.2 实验目标)
- [7.3 完整实验代码](#7.3 完整实验代码)
- [7.4 如果连续按下三次Ctrl+C会怎样?](#7.4 如果连续按下三次Ctrl+C会怎样?)
- 八、信号捕捉:自定义处理函数究竟什么时候执行?
-
- [8.1 回顾signal()](#8.1 回顾signal())
- [8.2 信号捕捉的典型过程](#8.2 信号捕捉的典型过程)
- [8.3 为什么说信号处理函数和main不是普通调用关系?](#8.3 为什么说信号处理函数和main不是普通调用关系?)
- [8.4 sigreturn是什么?](#8.4 sigreturn是什么?)
- 九、sigaction():更加规范的信号处理接口
-
- [9.1 函数原型](#9.1 函数原型)
- [9.2 struct sigaction中的重要成员](#9.2 struct sigaction中的重要成员)
- [9.3 一个完整的sigaction实验](#9.3 一个完整的sigaction实验)
- [9.4 signal和sigaction应该选择哪个?](#9.4 signal和sigaction应该选择哪个?)
- 十、理解操作系统的运行:硬件中断
-
- [10.1 操作系统如何知道硬件设备发生了事件?](#10.1 操作系统如何知道硬件设备发生了事件?)
- [10.2 什么是中断向量表?](#10.2 什么是中断向量表?)
- [10.3 为什么中断很重要?](#10.3 为什么中断很重要?)
- 十一、时钟中断与进程调度
-
- [11.1 为什么操作系统需要定时能力?](#11.1 为什么操作系统需要定时能力?)
- [11.2 时钟事件与进程调度的关系](#11.2 时钟事件与进程调度的关系)
- [11.3 为什么死循环程序不会永久独占CPU?](#11.3 为什么死循环程序不会永久独占CPU?)
- 十二、软中断、系统调用与内核态
-
- [12.1 什么是用户态?](#12.1 什么是用户态?)
- [12.2 什么是内核态?](#12.2 什么是内核态?)
- [12.3 系统调用怎样进入内核?](#12.3 系统调用怎样进入内核?)
- [12.4 软中断与系统调用不能完全混为一谈](#12.4 软中断与系统调用不能完全混为一谈)
- [12.5 为什么课件中的3GB/1GB划分不能直接套到现代系统?](#12.5 为什么课件中的3GB/1GB划分不能直接套到现代系统?)
- 十三、缺页异常:为什么不是所有内存异常都意味着程序错误?
- 十四、可重入函数:为什么信号处理函数不能随意调用普通库函数?
-
- [14.1 从链表插入开始分析](#14.1 从链表插入开始分析)
- [14.2 什么是重入?](#14.2 什么是重入?)
- [14.3 什么是可重入函数?](#14.3 什么是可重入函数?)
- [14.4 为什么printf可能不安全?](#14.4 为什么printf可能不安全?)
- [14.5 在信号处理函数中应该做什么?](#14.5 在信号处理函数中应该做什么?)
- 十五、volatile:为什么编译优化可能影响信号处理?
-
- [15.1 先看一个简单程序](#15.1 先看一个简单程序)
- [15.2 编译优化可能带来的问题](#15.2 编译优化可能带来的问题)
- [15.3 使用volatile sig_atomic_t](#15.3 使用volatile sig_atomic_t)
- [15.4 volatile有什么作用?](#15.4 volatile有什么作用?)
- 十六、竞态条件:为什么pause()也可能出现问题?
-
- [16.1 什么是竞态条件?](#16.1 什么是竞态条件?)
- [16.2 怎样避免这种问题?](#16.2 怎样避免这种问题?)
- 十七、SIGCHLD:如何通过信号回收子进程?
-
- [17.1 回顾僵尸进程](#17.1 回顾僵尸进程)
- [17.2 阻塞等待有什么问题?](#17.2 阻塞等待有什么问题?)
- [17.3 轮询有什么问题?](#17.3 轮询有什么问题?)
- [17.4 SIGCHLD的作用](#17.4 SIGCHLD的作用)
- 十八、SIGCHLD实验:让父进程处理子进程退出
-
- [18.1 为什么不能只调用一次waitpid?](#18.1 为什么不能只调用一次waitpid?)
- [18.2 完整代码](#18.2 完整代码)
- [18.3 编译运行](#18.3 编译运行)
- [18.4 为什么这里使用sigsuspend?](#18.4 为什么这里使用sigsuspend?)
- [18.5 为什么处理函数中不使用printf?](#18.5 为什么处理函数中不使用printf?)
- 十九、SIGCHLD还可以使用SIG_IGN吗?
- 二十、将信号知识与之前的MiniShell联系起来
-
- [20.1 用户按Ctrl+C时发生什么?](#20.1 用户按Ctrl+C时发生什么?)
- [20.2 父进程如何知道子进程被什么信号终止?](#20.2 父进程如何知道子进程被什么信号终止?)
- [20.3 为什么完整Shell还需要SIGCHLD?](#20.3 为什么完整Shell还需要SIGCHLD?)
- 二十一、常见问题与错误总结
-
- [21.1 信号产生后一定立即执行处理函数吗?](#21.1 信号产生后一定立即执行处理函数吗?)
- [21.2 Block和Pending是一回事吗?](#21.2 Block和Pending是一回事吗?)
- [21.3 阻塞一个信号等于忽略它吗?](#21.3 阻塞一个信号等于忽略它吗?)
- [21.4 使用sigaddset()就完成了信号阻塞吗?](#21.4 使用sigaddset()就完成了信号阻塞吗?)
- [21.5 连续产生五次SIGINT就必然捕捉五次吗?](#21.5 连续产生五次SIGINT就必然捕捉五次吗?)
- [21.6 SIGKILL可以被阻塞吗?](#21.6 SIGKILL可以被阻塞吗?)
- [21.7 信号处理函数可以任意调用printf、malloc吗?](#21.7 信号处理函数可以任意调用printf、malloc吗?)
- [21.8 volatile可以解决所有并发问题吗?](#21.8 volatile可以解决所有并发问题吗?)
- [21.9 Page Fault一定会导致SIGSEGV吗?](#21.9 Page Fault一定会导致SIGSEGV吗?)
- [21.10 SIGCHLD处理函数只调用一次waitpid就够了吗?](#21.10 SIGCHLD处理函数只调用一次waitpid就够了吗?)
- [21.11 SIGCHLD默认忽略是否表示子进程不会变成僵尸进程?](#21.11 SIGCHLD默认忽略是否表示子进程不会变成僵尸进程?)
- 二十二、完整知识体系总结
- 总结
前言:信号产生之后,操作系统究竟做了什么?
在上一篇《Linux进程信号(一)》中,我们已经学习了 Linux 信号的基本概念与常见产生方式。
例如:
bash
kill -SIGINT 12345
可以向指定进程发送 SIGINT。
使用:
c
raise(SIGUSR1);
可以主动产生信号。
使用:
c
alarm(1);
可以请求操作系统在定时器到期后产生 SIGALRM。
但前面只解决了一个问题:
信号是怎样产生的?
还有更加重要的问题:
- 如果信号产生后没有立即得到处理,它会保存在什么地方?
- 什么叫信号未决(Pending)?
- 信号阻塞(Block)和忽略(Ignore)有什么区别?
- 操作系统怎样决定什么时候调用信号处理函数?
- 为什么信号处理函数可以打断当前正在执行的用户代码?
- 为什么在信号处理函数中调用
printf()可能存在风险? - 为什么优化后的程序可能无法正确响应信号处理函数修改的变量?
- 子进程退出时,父进程如何通过
SIGCHLD完成回收?
本篇按照课程后半部分的顺序,从 信号保存 → 信号递达 → 信号捕捉 → 中断与内核态 → 可重入函数 → volatile → SIGCHLD 逐步展开。
一、重新建立信号处理的整体模型
1.1 信号处理分为三个阶段
首先回顾上一节形成的结论:
text
信号产生
|
v
信号保存
|
v
信号处理
例如,一个进程正在执行:
c
while (1)
{
// 正在进行某项计算
}
此时,操作系统向它产生:
c
SIGINT
这个信号未必在产生的瞬间就立即执行用户自定义处理函数。
因此,内核需要记录:
text
这个进程存在一个待处理的SIGINT
等到能够递达该信号时,再按照预先设置的处理方式处理它。
1.2 信号本质上属于进程控制机制
之前学习 fork() 时,我们已经知道:
操作系统负责管理进程。
既然信号是进程相关的事件通知,那么:
- 进程能够识别什么信号;
- 哪些信号正在等待处理;
- 哪些信号被阻塞;
- 收到信号后应该执行什么动作;
这些信息都需要由内核管理。
可以先把进程中的信号状态概念化为:
text
进程
|
+-- Block:哪些信号被阻塞?
|
+-- Pending:哪些信号处于未决状态?
|
+-- Handler:收到信号后怎样处理?
这就是理解后面所有信号接口的基础。

二、三个核心概念:Pending、Block与Delivery
2.1 什么是信号递达?
信号递达(Delivery),指信号实际进入相应处理阶段。
如果信号的处理方式是默认终止,那么递达可能导致进程结束。
如果设置了自定义处理函数,那么递达时可能执行用户注册的处理函数。
例如:
c
signal(SIGINT, Handler);
当 SIGINT 真正递达时,系统才会安排执行:
c
Handler(SIGINT);
这里不能把:
text
信号产生
与:
text
信号递达
当成完全相同的事件。
2.2 什么是未决信号?
从信号产生到它被递达之间的状态,称为:
text
Pending
即未决状态。
例如:
text
SIGINT已经产生
|
v
暂时不能递达
|
v
保持Pending状态
可以理解为:
操作系统已经记录了这次信号事件,但还没有完成对应的递达过程。
2.3 什么是信号阻塞?
进程可以选择阻塞某些信号。
例如:
text
Block SIGINT
表示暂时禁止 SIGINT 递达。
此时如果产生了 SIGINT,它可以继续保持未决状态,等待解除阻塞。
因此:
text
信号产生
|
v
该信号是否被阻塞?
|
+-- 是 --> 保持Pending
|
+-- 否 --> 可以进入递达处理
注意:
被阻塞的信号并不是简单地被删除。
2.4 阻塞与忽略有什么区别?
这是本节最重要的区别之一。
阻塞(Block)
表示暂时不允许信号递达。
忽略(Ignore)
表示信号的处理方式是忽略。
二者处于不同层次。
例如:
text
SIGINT产生
|
v
被阻塞
|
v
保持未决
|
v
解除阻塞
|
v
根据当前处理动作处理
如果当前处理动作是忽略,那么在允许递达后可能被直接忽略。
需要说明:对于设置为忽略的信号,具体未决行为还受到信号处置规则影响,不能把上面的概念流程机械地套到所有情况。
2.5 普通信号多次产生会怎样?
课件强调:
如果一个普通信号被阻塞,在尚未递达时重复产生多次,Linux 通常不会把每次产生都记录成一条独立消息。
例如:
text
SIGINT产生
SIGINT再次产生
SIGINT再次产生
在通常的普通信号未决表示中,只需要记录:
text
存在尚未递达的SIGINT
所以解除阻塞以后,并不保证自定义处理函数执行三次。
而实时信号可以具有排队语义。
因此:
普通信号不能当成可靠的消息计数机制。
如果需要逐条传递任务或者消息,应使用更适合的通信方式。

三、Linux内核怎样保存信号状态?
3.1 为什么可以使用位图思想?
对于普通信号,我们关注的状态通常包括:
text
这个信号是否被阻塞?
这个信号是否处于未决状态?
这些信息具有:
text
是 / 否
的特点。
因此,可以从位图的角度理解。
例如:
text
信号编号 1 2 3 4 5
阻塞状态 0 1 0 0 0
未决状态 0 1 0 0 0
表示:
text
2号信号SIGINT被阻塞
2号信号SIGINT已经产生但尚未递达
这里使用的位图只是教学模型。
真实内核还需要管理线程信号、进程共享的信号状态、实时信号队列等信息。
3.2 内核中的三类信息
课程通过旧版 Linux 内核源码介绍:
text
blocked
pending
sighand
对应的概念分别是:
text
阻塞信号集合
未决信号集合
信号处理动作
在现代 Linux 中,进程相关信号信息还涉及 task_struct、共享信号状态以及内核信号处理相关结构。
所以这里主要学习数据结构的组织思想:
操作系统必须保存信号状态,才能在未来合适的时候处理信号。
3.3 为什么不能直接操作内核中的Block位图?
因为用户程序运行在用户态。
我们不能直接修改内核用于管理进程信号的内部数据结构。
因此操作系统需要提供对应接口。
例如:
c
sigprocmask()
用于操作信号屏蔽字。
c
sigpending()
用于获取未决信号集合。
这与之前学习文件描述符时的思路相同:
用户不能随意直接修改内核文件描述符管理结构,而应该通过相应接口进行操作。
四、sigset_t:信号集
4.1 什么是sigset_t?
Linux/POSIX 提供:
c
sigset_t
类型,用于表示信号集合。
例如:
c
sigset_t set;
一个信号集可以表示哪些信号属于某个集合。
在不同上下文中,它可以作为:
text
阻塞信号集合
也可以用来接收:
text
未决信号集合
4.2 为什么不直接使用一个整数?
因为信号集合的底层表示由具体系统实现决定。
用户程序不应该假设:
c
sigset_t
一定就是一个普通整数。
因此,POSIX 提供了一组专门的信号集操作函数。
五、信号集的五个基本操作函数
5.1 函数原型
c
#include <signal.h>
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set, int signo);
int sigdelset(sigset_t* set, int signo);
int sigismember(const sigset_t* set, int signo);
5.2 sigemptyset()
作用:
初始化一个空信号集。
例如:
c
sigset_t set;
sigemptyset(&set);
表示当前集合中不包含任何信号。
5.3 sigfillset()
作用:
初始化包含系统支持的全部适用信号的集合。
例如:
c
sigfillset(&set);
但这并不意味着所有信号都能够成功被阻塞。
例如 SIGKILL 和 SIGSTOP 不允许被阻塞。
5.4 sigaddset()
作用:
向集合中添加指定信号。
例如:
c
sigaddset(&set, SIGINT);
表示将 SIGINT 加入当前集合。
5.5 sigdelset()
作用:
从集合中移除指定信号。
例如:
c
sigdelset(&set, SIGINT);
5.6 sigismember()
作用:
检查某个信号是否存在于指定集合。
例如:
c
int ret = sigismember(&set, SIGINT);
返回值:
| 返回值 | 含义 |
|---|---|
1 |
信号属于集合 |
0 |
信号不属于集合 |
-1 |
出错 |
5.7 一个简单实验
c
#include <stdio.h>
#include <signal.h>
int main(void)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
if (sigismember(&set, SIGINT) == 1)
{
printf("SIGINT belongs to set\n");
}
sigdelset(&set, SIGINT);
printf(
"SIGINT exists: %d\n",
sigismember(&set, SIGINT)
);
return 0;
}
编译:
bash
gcc sigset_test.c -o sigset_test
运行:
bash
./sigset_test
预期输出:
text
SIGINT belongs to set
SIGINT exists: 0
这里有一个容易混淆的地方:
c
sigaddset(&set, SIGINT);
只是修改了用户态的信号集变量。
它本身并没有让当前进程真正阻塞SIGINT。
要修改实际信号屏蔽字,还需要:
c
sigprocmask()

六、sigprocmask():真正修改信号屏蔽字
6.1 函数原型
c
#include <signal.h>
int sigprocmask(
int how,
const sigset_t* set,
sigset_t* oldset
);
参数:
c
how
表示怎样修改信号屏蔽字。
c
set
表示要使用的信号集合。
c
oldset
用于保存修改之前的信号屏蔽字。
注意:信号屏蔽字在多线程程序中是线程级状态 。本篇主要针对单线程进程;多线程程序通常应使用 pthread_sigmask()。
6.2 how的三个常见取值
| 参数 | 含义 |
|---|---|
SIG_BLOCK |
将指定信号加入当前阻塞集合 |
SIG_UNBLOCK |
从当前阻塞集合移除指定信号 |
SIG_SETMASK |
用指定集合替换当前阻塞集合 |
例如:
c
sigprocmask(SIG_BLOCK, &set, &oldset);
表示:
text
阻塞set中的信号
并将原来的信号屏蔽字保存到:
c
oldset
6.3 怎样阻塞SIGINT?
c
sigset_t block_set;
sigset_t old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
if (sigprocmask(
SIG_BLOCK,
&block_set,
&old_set
) < 0)
{
perror("sigprocmask");
}
现在:
text
SIGINT
已经加入当前线程的阻塞信号集合。
6.4 怎样恢复原来的信号屏蔽字?
c
sigprocmask(
SIG_SETMASK,
&old_set,
NULL
);
为什么推荐保存并恢复旧值?
因为当前程序执行时,信号屏蔽字不一定原本就是空集合。
如果直接将所有信号都解除阻塞,可能意外修改原来已经存在的设置。
所以更合理的做法是:
text
保存原信号屏蔽字
|
v
临时修改
|
v
完成操作
|
v
恢复原信号屏蔽字
这一思想与我们之前 MiniShell 重定向时:
c
dup()
dup2()
的备份和恢复思路相似。
七、sigpending():获取未决信号集合
7.1 函数原型
c
#include <signal.h>
int sigpending(sigset_t* set);
作用:
获取当前调用线程可观察到的未决信号集合。
成功:
text
返回0
失败:
text
返回-1
例如:
c
sigset_t pending;
if (sigpending(&pending) < 0)
{
perror("sigpending");
}
然后通过:
c
sigismember(&pending, SIGINT);
判断 SIGINT 是否处于未决状态。
7.2 实验目标
按照课件的思路,我们实现:
- 注册
SIGINT处理函数。 - 阻塞
SIGINT。 - 等待用户按下
Ctrl+C。 - 每秒检查并打印
SIGINT的未决状态。 - 一段时间后解除阻塞。
- 观察处理函数是否执行。
7.3 完整实验代码
pending_test.c:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void Handler(int signo)
{
(void)signo;
const char msg[] = "SIGINT delivered!\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main(void)
{
struct sigaction action = {0};
action.sa_handler = Handler;
sigemptyset(&action.sa_mask);
if (sigaction(SIGINT, &action, NULL) < 0)
{
perror("sigaction");
return 1;
}
sigset_t block_set;
sigset_t old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
// 阻塞SIGINT
if (sigprocmask(
SIG_BLOCK,
&block_set,
&old_set
) < 0)
{
perror("sigprocmask");
return 1;
}
printf("SIGINT is blocked. Press Ctrl+C.\n");
// 检查10秒
for (int i = 0; i < 10; ++i)
{
sigset_t pending;
if (sigpending(&pending) < 0)
{
perror("sigpending");
return 1;
}
int found = sigismember(&pending, SIGINT);
printf("SIGINT pending = %d\n", found);
fflush(stdout);
sleep(1);
}
printf("Now unblock SIGINT...\n");
fflush(stdout);
// 恢复信号屏蔽字
if (sigprocmask(
SIG_SETMASK,
&old_set,
NULL
) < 0)
{
perror("sigprocmask");
return 1;
}
printf("Program continues.\n");
return 0;
}
编译:
bash
gcc -Wall -Wextra pending_test.c -o pending_test
运行:
bash
./pending_test
在程序运行期间按下:
text
Ctrl+C
可能得到类似输出:
text
SIGINT is blocked. Press Ctrl+C.
SIGINT pending = 0
SIGINT pending = 0
^CSIGINT pending = 1
SIGINT pending = 1
SIGINT pending = 1
...
Now unblock SIGINT...
SIGINT delivered!
Program continues.
这说明:
信号已经产生,但因为被阻塞而暂时不能递达。
解除阻塞以后,信号得到了递达,执行对应处理函数。
7.4 如果连续按下三次Ctrl+C会怎样?
对于普通 SIGINT:
text
Ctrl+C
Ctrl+C
Ctrl+C
如果这几个信号都在阻塞期间产生,并保持同一个普通未决信号状态,那么解除阻塞后,通常只执行一次对应处理函数。
这体现了普通信号未决状态不负责精确记录发生次数的特点。

八、信号捕捉:自定义处理函数究竟什么时候执行?
8.1 回顾signal()
我们已经学习:
c
signal(SIGINT, Handler);
可以注册自定义处理函数。
但是信号捕捉过程并不是简单地在当前代码中直接调用一个普通函数。
它涉及:
text
用户态
内核态
信号状态检查
用户态处理函数
恢复原执行现场
等机制。
8.2 信号捕捉的典型过程
假设:
c
int main(void)
{
while (1)
{
// 正在运行
}
}
程序已经注册了:
c
SIGINT
处理函数。
当进程执行过程中发生中断、异常或系统调用等导致进入内核的情况时,内核会在适当的信号处理检查点判断是否需要递达相关信号。
如果需要执行自定义处理函数,内核会安排后续的用户态执行流进入该处理函数。
处理函数返回后,再通过信号返回机制恢复之前的执行上下文。
8.3 为什么说信号处理函数和main不是普通调用关系?
因为:
c
main()
并没有主动执行:
c
Handler();
信号处理函数的执行是由信号递达过程引入的。
可以理解为:
text
main正常执行
|
v
进入内核处理事件
|
v
发现需要递达的信号
|
v
返回用户态执行Handler
|
v
Handler返回
|
v
信号返回机制恢复上下文
|
v
main继续执行
因此:
信号处理函数虽然运行在用户态,但它的执行安排与内核信号递达机制密切相关。
8.4 sigreturn是什么?
课件介绍,信号处理函数返回以后,需要通过特殊的信号返回机制恢复被中断的用户态执行现场。
在现代 Linux 中,这通常与:
text
rt_sigreturn
系统调用机制有关。
程序员一般不需要手动调用它。
相关的信号处理返回代码通常由系统运行环境提供。

九、sigaction():更加规范的信号处理接口
9.1 函数原型
c
#include <signal.h>
int sigaction(
int signum,
const struct sigaction* act,
struct sigaction* oldact
);
参数:
c
signum
指定信号。
c
act
指定新的处理方式。
c
oldact
用于取得原来的处理设置。
9.2 struct sigaction中的重要成员
可以从以下三个成员开始理解。
sa_handler
指定处理函数:
c
action.sa_handler = Handler;
也可以指定:
c
SIG_DFL
SIG_IGN
sa_mask
指定在处理该信号期间,需要额外阻塞哪些信号。
例如:
c
sigemptyset(&action.sa_mask);
sigaddset(&action.sa_mask, SIGUSR2);
表示执行当前信号处理函数期间,额外阻塞 SIGUSR2。
默认情况下,当前正在处理的信号通常也会被临时阻塞,避免相同信号在处理函数内部无限嵌套。
sa_flags
用于设置额外行为选项。
常见选项包括:
c
SA_RESTART
SA_NOCLDSTOP
SA_SIGINFO
其中:
c
SA_RESTART
可以让某些被信号中断的系统调用自动重新开始。
但并不是所有系统调用都受到这个选项影响。
9.3 一个完整的sigaction实验
c
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void Handler(int signo)
{
(void)signo;
const char msg[] = "SIGINT caught\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main(void)
{
struct sigaction action = {0};
// 自定义处理函数
action.sa_handler = Handler;
// 不额外阻塞其他信号
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &action, NULL) < 0)
{
perror("sigaction");
return 1;
}
printf("pid = %ld\n", (long)getpid());
while (1)
{
pause();
}
return 0;
}
编译:
bash
gcc sigaction_test.c -o sigaction_test
运行后按下 Ctrl+C,处理函数会输出:
text
SIGINT caught
程序仍然继续等待信号。
9.4 signal和sigaction应该选择哪个?
对于简单课堂实验:
c
signal()
已经足够帮助我们认识信号捕捉。
但实际 POSIX/Linux 编程中,通常更推荐:
c
sigaction()
因为它能更加明确地配置:
- 处理函数。
- 临时阻塞信号集合。
- 信号处理标志。
- 原处理动作的保存。
十、理解操作系统的运行:硬件中断
课程在信号捕捉之后,专门安排了一部分内容来解释:
操作系统为什么有机会检查和处理信号?
这一部分可以与之前的操作系统学习联系起来。
10.1 操作系统如何知道硬件设备发生了事件?
假设 CPU 正在运行一个用户程序。
与此同时,键盘产生了输入事件。
CPU 不需要依赖用户程序主动调用:
c
read()
才能处理所有硬件事件。
硬件可以通过中断机制通知处理器和操作系统。
操作系统则根据相应的中断处理机制完成事件响应。
10.2 什么是中断向量表?
中断处理机制需要知道:
text
某类中断或者异常应该进入哪个处理入口?
因此操作系统和处理器架构会使用相应的中断描述、向量和处理入口组织机制。
例如,在 x86 架构中,可以使用:
text
IDT
即中断描述符表。
不同的向量号与相应处理入口关联。
10.3 为什么中断很重要?
如果没有有效的事件通知机制,系统可能需要不断查询:
text
键盘是否有输入?
磁盘操作是否完成?
设备是否发生变化?
硬件中断使操作系统能够在相应事件发生时获得通知,而不必仅仅依赖对所有设备进行持续轮询。
当然,现代系统中某些高性能设备驱动也会采用中断与轮询结合的设计。
十一、时钟中断与进程调度
11.1 为什么操作系统需要定时能力?
前面已经学习:
c
alarm()
能够安排定时信号。
但操作系统本身也需要处理:
- 定时任务。
- 进程调度。
- 超时管理。
- 时间统计。
- 内核定时器。
因此,操作系统必须具备相应的时间管理机制。
11.2 时钟事件与进程调度的关系
可以先建立一个简化模型:
text
当前进程运行
|
v
发生时钟相关事件
|
v
进入内核处理
|
v
判断调度需求
|
v
可能切换到其他进程
但不能简单认为:
每次时钟中断都会强制切换进程。
是否调度以及何时调度,还取决于当前系统状态和调度策略。
现代 Linux 也使用更复杂的时钟事件机制,不完全等同于早期固定周期时钟中断模型。
11.3 为什么死循环程序不会永久独占CPU?
例如:
c
while (1)
{
}
假设它运行在正常调度环境中。
操作系统仍然可以通过中断、异常和调度机制重新获得 CPU 控制权,并在需要时安排其他任务执行。
这也是多任务操作系统能够管理多个执行流的重要基础之一。
十二、软中断、系统调用与内核态
12.1 什么是用户态?
平时我们编写:
c
int main(void)
{
// 用户程序
}
这些普通应用代码主要运行在用户态。
用户态代码不能随意执行特权操作,也不能任意访问内核受保护的内存。
12.2 什么是内核态?
操作系统内核需要执行:
- 进程调度。
- 内存管理。
- 文件系统管理。
- 设备控制。
- 中断与异常处理。
因此它需要更高的执行权限。
在 x86 架构中,常用:
text
Ring 3:用户态
Ring 0:内核态
帮助理解权限级别差异。
12.3 系统调用怎样进入内核?
例如:
c
write(fd, buffer, size);
虽然由用户程序发起,但实际文件 IO 需要内核参与。
因此需要通过系统调用机制切换到内核执行。
在不同体系结构与历史时期,相关机制可能使用:
text
int 0x80
syscall
sysenter
等不同方式。
现代 Linux 的具体入口和调用约定依赖架构。
12.4 软中断与系统调用不能完全混为一谈
课件通过历史源码解释早期系统调用与软中断相关的实现。
但从现代 Linux 的角度看:
- 硬件中断。
- 同步异常。
- 系统调用。
- Linux内核中的SoftIRQ机制。
这些属于不同概念或机制。
因此不能认为:
text
所有系统调用都是Linux SoftIRQ
或者:
text
所有进程信号都来自硬件中断
12.5 为什么课件中的3GB/1GB划分不能直接套到现代系统?
课件使用了经典的 32 位 Linux 地址空间划分案例。
例如:
text
用户空间:较低3GB
内核空间:较高1GB
这对于理解历史的 32 位 x86 内核虚拟地址空间布局有帮助。
但现代 x86-64 Linux 的地址空间布局不同。
因此:
用户态与内核态的本质区别是执行权限和内存访问保护机制,而不是固定的3GB与1GB地址分界。

十三、缺页异常:为什么不是所有内存异常都意味着程序错误?
13.1 回顾虚拟内存
进程访问的是:
text
虚拟地址
操作系统和硬件内存管理机制负责将其转换为相应的物理内存访问。
但某个虚拟地址对应的物理页,可能暂时还没有建立完整映射。
例如:
- 按需分配内存。
- 写时复制。
- 文件映射页面尚未进入内存。
这时访问可能触发缺页异常。
13.2 什么是缺页异常?
当处理器无法按照当前页表状态完成某次内存访问时,可能产生 Page Fault。
操作系统需要判断它的原因。
例如:
text
进程访问虚拟地址
|
v
CPU/MMU检查映射和权限
|
v
产生Page Fault
|
v
内核检查原因
然后分成不同情况。
合法的缺页情况
例如该地址属于合法映射区域,但对应页面尚未分配或者载入。
操作系统可能通过分配物理页、建立映射或读取文件页面等方式解决问题。
完成后,原来的指令可以继续执行。
非法的内存访问
例如访问没有权限的区域,且无法通过正常缺页处理解决。
那么内核可能向进程产生:
c
SIGSEGV
或者与具体错误相关的其他信号。
所以:
Page Fault不等于Segmentation Fault。
Page Fault 是处理器/内核处理内存访问的一种机制;Segmentation Fault 则通常是无法正常处理的非法访问导致程序收到 SIGSEGV 的结果。
13.3 与fork写时复制的联系
之前学习 fork() 时,我们知道:
父子进程可能通过写时复制(COW)共享初始的物理页。
当某个进程准备修改相关页面时,可能触发缺页异常。
内核可以为它建立新的私有页面,并修改对应映射。
这样就实现了:
text
初始共享
|
v
发生写入
|
v
按需复制
|
v
父子进程数据隔离
因此,缺页异常不仅可以反映错误,也可以帮助实现正常的内存管理功能。
十四、可重入函数:为什么信号处理函数不能随意调用普通库函数?
14.1 从链表插入开始分析
课件给出了一个非常典型的案例。
假设程序正在向全局链表插入节点:
text
head -> A
主程序准备插入:
text
node1
插入操作可能涉及多个步骤。
例如:
c
node1->next = head;
head = node1;
但是执行到一半时,进程突然收到信号。
信号处理函数又尝试向同一个全局链表插入:
text
node2
于是可能出现:
- 主程序执行了链表插入的第一步。
- 信号处理函数打断主程序。
- 信号处理函数完成第二个节点插入。
- 主程序恢复执行。
- 主程序继续使用之前的中间状态完成插入。
结果可能造成链表结构不符合预期。
14.2 什么是重入?
如果一个函数还没有完成之前的调用,就又被另一条执行路径进入,这种现象称为重入。
例如:
text
main调用insert
|
v
insert执行一半
|
v
信号到来
|
v
Handler再次调用insert
如果 insert() 使用了没有得到保护的共享状态,就可能出现问题。
14.3 什么是可重入函数?
可重入函数要求在发生重入时仍然能安全工作。
通常需要避免依赖未经适当保护的可修改共享状态。
例如,一个只根据参数进行局部计算、没有共享可变状态的函数,通常更容易实现为可重入函数。
但这里需要注意:
可重入和线程安全是两个不同概念。
某个函数具有线程安全措施,不代表它一定适合在异步信号处理函数中调用。
14.4 为什么printf可能不安全?
printf() 通常涉及标准 IO 缓冲和内部状态。
假设主程序正在执行:
c
printf("hello");
此时信号到来。
信号处理函数又执行:
c
printf("signal");
就可能重入标准 IO 相关代码。
这在异步信号处理场景中不具有普遍安全保证。
类似地:
c
malloc()
free()
也不适合随意从异步信号处理函数中调用。
14.5 在信号处理函数中应该做什么?
通常建议:
- 尽量保持处理函数简短。
- 避免复杂的动态内存操作。
- 避免普通标准 IO 操作。
- 使用规定的异步信号安全接口。
- 或者只设置一个适当类型的通知标志,让正常执行流程处理复杂工作。
例如:
c
#include <signal.h>
volatile sig_atomic_t stop = 0;
void Handler(int signo)
{
(void)signo;
stop = 1;
}
信号处理函数只负责:
text
通知主程序需要停止
实际清理资源、打印日志等工作,由主程序在正常执行流程中完成。

十五、volatile:为什么编译优化可能影响信号处理?
15.1 先看一个简单程序
c
#include <signal.h>
#include <stdio.h>
int flag = 0;
void Handler(int signo)
{
(void)signo;
flag = 1;
}
int main(void)
{
signal(SIGINT, Handler);
while (!flag)
{
}
printf("process quit normally\n");
return 0;
}
逻辑很简单:
text
flag初始值为0
|
v
while循环
|
v
按Ctrl+C
|
v
Handler把flag改成1
|
v
退出循环
15.2 编译优化可能带来的问题
如果编译时使用:
bash
gcc -O2 test.c -o test
编译器可能基于普通变量的语义,对循环访问进行优化。
由于这种普通变量并不是为异步信号处理通信而声明,程序不能可靠地依赖信号处理函数改变它来结束循环。
课件通过优化前后的运行现象说明:
text
变量已经在处理函数中被修改
但主循环未必会重新观察该值。
15.3 使用volatile sig_atomic_t
用于简单信号通知的典型写法是:
c
volatile sig_atomic_t flag = 0;
完整代码:
c
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void Handler(int signo)
{
(void)signo;
flag = 1;
}
int main(void)
{
struct sigaction action = {0};
action.sa_handler = Handler;
sigemptyset(&action.sa_mask);
if (sigaction(SIGINT, &action, NULL) < 0)
{
perror("sigaction");
return 1;
}
while (!flag)
{
// 教学演示用忙等待
}
printf("process quit normally\n");
return 0;
}
编译:
bash
gcc -O2 test.c -o test
运行后按下 Ctrl+C,可以观察循环结束。
15.4 volatile有什么作用?
在这个场景中:
c
volatile
告诉编译器,对该对象的访问具有特殊的可观察性要求,不能简单按照普通变量的方式省略或合并相关访问。
而:
c
sig_atomic_t
提供适合此类简单异步信号通知的类型。
但是需要特别注意:
volatile不是线程同步原语,不提供一般意义上的互斥,也不等于内存屏障。
它不能代替:
text
互斥锁
原子操作
信号屏蔽与同步机制
等工具。
另外,上面的空循环只用于观察优化问题,真实程序不应无理由地持续占用 CPU 忙等待。
十六、竞态条件:为什么pause()也可能出现问题?
课件的学习目标还强调:
text
了解竞态条件的情景和处理方式
这里结合信号等待补充一个典型案例。
16.1 什么是竞态条件?
如果程序最终执行结果依赖于两个事件发生的先后顺序,并且不同顺序可能导致错误,就可能存在竞态条件。
例如:
c
if (!flag)
{
pause();
}
看起来表示:
text
如果还没有收到信号,就等待信号
但存在一种可能的执行顺序:
- 主程序检查
flag == 0。 - 在真正调用
pause()前,信号到来。 - 处理函数将
flag改为 1。 - 信号处理函数返回。
- 主程序继续执行
pause()。
此时:
text
flag已经变成1
但信号已经处理完了。
于是 pause() 仍然可能阻塞,等待下一次信号。
这就是典型的:
检查条件与进入等待不是原子操作,导致丢失唤醒。
16.2 怎样避免这种问题?
POSIX 提供:
c
sigsuspend()
用于临时替换当前线程的信号屏蔽字,并以原子方式进入等待状态。
原型:
c
#include <signal.h>
int sigsuspend(const sigset_t* mask);
经典思路是:
text
先阻塞目标信号
|
v
准备需要等待的条件
|
v
检查条件
|
v
使用sigsuspend临时解除阻塞并等待
|
v
信号处理函数执行
|
v
sigsuspend返回并恢复旧阻塞状态
因此它能够避免:
text
检查完成
与:
text
真正进入等待
之间的丢失唤醒窗口。
这部分与同步编程中的条件等待问题非常相似。

十七、SIGCHLD:如何通过信号回收子进程?
这是课件最后一个重点部分。
17.1 回顾僵尸进程
之前学习进程等待时,我们知道:
子进程终止后,父进程需要通过:
c
wait()
或者:
c
waitpid()
回收子进程的退出状态。
如果父进程长时间不回收已经终止的子进程,就可能产生僵尸进程。
17.2 阻塞等待有什么问题?
例如:
c
waitpid(pid, &status, 0);
如果子进程暂时没有退出,父进程可能阻塞。
对于一个需要同时执行其他工作的父进程,这可能不够方便。
17.3 轮询有什么问题?
另一种方法是:
c
waitpid(-1, &status, WNOHANG);
让父进程非阻塞地查询。
但如果程序反复查询,就需要自己决定轮询频率,并处理额外的控制逻辑。
能不能在子进程退出时,主动通知父进程?
答案就是:
c
SIGCHLD
17.4 SIGCHLD的作用
在 Linux 中,当子进程终止,或者发生某些停止、继续等状态变化时,父进程通常会收到相应的 SIGCHLD 通知。
因此,父进程可以注册:
c
SIGCHLD
处理函数。
当子进程状态变化时,通过处理函数执行适当的子进程回收工作。
这里要注意:
SIGCHLD只负责通知,不能自动代替所有子进程退出状态的处理逻辑。
十八、SIGCHLD实验:让父进程处理子进程退出
18.1 为什么不能只调用一次waitpid?
假设多个子进程在很短时间内退出。
由于普通信号不保证对每个事件分别递达一次,父进程可能收到少于子进程退出次数的 SIGCHLD 通知。
因此处理函数中一般需要循环:
c
while (waitpid(-1, &status, WNOHANG) > 0)
{
// 回收已退出子进程
}
尽可能回收已经终止、等待被回收的子进程。
18.2 完整代码
下面实现:
- 父进程注册
SIGCHLD。 - 子进程退出码为 2。
- 信号处理函数调用
waitpid()。 - 父进程安全等待子进程被回收。
- 父进程打印退出状态。
sigchld_test.c:
c
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
volatile sig_atomic_t child_reaped = 0;
volatile sig_atomic_t child_exit_code = -1;
void Handler(int signo)
{
(void)signo;
int saved_errno = errno;
int status = 0;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
{
if (WIFEXITED(status))
{
child_exit_code = WEXITSTATUS(status);
}
child_reaped = 1;
}
errno = saved_errno;
}
int main(void)
{
sigset_t block_set;
sigset_t old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGCHLD);
// 先阻塞SIGCHLD,防止后续出现竞态
if (sigprocmask(
SIG_BLOCK,
&block_set,
&old_set
) < 0)
{
perror("sigprocmask");
return 1;
}
struct sigaction action = {0};
action.sa_handler = Handler;
action.sa_flags = SA_NOCLDSTOP;
sigemptyset(&action.sa_mask);
if (sigaction(SIGCHLD, &action, NULL) < 0)
{
perror("sigaction");
return 1;
}
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 1;
}
if (pid == 0)
{
// 子进程正常结束,退出码为2
_exit(2);
}
// 安全地等待SIGCHLD
while (!child_reaped)
{
sigsuspend(&old_set);
}
// 恢复原来的信号屏蔽字
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf(
"child exit code = %d\n",
(int)child_exit_code
);
return 0;
}
18.3 编译运行
bash
gcc -Wall -Wextra sigchld_test.c -o sigchld_test
运行:
bash
./sigchld_test
预期输出:
text
child exit code = 2
18.4 为什么这里使用sigsuspend?
因为在正式等待之前,子进程可能已经退出。
如果使用普通:
c
pause();
可能发生:
text
子进程已经退出
SIGCHLD已经处理
父进程才进入pause
从而造成丢失唤醒。
这里通过:
c
sigprocmask()
先阻塞 SIGCHLD。
再使用:
c
sigsuspend()
原子地临时解除阻塞并等待。
从而避免这个典型问题。
18.5 为什么处理函数中不使用printf?
因为信号处理函数可能打断正常的标准 IO 调用。
所以这里选择:
c
child_exit_code = ...
child_reaped = 1;
让主程序在正常执行流程中打印。
这样更符合异步信号处理的基本原则。

十九、SIGCHLD还可以使用SIG_IGN吗?
课件最后讨论了一个特殊情况。
在 Linux 中,如果通过 sigaction() 显式将 SIGCHLD 的处理动作设置为:
c
SIG_IGN
那么子进程终止时,可以由系统自动回收相关退出状态,从而避免产生普通意义上需要父进程 wait() 回收的僵尸子进程。
例如:
c
#include <signal.h>
int main(void)
{
struct sigaction action = {0};
action.sa_handler = SIG_IGN;
sigemptyset(&action.sa_mask);
sigaction(SIGCHLD, &action, NULL);
// 后续创建子进程
return 0;
}
但这里有一个容易混淆的细节。
SIGCHLD的默认动作虽然被描述为Ignore,但"保持默认处置"与"显式设置SIG_IGN"在Linux子进程回收语义上并不完全相同。
也就是说:
c
SIG_DFL
和:
c
SIG_IGN
不能在这个场景中简单视为完全等价。
此外,SA_NOCLDWAIT 也能够影响子进程退出状态的保留与回收。
如果父进程还需要通过 waitpid() 获取每个子进程的退出码,就不应该轻易启用自动回收配置。
所以进程管理中需要根据目标选择:
- 希望准确收集退出状态:显式使用
wait()、waitpid()。 - 希望在 Linux 上自动回收子进程:研究
SIGCHLD的显式忽略和SA_NOCLDWAIT。
二十、将信号知识与之前的MiniShell联系起来
我们已经实现了一个简单的 MiniShell。
其中有:
c
fork()
execvp()
waitpid()
以及输入输出重定向。
学习信号以后,可以更深入理解下面几个问题。
20.1 用户按Ctrl+C时发生什么?
对于一个支持作业控制的正常 Shell:
bash
sleep 100
运行在前台时,用户按下:
text
Ctrl+C
终端会向前台进程组产生 SIGINT。
因此执行 sleep 的进程可能被信号终止。
但交互式 Shell 自身通常不应该因此直接退出。
20.2 父进程如何知道子进程被什么信号终止?
我们之前已经使用:
c
waitpid()
和:
c
WIFSIGNALED(status)
判断子进程是否由信号终止。
再通过:
c
WTERMSIG(status)
获取终止信号编号。
所以 MiniShell 中常见的:
c
128 + WTERMSIG(status)
是一种常用的 Shell 退出状态编码约定。
例如子进程因 SIGINT 终止,Shell 可能记录:
text
130
而不是普通的:
text
2
20.3 为什么完整Shell还需要SIGCHLD?
如果 Shell 支持后台运行:
bash
sleep 10 &
就不能始终阻塞等待这个后台进程退出。
它还需要继续接受新的命令。
因此可以利用 SIGCHLD 等机制及时获知子进程状态变化。
但要实现完整的 Shell 作业控制,还需要管理:
text
进程组
终端前台控制
停止与继续
后台任务回收
等内容。
所以:
信号机制是把我们之前的MiniShell从简单命令执行器扩展为更完整Shell的重要基础。
二十一、常见问题与错误总结
21.1 信号产生后一定立即执行处理函数吗?
不是。
产生与递达是不同阶段,信号可能因阻塞等原因保持未决状态。
21.2 Block和Pending是一回事吗?
不是。
Block 描述某个信号是否被阻塞。
Pending 描述某个信号是否尚未递达。
21.3 阻塞一个信号等于忽略它吗?
不是。
阻塞会阻止相应递达,忽略则是信号处置方式的一种。
21.4 使用sigaddset()就完成了信号阻塞吗?
没有。
sigaddset() 只修改信号集变量。
还需要通过 sigprocmask() 等接口改变实际信号屏蔽字。
21.5 连续产生五次SIGINT就必然捕捉五次吗?
不一定。
普通信号的未决状态不保证精确排队计数。
21.6 SIGKILL可以被阻塞吗?
不能。
SIGKILL 和 SIGSTOP 不能被阻塞、忽略或捕捉。
21.7 信号处理函数可以任意调用printf、malloc吗?
不应该。
这些函数不具有普遍的异步信号安全保证。
21.8 volatile可以解决所有并发问题吗?
不能。
它不能替代同步原语,也不能保证复杂操作的原子性。
21.9 Page Fault一定会导致SIGSEGV吗?
不会。
合法的缺页可以由内核正常处理,例如按需分配、文件映射和写时复制。
21.10 SIGCHLD处理函数只调用一次waitpid就够了吗?
通常不够可靠。
多个子进程可能陆续或同时退出,应该考虑循环执行非阻塞回收。
21.11 SIGCHLD默认忽略是否表示子进程不会变成僵尸进程?
不能这样理解。
Linux 对 SIGCHLD 的默认处置和显式设置 SIG_IGN 具有特殊的回收语义差异。
二十二、完整知识体系总结
到这里,我们已经把 Linux 进程信号的主要知识联系起来。
首先:
text
信号产生
|
v
内核记录信号状态
|
v
信号可能处于Pending
|
v
检查信号是否被Block
|
v
允许递达
|
v
执行信号处理动作
其中:
text
Pending
表示尚未递达。
text
Block
表示阻塞条件。
text
Handler
表示信号的处理动作。
然后,信号捕捉过程又与操作系统的:
text
用户态
内核态
硬件中断
系统调用
异常处理
密切相关。
最后,信号处理函数的异步执行特点,又引出了:
text
可重入函数
异步信号安全
volatile sig_atomic_t
竞态条件
SIGCHLD
等更加深入的问题。
这些知识不是孤立的,而是围绕一个核心问题展开:
操作系统怎样在不破坏进程基本执行模型的前提下,异步通知、管理并处理进程相关事件?
总结
本篇按照课程后半部分的顺序,系统学习了 Linux 信号的保存和处理机制。
首先,我们认识了:
text
Pending
Block
Delivery
这三个基本概念。
随后,通过:
c
sigset_t
sigemptyset()
sigaddset()
sigprocmask()
sigpending()
理解了进程如何控制信号阻塞状态,以及怎样观察尚未递达的信号。
在信号捕捉部分,我们学习了:
c
sigaction()
并解释了信号处理函数为什么能够打断用户程序的正常执行流程。
接下来,结合操作系统中断机制,进一步理解了:
text
硬件中断
时钟事件
系统调用
用户态与内核态
Page Fault
之间的联系。
在编程实践中,我们又认识了异步信号处理中的几个关键问题:
- 不可重入函数可能导致共享状态错误。
volatile sig_atomic_t适合简单的信号通知标志。pause()与条件检查之间可能存在丢失唤醒的竞态。sigsuspend()可以帮助安全等待信号。SIGCHLD可以配合waitpid()管理子进程退出。
至此,两篇 Linux 进程信号文章的主要知识链路就建立起来了:
信号产生 → 信号保存 → 信号递达 → 信号捕捉 → 操作系统底层机制 → 信号安全编程 → 子进程管理。
这不仅是学习 Linux 进程信号的基础,也将为后续理解多进程服务器、并发编程和完整 Shell 作业控制提供必要的知识储备。