Linux进程信号

本节重点:

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 步:

  1. 键盘按键触发硬件中断
  2. 操作系统拿到这个中断
  3. OS 把它解释成某种信号
  4. 把信号发给前台进程

然后这个信号其实就是 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 == NULLSIGSEGV

总结:在 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:看/改 block
  • sigpending:看 pending

5. 实验

一个经典实验:先阻塞 SIGINT,然后按 Ctrl+C,程序每秒打印 pending 表。你会看到 2 号信号变成未决;等解除阻塞后,这个信号才被递达。

这个实验说明了什么

  1. 信号到了,不一定马上处理
  2. 被阻塞时,会进入 pending
  3. 解除阻塞后,才递达

这就是"产生 → 保存 → 处理"三阶段的直接证据。

信号捕捉

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_handler
  • sa_mask
  • sa_flags

强调:

  • sa_handler = SIG_IGN → 忽略
  • sa_handler = SIG_DFL → 默认动作
  • sa_handler = 函数指针 → 自定义捕捉

sa_mask 的作用

当某个信号处理函数被调用时,内核会自动把当前这个信号 加入屏蔽字,避免它在处理过程中再次打断自己;如果还想额外屏蔽别的信号,就用 sa_mask 指定。处理函数返回后,原来的屏蔽字会自动恢复。处理某个信号时,内核会自动暂时阻塞该信号自身,防止处理函数重入。

操作系统是怎么跑起来的

1. 硬件中断

• 中断向量表就是操作系统的⼀部分,启动就加载到内存中了

• 通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询

• 由外部设备触发的,中断系统运⾏流程,叫做硬件中断

键盘、网卡、磁盘这些设备一有事,不是 OS 自己一直问"你有事吗",而是设备主动打断 CPU,OS 再去处理。


2. 时钟中断

提出问题:如果没有人按键,没有外设中断,操作系统靠谁推动?

答案就是:时钟中断 。内核代码片段说明定时器中断会进入调度逻辑,最终调用 schedule() 进行任务切换。

理解

  • 时钟定期触发中断
  • 内核借这个时机检查时间片
  • 需要时就切换进程

所以操作系统能"自动调度",靠的就是硬件时钟不断推动。说得直白点:操作系统在硬件的推动下自动调度。


3. 软中断、系统调用、异常

  • 用户执行 int 0x80syscall 进入内核,这本质上是触发软中断 ,我们叫陷阱
  • CPU 内部因除零、野指针等触发的软中断,叫异常
  • 缺页异常、除零异常、非法内存访问,都会进入对应的内核处理流程

**中断、异常、系统调用,都会让 CPU 从用户态切到内核态。**而信号,很多时候正是内核对这些异常的"进程级通知手段"。

相关推荐
水木兰亭2 小时前
多进程编程总结
linux·运维·服务器
暮冬-  Gentle°2 小时前
C++与区块链智能合约
开发语言·c++·算法
JobDocLS2 小时前
Bash调试方法
开发语言·bash
Oueii2 小时前
C++中的代理模式实现
开发语言·c++·算法
梦想是造卫星2 小时前
如何从零开始构建一个ros开发项目?
linux·ros开发
艾莉丝努力练剑2 小时前
【Linux:文件 + 进程】理解IPC通信
linux·运维·服务器·开发语言·网络·c++·ide
开开心心就好2 小时前
安卓免费证件照制作软件,无广告弹窗
linux·运维·安全·pdf·迭代器模式·依赖倒置原则·1024程序员节
洋不写bug2 小时前
Java线程(二):线程特点、状态、终止开始控制(
java·开发语言
ZTLJQ2 小时前
挖掘金矿:Python数据解析库完全解析
开发语言·python