本节重点:
1. 掌握Linux信号的基本概念。
2. 掌握信号产⽣的⼀般⽅式。
3. 理解信号递达和阻塞的概念,原理。
4. 掌握信号捕捉的⼀般⽅式。
5. 了解中断过程,理解中断的意义
6. 掌握操作系统运⾏,系统调⽤原理,理解缺⻚异常或其他软件异常的基本原理
7. 重新了解可重⼊函数的概念。
8. 了解竞态条件的情景和处理⽅式。
9. 了解SIGCHLD信号,重新编写信号处理函数的⼀般处理机制。
内容预览
信号是 Linux 中进程收到"异步通知"的机制,本质上是一种软中断。 正式定义就是:"信号是进程之间事件异步通知的一种方式,属于软中断。"
- 异步:不是你程序按顺序主动调用的,可能在你程序执行的任何位置突然来
- 软中断:它不像键盘、网卡那样是硬件直接中断 CPU,而是 OS 用软件机制通知进程
可以把它记成:
某个事件发生了 → 内核通知进程 → 进程在合适的时候处理。
这里最重要的是"异步 "二字。因为信号不是按你程序正常调用流程来的,它可能在你程序执行的任何位置突然到来。前台进程在运行过程中用户随时可能按下 Ctrl+C,所以信号相对于进程控制流程来说是异步的。
信号快速认识
五个结论:
第一,进程天生就能识别信号。
就像你知道"快递到了"是什么意思一样,进程识别信号是内核预先设计好的能力。识别信号是内置的,是内核程序员写进去的。
第二,信号的处理方式在信号产生前就已经准备好了。
不是说收到信号后才临时决定怎么办,而是早就规定好了:
- 默认处理
- 忽略
- 自定义处理
- 处理方法在信号产生之前已经准备好了。
第三,信号不一定立刻处理。
你正在打游戏,快递到了,不一定马上下楼;进程正在执行更重要的事,也不一定马上响应。不会立即处理,而是在合适的时候处理。
第四,信号产生后需要先"记住"。
这就引出了后面要学的**pending(未决)**概念。先记下来,等时机合适再处理。
第五,信号处理只有三类。
- 默认动作
- 忽略
- 自定义捕捉
Ctrl+C 到底发生了什么
前台程序运行时,按 Ctrl+C ,程序退出。这里你一定要会解释全过程:
用户在 Shell 中启动前台进程,按下 Ctrl+C 后,键盘输入先产生硬件中断,被操作系统解释成信号,再发送给前台进程,前台进程收到信号后退出。
4 步:
- 键盘按键触发硬件中断
- 操作系统拿到这个中断
- OS 把它解释成某种信号
- 把信号发给前台进程
然后这个信号其实就是 SIGINT(2号信号)。明确说:Ctrl+C 的本质是向前台进程发送 SIGINT,即 2 号信号。
为什么注册了 handler 之后 Ctrl+C 不退出
signal(SIGINT, handler);
然后按 Ctrl+C,进程不退出了,而是进入你写的 handler。这是因为:我们把 SIGINT 的默认处理动作,改成了自定义处理函数。
signal 函数只是设置处理方式,不是立即调用处理动作;如果后面这个信号一直没产生,这个函数也永远不会被调用。
这里常考:
signal 是"注册处理方式",不是"触发信号处理"。
前台进程和后台进程
Ctrl+C 产生的信号只能发给前台进程,后台进程收不到这种终端控制键产生的信号。Shell 同时可以运行一个前台进程和多个后台进程。
信号概念与处理方式
1. 信号有哪些(知道常见的就行)
课件说每个信号都有一个编号和宏定义名,比如 SIGINT 定义为 2。34 号以上是实时信号,本章不讨论。
重点记下面这些常见信号
SIGINT:2,Ctrl+C,中断SIGQUIT:3,Ctrl+\SIGKILL:9,强制杀死SIGSEGV:11,段错误SIGALRM:14,闹钟超时SIGTERM:15,终止SIGCHLD:子进程退出时给父进程发SIGTSTP:终端停止,常见 Ctrl+Z
不用背全表,但这些要会认。
信号处理的三种方式
处理动作有三种:
- **忽略:**SIG_IGN
cpp
signal(SIGINT, SIG_IGN);
那么 Ctrl+C 发来了,但进程不理它。课件示例中按了很多次 Ctrl+C,程序继续打印。
- 执行默认动作
SIG_DFL
cpp
signal(SIGINT, SIG_DFL);
Ctrl+C 会按系统默认动作处理,一般就是终止进程。此处Ctrl+C 后进程退出
- 自定义捕捉
cpp
就是把处理函数注册进去:
signal(SIGINT, handler);
以后 SIGINT 一到,内核就切到你的处理函数执行。这叫自定义捕捉(Catch)信号。
信号如何产生
1. 通过终端按键产生
三个最常见的:
Ctrl+C → SIGINT
Ctrl+\ → SIGQUIT
它可以发送终止信号,并生成 core dump 文件,方便事后调试。
Ctrl+Z → SIGTSTP
把当前前台进程挂起。此处按下 Ctrl+Z 后显示:
cpp
[1]+ Stopped ./sig
说明进程被停止并放到后台作业中。
区分清楚
- Ctrl+C:中断,通常让程序结束
- **Ctrl+**:退出并可能 core dump
- Ctrl+Z:挂起,不是终止
2.调用命令给进程发信号
kill 不是"杀死"的意思那么简单,本质是发送信号。
比如:
cpp
kill -SIGSEGV pid
kill -11 pid
这两种写法都可以。
3、使用函数产生信号
(1)kill(pid, sig)
这是系统调用版本的发送信号。课件说明它可以给指定进程发送指定信号。
要会写基本原型:
int kill(pid_t pid, int sig);
(2)raise(sig)
给当前进程自己发送信号。它就是"自己给自己发信号"。
比如:
raise(2);
就是自己给自己发 SIGINT。
(3)abort()
这个更特殊:abort() 会使当前进程收到固定的 SIGABRT(6号信号),即使你捕捉了它,最后还是会异常终止
abort 本质上就是让进程异常中止。
4. 软件条件产生信号:alarm
调用 alarm(seconds) 后,内核会在 seconds 秒后给当前进程发 SIGALRM,默认处理动作是终止进程。
函数原型
unsigned int alarm(unsigned int seconds);
它的特点你要记住
- 只设置一次性闹钟
- 到时间发
SIGALRM - 默认终止进程
- 返回值是上一个闹钟还剩多少秒
闹钟设置一次,起效一次;要重复闹钟,就要在处理函数中重新设置。
5. 硬件异常产生信号
- 除以 0 → CPU 检测到异常 → 内核解释成
SIGFPE - 访问非法地址 → MMU 检测异常 → 内核解释成
SIGSEGV
必须建立这个认识
C/C++ 里的很多"运行错误",在系统层面最终都表现为信号。
比如:
a /= 0;→SIGFPE*p = 100;且p == NULL→SIGSEGV
总结:在 C/C++ 中,除零、内存越界等异常,在系统层面上,是被当成信号处理的。
Core Dump

