Linux 信号的保存机制

目录

一、信号其他相关常见概念详解

1、信号递达(Delivery)

2、信号未决(Pending)

3、信号阻塞(Block)

4、阻塞信号的处理流程

5、阻塞与忽略的区别

二、信号在内核中的表示

1、信号在内核中表示的意图

2、多次产生信号的处理方式

[3、信号在内核中的核心数据结构(以内核结构 2.6.18 为例)](#3、信号在内核中的核心数据结构(以内核结构 2.6.18 为例))

[task_struct 结构体](#task_struct 结构体)

[1. pending位图(unsigned int pending)](#1. pending位图(unsigned int pending))

信号产生的五种方式(也就是修改pending位图)

[2. block位图(unsigned int block)](#2. block位图(unsigned int block))

[3. handler函数指针数组(sighandler_t handler[64])](#3. handler函数指针数组(sighandler_t handler[64]))

[sighand_struct 结构体](#sighand_struct 结构体)

[__new_sigaction 结构体](#__new_sigaction 结构体)

[k_sigaction 结构体](#k_sigaction 结构体)

信号处理函数类型定义

[sigpending 结构体](#sigpending 结构体)

三、sigset_t:信号集的精妙设计与核心作用

1、信号状态表示的精简之道

2、sigset_t:统一存储的智慧之选

[3、信号屏蔽字(Signal Mask)与 sigset_t 的紧密关联](#3、信号屏蔽字(Signal Mask)与 sigset_t 的紧密关联)

4、后续展望:信号集操作的深度剖析

5、信号处理函数类型

[6、signal 函数](#6、signal 函数)

7、特殊处理方式

8、完整工作流程

关键点

9、信号处理的用户态接口

[(1) 注册信号处理函数](#(1) 注册信号处理函数)

四、信号集操作函数

1、信号集操作函数详解

[1. 信号集初始化函数](#1. 信号集初始化函数)

[2. 信号集添加与删除函数](#2. 信号集添加与删除函数)

[3. 信号成员判断函数](#3. 信号成员判断函数)

[2、sigprocmask 函数](#2、sigprocmask 函数)

[3、sigpending 函数](#3、sigpending 函数)

4、信号阻塞与未决实验

[1. 详细过程](#1. 详细过程)

[2. 执行顺序分析](#2. 执行顺序分析)

[3. 关键的执行顺序](#3. 关键的执行顺序)

[4. 验证结论](#4. 验证结论)

[5. 当前代码状态分析](#5. 当前代码状态分析)

行为流程

[6. 情况一:执行 signal(2, SIG_IGN)](#6. 情况一:执行 signal(2, SIG_IGN))

三个表的变化流程

关键区别

[7. 情况二:执行 signal(2, SIG_DFL)](#7. 情况二:执行 signal(2, SIG_DFL))

三个表的变化流程

关键区别

[8. 三种情况的对比总结](#8. 三种情况的对比总结)

[9. 关键结论](#9. 关键结论)

五、相关细节与回顾

1、特殊信号与注意事项

2、信号递达与重复处理机制

[(1) 常规信号 vs 实时信号](#(1) 常规信号 vs 实时信号)

常规信号(如SIGINT、SIGSEGV)

实时信号(SIGRTMIN~SIGRTMAX)

[(2) 为什么常规信号不队列化?](#(2) 为什么常规信号不队列化?)

[3、核心转储(Core Dump)机制](#3、核心转储(Core Dump)机制)

[(1) 核心转储的作用](#(1) 核心转储的作用)

[(2) 核心转储文件的生成条件](#(2) 核心转储文件的生成条件)

[(3) 云服务器上的限制](#(3) 云服务器上的限制)

[4、案例分析:Floating point exception导致核心转储](#4、案例分析:Floating point exception导致核心转储)

[(1) 案例复现](#(1) 案例复现)

[(2) 调试步骤](#(2) 调试步骤)

[(3) 关键命令总结](#(3) 关键命令总结)

5、常见问题解答

[(1) 为什么没见过核心转储文件?](#(1) 为什么没见过核心转储文件?)

[(2) 如何自定义核心文件路径?](#(2) 如何自定义核心文件路径?)

[(3) 云服务器如何安全启用核心转储?](#(3) 云服务器如何安全启用核心转储?)

6、总结


一、信号其他相关常见概念详解

在计算机系统的信号处理机制中,存在一些与信号紧密相关且容易混淆的关键概念,以下将对这些概念进行详细阐述。

1、信号递达(Delivery)

  • 当系统针对某个进程实际执行信号所对应的处理动作时,这一过程被定义为信号递达。

  • 简单来说,就是信号触发了相应的操作,比如调用特定的信号处理函数来执行预设的任务,或者执行系统默认的针对该信号的处理行为。

  • 例如,当进程接收到 SIGINT 信号(通常由用户按下 Ctrl + C 触发)时,系统若执行终止进程的操作,这便是 SIGINT 信号的递达过程。

2、信号未决(Pending)

  • 信号从产生到最终递达给进程进行处理,中间会经历一个特定的状态,这个状态就被称为信号未决。

  • 也就是说,当信号被触发生成后,由于某些原因(如进程正在处理其他事务、信号被阻塞等)尚未立即被进程处理时,它就处于未决状态。

  • 以多线程程序为例,某个线程产生了信号,但主线程由于正在执行关键代码段,暂时无法处理该信号,此时这个信号就处于未决状态,等待合适的时机进行递达。

3、信号阻塞(Block)

  • 进程具备一种能力,即可以选择阻塞某个特定的信号。

  • 当进程对某个信号进行阻塞操作后,该信号将被暂时搁置,不会立即触发相应的处理动作。

  • 例如,在一个需要高度专注执行特定任务的进程中,为了避免一些非关键信号(如定期的系统状态报告信号)的干扰,进程可以选择阻塞这些信号。

4、阻塞信号的处理流程

  • 当一个被进程阻塞的信号产生时,它不会马上递达给进程,而是会保持在未决状态。

  • 只有当进程解除对该信号的阻塞设置后,系统才会执行信号的递达动作,将信号的处理任务交给进程。

  • 比如,进程在执行一段对实时性要求极高的代码时,阻塞了 SIGALRM 信号(通常用于定时器到期通知),在这段代码执行完毕后,进程解除了对 SIGALRM 信号的阻塞,此时若之前有该信号产生且处于未决状态,那么系统就会将该信号递达给进程,触发相应的处理逻辑。

5、阻塞与忽略的区别

  • 需要注意的是,信号的阻塞和忽略是两个完全不同的概念。

  • 只要信号被进程阻塞,那么无论何种情况下,该信号都不会递达给进程进行处理,它会被一直保留在未决状态,直到阻塞被解除。

  • 而忽略信号则是在信号已经递达给进程之后,进程可以选择的一种处理动作。

  • 也就是说,忽略是对已经到达进程的信号的一种处置方式,进程可以选择不执行该信号对应的默认处理动作 或 自定义处理动作,相当于对信号"视而不见"。

  • 例如,进程可以设置忽略 SIGCHLD 信号(子进程状态改变时产生的信号),当有子进程状态发生变化产生该信号并递达给父进程时,父进程选择忽略它,不进行任何相关的处理操作。

综上所述,准确理解信号递达、信号未决、信号阻塞以及阻塞与忽略的区别,对于深入掌握计算机系统的信号处理机制,编写稳定、可靠的程序至关重要。


二、信号在内核中的表示

1、信号在内核中表示的意图

  • 在计算机系统的内核层面,信号有着一套精细且严谨的表示机制,旨在准确管理信号的生成、阻塞、未决状态以及最终的处理动作。这一机制对于确保进程能够正确响应各种外部和内部事件起着关键作用。

  • 每个信号在内核中都有特定的关联元素来进行状态和行为的描述。具体而言,存在两个标志位,分别用于表示信号的阻塞(block)状态和未决(pending)状态,同时还配备了一个函数指针,用以指向该信号对应的处理动作。

  • 当信号产生时,内核会迅速在进程控制块(Process Control Block,PCB)中设置该信号的未决标志。这个未决标志就如同一个"待处理"的标记,一直持续到信号成功递达给进程,此时内核才会清除该标志,表明信号已经完成了从产生到处理的完整流程。

下面通过几个具体的信号示例来进一步说明:

  • SIGHUP 信号 :在该示例情境中,SIGHUP 信号既未被阻塞,也未曾产生过 。**当这个信号递达给进程时,内核会按照预设的规则,执行该信号的默认处理动作。**例如,在某些情况下,SIGHUP 信号的默认处理动作可能是终止进程,或者重新加载进程的配置文件等,具体取决于系统的设定和进程的性质。

  • SIGINT 信号SIGINT 信号曾经产生过,但当前正处于被阻塞的状态 。这就意味着,尽管信号已经生成,但由于阻塞的设置,它暂时无法递达给进程。**虽然为 SIGINT 信号设置的处理动作是忽略,但在没有解除阻塞之前,不能真正地忽略这个信号。这是因为进程在阻塞期间仍有机会改变对该信号的处理动作,之后再解除阻塞。**例如,进程可能在阻塞 SIGINT 信号期间,重新注册了一个自定义的处理函数,待解除阻塞后,就会按照新的处理方式来响应该信号。

  • SIGQUIT 信号SIGQUIT 信号尚未产生过。不过,一旦该信号产生,它将被设置为阻塞状态。 并且,已经为 SIGQUIT 信号指定了用户自定义函数 sighandler 作为其处理动作。这意味着当 SIGQUIT 信号最终能够递达时(对应的block位被清除,也就是解除阻塞),内核会调用 sighandler 函数来执行相应的操作,这个自定义函数可以根据程序的具体需求来实现各种功能,如记录日志、执行特定的清理工作等。

bash 复制代码
kill -l

2、多次产生信号的处理方式

在实际的系统运行过程中,可能会出现这样一种情况:在进程解除对某信号的阻塞之前,这种信号产生了多次。那么,系统将如何处理这些多次产生的信号呢?POSIX.1 标准允许系统采用两种不同的处理方式:递送该信号一次或者多次。

Linux 系统针对不同类型的信号采用了不同的实现策略:

  • 常规信号 :**对于常规信号,在递达之前即使产生了多次,内核也只会记录一次。也就是说,无论该信号产生了多少次,在解除阻塞后,进程只会接收到一次该信号的递达通知。**这种设计主要是考虑到常规信号通常用于表示一些简单的、不需要精确计数的事件,一次递达就足以让进程知晓该事件的发生。

  • 实时信号 :**与常规信号不同,实时信号在递达之前产生多次时,可以依次放在一个队列里。这样,当解除阻塞后,进程会按照信号产生的顺序依次接收到这些信号的递达通知,能够更精确地处理多次产生的信号。**不过,本章主要聚焦于常规信号的讨论,暂不涉及实时信号的详细内容。

简要总结:(可以理解为里面包含三张表)

  • block位图:比特位的位置对应特定信号,其值(0/1)表示该信号是否被阻塞

  • pending位图:比特位的位置对应特定信号,其值表示是否收到该信号

  • handler表:作为函数指针数组,其下标对应特定信号,元素值代表信号递达时的处理动作(默认/忽略/自定义)

这三张表通过位置一一对应,形成完整的信号处理机制。

3、信号在内核中的核心数据结构(以内核结构 2.6.18 为例)

在 Linux 内核 2.6.18 版本中,与信号处理相关的数据结构起着至关重要的作用,它们共同协作来完成信号的管理和处理。以下是主要的数据结构及其关系的详细说明:

Linux内核通过task_struct(进程描述符)管理进程的信号状态,其中涉及三张关键表:

task_struct 结构体

task_struct 结构体是进程控制块的核心,它包含了进程的各种信息,其中与信号处理相关的字段如下:(可以理解为里面包含三张表)

cpp 复制代码
struct task_struct {
    ...
    /* signal handlers */
    struct sighand_struct *sighand;  // 指向信号处理函数结构体的指针
    sigset_t blocked;                // 表示被阻塞的信号集合
    struct sigpending pending;       // 表示未决信号的结构体
    ...
};
  • sighand:这是一个指向 sighand_struct 结构体的指针,sighand_struct 结构体存储了进程对各个信号的处理函数信息。

  • blockedsigset_t 类型,用于表示当前进程阻塞的信号集合。通过这个集合,内核可以快速判断某个信号是否被进程阻塞。

  • pendingstruct sigpending 类型,用于记录当前进程的未决信号信息。

1. pending位图unsigned int pending
  • 作用:记录进程已收到但尚未处理的信号。

  • 位图结构 :每个比特位对应一个信号(如SIGHUP=1对应第1位)。比特位为1表示信号已挂起(pending),需处理。

示例 :(0b二进制字面量(binary literal) 的前缀。)

cpp 复制代码
pending = 0b00000100; // 表示SIGQUIT(3)已挂起
信号产生的五种方式(也就是修改pending位图)
产生方式 具体示例 说明
1. 键盘输入 Ctrl+C 产生 SIGINT (2号信号)
1. 键盘输入 Ctrl+\ 产生 SIGQUIT (3号信号)
1. 键盘输入 Ctrl+Z 产生 SIGTSTP (20号信号)
2. 系统调用 kill(pid, sig) 向指定进程发送信号
2. 系统调用 raise(sig) 向当前进程发送信号
2. 系统调用 abort() 产生 SIGABRT (6号信号)
2. 系统调用 alarm(seconds) 定时产生 SIGALRM (14号信号)
3. 系统命令 kill -SIG pid 通过kill命令发送信号
3. 系统命令 killall procname 向同名进程发信号
3. 系统命令 pkill pattern 按模式匹配发信号
4. 硬件异常 除零错误 产生 SIGFPE (8号信号)
4. 硬件异常 段错误/非法访问 产生 SIGSEGV (11号信号)
4. 硬件异常 非法指令 产生 SIGILL (4号信号)
5. 软件条件 管道破裂 产生 SIGPIPE (13号信号)
5. 软件条件 子进程终止 产生 SIGCHLD (17号信号)
5. 软件条件 定时器到期 产生 SIGALRM (14号信号)
2. block位图unsigned int block
  • 作用 :标记被阻塞(屏蔽)的信号。若信号被阻塞,即使收到也会暂存在pending中,直到解除阻塞后才处理。

  • 逻辑关系 :实际待处理信号 = pending & (~block)

示例 :(0b二进制字面量(binary literal) 的前缀。)

cpp 复制代码
block = 0b00000010;   // 阻塞SIGINT(2)
pending = 0b00000110; // 收到SIGINT(2)和SIGQUIT(3)
// 实际待处理信号:0b00000100 (仅SIGQUIT)
3. handler函数指针数组sighandler_t handler[64]
  • 作用 :存储每个信号的处理函数**(用户自定义、内核默认或者忽略)**。

  • 关键值

    • SIG_DFL(默认行为,如终止进程)

    • SIG_IGN(忽略信号)

    • 用户自定义函数(如void sighandler(int signo))。

sighand_struct 结构体

sighand_struct 结构体主要存储了进程对不同信号的处理函数,其定义如下:

cpp 复制代码
struct sighand_struct {
    atomic_t count;                  // 引用计数,用于管理该结构体的使用情况
    struct k_sigaction action[_NSIG]; // #define _NSIG 64,存储每个信号的处理动作
    spinlock_t siglock;               // 自旋锁,用于保证对信号处理结构的并发访问安全
};
  • count:原子类型的引用计数,用于记录有多少个进程共享这个 sighand_struct 结构体。当引用计数为 0 时,表示没有进程使用该结构体,可以进行释放操作。

  • action:这是一个数组,数组元素为 struct k_sigaction 类型,大小为 _NSIG(通常定义为 64),用于存储每个信号对应的处理动作。

  • siglock:自旋锁,用于在多线程或多进程环境下,保证对 sighand_struct 结构体的并发访问安全,防止出现数据竞争问题。

__new_sigaction 结构体

__new_sigaction 结构体定义了信号处理动作的具体信息,其内容如下:

cpp 复制代码
struct __new_sigaction {
    __sighandler_t sa_handler;       // 信号处理函数指针
    unsigned long sa_flags;          // 信号处理标志,用于指定信号处理的行为方式
    void (*sa_restorer)(void);       /* Not used by Linux/SPARC,该函数指针在 Linux/SPARC 架构下未使用 */
    __new_sigset_t sa_mask;          // 在信号处理函数执行期间需要阻塞的信号集合
};
  • sa_handler:函数指针,指向处理该信号的具体函数。当信号递达时,内核会调用这个函数来执行相应的操作。

  • sa_flags:无符号长整型,用于设置信号处理的各种标志,例如是否在信号处理函数执行期间重置信号处理方式等。

  • sa_restorer:函数指针,在 Linux/SPARC 架构下未使用,在其他架构下可能用于在信号处理函数执行完毕后恢复进程的上下文。

  • sa_mask__new_sigset_t 类型,表示在信号处理函数执行期间需要阻塞的信号集合。这样可以避免在处理一个信号时,被其他信号干扰。

k_sigaction 结构体

k_sigaction 结构体是对 __new_sigaction 结构体的封装,并增加了一些与内核相关的信息,其定义如下:

cpp 复制代码
struct k_sigaction {
    struct __new_sigaction sa;       // 包含信号处理的具体信息
    void __user *ka_restorer;        // 指向用户空间中用于恢复上下文的函数(如果需要)
};
  • sastruct __new_sigaction 类型,存储了信号处理的主要信息。

  • ka_restorer:指向用户空间中用于恢复上下文的函数指针(如果需要)。这个指针主要用于在一些特定的架构下,在信号处理函数执行完毕后恢复进程的用户态上下文。

信号处理函数类型定义

cpp 复制代码
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
  • 这个类型定义指定了信号处理函数的类型,即一个接受一个整数参数(表示信号编号)且无返回值的函数指针。

  • 所有自定义的信号处理函数都需要符合这个类型定义。

sigpending 结构体

sigpending 结构体用于记录进程的未决信号信息,其定义如下:

cpp 复制代码
struct sigpending {
    struct list_head list;           // 链表头,用于组织未决信号队列
    sigset_t signal;                 // 表示未决信号的集合
};
  • list:链表头结构体,用于将多个未决信号组织成一个链表队列,方便内核进行管理和处理。

  • signalsigset_t 类型,表示当前处于未决状态的信号集合。通过这个集合,内核可以快速知晓哪些信号已经产生但尚未递达给进程。

通过以上这些精细的数据结构,Linux 内核能够高效地管理信号的阻塞、未决状态以及处理动作,确保进程能够准确、及时地响应各种信号事件,从而保证系统的稳定运行和进程的正常执行。


三、sigset_t:信号集的精妙设计与核心作用

cpp 复制代码
/* 
 * 计算信号集数据结构需要多少个 unsigned long int 单元。
 * 总信号数量(如1024)除以一个unsigned long int的位数,得到所需的数组长度。
 */
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))

/* 
 * 信号集(Signal Set)的内部结构体定义,用于在操作系统内核或C库内部使用。
 * 一个信号集代表一个信号的集合,通常用于表示阻塞信号、等待信号等。
 */
typedef struct
{
    /* 
     * 用一个无符号长整型数组来存储信号集的位掩码(bitmask)。
     * 数组中的每一个位(bit)代表一个信号编号。
     * 例如:
     *   - __val[0] 的第0位 代表 1号信号 (SIGHUP)
     *   - __val[0] 的第1位 代表 2号信号 (SIGINT)
     *   - ...以此类推
     * 如果某个信号的对应位被设置为1,则表示该信号在集合中;0表示不在集合中。
     */
    unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t; // 前置双下划线通常表示这是一个内部、系统级的数据类型

/* 
 * 提供给应用程序员使用的信号集类型。
 * 它和内部的 __sigset_t 是相同的类型,但移除了双下划线,名称更清晰。
 * 程序员在调用 sigprocmask, sigemptyset, sigaddset 等函数时会使用此类型。
 */
typedef __sigset_t sigset_t;

在深入探究信号处理机制的过程中,sigset_t 这一关键数据类型扮演着举足轻重的角色。它犹如一个精巧的容器,以高效且简洁的方式对信号的未决状态和阻塞状态进行管理与表示。

1、信号状态表示的精简之道

  • 从相关数据结构及原理来看,每个信号在系统中都有着明确的两种关键状态:未决状态和阻塞状态。

  • 对于未决标志而言,它仅占用一个 bit 的存储空间,采用非 0 即 1 的简单二进制表示方式。这种设计意味着它并不记录该信号产生的具体次数,仅仅关注信号是否已经产生但尚未递达给进程。

  • 例如,当某个信号因外部事件触发而产生时,其对应的未决标志位会被设置为 1,表示该信号处于未决状态;而当信号成功递达给进程并完成处理后,这个标志位会被重置为 0。

  • 同样地,阻塞标志也采用一个 bit 来表示,其逻辑与未决标志类似,非 0 即 1 的取值用于表明该信号是否被进程阻塞。若标志位为 1,则表示该信号当前处于被阻塞状态,即使信号产生也无法立即递达给进程;若为 0,则表示信号可以正常递达。

2、sigset_t:统一存储的智慧之选

  • 鉴于未决标志和阻塞标志都采用这种简洁的单个 bit 表示方式,sigset_t 这种数据类型应运而生,它能够完美地存储这些标志信息。

  • sigset_t 可以形象地看作是一个由多个 bit 组成的集合,每个 bit 对应一个特定的信号,精准地表示该信号的"有效"或"无效"状态。

  • 在不同的信号集合场景中,"有效"和"无效"有着各自独特的含义。在阻塞信号集中,"有效"表示该信号被进程阻塞,即进程暂时不希望接收该信号;而"无效"则表示该信号未被阻塞,可以正常递达给进程。

  • 例如,当进程正在执行一段关键代码,不希望被某些特定信号打断时,就可以将这些信号设置为"有效"状态(即阻塞),确保代码的顺利执行。

  • 在未决信号集中,"有效"意味着该信号已经产生,但由于某些原因(如被阻塞)尚未递达给进程;而"无效"则表示该信号未产生或者已经成功递达并处理完毕。比如,当一个外部设备触发了一个中断信号,但由于进程之前设置了阻塞,该信号就会处于未决的"有效"状态,直到进程解除阻塞,信号才能递达。

3、信号屏蔽字(Signal Mask)与 sigset_t 的紧密关联

  • sigset_t 还与一个重要的概念------信号屏蔽字(Signal Mask)密切相关。信号屏蔽字本质上也是一个 sigset_t 类型的变量,它用于记录当前进程所阻塞的信号集合。可以将其类比于文件权限管理中的 umask 概念。

  • 在文件权限管理中,umask 用于设置默认的文件权限屏蔽位,从而控制新创建文件的最终权限;而在信号处理中,信号屏蔽字则用于控制哪些信号被阻塞,哪些信号可以递达给进程。

  • 例如,在一个多线程的服务器程序中,主线程可能负责监听网络连接,而子线程则负责处理具体的业务逻辑。为了确保子线程在执行关键业务逻辑时不被某些不必要的信号(如 SIGINT)打断,可以在子线程启动时设置相应的信号屏蔽字,将这些信号设置为阻塞状态。这样,即使外部发送了这些信号,子线程也不会受到影响,能够专注于业务处理,直到完成关键任务后,再解除对这些信号的阻塞。

4、后续展望:信号集操作的深度剖析

  • sigset_t 作为信号集的基础数据类型,为信号的各种操作提供了坚实的基础。

  • 在后续的内容中,我们将详细介绍针对信号集的各种操作,如信号集的初始化、添加信号、删除信号、判断信号是否在信号集中等。

  • 这些操作将进一步揭示如何灵活运用 sigset_t 来实现对信号的精细控制,从而满足不同应用程序在信号处理方面的多样化需求。

  • 总之,sigset_t 以其简洁高效的设计和强大的功能,在信号处理机制中占据着核心地位。它不仅简化了信号状态的管理,还为进程对信号的灵活控制提供了有力支持,使得系统能够更加稳定、可靠地运行。

结合之前的相关知识讲解和联系起来:

cpp 复制代码
#include <signal.h>

// 信号处理函数的类型定义
typedef void (*sighandler_t)(int);

// signal 函数声明
sighandler_t signal(int signum, sighandler_t handler);

// 特殊的 handler 参数值
#define SIG_DFL ((sighandler_t) 0)   /* Default action. */
#define SIG_IGN ((sighandler_t) 1)   /* Ignore signal. */

// 信号处理函数示例
void handlersig(int signo) {
    // 处理 SIGALRM 信号
}

int main() {
    // 设置 SIGALRM 信号的处理函数
    signal(SIGALRM, handlersig);
    return 0;
}

5、信号处理函数类型

cpp 复制代码
typedef void (*sighandler_t)(int);
  • 定义了一个函数指针类型 sighandler_t

  • 这种函数接受一个 int 参数(信号编号),返回 void

6、signal 函数

cpp 复制代码
sighandler_t signal(int signum, sighandler_t handler);
  • 作用:设置信号的处理方式

  • 参数

    • signum:信号编号(如 SIGALRM, SIGINT

    • handler:处理函数指针

  • 返回值:之前信号处理函数的指针

7、特殊处理方式

cpp 复制代码
#define SIG_DFL ((sighandler_t) 0)  // 默认处理
#define SIG_IGN ((sighandler_t) 1)  // 忽略信号

使用示例:

cpp 复制代码
signal(SIGINT, SIG_IGN);   // 忽略 Ctrl+C
signal(SIGTERM, SIG_DFL);  // 恢复默认处理
signal(SIGALRM, handlersig); // 使用自定义处理函数

8、完整工作流程

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handlersig(int signo) {
    printf("收到信号: %d\n", signo);
}

int main() {
    // 设置警报信号处理
    signal(SIGALRM, handlersig);
    
    printf("5秒后将收到 SIGALRM 信号...\n");
    alarm(5);  // 设置5秒后发送 SIGALRM
    
    pause();   // 等待信号
    return 0;
}

关键点

  • signal() 用于设置信号处理方式

  • 处理函数必须符合 void func(int) 的格式

  • 可以使用 SIG_DFL(默认)或 SIG_IGN(忽略)作为特殊处理方式

9、信号处理的用户态接口

(1) 注册信号处理函数

signal()(传统接口,已逐渐被sigaction()取代):

cpp 复制代码
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

行为

  • handler=SIG_IGN:忽略信号。

  • handler=SIG_DFL:恢复默认行为。

  • 自定义函数:捕获并处理信号。

sigaction()(推荐接口,支持更多控制):

cpp 复制代码
struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;      // 处理时额外阻塞的信号
    int sa_flags;          // 标志位(如SA_RESTART)
};

四、信号集操作函数

  • 在 Unix/Linux 系统的信号处理机制中,sigset_t 类型发挥着至关重要的作用。

  • 它以一种高效且系统无关的方式,通过为每种信号分配一个 bit 来表示信号的"有效"或"无效"状态。

  • 这里的"有效"和"无效"在不同信号集合中有不同含义,在阻塞信号集中,"有效"表示信号被阻塞,"无效"表示信号未被阻塞;在未决信号集中,"有效"表示信号处于未决状态,"无效"表示信号未产生或已处理完毕。

  • 值得注意的是,sigset_t 类型内部如何具体存储这些 bit 是依赖于系统实现的,对于使用者而言无需关心其内部细节。

  • 直接使用诸如 printf 等函数打印 sigset_t 变量是没有实际意义的,因为其内部存储结构可能不符合常规的二进制表示形式。

1、信号集操作函数详解

以下是对一系列用于操作 sigset_t 变量的函数的详细介绍:

1. 信号集初始化函数

cpp 复制代码
#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
  • sigemptyset 函数 :该函数用于初始化 set 所指向的信号集,将其中所有信号对应的 bit 清零。这意味着初始化后的信号集不包含任何有效信号,处于一个"空"的状态。

  • sigfillset 函数 :此函数同样用于初始化 set 所指向的信号集,但它会将其中所有信号对应的 bit 置位。这样一来,初始化后的信号集的有效信号就包含了系统支持的所有信号。

在使用 sigset_t 类型的变量之前,必须调用 sigemptysetsigfillset 对其进行初始化,以确保信号集处于一个确定的状态。因为未初始化的 sigset_t 变量可能包含不确定的值,这可能导致后续的信号操作出现不可预测的结果。

2. 信号集添加与删除函数

cpp 复制代码
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
  • sigaddset 函数 :在完成信号集的初始化之后,可以使用 sigaddset 函数向 set 所指向的信号集中添加指定的有效信号。signo 参数表示要添加的信号编号。如果添加成功,函数返回 0;如果出错,例如 signo 不是一个有效的信号编号,函数返回 -1。

  • sigdelset 函数 :与 sigaddset 函数相反,sigdelset 函数用于从 set 所指向的信号集中删除指定的信号。同样,signo 参数表示要删除的信号编号。删除成功返回 0,出错返回 -1。

注意:上面的四个函数都是成功返回0,出错返回-1!!!

3. 信号成员判断函数

cpp 复制代码
int sigismember(const sigset_t *set, int signo);

sigismember 函数是一个布尔函数,用于判断 set 所指向的信号集的有效信号中是否包含指定的信号 signo。如果包含,函数返回 1;如果不包含,返回 0;如果出错,例如 signo 不是一个有效的信号编号,函数返回 -1。

下面将演示如何使用这些信号集函数:

注意: 在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号处于确定的状态。

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t set;  // 定义一个信号集
    
    // 1. 初始化空信号集
    sigemptyset(&set);
    printf("信号集初始化为空\n");
    
    // 2. 检查SIGINT是否在集合中
    if (sigismember(&set, SIGINT) == 0) {
        printf("SIGINT 不在信号集中\n");
    }
    
    // 3. 添加SIGINT到信号集
    sigaddset(&set, SIGINT);
    printf("已添加 SIGINT 到信号集\n");
    
    // 4. 再次检查SIGINT
    if (sigismember(&set, SIGINT) == 1) {
        printf("现在 SIGINT 在信号集中\n");
    }
    
    // 5. 添加另一个信号
    sigaddset(&set, SIGTERM);
    printf("已添加 SIGTERM 到信号集\n");
    
    // 6. 从集合中删除SIGINT
    sigdelset(&set, SIGINT);
    printf("已从信号集中删除 SIGINT\n");
    
    // 7. 最终检查
    if (sigismember(&set, SIGINT) == 0) {
        printf("SIGINT 已不在信号集中\n");
    }
    if (sigismember(&set, SIGTERM) == 1) {
        printf("SIGTERM 仍在信号集中\n");
    }
    
    // 8. 填充所有信号(实际使用中要小心)
    sigfillset(&set);
    printf("\n已填充所有信号到信号集\n");
    
    // 检查某个信号是否在全集中
    if (sigismember(&set, SIGHUP) == 1) {
        printf("SIGHUP 在全信号集中\n");
    }
    
    return 0;
}

编译和运行:

bash 复制代码
gcc -o signal_example signal_example.c
./signal_example

输出示例:

关键点:

  • sigemptyset(&set) - 从空集合开始

  • sigaddset(&set, SIGXXX) - 添加特定信号

  • sigdelset(&set, SIGXXX) - 删除特定信号

  • sigismember(&set, SIGXXX) - 检查信号是否存在(返回1=存在,0=不存在)

  • sigfillset(&set) - 包含所有信号(谨慎使用)

这是信号集的基本操作,通常与 sigprocmask() 配合使用来实现信号阻塞。

重点:

需要注意的是,代码中定义的sigset_t类型变量set与普通变量一样位于用户空间。后续通过信号集操作函数对set的操作仅会修改用户空间的变量set值,而不会影响进程的实际行为。要真正生效,必须通过系统调用将这些修改传递给操作系统内核。

2、sigprocmask 函数

sigprocmask 函数是一个非常重要的函数,它可以读取或更改进程的信号屏蔽字(阻塞信号集)。

cpp 复制代码
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  • 返回值:函数调用成功返回 0,出错返回 -1。

参数说明

  • oset:如果 oset 是非空指针,则函数会将进程当前的信号屏蔽字通过 oset 参数传出。这样,调用者就可以获取到进程当前的阻塞信号集状态。(输出型参数)

  • set:如果 set 是非空指针,则函数会根据 how 参数指示的方式来更改进程的信号屏蔽字。(输入型参数)

  • howhow 参数用于指定如何更改信号屏蔽字,它有以下几种可选值:

    • SIG_BLOCK:将 set 所指向的信号集中的信号添加到当前进程的信号屏蔽字中,即阻塞这些信号。

    • SIG_UNBLOCK:从当前进程的信号屏蔽字中删除 set 所指向的信号集中的信号,即解除对这些信号的阻塞。

    • SIG_SETMASK:将当前进程的信号屏蔽字设置为 set 所指向的信号集,即完全替换原来的阻塞信号集。

信号屏蔽字(Signal Mask) 决定了当前哪些信号会被阻塞 (即,信号递送被推迟,直到信号被解除阻塞)。下面的表格描述了 sigprocmask 函数中 how 参数的行为:

选项 含义 等价操作
SIG_BLOCK set 包含了我们希望添加到当前信号屏蔽字的信号。 `mask = mask
SIG_UNBLOCK set 包含了我们希望从当前信号屏蔽字中解除阻塞的信号。 mask = mask & ~set(表示从屏蔽字中清除 set中的信号位
SIG_SETMASK 设置 当前信号屏蔽字为 set 所指向的值。 mask = set

注意:

  • 如果 osetset 都是非空指针,则函数会先将原来的信号屏蔽字备份到 oset 里,然后根据 sethow 参数更改信号屏蔽字。

  • 当调用 sigprocmask 解除了对当前若干个未决信号的阻塞时,在 sigprocmask 返回前,至少会将其中至少一个信号递达。这是因为信号的递达是在解除阻塞后立即进行的,以确保信号能够及时处理。

3、sigpending 函数

sigpending 函数用于读取当前进程的未决信号集。

cpp 复制代码
#include <signal.h>

int sigpending(sigset_t *set);
  • 返回值:调用成功则返回 0,出错则返回 -1。

  • 参数说明set 参数用于传出当前进程的未决信号集。调用该函数后,set 所指向的信号集将包含当前处于未决状态的信号信息。

4、信号阻塞与未决实验

我的云服务器崩溃了(华为云是这样的),所以我借了同学的虚拟机来做验证实验,下面通过一个具体的实验程序来展示上述函数的使用方法:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "curr process[" << getpid() << "]pending: ";
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << "\n";
}

void handler(int signo)
{
    std::cout << signo << " 号信号被递达!!!" << std::endl;
    std::cout << "-------------------------------" << std::endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
    std::cout << "-------------------------------" << std::endl;
}

int main()
{
    // 0. 捕捉 2 号信号
    signal(2, handler); // 自定义捕捉
    // signal(2, SIG_IGN); // 忽略一个信号
    // signal(2, SIG_DFL); // 信号的默认处理动作

    // 1. 屏蔽 2 号信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set, SIGINT); // 我们有没有修改当前进程的内核 block 表呢???

    // 1.1 设置进入进程的 Block 表中
    sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进程的内核 block 表,完成了对 2 号信号的屏蔽!

    int cnt = 15;
    while (true)
    {
        // 2. 获取当前进程的 pending 信号集
        sigset_t pending;
        sigpending(&pending);
        // 3. 打印 pending 信号集
        PrintPending(pending);
        cnt--;
        // 4. 解除对 2 号信号的屏蔽
        if (cnt == 0)
        {
            std::cout << "解除对 2 号信号的屏蔽!!!" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }
        sleep(1);
    }
}

在程序运行过程中,每秒钟会将各信号的未决状态打印一遍。由于程序屏蔽了 SIGINT 信号(即 Ctrl - C 对应的信号),当按下 Ctrl+C 时,SIGINT 信号会处于未决状态,而按 Ctrl+\(对应 SIGQUIT 信号,程序未屏蔽)仍然可以终止程序。

1. 详细过程

  • 程序初始运行时未收到任何信号,此时进程的pending表显示全0状态。

  • 当我们通过kill命令向该进程发送2号信号后,由于该信号处于阻塞状态,会一直保持未决状态,因此pending表中第二位持续显示为1。

  • 为了观察2号信号递达后pending表的变化,我们设置了定时自动解除信号阻塞的功能。

  • 当2号信号解除阻塞后立即被递达。

  • 由于2号信号的默认处理方式是终止进程,为了能观察到递达后的pending表状态,我们特别设置了信号捕捉机制,使2号信号递达时执行我们自定义的处理动作。

  • 可以看到,当进程收到2号信号时,该信号会暂时处于未决状态。

  • 待解除对2号信号的屏蔽后,信号立即被递达,执行我们设定的自定义处理动作,同时pending表也恢复为全0状态。

2. 执行顺序分析

根据代码逻辑,当执行到 cnt == 0 时:

cpp 复制代码
std::cout << "解除对 2 号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);  // 解除2号信号阻塞

3. 关键的执行顺序

  1. 先打印 "解除对 2 号信号的屏蔽!!!"

  2. 然后调用 sigprocmask() 解除阻塞

  3. sigprocmask() 返回前(要记住这个顺序!!!)

    • 内核先清除 pending 表中的对应位(设为0)

    • 然后立即执行 信号处理函数 handler()

4. 验证结论

  • pending位先被清除为0

  • 然后才执行信号处理函数

这是因为在信号处理函数中打印的pending表已经显示2号信号位为0了。

细节:在解除2号信号的屏蔽后,2号信号的自定义动作会在打印"恢复信号屏蔽字"之前执行。这是因为调用sigprocmask解除对当前若干未决信号的阻塞时,在sigprocmask函数返回前,至少会递达其中一个信号。

详细分析这三种情况下 blockpendinghandler 三个表的变化流程。

5. 当前代码状态分析

当前代码执行的是:

cpp 复制代码
signal(2, handler); // 自定义捕捉
行为流程
  1. block表:2号信号被阻塞

  2. handler表:2号信号设置为自定义处理函数

  3. 发送2号信号时:信号进入pending表(第2位变为1)

  4. 15秒后解除阻塞:pending表清除 → 执行handler函数

6. 情况一:执行 signal(2, SIG_IGN)

cpp 复制代码
// signal(2, handler); // 自定义捕捉
signal(2, SIG_IGN); // 忽略一个信号
三个表的变化流程
时间点 block表 pending表 handler表 行为
程序开始 2号信号被阻塞 全0 2号信号 = SIG_IGN 设置忽略处理
发送2号信号 2号信号被阻塞 第2位=1 2号信号 = SIG_IGN 信号被阻塞,进入pending
15秒解除阻塞 2号信号解除阻塞 第2位立即清0 2号信号 = SIG_IGN 直接忽略,不执行任何代码
关键区别
  • 不会执行 handler 函数

  • 不会打印 "2 号信号被递达!!!"

  • 信号被默默忽略,程序继续运行

7. 情况二:执行 signal(2, SIG_DFL)

cpp 复制代码
// signal(2, handler); // 自定义捕捉  
// signal(2, SIG_IGN); // 忽略一个信号
signal(2, SIG_DFL); // 信号的默认处理动作
三个表的变化流程
时间点 block表 pending表 handler表 行为
程序开始 2号信号被阻塞 全0 2号信号 = SIG_DFL 设置默认处理
发送2号信号 2号信号被阻塞 第2位=1 2号信号 = SIG_DFL 信号被阻塞,进入pending
15秒解除阻塞 2号信号解除阻塞 第2位立即清0 2号信号 = SIG_DFL 立即终止进程
关键区别
  • 不会执行 handler 函数

  • 进程直接终止(SIGINT的默认行为)

  • 看不到后续的任何输出

8. 三种情况的对比总结

处理方式 block表 pending表变化 handler表 最终行为
signal(2, handler) 阻塞2号信号 1→0(解除时) 自定义函数 执行handler,继续运行
signal(2, SIG_IGN) 阻塞2号信号 1→0(解除时) 忽略 默默忽略,继续运行
signal(2, SIG_DFL) 阻塞2号信号 1→0(解除时) 默认处理 立即终止进程

9. 关键结论

  1. block表:三种情况都一样,2号信号都被阻塞

  2. pending表:三种情况都一样,信号都先进入pending,解除时清0

  3. handler表 :决定了解除阻塞后的最终行为

  4. 执行顺序 :都是 pending清0 → 执行handler表对应的动作

通过这个实验,可以直观地看到信号的阻塞、未决状态的变化以及信号递达的过程,可以清晰展示信号处理中三个关键表(block、pending、handler)的协同工作原理!从而更好地理解和掌握信号集操作函数的使用方法和信号处理机制。

综上总结一下:

Linux信号机制围绕task_struct中的三张表(pendingblockhandler)实现,通过系统调用(如sigprocmasksigpending)动态控制信号的阻塞与处理。用户态可通过signal()sigaction()注册处理函数,而内核负责信号的递达与默认行为执行。理解这一机制对调试信号相关问题(如丢失信号、阻塞失效)至关重要。


五、相关细节与回顾

1、特殊信号与注意事项

  • 不可捕获信号 :如SIGKILL(9)和SIGSTOP(19)始终由内核处理,无法被阻塞或忽略。

  • 默认行为差异

    • SIGTERM(15):默认终止进程,可被捕获。

    • SIGSEGV(11):默认终止并核心转储,通常不应忽略。

2、信号递达与重复处理机制

(1) 常规信号 vs 实时信号

常规信号(如SIGINTSIGSEGV
  • POSIX.1允许两种行为

    • 递送一次:即使信号在阻塞期间产生多次,内核仅递送一次(Linux默认行为)。

    • 递送多次:依赖系统实现(如某些Unix变种支持)。

  • Linux实现 :通过pending位图标记信号是否挂起,多次触发仅置位一次,解除阻塞后仅递送一次。

实时信号(SIGRTMIN~SIGRTMAX
  • 队列化存储:多次产生的实时信号会按顺序存入队列,解除阻塞后依次递送。

  • 应用场景:需要精确处理多次事件的场景(如多消息通知)。

(2) 为什么常规信号不队列化?

  • 历史原因:早期Unix设计为简化信号处理,避免队列化带来的性能开销。

  • 典型场景 :如SIGSEGV(段错误)只需处理一次,重复递送无意义。

3、核心转储(Core Dump)机制

关于上面的这个图,如果忘记了的话回看上几篇博客有关core dump位的详细讲解,这里只是简单回顾一下,不详细讲解。

(1) 核心转储的作用

  • 定义 :进程异常终止时,内核将其内存数据(寄存器、栈、堆等)保存到磁盘文件(默认名为corecore.<pid>)。

  • 用途

    • 事后调试 :结合调试器(如gdb)分析崩溃原因(如空指针解引用、除零错误)。

    • 问题复现:保存进程崩溃时的完整状态,便于复现问题。

(2) 核心转储文件的生成条件

触发条件

  • 进程收到未捕获的致命信号(如SIGSEGVSIGABRTSIGFPE)。

  • 系统未禁用核心转储功能(通过ulimit -c检查)。

文件位置与命名

  • 默认路径:当前工作目录。

  • 命名规则

    • CentOS 7:core.pid(如core.1234)。

    • Ubuntu:corecore.<pid>(如core.4567)。

  • 环境变量影响kernel.core_pattern:可自定义路径和命名格式(如/var/crash/core-%e-%p)。

(3) 云服务器上的限制

  • 常见现象 :云服务器默认禁用核心转储(ulimit -c输出为0)。

  • 原因

    • 安全考虑:防止敏感数据泄露(如内存中的密码、密钥)。

    • 存储成本:核心文件可能较大,频繁生成占用磁盘空间。

  • 启用方法

    bash 复制代码
    # 临时启用(当前会话有效)
    ulimit -c unlimited
    
    # 永久启用(需修改/etc/security/limits.conf)
    echo "* soft core unlimited" >> /etc/security/limits.conf

4、案例分析:Floating point exception导致核心转储

(1) 案例复现

bash 复制代码
# 编译并运行触发除零错误的程序
echo 'int main() { int x = 0; return 1/x; }' > test.c
gcc test.c -o test
./test
# 输出:Floating point exception (core dumped)

(2) 调试步骤

1. 检查核心转储是否生成

bash 复制代码
ls core*  # 查看是否存在core文件

2. 使用gdb加载核心文件

bash 复制代码
gdb ./test core  # 或 gdb -c core

3. 分析崩溃原因

查看崩溃信号与调用栈

cpp 复制代码
(gdb) bt  # 打印backtrace
#0  0x0000000000400512 in main () at test.c:2

定位代码行

cpp 复制代码
(gdb) list  # 显示崩溃点附近代码
1   #include <stdio.h>
2   int main() { int x = 0; return 1/x; }

(3) 关键命令总结

命令 作用
ulimit -c 检查核心转储大小限制
ulimit -c unlimited 解除限制
gdb <程序> <core> 加载核心文件调试
bt (backtrace) 打印崩溃时的调用栈
list 显示源代码上下文

5、常见问题解答

(1) 为什么没见过核心转储文件?

可能原因

  • 系统或用户限制了核心转储大小(ulimit -c 0)。

  • 进程崩溃时当前目录不可写。

  • 信号被捕获且未主动触发转储(如自定义SIGSEGV处理函数)。

(2) 如何自定义核心文件路径?

临时设置 (需root权限):%e:程序文件名、%p:进程ID

bash 复制代码
echo "/tmp/core-%e-%p" > /proc/sys/kernel/core_pattern

永久设置 :修改/etc/sysctl.conf,添加:

bash 复制代码
kernel.core_pattern = /tmp/core-%e-%p

然后执行:

bash 复制代码
sysctl -p

(3) 云服务器如何安全启用核心转储?

限制核心文件大小

bash 复制代码
ulimit -c 1024  # 限制为1MB

指定存储路径

bash 复制代码
mkdir /var/crash
chmod 777 /var/crash
echo "/var/crash/core-%e-%p" > /proc/sys/kernel/core_pattern

6、总结

  • 信号处理:常规信号多次触发仅递送一次,实时信号支持队列化。

  • 核心转储:是调试崩溃进程的关键工具,需确保系统未禁用且路径可写。

  • 调试流程 :通过ulimit启用转储 → 复现崩溃 → 用gdb加载核心文件分析。

掌握这些机制后,可以更高效地诊断进程崩溃问题,尤其是在嵌入式开发或服务器运维场景中。

相关推荐
郝学胜-神的一滴3 小时前
使用现代C++构建高效日志系统的分步指南
服务器·开发语言·c++·程序人生·个人开发
kgduu3 小时前
go-ethereum core之交易索引txIndexer
服务器·数据库·golang
柱子子子子3 小时前
【邪修】linux (ubuntu/fedora/arch/debian) wifi hard blocked解决方法-AX210
linux·网络·ubuntu·debian
大龄Python青年4 小时前
Linux发行版Ubuntu24.04安装教程
linux·ubuntu·1024程序员节
强里秋千墙外道4 小时前
【Linux】ssh升级到最新版本-以ubuntu为例
linux·运维·ssh
QC七哥5 小时前
关于宽带网络下公网地址的理解
服务器·网络
9ilk5 小时前
【仿RabbitMQ的发布订阅式消息队列】--- 介绍
linux·笔记·分布式·后端·rabbitmq
馨谙5 小时前
OpenSSH 安全配置核心概念解析
linux·服务器·网络
半桔5 小时前
【IO多路转接】IO 多路复用之 select:从接口解析到服务器实战
linux·服务器·c++·github·php