mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4480 字
18 分钟
Linux 服务器高阶编程(11):信号基础——触发、未决、屏蔽与处理方式

Linux 服务器高阶编程(11):信号基础——触发、未决、屏蔽与处理方式#

一、信号是什么,与 Qt 信号槽有什么联系#

信号是一种通知机制。先从几个直观效果引入:结束进程、暂停进程、让停止的进程继续运行,以及通知进程某些条件已经发生。

注意“通知发生”和“最终怎么做”不是同一件事。以 SIGINT 为例,默认设置下它会结束进程,但程序可以修改相应处理方式,让这个信号到来时执行自定义函数。

因此围绕三层问题展开:

  1. 什么事件使信号产生?
  2. 信号是否可以立即递达?
  3. 递达后采用什么处理方式?

二、查看信号列表:名字与编号要一起记#

2.1 kill -l#

kill -l

这里是小写字母 l,表示列出信号,不是数字 1。

重点说明:

  • 1~31:主要讨论的 UNIX 经典信号。
  • 34~64:环境中列出的实时信号范围。
  • 32、33:未直接显示,为 NPTL 线程实现保留使用。

2.2 反复使用的信号#

名称编号用途
SIGINT2Ctrl+C;后续屏蔽、忽略和捕捉的主要对象
SIGQUIT3Ctrl+\,观察退出与核心转储提示
SIGABRT6介绍 abort() 时提到
SIGBUS7总线错误相关异常
SIGFPE8运算异常
SIGKILL9指定目标进程结束
SIGSEGV11非法内存访问相关异常
SIGPIPE13管道没有读端后继续写入
SIGALRM14alarm() 到期通知
SIGCONT18继续已经停止的进程
SIGSTOP19停止进程,与终端暂停信号对照
SIGTSTP20Ctrl+Z 暂停前台作业

三、第一种触发方式:终端组合键#

3.1 先运行一个不会立即结束的程序#

创建 while.c,核心是一个无限循环。作用不是完成业务,而是让程序一直留在前台,给终端信号留出测试时间。

核心片段:

while(1);

编译运行后,Shell 暂时不能继续处理新的命令输入;结束或暂停前台程序后,提示符才会重新出现

3.2 三组按键与三个信号#

操作信号效果
Ctrl+CSIGINT终止前台作业
Ctrl+\SIGQUIT终止,并观察到核心转储相关提示
Ctrl+ZSIGTSTP停止运行,但进程还存在

Ctrl+\ 的第二个键是反斜杠,不是正斜杠 /。

对照 Ctrl+C 与 Ctrl+\ 的输出,说明“都结束程序”不代表默认动作完全一样。核心转储可为后续调试提供材料;实际是否生成文件,还取决于系统设置,不能仅凭某条提示推断当前目录必然出现 core 文件。

3.3 暂停不是结束#

  • R:运行或可运行。
  • T:停止。
  • +:在本次观察中用于识别终端前台进程组成员。

3.4 jobs、fg、bg#

恢复运行:

jobs

查看当前 Shell 管理的作业及其编号,再根据需要恢复到前台或后台

fg %1
bg %1

%1 是作业号引用,不是 PID

  • fg:让作业在前台继续运行,Shell 通常继续等待它。
  • bg:让停止的作业在后台继续运行,Shell 可继续接收命令。

3.5 为什么快捷键不能直接结束那个后台作业#

终端按键没有让用户输入 PID,但仍知道信号该发给谁,因为它针对这个终端的前台对象。

把作业转到后台后,再按 Ctrl+C,不能期待原后台作业像先前一样结束

3.6 线路规程示意#

普通字符输入与具有特殊含义的终端字符,不一定走相同的处理路线。

终端输入
├─ 普通字符 → 作为输入数据供程序读取
└─ 特殊控制字符 → 终端驱动/线路规程识别 → 产生相应信号

这是用来解释为什么终端里的 Ctrl+C 通常不是普通“复制”。同一组键在不同应用上下文里,可能具有不同作用。

3.7 当场验证#

kill -3 目标PID

这里的“目标PID”是占位说明,实际输入时替换为自己的测试进程编号

四、第二、三种触发方式:命令与函数#

4.1 kill 命令不是只能“杀”#

kill -signo pid

