Linux 进程信号深度解析(下):信号的保存、阻塞与捕捉


🔥草莓熊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 多场景测试以及对应测试结果(在上述代码上进行微调即可)




五. 信号捕捉:自定义处理信号的完整流程

信号捕捉是指进程自定义信号的处理动作(而非默认动作或忽略),核心依赖signalsigaction函数,其中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, &divide_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, &parallel_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),什么都不做,等待下一次中断。
  • 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
    • 回答 :有,最常见的是定时器(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 回到用户态
    // ...

系统调用流程简要说明

  1. 用户程序执行 int $0x80,传入系统调用号(eax)和参数(ebx, ecx, edx)。
  2. CPU 通过中断门跳转到 system_call 汇编入口。
  3. 保存现场,将段寄存器指向内核段,fs 指向用户段(便于访问用户空间)。以系统调用号为索引,从 sys_call_table 中取出对应函数地址并调用。
  4. C 函数执行完毕,返回值存于 eax
  5. 检查当前进程是否需要重新调度(状态或时间片),若需要则切换进程。
  6. 最后恢复现场,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 信号处理僵尸进程)等。如果觉得有帮助,欢迎点赞、收藏、关注三连~ 有疑问也可以在评论区交流,一起进步!

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
ALex_zry4 小时前
C++ ORM与数据库访问层设计:Repository模式实战
开发语言·数据库·c++
浅念-8 小时前
Linux 开发环境与工具链
linux·运维·服务器·数据结构·c++·经验分享
旺仔.2919 小时前
容器适配器:stack栈 、queue队列、priority queue优先级队列、bitset位图 详解
c++
Arva .10 小时前
深分页与游标
数据库·oracle
idolao10 小时前
MySQL 5.7 安装教程:详细步骤+自定义安装+命令行客户端配置(Windows版)
数据库·windows·mysql
刘景贤10 小时前
C/C++开发环境
开发语言·c++
似水এ᭄往昔10 小时前
【Linux】gdb的使用
linux·运维·服务器
优雅的造轮狮11 小时前
WSL2 Docker Desktop配置优化及迁移D盘指南
运维·docker·容器
tian_jiangnan11 小时前
grafana白皮书
linux·服务器·grafana