1. 什么是 core dump
一个进程异常终止时,可以把用户空间内存数据保存到磁盘上,通常文件名是 core,这叫 Core Dump。
2. 有什么用
为了事后调试 。程序已经死了,但你可以用 gdb 打开 core 文件,查看程序死在哪儿。把这叫 Post-mortem Debug。
3. 默认为什么不开
默认不允许产生 core 文件,因为里面可能包含敏感信息,例如密码,不安全。
4. 如何打开
用:
ulimit -c 1024
去修改 shell 的资源限制。Shell 的 Resource Limit 会被子进程继承,所以从这个 Shell 启动的测试程序也能产生 core。
信号保存------pending 与 block(必须掌握)

1. 三个基本概念(必须掌握)
(1)递达 Delivery
真正执行信号处理动作,叫信号递达。
(2)未决 Pending
信号从产生到递达之间的状态,叫未决。
(3)阻塞 Block
进程可以选择阻塞某个信号;被阻塞的信号产生后,不会立刻递达,而是保持未决,直到解除阻塞。
2. 阻塞和忽略不是一回事(必须掌握)
这是非常容易考的点,提醒:
阻塞和忽略不同。
- 阻塞:信号先记着,不递达
- 忽略:信号递达后,处理动作选择"丢掉不处理"
一句话:阻塞发生在递达之前,忽略发生在递达时。
3. 信号在内核里怎么表示(必须理解)

