
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- [一. 信号的核心概念:先理清 3 个关键术语](#一. 信号的核心概念:先理清 3 个关键术语)
- [二. 信号在内核中的保存机制:进程 PCB 里的 3 个关键结构](#二. 信号在内核中的保存机制:进程 PCB 里的 3 个关键结构)
- [三. 信号集操作函数:手动控制阻塞与未决](#三. 信号集操作函数:手动控制阻塞与未决)
-
- [3.1 核心信号集操作函数(5 个常用)](#3.1 核心信号集操作函数(5 个常用))
- [3.2 sigprocmask 函数详解(修改阻塞信号集)](#3.2 sigprocmask 函数详解(修改阻塞信号集))
- [3.3 sigpending 函数(读取未决信号集)](#3.3 sigpending 函数(读取未决信号集))
- [四. 实战示例:验证信号的阻塞与未决状态(多场景测试)](#四. 实战示例:验证信号的阻塞与未决状态(多场景测试))
-
- [4.1 代码实现(带详细注释)](#4.1 代码实现(带详细注释))
- [4.2 多场景测试以及对应测试结果(在上述代码上进行微调即可)](#4.2 多场景测试以及对应测试结果(在上述代码上进行微调即可))
- [五. 信号捕捉:自定义处理信号的完整流程](#五. 信号捕捉:自定义处理信号的完整流程)
-
- [5.1 信号捕捉的底层流程(补充:建立对内核态和用户态的初步认知)](#5.1 信号捕捉的底层流程(补充:建立对内核态和用户态的初步认知))
- [5.2 话题穿插:操作系统是怎么运行的(附:硬件中断,时钟中断,软中断......)](#5.2 话题穿插:操作系统是怎么运行的(附:硬件中断,时钟中断,软中断……))
-
- [5.2.1 硬件中断](#5.2.1 硬件中断)
- [5.2.2 时钟中断](#5.2.2 时钟中断)
- [5.2.3 软中断](#5.2.3 软中断)
- [5.2.4 缺页中断?内存碎片处理?除零野指针错误?](#5.2.4 缺页中断?内存碎片处理?除零野指针错误?)
- [5.3 再次理解内核态和用户态](#5.3 再次理解内核态和用户态)
- [5.4 sigaction 函数(推荐使用,功能更强)](#5.4 sigaction 函数(推荐使用,功能更强))
- 结尾:
前言:
大家好!我是一名正在深耕 Linux 的学习者,上一篇博客整理了信号的产生与 Core Dump,这篇继续拆解信号的核心流程 ------保存、阻塞与捕捉。这部分是信号机制的灵魂,也是面试高频考点,我会用 "原理 + 代码 + 实测" 的方式,把复杂概念讲透,就像整理自己的学习笔记一样,力求通俗易懂~
一. 信号的核心概念:先理清 3 个关键术语
在学习信号保存和阻塞前,必须先搞懂这 3 个易混淆的概念,否则后续理解会很吃力:
- 未决(Pending):信号从产生到递达之间的状态(比如你收到快递通知,但还没去取);
- 递达(Delivery):实际执行信号处理动作(比如你拿到快递并处理);
- 阻塞(Block):进程主动 "屏蔽" 某个信号,被阻塞的信号即使产生,也会保持未决状态,直到解除阻塞(比如你设置 "免打扰",快递通知暂时不处理)。
⚠️ 关键区别:阻塞 ≠ 忽略
- 阻塞是 "信号进不来"(未决状态,不递达);
- 忽略是 "信号进来了,但不处理"(已递达,处理动作是忽略)。

二. 信号在内核中的保存机制:进程 PCB 里的 3 个关键结构
信号产生后,内核会在进程的 PCB(task_struct)中记录信号的状态,核心依赖 3 个结构:
- 阻塞信号集(block) :
sigset_t类型,一个位图(每个 bit 对应一个信号),bit=1 表示该信号被阻塞; - 未决信号集(pending) :同样是
sigset_t位图,bit=1 表示该信号已产生但未递达; - 信号处理动作(handler) :
struct sighand_struct类型,存储每个信号的处理方式(默认、忽略、自定义捕捉)。

cpp
// 内核结构 2.6.18
// 每个进程的 task_struct 包含与信号处理相关的字段
struct task_struct {
// ... 其他字段 ...
/* signal handlers */
struct sighand_struct *sighand; // 指向信号处理函数描述符的指针
sigset_t blocked; // 当前被阻塞的信号集
struct sigpending pending; // 挂起信号队列(等待处理的信号)
// ...
};
// 信号处理函数描述符,通常由多个进程共享(如克隆出的线程)
struct sighand_struct {
atomic_t count; // 引用计数,表示有多少个 task_struct 共享此结构
struct k_sigaction action[_NSIG]; // 信号动作数组,_NSIG 通常为 64,索引为信号编号
spinlock_t siglock; // 保护该结构及关联进程信号状态的自旋锁
};
// 新风格的信号动作定义(用户空间可见的 sigaction 结构的内核表示)
struct __new_sigaction {
__sighandler_t sa_handler; // 信号处理函数指针(SIG_IGN, SIG_DFL 或用户函数)
unsigned long sa_flags; // 信号处理标志(如 SA_RESTART, SA_SIGINFO 等)
void (*sa_restorer)(void); // 恢复函数(某些架构上用于从信号处理返回,Linux/SPARC 未使用)
__new_sigset_t sa_mask; // 在信号处理函数执行期间需要额外阻塞的信号集
};
// 内核使用的信号动作表示,包含用户空间的 sa 和可能的重设恢复函数
struct k_sigaction {
struct __new_sigaction sa; // 实际的信号动作
void __user *ka_restorer; // 可选的恢复函数指针(用户空间地址)
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int); // 标准信号处理函数类型,接受一个整型信号编号
// 挂起的信号队列
struct sigpending {
struct list_head list; // 挂起信号的链表头,链接 sigqueue 结构
sigset_t signal; // 当前挂起信号集(所有待处理信号的位掩码)
};
信号保存的核心逻辑
- 信号产生时,内核先检查进程的阻塞信号集 :
- 若该信号未被阻塞(block 对应 bit=0),则设置未决信号集对应 bit=1,等待递达;
- 若该信号已被阻塞(block 对应 bit=1),则仅设置未决信号集对应 bit=1,不递达;
- 进程从内核态切换到用户态前,会检查未决信号集:
- 若有未被阻塞的信号(pendingbit=1 且 blockbit=0),则执行递达(调用处理动作);
- 递达后,清除未决信号集对应 bit=0。

三. 信号集操作函数:手动控制阻塞与未决
内核提供了一套sigset_t相关函数,用于操作阻塞信号集和未决信号集,这是实战的核心。
3.1 核心信号集操作函数(5 个常用)
| 函数名 | 功能描述 | 用法示例 |
|---|---|---|
| sigemptyset | 初始化信号集,所有 bit 置 0 | sigemptyset(&block_set); |
| sigfillset | 初始化信号集,所有 bit 置 1(包含所有信号) | sigfillset(&all_set); |
| sigaddset | 向信号集添加某个信号(bit 置 1) | sigaddset(&block_set, SIGINT); |
| sigdelset | 从信号集删除某个信号(bit 置 0) | sigdelset(&block_set, SIGINT); |
| sigismember | 判断信号是否在信号集中(返回 1/0/-1) | sigismember(&block_set, SIGINT); |

3.2 sigprocmask 函数详解(修改阻塞信号集)
sigprocmask是控制阻塞的核心,通过how参数指定修改方式:
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
how:修改方式(3 种):SIG_BLOCK:添加阻塞(block = block | set);SIG_UNBLOCK:解除阻塞(block = block & ~set);SIG_SETMASK:直接设置阻塞集(block = set);
set:新的信号集(NULL 表示仅读取);oset:保存原来的阻塞集(NULL 表示不保存)。

3.3 sigpending 函数(读取未决信号集)
用于读取当前进程的未决信号集,验证信号是否处于未决状态:
cpp
#include <signal.h>
int sigpending(sigset_t *set); // 成功返回0,失败返回-1

四. 实战示例:验证信号的阻塞与未决状态(多场景测试)
4.1 代码实现(带详细注释)
cpp
#include <bits/types/sigset_t.h>
#include <csignal>
#include <iostream>
#include <cstdio>
#include <ostream>
#include <signal.h>
#include <unistd.h>
// 打印未决信号集(直观查看哪些信号处于未决状态)
void Printf(sigset_t& pending)
{
for(int signum = 31; signum >= 1; signum--)
{
if(sigismember(&pending, signum))
{
printf("1");
}
else {
printf("0");
}
}
printf("\n");
}
void handler(int sig)
{
std::cout << "处理了" << sig << "号信号" << std::endl;
// 测试: pending信号被递达的时候,是跑handler之前还是之后1 -> 0
std::cout << "***************" <<std::endl;
sigset_t pending;
sigemptyset(&pending); // 不会对后续有影响,只是在栈上
int m = sigpending(&pending);
(void)m;
Printf(pending);
std::cout << "***************" <<std::endl;
}
int main()
{
std::cout << "pid: " << getpid() << std::endl;
// 0. 自定义捕获2号信号
signal(SIGINT, handler);
// 1. 定义&&初始化信号集(定义变量而已,没多高级,在用户栈上)
sigset_t block, old_block;
sigemptyset(&block); // 全设为0先
sigemptyset(&old_block);
for(int signum = 1; signum <= 31; signum++)
{
sigaddset(&block, signum);
}
// 2. 向block信号集添加2号信号
// sigaddset(&block, SIGINT);
// 3. 屏蔽2号信号
int n = sigprocmask(SIG_SETMASK, &block, &old_block);
(void)n;
// 4. 不断获取pending信号集 && 打印出来观察
sigset_t pending;
int cnt = 0;
while(true)
{
sigemptyset(&pending);
// 获取pending信号集
int m = sigpending(&pending);
(void)m;
// 打印pending信号集进行观察
Printf(pending);
sleep(1);
// 尝试恢复
cnt++;
if(cnt == 20)
{
std::cout << "恢复2号信号, 解除屏蔽" << std::endl;
sigprocmask(SIG_SETMASK, &old_block,nullptr); // 恢复
}
}
}
4.2 多场景测试以及对应测试结果(在上述代码上进行微调即可)



五. 信号捕捉:自定义处理信号的完整流程
信号捕捉是指进程自定义信号的处理动作(而非默认动作或忽略),核心依赖signal或sigaction函数,其中sigaction是更推荐的标准接口。

5.1 信号捕捉的底层流程(补充:建立对内核态和用户态的初步认知)
当进程注册了自定义处理函数后,信号递达的流程如下(用户态↔内核态切换):
- 进程在用户态执行主流程,因中断 / 异常 / 系统调用进入内核态;
- 内核处理完中断后,准备返回用户态前,检查未决信号集;
- 若有可递达的信号(未阻塞且有自定义处理函数),内核不直接返回主流程,而是切换到用户态执行自定义处理函数;
- 处理函数执行完毕后,自动调用
sigreturn系统调用再次进入内核态; - 内核确认无其他未递达信号后,返回用户态,恢复主流程的执行。



5.2 话题穿插:操作系统是怎么运行的(附:硬件中断,时钟中断,软中断......)
5.2.1 硬件中断
- 中断向量表就是操作系统的⼀部分,启动就加载到内存中了
- 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
- 由外部设备触发的,中断系统运行流程,叫做硬件中断

cpp
// Linux 内核 0.11 源码片段,带详细注释
void trap_init(void)
{
int i;
// 设置陷阱门(trap gate),用于处理 CPU 异常
// 除零错误
set_trap_gate(0, ÷_error);
// 单步调试
set_trap_gate(1, &debug);
// 非屏蔽中断 NMI
set_trap_gate(2, &nmi);
// 以下 3-5 号陷阱门允许从任何特权级调用(系统门)
set_system_gate(3, &int3); // int3 断点
set_system_gate(4, &overflow); // 溢出
set_system_gate(5, &bounds); // 边界检查
// 继续设置陷阱门
set_trap_gate(6, &invalid_op); // 无效操作码
set_trap_gate(7, &device_not_available); // 设备不可用
set_trap_gate(8, &double_fault); // 双重故障
set_trap_gate(9, &coprocessor_segment_overrun); // 协处理器段越界
set_trap_gate(10, &invalid_TSS); // 无效 TSS
set_trap_gate(11, &segment_not_present); // 段不存在
set_trap_gate(12, &stack_segment); // 栈段错误
set_trap_gate(13, &general_protection); // 一般保护异常
set_trap_gate(14, &page_fault); // 页错误
set_trap_gate(15, &reserved); // 保留
set_trap_gate(16, &coprocessor_error); // 协处理器错误
// 将 int 17~47 的陷阱门暂时都设置为 reserved(保留处理)
// 后续各个硬件初始化时会重新设置自己的中断向量
for (i = 17; i < 48; i++)
set_trap_gate(i, &reserved);
// 设置协处理器中断(IRQ13,中断号 45)
set_trap_gate(45, &irq13);
// 允许主 8259A 芯片的 IRQ2 中断请求(用于级联从片)
outb_p(inb_p(0x21) & 0xfb, 0x21);
// 允许从 8259A 芯片的 IRQ13 中断请求(协处理器)
outb(inb_p(0xA1) & 0xdf, 0xA1);
// 设置并行口的陷阱门(IRQ7 或 中断号 39,具体取决于硬件)
set_trap_gate(39, ¶llel_interrupt);
}
void rs_init(void)
{
// 设置串行口1的中断门,硬件 IRQ4 对应中断号 0x24
set_intr_gate(0x24, rs1_interrupt);
// 设置串行口2的中断门,硬件 IRQ3 对应中断号 0x23
set_intr_gate(0x23, rs2_interrupt);
// 初始化 tty 表对应串行口1和串行口2的读队列端口号
init(tty_table[1].read_q.data); // 串行口1
init(tty_table[2].read_q.data); // 串行口2
// 允许主 8259A 芯片的 IRQ3 和 IRQ4 中断请求(分别对应串口2和串口1)
outb(inb_p(0x21) & 0xE7, 0x21);
}
5.2.2 时钟中断
问题:
- 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
- 回答 :操作系统不是一个独立的"进程",它本质上是硬件的"管家"。当用户程序运行时,CPU 在执行用户代码;只有发生硬件中断 (如时钟中断、键盘中断)、异常 (如缺页)或系统调用 (程序主动请求内核服务)时,CPU 才会切换到内核态,执行操作系统代码。
也就是说,操作系统是靠硬件事件 和程序主动调用来"唤醒"的。没有事件时,CPU 会执行一个特殊的"空闲进程"(idle),什么都不做,等待下一次中断。
- 回答 :操作系统不是一个独立的"进程",它本质上是硬件的"管家"。当用户程序运行时,CPU 在执行用户代码;只有发生硬件中断 (如时钟中断、键盘中断)、异常 (如缺页)或系统调用 (程序主动请求内核服务)时,CPU 才会切换到内核态,执行操作系统代码。
- 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
- 回答 :有,最常见的是定时器(Timer)。例如 PC 上的可编程间隔定时器(PIT)可以设置一个时间间隔,每隔固定时间(比如 10 毫秒)自动向 CPU 发送一个时钟中断。操作系统利用这个中断来切换进程、统计时间等。这类设备不需要人为触发,只要通电并按设定运行,就会一直自己"滴答"地产生中断。
cpp
// Linux 内核 0.11
// main.c
sched_init(); // 调度程序初始化(加载了任务0 的 tr, ldtr) (kernel/sched.c)
// 调度程序的初始化子程序。
void sched_init(void)
{
// ... 其他初始化代码 ...
// 设置时钟中断门,中断号 0x20(对应硬件 IRQ0)
set_intr_gate(0x20, &timer_interrupt);
// 修改中断控制器屏蔽码,允许时钟中断(清除 IRQ0 的屏蔽位)
outb(inb_p(0x21) & ~0x01, 0x21);
// 设置系统调用中断门,中断号 0x80(int 0x80 用于系统调用)
set_system_gate(0x80, &system_call);
// ...
}
// system_call.s 中的汇编代码片段
_timer_interrupt:
// ... 保存现场等操作 ...
; // do_timer(CPL) 执行任务切换、计时等工作,在 kernel/sched.c 第305行实现
call _do_timer // do_timer(long CPL) 负责所有定时器相关处理,最终可能调用 schedule()
// ... 恢复现场、iret 等 ...
// 调度入口函数(kernel/sched.c)
void do_timer(long cpl)
{
// ... 更新进程时间片、处理定时器等 ...
schedule(); // 调用主调度函数
}
// 进程调度函数
void schedule(void)
{
// ... 选择下一个要运行的任务(next) ...
switch_to(next); // 切换到任务号为 next 的任务,并运行之
}

OS的部分源码
cpp
void main(void) /* 这里确实是void,并没错。 */
{ /* 在startup 程序(head.s)中就是这样假设的。 */
// ... 前面的初始化代码 ...
/*
* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
* 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任
* 务0 在任何空闲时间里都会被激活(当没有其它任务在运行时),
* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运行,如果没
* 有的话我们就回到这里,一直循环执行'pause()'。
*/
for (;;)
pause(); // 任务0进入空闲循环,当没有其他任务时被激活,不断检查是否有任务可运行
} // end main

- 这样,操作系统,就可以在硬件时钟的推动下,自动调度了.
- 所以,什么是时间片?CPU为什么会有主频?为什么主频越快,CPU越快?主频可以作为OS调度执
行速度的参考之一

概念矫正


5.2.3 软中断
- 上述外部硬件中断,需要硬件设备触发。
- 有没有可能,因为软件原因,也触发上面的逻辑?有!
- 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑。

问题:
- 用户层怎么把系统调用号给操作系统? - 寄存器(比如EAX)
- 操作系统怎么把返回值给用户?- 寄存器或者用户传入的缓冲区地址
- 系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法
- 系统调用号的本质:数组下标!

cpp
// sys.h 中定义的系统调用函数指针表,用于 int 0x80 中断处理程序跳转
extern int sys_setup(); // 系统启动初始化设置
extern int sys_exit(); // 程序退出
// ... 其他系统调用声明省略 ...
// 系统调用函数指针表(跳转表),按系统调用号索引
fn_ptr sys_call_table[] = {
sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
// 调度程序初始化
void sched_init(void)
{
// ...
// 设置系统调用中断门,中断号 0x80,入口为 system_call 汇编例程
set_system_gate(0x80, &system_call);
}
// system_call.s 中的汇编代码(简化)
_system_call:
// 检查系统调用号(eax)是否超出范围
cmp eax, nr_system_calls-1
ja bad_sys_call
// 保存用户态段寄存器及参数
push ds
push es
push fs
push edx // 第3个参数
push ecx // 第2个参数
push ebx // 第1个参数
// 设置 ds, es 指向内核数据段
mov edx, 10h
mov ds, dx
mov es, dx
// 设置 fs 指向用户数据段(局部描述符表)
mov edx, 17h
mov fs, dx
// 通过系统调用号索引跳转表,调用对应的 C 函数
// 等价于 call sys_call_table[eax*4]
call [_sys_call_table + eax*4]
// 返回值已在 eax,将其压栈以备后续处理
push eax
// 取当前任务(进程)结构指针
mov eax, _current
// 检查当前进程状态:如果 state != 0 或 counter == 0,则重新调度
cmp dword ptr [state+eax], 0
jne reschedule
cmp dword ptr [counter+eax], 0
je reschedule
ret_from_sys_call:
// 从系统调用返回后,会检查信号并恢复现场,最终 iret 回到用户态
// ...
系统调用流程简要说明:
- 用户程序执行
int $0x80,传入系统调用号(eax)和参数(ebx, ecx, edx)。 - CPU 通过中断门跳转到
system_call汇编入口。 - 保存现场,将段寄存器指向内核段,
fs指向用户段(便于访问用户空间)。以系统调用号为索引,从sys_call_table中取出对应函数地址并调用。 - C 函数执行完毕,返回值存于
eax。 - 检查当前进程是否需要重新调度(状态或时间片),若需要则切换进程。
- 最后恢复现场,
iret返回用户态继续执行。
- 可是为什么我们用的系统调用,从来没有见过什么 int 0x80 或者 syscall 呢?都是直接调用上层的函数的啊?
- 那是因为Linux的gnu C标准库,给我们把几乎所有的系统调用全部封装了。



cpp
// 源代码路径:linux-2.6.18\linux-2.6.18\include\asm-x86_64\unistd.h
// 该头文件定义了 x86_64 架构的系统调用号及对应的内核函数入口
/* at least 8 syscall per cacheline */ // 注释:每个缓存行至少包含 8 个系统调用(优化缓存局部性)
// 系统调用号从 0 开始递增,每个宏定义分配一个唯一的编号
#define __NR_read 0 // 系统调用号 0:读取文件
__SYSCALL(__NR_read, sys_read) // __SYSCALL 宏将系统调用号与内核实现函数关联(用于构建系统调用表)
#define __NR_write 1 // 系统调用号 1:写入文件
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2 // 系统调用号 2:打开文件
__SYSCALL(__NR_open, sys_open)
#define __NR_close 3 // 系统调用号 3:关闭文件
__SYSCALL(__NR_close, sys_close)
#define __NR_stat 4 // 系统调用号 4:获取文件状态(通过路径)
__SYSCALL(__NR_stat, sys_newstat) // 使用新的 stat 结构(sys_newstat)
#define __NR_fstat 5 // 系统调用号 5:获取文件状态(通过文件描述符)
__SYSCALL(__NR_fstat, sys_newfstat)
#define __NR_lstat 6 // 系统调用号 6:获取链接文件状态(不跟随符号链接)
__SYSCALL(__NR_lstat, sys_newlstat)
#define __NR_poll 7 // 系统调用号 7:poll 多路复用 I/O
__SYSCALL(__NR_poll, sys_poll)
#define __NR_lseek 8 // 系统调用号 8:重定位文件读写偏移
__SYSCALL(__NR_lseek, sys_lseek)
#define __NR_mmap 9 // 系统调用号 9:内存映射
__SYSCALL(__NR_mmap, sys_mmap)
#define __NR_mprotect 10 // 系统调用号 10:修改内存保护属性
__SYSCALL(__NR_mprotect, sys_mprotect)
#define __NR_munmap 11 // 系统调用号 11:解除内存映射
__SYSCALL(__NR_munmap, sys_munmap)
#define __NR_brk 12 // 系统调用号 12:调整数据段(堆)大小
__SYSCALL(__NR_brk, sys_brk)
#define __NR_rt_sigaction 13 // 系统调用号 13:实时信号处理函数设置
__SYSCALL(__NR_rt_sigaction, sys_rt_sigaction)
#define __NR_rt_sigprocmask 14 // 系统调用号 14:实时信号屏蔽字操作
__SYSCALL(__NR_rt_sigprocmask, sys_rt_sigprocmask)
#define __NR_rt_sigreturn 15 // 系统调用号 15:从实时信号处理函数返回
__SYSCALL(__NR_rt_sigreturn, stub_rt_sigreturn) // 使用汇编桩(stub)实现
// ... 后续还有更多系统调用定义 ...
5.2.4 缺页中断?内存碎片处理?除零野指针错误?
- 缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

5.3 再次理解内核态和用户态




不会不安全
5.4 sigaction 函数(推荐使用,功能更强)

signal函数简单但兼容性差,sigaction支持更精细的控制(如设置额外阻塞信号、保留信号上下文),原型如下:
cpp
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
struct sigaction 结构体解析:
cpp
struct sigaction {
void (*sa_handler)(int); // 信号处理函数(与signal兼容)
void (*sa_sigaction)(int, siginfo_t *, void *); // 实时信号处理函数
sigset_t sa_mask; // 处理信号时,额外阻塞的信号集
int sa_flags; // 选项(0表示默认)
void (*sa_restorer)(void); // 已废弃,不用关注
};
关键特性:
sa_mask:处理信号时,内核会自动将当前信号加入阻塞集,同时阻塞sa_mask中的信号,避免嵌套处理;sa_flags:常用SA_RESTART(被信号中断的系统调用自动重启)。


实战 :用 sigaction 捕捉信号
cpp
#include <bits/types/sigset_t.h>
#include <csignal>
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <signal.h>
void Printf(sigset_t& pending)
{
for(int signum = 31; signum >= 1; signum--)
{
if(sigismember(&pending, signum))
{
printf("1");
}
else {
printf("0");
}
}
printf("\n");
}
void handler(int sig)
{
std::cout << "get a signal " << sig << std::endl;
// 在处理2号信号期间,2号信号被自动blcok了!
while(true)
{
sigset_t pending;
sigpending(&pending);
Printf(pending);
sleep(1);
}
// exit(0);
}
int main()
{
struct sigaction act, old_act;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&(act.sa_mask)); // ??? 进程收到了大量的重复信号?
sigaddset(&(act.sa_mask), 1);
sigaddset(&(act.sa_mask), 3);
sigaddset(&(act.sa_mask), 4);
sigaddset(&(act.sa_mask), 5);
sigaction(2, &act, &old_act); // signal
while(true)
{
pause(); //等待
}
}

- 第一次发送2号信号 -- 触发了,打印get a signal2 程序开始运行 -- pending信号集都是0,此时 2号信号还没被阻塞

- 第二次发生2号信号时pending信号集对应比特位为1了,证明被阻塞了所以一直处于未决状态。再测试其他几个额外阻塞的也是一样。

结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:这基本覆盖了 Linux 信号的核心知识点,都是我结合资料整理的学习心得,后续会继续深入实战场景(如 SIGCHLD 信号处理僵尸进程)等。如果觉得有帮助,欢迎点赞、收藏、关注三连~ 有疑问也可以在评论区交流,一起进步!
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