signo 表示信号编号,pid 表示目标进程。它的核心功能是发送信号,最终是否终止,要由这个信号及其处理方式决定。

4.2 kill() 与命令的参数顺序不同#

命令写“信号、目标”,函数则写“目标、信号”。

标准接口补充:

#include <signal.h>
int kill(pid_t pid, int sig);

以指定正 PID 为主理解,成功返回 0,失败返回 -1。“任意进程、任意信号”概括选择能力,但仍受权限与目标存在性限制,不能据此认为可以无条件控制任何进程。

4.3 raise() 与 abort()#

函数重点
kill(pid, sig)选择目标及信号
raise(sig)向调用者自身发送指定信号
abort()与自身 SIGABRT 异常终止有关

标准声明是 void abort(void);,头文件为 <stdlib.h>

自己发信号并不一定没用;若某个可捕捉信号绑定了处理函数,可以利用通知触发处理

但后面的 SIGINT,不是完成一个基于 abort() 的定时业务框架

五、第四种触发方式:硬件或执行异常#

5.1 SIGSEGV:从只读内存写入理解#

以对只读内存进行写操作说明非法访问。程序违反访问约束时,可能收到 SIGSEGV,并出现段错误与核心转储提示。

地址是否有效、对象是否仍存活、是否越过合法访问范围、是否向不允许写入的区域写入。

5.2 SIGBUS:不能机械等同所有越界#

越界、栈和野地址问题与总线错误联系起来,目的是帮助建立“异常信息能缩小排错范围”的意识

数组越界、栈溢出或野指针属于不合法访问或未定义行为,不能保证统一产生 SIGBUS

也可能出现 SIGSEGV、没有立即崩溃或其他结果

5.3 SIGFPE:除零演示#

设置整数变量后进行除零计算。编译器给出警告,继续运行后观察到运算异常相关结果。

开发者未必会故意写一个除零表达式,但环形队列等代码中,若参与取模的大小意外为 0,也可能走到相似错误。

5.4 看错误信息能做什么#

它不能自动指出唯一一行错误,但可以给出排查方向:

  • 内存类异常:检查指针、对象生存期、权限和边界。
  • 运算异常:检查除数、取模分母、输入与计算条件。
  • 保留并分析编译警告,不要因为能生成可执行文件就认为代码正确。

六、第五种触发方式:软条件与 alarm()#

6.1 管道条件与 SIGPIPE#

管道读端关闭后,写入者继续向管道写入,会触发 SIGPIPE,默认可能终止写进程

6.2 alarm() 到期不等于直接调用自己的函数#

定时到期后,系统产生 SIGALRM。如果没有改变默认处理,它会终止进程。因此只调用 alarm(),并不会自动获得一个“时间到后正常继续”的业务定时器。

从这里联系 sleep 的效果:定时、等待、唤醒以及对信号处理方式的设置,需要放在同一个逻辑中理解

6.3 alarm.c 核心片段#

int main(void){
int cont = 0;
alarm(1);
while(1)
printf("++cont = %d\t",++cont);
}

这是代码片段,不是声称只有这些行便是完整源文件。alarm() 和 printf() 分别需要相应声明。

程序不断计数并打印,约一秒后因默认 SIGALRM 行为结束。不同机器和运行条件下,最后显示的数值不同。

6.4 为什么不能拿最后的计数当作 CPU 基准#

循环还包含格式化输出、缓冲和终端显示等开销,最后可见的计数不等于处理器每秒能做多少次纯算术操作。

应当得出的结论是“这个混合了输出的程序不是准确的 CPU 性能测试”,而不是根据某一次显示值判断电脑真假或好坏

对于这段代码,++cont 本身也在循环内,不能进一步假定它实际增长到了一个未显示出来的任意巨大数值

6.5 alarm() 返回值#

接口:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

“剩余未定时完的秒数”解释返回值,但容易与阻塞等待函数混淆。

alarm() 设置后立即返回;返回的是此前闹钟被替换或取消时剩余的秒数,没有此前闹钟则返回 0。alarm(0) 可取消原闹钟。返回 0 不表示刚设的闹钟已经等到时间,不能套一个反复调用的循环来等待它返回 0。

七、信号从产生到处理:未决、屏蔽、递达#

7.1 先区分状态和处理策略#

  • 产生:发生了应当通知目标的事件。
  • 未决:信号已经产生,尚未递达处理。
  • 递达:开始根据当前处理方式处置信号。