结论:
每个信号都有:
- 一个 block 标志位:是否阻塞
- 一个 pending 标志位:是否未决
- 一个处理动作指针:默认/忽略/用户处理函数
而且信号产生时,内核是在进程控制块里设置该信号的未决标志,直到递达才清除。
这张图你应该这样理解:进程的 PCB 里相当于有三张表:
- block 表:哪些信号现在不准递达
- pending 表:哪些信号已经来了但还没处理
- handler 表:这个信号来了之后该怎么处理
这就是信号机制的底层骨架。
普通信号为什么"来多次只记一次"(必须掌握)
常规信号在递达前产生多次,只计一次;实时信号则可以排队。
原因也很简单,因为普通信号在 pending 里本来就是一个 bit ,只有 0 和 1,没法记录次数。在 sigset_t 一节再次强调:每个信号只有一个 bit 的未决标志,不记录产生多少次。
普通信号不排队,实时信号才排队。
sigset_t 与信号集操作
1. 什么是 sigset_t
sigset_t 称为信号集 ,本质是一个位图,用 bit 表示某个信号是否有效。阻塞集合里表示是否阻塞,未决集合里表示是否未决。阻塞信号集还有个名字,叫信号屏蔽字(Signal Mask)。
2. 五个基本函数
sigemptyset
sigfillset
sigaddset
sigdelset
sigismember
功能:
sigemptyset:清空sigfillset:全部置上sigaddset:添加某个信号sigdelset:删除某个信号sigismember:检查某个信号是否在集合里
特别提醒:在使用 sigset_t 变量之前,必须先初始化。
3. sigprocmask
这是本章非常重要的系统调用。它可以读取或更改进程的信号屏蔽字(阻塞信号集)。
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
how 三种模式一定要记住
SIG_BLOCK:把 set 中的信号加入当前屏蔽字SIG_UNBLOCK:把 set 中信号从当前屏蔽字去掉SIG_SETMASK:直接把当前屏蔽字设置成 set
最重要一句
如果解除某些未决信号的阻塞,那么在 sigprocmask 返回前,至少会递达其中一个。
4. sigpending
作用是读取当前进程的未决信号集。
cpp
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
区分
sigprocmask:看/改 blocksigpending:看 pending
5. 实验
一个经典实验:先阻塞 SIGINT,然后按 Ctrl+C,程序每秒打印 pending 表。你会看到 2 号信号变成未决;等解除阻塞后,这个信号才被递达。
这个实验说明了什么
- 信号到了,不一定马上处理
- 被阻塞时,会进入 pending
- 解除阻塞后,才递达
这就是"产生 → 保存 → 处理"三阶段的直接证据。
信号捕捉

1. 捕捉信号的完整流程

结论:如果信号处理动作是用户自定义函数,那么信号递达时就调用这个函数,这叫捕捉信号。
按步骤解释:
- 程序本来在执行
main - 因中断/异常/系统调用进入内核
- 在返回用户态前,内核检查到有信号要递达
- 内核不直接回到
main原位置,而是先切到sighandler sighandler返回时自动执行sigreturn- 如果没有新的信号,再恢复
main的上下文继续执行 
要抓住 3 个关键点
第一,handler 不是 main 调的。
它是内核在合适时机"插进来"的。
第二,handler 和 main 是两个独立控制流。
它们使用不同栈空间,不存在普通函数调用关系。
第三,信号通常是在"从内核返回用户态前"检查并递达的。
这个理解非常关键。
2. sigaction(必须掌握,优先于 signal)
原型:
cpp
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
它的作用是读取和修改指定信号的处理动作。
为什么它比 signal 更重要因为它更完整、更标准,可以额外设置:
sa_handlersa_masksa_flags
强调:
sa_handler = SIG_IGN→ 忽略sa_handler = SIG_DFL→ 默认动作sa_handler = 函数指针→ 自定义捕捉
sa_mask 的作用
当某个信号处理函数被调用时,内核会自动把当前这个信号 加入屏蔽字,避免它在处理过程中再次打断自己;如果还想额外屏蔽别的信号,就用 sa_mask 指定。处理函数返回后,原来的屏蔽字会自动恢复。处理某个信号时,内核会自动暂时阻塞该信号自身,防止处理函数重入。
操作系统是怎么跑起来的
1. 硬件中断

• 中断向量表就是操作系统的⼀部分,启动就加载到内存中了
• 通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
• 由外部设备触发的,中断系统运⾏流程,叫做硬件中断
键盘、网卡、磁盘这些设备一有事,不是 OS 自己一直问"你有事吗",而是设备主动打断 CPU,OS 再去处理。
2. 时钟中断

提出问题:如果没有人按键,没有外设中断,操作系统靠谁推动?
答案就是:时钟中断 。内核代码片段说明定时器中断会进入调度逻辑,最终调用 schedule() 进行任务切换。
理解
- 时钟定期触发中断
- 内核借这个时机检查时间片
- 需要时就切换进程
所以操作系统能"自动调度",靠的就是硬件时钟不断推动。说得直白点:操作系统在硬件的推动下自动调度。
3. 软中断、系统调用、异常
- 用户执行
int 0x80或syscall进入内核,这本质上是触发软中断 ,我们叫陷阱 - CPU 内部因除零、野指针等触发的软中断,叫异常
- 缺页异常、除零异常、非法内存访问,都会进入对应的内核处理流程
**中断、异常、系统调用,都会让 CPU 从用户态切到内核态。**而信号,很多时候正是内核对这些异常的"进程级通知手段"。