“已经发送”不等于“接收方已经执行了处理函数”。阻塞某个信号时,通知可以先处于未决状态。

7.2 SIGINT#

SIGINT 产生
↓
存在待处理通知:处于未决
↓
检查是否阻塞 SIGINT
├─ 阻塞 → 暂不递达,保持等待
└─ 允许 → 递达,按当前处理方式执行
├─ 默认
├─ 忽略
└─ 自定义捕捉

用“两扇门之间”描述被阻塞的未决信号。这是概念图,不是说内核真的有一个让信号对象通过两个数组格子的空间。

7.3 相同经典信号为什么不会累计成无限队列#

以连续多次产生相同信号说明:已有一个未决的经典信号时,后续同类通知不按发生次数分别排队。

所以连续产生十次,不应直接推断以后会执行十次处理函数。更适合记为“标准信号可能合并”,而不是把它设计成可靠事件计数器。

对比实时信号,用连续按“下一首”的类比解释:某些事件需要记录多次发生,因此需要队列语义。

标准信号与实时信号的队列语义不同;不能把“信号都不排队”推广到全部信号。

7.4 想屏蔽与已经来了,是两个问题#

要查看“我配置了要屏蔽哪些信号”,还是“哪些信号已经产生但尚未处理”?

  • 看屏蔽集合:知道阻塞策略,不能证明通知已经发生。
  • 看未决集合:知道哪些通知在等待,不能仅凭它判断全部阻塞策略。

7.5 默认集合不一定永久全零#

不能把图当成所有进程在任何时刻的状态保证。信号屏蔽状态涉及继承和运行中的修改;多线程场景中屏蔽掩码还具有线程属性

八、设置屏蔽字:创建、初始化、设置、替换#

8.1 替换原则#

先准备新的对象,再替换旧对象;旧对象可以保存,便于恢复,也可以不保存。

对应到屏蔽集合:

  1. 声明新的 sigset_t 对象。
  2. 初始化。
  3. 把需要阻塞的信号加入集合。
  4. 用 sigprocmask() 将其应用到当前执行上下文。
  5. 若后续要恢复,提前保存旧集合。

不要只声明并修改一个局部集合,就认为系统屏蔽策略已经改变;最后的应用步骤不能遗漏。

8.2 信号集操作函数#

接口作用
sigemptyset(&set)初始化为空集合
sigfillset(&set)初始化为包含信号的集合
sigaddset(&set, signo)加入某信号
sigdelset(&set, signo)移除某信号
sigismember(&set, signo)查询是否属于集合

这些操作需要 <signal.h>。sigismember() 的正常结果是 1 或 0,错误时为 -1;不能只把所有返回值都理解为一个合法位码。

特别注意,sigdelset() 从一个局部集合移除信号,并不自动修改系统当前的屏蔽字;只有把修改后的集合应用回去,才完成了这套设置流程。

8.3 sigprocmask() 的三个位置#

sigprocmask(SIG_SETMASK, &newset, &oldset);
  • 第一个参数:采用替换集合的方式。
  • 第二个参数:新的集合地址。
  • 第三个参数:接收此前集合的地址;不保存时传 NULL。

传 NULL 表示不向用户变量输出旧值,不是将某个内核对象粗暴“扔掉”。若以后需要恢复,保存 oldset 的意义就在于不用猜测原来有哪些信号被阻塞。

8.4 屏蔽 SIGINT 的画面代码#

保留 newset、oldset 及 NULL 的写法:

#include <signal.h>
//屏蔽sigint(2)
int main(void){
/*声明屏蔽字 newset , oldset*/
sigset_t newset, oldset;
sigemptyset(&newset);//初始化
sigaddset(&newset, SIGINT); //设置屏蔽2信号
sigprocmask(SIG_SETMASK,&newset, NULL);//替换进程屏蔽字,使用newset
while(1);
}

这虽然声明了 oldset,实际调用却传 NULL,所以并没有保存旧集合。不能仅看变量存在,就说已经完成了恢复逻辑。

8.5 测试观察与清理#

运行后按 Ctrl+C,程序不会像此前的无限循环那样立即退出,因为 SIGINT 被阻塞,不能按默认方式递达。

while(1); 的作用是留下测试时间。若程序立即结束,便来不及按键验证,不能据此认为屏蔽没有作用。

测试时还要准备结束这个程序的办法,例如另一个终端对自己的测试 PID 发送未被阻塞的终止信号。不要因为 Ctrl+C 无效就把系统误判为失去响应。

SIGKILL 与 SIGSTOP 不能被阻塞、忽略或捕捉

九、三种处理方式与默认方式下的五种动作#

9.1 三种方式#

处理方式如何设置理解递达后发生什么
默认SIG_DFL执行该信号预设的默认动作
忽略SIG_IGN不执行通常的终止等动作
捕捉安装处理函数调用用户定义的信号处理函数

“三种方式”不是说一个信号到来后依次把三种都执行一次,而是根据当前设置选择对应的处理。

9.2 默认方式并不只有终止#

默认动作名称含义关联
Term终止SIGINT 的默认结果
Core终止并涉及核心转储SIGQUIT 与一些异常信号
Stop停止SIGTSTP、SIGSTOP
Cont继续SIGCONT
Ign默认忽略通知不会按终止或停止处理

这是默认动作分类,不是五种额外的用户回调接口

9.3 “忽略方式”和“默认忽略动作”#

先决定走默认、忽略还是捕捉分支;只有走默认分支时,才查看该信号默认属于哪类动作。

因此 SIG_DFL 不等于“必定终止”。如果某信号本来默认忽略,选择默认方式仍可能是不做终止动作。也不要据此认为把任何信号显式设置为 SIG_IGN,都和保持其默认方式在所有细节上相同。

十、sigaction():像换屏蔽字一样换处理方式#

10.1 仍然是替换原则#

  1. 声明新的 struct sigaction。
  2. 配置本次要采用的处理方式。
  3. 初始化相关集合和选项。
  4. 对指定信号安装新设置。
  5. 按需要保存旧设置,便于恢复。

这与屏蔽集合的流程相似,但替换对象不同:一个决定是否允许递达,一个决定递达后如何处理。

10.2 三个重点成员#

成员设置
sa_handlerSIG_DFL、SIG_IGN 或处理函数名三选一
sa_flags设置为 0
sa_mask使用 sigemptyset() 初始化

文档画面曾用 SIG_DFL|SIG_IGN|sig_do 并列展示三个选择

10.3 安装与保存旧方式#

实际变量名为 act、oact:

sigaction(SIGINT,&act,&oact);
  • SIGINT:要修改哪个信号。
  • &act:新的行为设置。
  • &oact:取得旧设置;若确实不需要,可按接口约定不保存。

通过同一个测试文件依次修改 sa_handler,不是要求每种方式都另建一套完整工程。

10.4 三次测试分别修改什么#

第一次:

act.sa_handler = SIG_DFL;

运行后按 Ctrl+C,仍按 SIGINT 默认方式结束。

第二次:

act.sa_handler = SIG_IGN;

运行后按 Ctrl+C,程序不因该信号结束。又使用 Ctrl+\ 结束测试

第三次:

act.sa_handler = sig_do;

信号到来时执行自己定义的处理函数,观察函数输出,程序不因原来的默认终止动作而退出。

十一、屏蔽、忽略、捕捉:外观看似相同,内部意义不同#

这三种设置都可能表现为“按 Ctrl+C 后程序没有退出”,但不能据此说它们是一回事。

比较项屏蔽忽略捕捉
改变什么允许递达的时机信号处理方式信号处理方式
是否只是暂缓是,信号可保持未决不是用屏蔽来等待不是用屏蔽来等待
是否执行用户处理函数不因单纯屏蔽而执行不执行自定义处理执行已安装的处理函数
为什么没按默认动作退出信号尚不能递达没有采用原来的终止动作改为执行用户函数
后续要注意解除阻塞可能使未决信号递达不可把它当成暂存通知仍受信号合并与处理函数约束影响

用“是否已经处理”突出区别:屏蔽是延迟,忽略和捕捉则不是将同一条通知长期卡在原来的等待位置。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Linux 服务器高阶编程(11):信号基础——触发、未决、屏蔽与处理方式
https://joyanblog.com/posts/linux-server-advanced-programming-11-signal-basics-mask-actions/
作者
Joyan
发布于
2026-11-01
许可协议
CC BY-NC-SA 4.0

目录