当前位置: 首页 > news >正文

Linux -- 信号【中】

目录

一、信号的保存

1、信号相关概念

1.1 信号递达 (Delivery)

1.2 信号未决 (Pending)

1.3 信号阻塞 (Block)

1.4 阻塞 vs 忽略

1.5 阻塞 vs 未决

​编辑

2、信号在内核中的表示

2.1 未决信号表 (pending)

2.2 阻塞信号表 (block)

2.3 信号处理动作表 (handler)

2.4 信号处理示例分析

2.5 内核数据结构

3、sigset_t 信号集

3.1 sigprocmask

3.2 sigpending

二、信号的捕捉

2.1 内核空间与用户空间

2.2 用户态与内核态

2.3 内核如何实现对信号的捕捉

2.4 sigaction 函数


一、信号的保存

# 信号被操作系统发送给进程之后,进程可能并不会立即处理该信号,此时为了让进程之后能够执行相应的信号,我们必须将对应的信号保存下来。

1、信号相关概念

# 在 Linux中,是通过位图结构保存的,而在了解信号的保存原理之前,我们需要先明白几个重要的概念:

1.1 信号递达 (Delivery)

# 信号递达指的是操作系统实际执行信号处理程序的过程。当信号递达时,系统会根据以下三种可能的处理方式之一来响应:

  1. 默认处理:执行系统预定义的操作(如终止进程)
  2. 忽略处理:完全丢弃该信号
  3. 自定义处理:执行用户注册的信号处理函数

# 例如,当进程收到 SIGINT 信号 (Ctrl+C) 时,默认处理方式是终止进程,但如果用户注册了处理函数,则会执行该函数。

1.2 信号未决 (Pending)

# 信号从产生到递达之间会经历未决状态。这个过程中:

  • 信号被记录在进程的未决信号集合中
  • 每个信号都有一个对应的未决标志位
  • 信号可能因为阻塞而长时间保持未决状态

1.3 信号阻塞 (Block)

# 进程可以通过信号掩码主动阻塞某些信号:

  1. 阻塞的信号仍可被接收,但不会立即递达
  2. 被阻塞的信号会一直保持在未决状态
  3. 常见阻塞场景包括:
     
    • 关键代码段执行期间
    • 信号处理函数执行时
    • 进程初始化阶段

# 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。信号的阻塞也叫做信号的屏蔽

1.4 阻塞 vs 忽略

# 需要注意的是,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后的一种处理动作。

特性阻塞 (Block)忽略 (Ignore)
作用时机信号递达前信号递达后
信号状态保持未决已被处理
后续影响解除阻塞后会递达直接丢弃
典型应用临时屏蔽关键信号永久忽略无关信号

# 例如,在银行交易处理中:

  • 阻塞 SIGINT 可防止交易中途被中断
  • 忽略 SIGCHLD 可避免处理子进程状态变化

1.5 阻塞 vs 未决

2、信号在内核中的表示

# 信号在内核中是通过两个位图与一个函数指针数组表示的,其中 block 位图每一个比特位代表对应信号是否被阻塞,pending 位图每一个比特位表示对应信号是否未决,handler 数组表示存放每个信号处理的默认或者自定义方法。并且这两个位图结构与函数指针数组也是被我们的进程控制块 task_struct所管理的。

2.1 未决信号表 (pending)

        位置:task_struct->pending

        作用:一个 unsigned int 的位图,记录已经产生但尚未递达(处理)的信号,从右往左数,比特位的位置表示的是第几个信号,比特示位的内容表示是否收到,1表示收到了信号,0表没有收到信号。

        结构:包含一个sigset_t(位图)和一个list_head(链表)

        功能:对于实时信号,list_head 用于实现信号队列,可以存储多个相同的信号        

2.2 阻塞信号表 (block)

  • 位置task_struct->blocked

  • 作用:一个 unsigned int 的位图,记录当前被进程阻塞(屏蔽)的信号,从右往左数,比特位的位置表示的是第几个信号,比特位的内容表示是否阻塞,1表示被阻塞,0表示没被阻塞。因此,pending & (~block)后,1表示该信号可以被递达,0表示该信号不可以被递达。

  • 类型sigset_t(一个位掩码,每位对应一个信号)

  • 功能:即使信号产生,如果它在阻塞集中,也不会被递送给进程,直到解除阻塞

2.3 信号处理动作表 (handler)

        位置:task_struct->sighand->action[64]

        作用:一个 sighandler_t handler[31] 的函数指针数组,定义了对每个信号的处理方式,数组下标就是信号编号

        大小_NSIG(通常为64),对应64种可能的信号

        内容:每个元素是一个k_sigaction结构,包含:

                sa_handler:信号处理函数指针(可以是SIG_DFL、SIG_IGN或用户自定义函数)

                sa_flags:控制信号处理的各种标志

                sa_mask:在执行此信号处理函数时,需要阻塞的其他信号集

                sa_restorer:恢复函数(通常不由应用程序直接使用)

# 所以我们之前 signal 自定义捕捉方法时传入的 SIGALRM 就是宏定义为14的数组下标,之前写的 handlerSig 方法就是数组元素的函数指针。

2.4 信号处理示例分析

# 我们通过上图中的例子来解释这三种表如何协同工作:

SIGHUP 信号(信号1)

  • 阻塞位:0(未阻塞)

  • 未决位:0(未产生)

  • 处理动作:默认处理动作(SIG_DFL

  • 行为:当SIGHUP信号产生时,内核会设置未决标志,然后在合适的时候执行默认处理动作

SIGINT 信号(信号2)

  • 阻塞位:1(被阻塞)

  • 未决位:1(已产生但未处理)

  • 处理动作:忽略(SIG_IGN

  • 行为:虽然处理动作是忽略,但由于信号被阻塞,它暂时不能被处理。进程有机会在解除阻塞前改变处理动作

SIGQUIT 信号(信号3)

  • 阻塞位:1(被阻塞)

  • 未决位:0(未产生)

  • 处理动作:用户自定义函数sighandler

  • 行为:一旦产生SIGQUIT信号,它将被阻塞,直到解除阻塞后才会调用sighandler

2.5 内核数据结构

// 进程描述符中与信号相关的字段
struct task_struct {// ...struct sighand_struct *sighand;  // 指向信号处理表sigset_t blocked;                // 阻塞信号表(位图)struct sigpending pending;       // 未决信号表// ...
};// 信号处理表结构
struct sighand_struct {atomic_t count;                  // 引用计数struct k_sigaction action[_NSIG]; // 每个信号的处理动作spinlock_t siglock;              // 保护该结构的自旋锁
};// 信号处理动作详情
struct k_sigaction {struct __new_sigaction sa;       // 信号处理结构void __user *ka_restorer;        // 恢复函数指针
};// 信号处理结构
struct __new_sigaction {__sighandler_t sa_handler;       // 信号处理函数指针unsigned long sa_flags;          // 标志位void (*sa_restorer)(void);       // 恢复函数(通常不使用)__new_sigset_t sa_mask;          // 执行处理函数时要阻塞的信号集
};// 未决信号结构
struct sigpending {struct list_head list;           // 实时信号的队列sigset_t signal;                 // 未决信号的位图
};

# Linux 内核通过三张表精细地管理信号:

  • 处理动作表决定了信号最终如何被处理
  • 阻塞表控制哪些信号暂时不被处理
  • 未决表记录已产生但尚未处理的信号

3、sigset_t 信号集

# sigset_t 被称为信号集,也叫做信号屏蔽字(Signal Mask),是操作系统给用户提供的一种数据类型,用来描述和 block 、 pending 一样的位图,其结构具体如下:

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
    unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef __sigset_t sigset_t;

# 从下图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集。

  • 这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,
  • 而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
  • 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

 # sigset_t 类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit 则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用 printf 直接打印 sigset_t 变量是没有意义的。

# 于此同时,操作系统还给我们提供了很多信号集操作函数,并且我们只能通过这些函数去修改信号集。

#include <signal.h>

  • int sigemptyset(sigset_t *set); // 将位图全部设置为 0
  • int sigfillset(sigset_t *set); // 将位图全部都设置为 1
  • int sigaddset (sigset_t *set, int signo); // 将位图中的某一位设置为 1
  • int sigdelset(sigset_t *set, int signo); // 将位图中的某一位设置为 0
  • int sigismember(const sigset_t *set, int signo); // 判断一个信号是否在信号集中,不在返回0,在返回1,出错返回-1

# 但是这些都只是对我们自己定义的变量进行了修改,并没有对我们的内核数据有任何影响,为了能让我们真正意义上修改内核中的 block与 pending位图,我们还需要借助以下两个接口。

3.1 sigprocmask

# 我们可以使用 sigprocmask函数读取或者修改阻塞信号集(block位图),其具体用法如下:

  1. 函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  2. 参数:
  • 如果 oldset 是非空指针,则读取进程当前的信号屏蔽字,然后通过 oldset 参数传出,是一个输出型参数。
  • 如果 set 是非空指针,则更改进程的信号屏蔽字,参数 how决定如何更改。
  • 如果 oldset set 都是非空指针,则先将原来的信号屏蔽字备份到 oldset 里,然后根据 set how 参数更改信号屏蔽字

     3. 返回值:如果调用成功返回0,出错返回-1。

# 如果我们假设当前的信号屏蔽字为 mask,下表说明了 how 参数的可选值。

选项含义
SIG_BLOCKset 包含了我们希望添加到当前信号屏蔽字的信号,相当于 mask=mask|set
SIG_UNBLOCKset 包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于 mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为 set 所指向的值,相当于 mask=set

# 如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递达。

示例:阻塞 SIGINT 信号:

sigset_t new_set, old_set;
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);// 阻塞SIGINT,并保存旧的屏蔽字到old_set
if (sigprocmask(SIG_BLOCK, &new_set, &old_set) == -1) {perror("sigprocmask");
}
// ... 在这段代码中,SIGINT信号会被阻塞 ...
// 恢复旧的屏蔽字(解除对SIGINT的阻塞)
if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {perror("sigprocmask");
}

3.2 sigpending

# 我们同样可以通过 sigpending函数来读取对应的未决信号集(pending位图),其原型如下:

#include <signal.h>  

  • 函数原型:int sigpending(sigset_t \*set);  
  • 作用:读取当前进程的未决信号集,通过 set 参数传出,是一个输出型参数。  
  • 返回值:调用成功则返回0,出错则返回-1

# 知道如上接口的用法的作用后,我们就可以编写一段程序来验证以下阻塞信号集:

#include<iostream>
#include<cstdio>
#include<vector>
#include<functional>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>void PrintPending(sigset_t &pending)
{printf("我是一个进程(%d), pending: ", getpid());for(int signo = 31; signo >= 1; signo--){// 在pending表里就输出1,不在输出0if(sigismember(&pending, signo)){std::cout << "1";}else{std::cout << "0";}}std::cout << std::endl;
}void handler(int sig)
{std::cout << "#####################################" << std::endl;std::cout << "递达" << "sig" << "信号" << std::endl;sigset_t pending;int m = sigpending(&pending);PrintPending(pending); // 0000 0010(信号被处理完, 2号才会被设置为0)// 0000 0000(执行handler方法之前,2对应的pending已经被清理了)std::cout << "#####################################" << std::endl;
}int main()
{signal(SIGINT, handler); // 解除对2号的屏蔽后就直接终止进程了,想要看到效果就进行自定义捕捉// 1.屏蔽2号信号sigset_t block, old_block;sigemptyset(&block); // 清空信号集的block位图sigemptyset(&old_block);sigaddset(&block, SIGINT); // 把2号信号添加到block里,但是此时还没有对2号信号进行屏蔽,因为这个block是自定义的,不是进程当前的信号集// for(int i = 1; i < 32; i++)//     sigaddset(&block, i);int n = sigprocmask(SIG_SETMASK, &block, &old_block); // 写入到内核中(void)n; // 防止n被定义但未被使用的警告// 4.重复获取打印过程int cnt = 0;while (true){// 2.获取pending信号集sigset_t pending;int m = sigpending(&pending);// 3. 打印PrintPending(pending);if(cnt == 10){// 5. 恢复对2号信号的block情况,解除屏蔽sigprocmask(SIG_SETMASK, &old_block, nullptr);std::cout << "解除对2号的屏蔽" << std::endl;}sleep(1);cnt++;}return 0;
}

# 我们把所有普通信号的阻塞表都设为屏蔽,可以看到我们通过 kill 命令发送信号时,信号被屏蔽了,所以可以看到未决表中记录的已产生但未递达的信号集,我们发送一个信号,该信号集对应位置为1,但是由于9号信号比较特殊,无法被捕获,阻塞和忽略,我们发送9号信号时,进程就被杀死了。


二、信号的捕捉

# 信号由键盘、系统调用、硬件异常、软件条件等方式产生,然后被保存在三张表中,再将信号递达,操作系统有三种处理方式:默认处理、忽略处理、自定义处理。虽然我们现在已经知道这三种处理方式,但是操作系统底层是到底怎么做到在合适的时候处理信号?合适的时候又是什么时候呢?下面我们就来深入了解。

2.1 内核空间与用户空间

# 我们知道每一个进程都有自己的进程地址空间(mm_struct),该进程地址空间其实是由内核空间用户空间组成的,如果一个进程地址空间的表示范围为4G,那么内核空间占 1G,用户空间占 3G。

  • 用户所写的代码和数据位于用户空间,通过用户级页表与物理内存之间建立映射关系。
  • 内核空间存储的实际上是操作系统代码和数据,通过内核级页表与物理内存之间建立映射关系。

# 其中内核级页表是一张全局的页表,它用来维护操作系统的代码与进程之间的关系。因此,在每个进程的进程地址空间中,用户空间是属于当前进程,每个进程的代码和数据可能是不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程中都是一样的内容。

2.2 用户态与内核态

# 虽然每一个进程中都有对应的操作系统的代码与数据,但并不是所有进程都能访问的,一般只有处于内核态的进程才能访问操作系统的代码与数据,而处于用户态的进程只能访问对应的用户代码与数据。

  • 用户态:用户态是普通程序的运行模式,具有较低的特权级别。在用户态下运行的代码不能直接访问硬件资源和其它受限资源,例如内存管理、设备驱动程序和文件系统等。用户态程序只能通过系统调用与内核态交互,以访问这些受限资源。
  • 内核态:内核态是操作系统内核的运行模式,具有较高的特权级别。在内核态下运行的代码可以访问所有系统资源和设备,并可以执行任何指令。内核态负责管理系统资源、硬件设备和用户程序,以及处理系统中断和异常。

# 在现代操作系统中,一个进程根据其运行的代码所处的特权级别,可以在用户态和内核态之间切换。例如,当用户程序通过系统调用请求操作系统服务时,进程将从用户态切换到内核态,以允许内核代码执行相应的服务。当内核完成系统调用服务时,进程将切换回用户态,以便继续执行用户代码。

# 那么问题来了,操作系统为什么不以内核态的方式去执行用户代码呢?

理论上来说是可以的,因为内核态是一种权限非常高的状态,但是绝对不能这样设计。

因为如果允许在内核态直接执行用户空间的代码,那么用户就可以在代码中设计一些非法操作,比如清空数据库,窃取密码等。这种操作在用户态是完全不可行的,但如果以内核态形式去执行,这些非法操作就能被实现。

2.3 内核如何实现对信号的捕捉

# 我们知道前面这些概念之后,我们就能解释内核是如何实现对信号的捕捉的:

# 当我们进程执行主控制流程的某条指令时可能因为中断,异常,或系统调用会陷入内核(变为内核态),在内核处理完毕准备返回用户态时,就会进行未决信号 pending 的检查。

# 查看 pending 位图时,若发现有未决信号且该信号未被阻塞,就需要对该信号进行处理。如果待处理信号的处理动作是默认或者忽略,那么执行该信号的处理动作后清除对应的 pending 标志位,若没有新的信号要递达,就直接返回用户态,从主控制流程中上次被中断的地方继续向下执行。

# 但如果待处理信号是自定义捕捉的,即该信号的处理方式是由用户提供的,那么处理该信号时就需要先返回用户态执行对应的自定义处理操作,先清除对应的 pending 标志位,执行完后再通过特殊的系统调用 sigreturn 再次陷入内核并,如果没有新的信号要递达,就直接返回用户态,继续执行主控制流程的代码。

# 其中需要注意的是:sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

# 四重状态切换的本质:

切换点方向CPU特权级触发机制目的
1 (用户→内核)系统调用/中断3→0硬件中断指令进入内核处理事件
2 (内核→用户)执行handler0→3内核修改CS:EIP安全执行用户代码
3 (用户→内核)调用sigreturn3→0软中断(int 0x80)返回内核恢复上下文
4 (内核→用户)恢复主程序0→3恢复原CS:EIP继续执行被中断代码

2.4 sigaction 函数

# 捕捉信号除了我们前面使用过的 signal 函数之外,我们还可以使用 sigaction 函数对信号进行捕捉,sigaction 是 Linux / UNIX 系统中用于精细控制信号行为的核心系统调用,相比传统的 signal () 函数,它提供了更强大的功能(如信号屏蔽、附加数据传递)和更可靠的行为。其用法如下:

  • 函数原型: int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 参数:signum 代表指定信号的编号。若 act 指针非空,则根据 act 修改该信号的处理动作。若 oldact 指针非空,则通过 oldact传出该信号原来的处理动作。
  • 返回值:成功返回 0;失败返回 -1。

# 其中 act 与 oldact的类型是一个结构体指针,这个结构体原型如下:

struct sigaction {void     (*sa_handler)(int);         // 简单的信号处理函数void     (*sa_sigaction)(int, siginfo_t *, void *); // 高级信号处理函数sigset_t   sa_mask;                  // 在执行处理函数期间要阻塞的信号集int        sa_flags;                 // 修改信号行为的标志位void     (*sa_restorer)(void);       // 已废弃,不应使用
};

# 其中这五个参数我们只需要关注 sa_handler 和 sa_mask,其他参数默认设为 0。

  • sa_handler:指向自定义的捕捉函数。
  • sa_mask:一个信号集,里面记录了在处理 signum 时需要额外屏蔽掉的信号。

# 其中需要注意的是:当某个信号的处理函数被调用时,内核会在调用之前自动将当前信号加入进程的信号屏蔽字,待信号处理函数返回时又自动恢复原来的信号屏蔽字,以此保证在处理某个信号时,若该信号再次产生会被阻塞到当前处理结束。

# 如果在调用信号处理函数时,除当前信号被自动屏蔽外还希望自动屏蔽另外一些信号,可通过 sa_mask 字段说明这些需额外屏蔽的信号,同样在信号处理函数返回时会自动恢复原来的信号屏蔽字。

# 与 signal() 的区别:

特性signal()sigaction()
可移植性不同系统行为不一致POSIX 标准,行为一致
控制精度有限精细控制
信号阻塞自动阻塞当前信号可自定义阻塞信号集
系统调用重启依赖具体实现可通过 SA_RESTART 明确控制
信号信息只能获取信号编号可获取详细信息(siginfo_t
推荐程度已过时,不推荐在新代码中使用现代程序的首选

# 我们可以通过这个函数来验证一下:

void handler(int signum)
{std::cout << "hello signal: " << signum << std::endl;while(true){//不断获取pending表!sigset_t pending;sigpending(&pending);for(int i = 31; i >= 1; i--){if(sigismember(&pending, i))std::cout << "1";elsestd::cout << "0";}std::cout << std::endl;sleep(1);}exit(0);
}int main()
{struct sigaction act, oldact;act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);act.sa_flags = 0;// 默认信号处理逻辑sigaction(SIGINT, &act, &oldact); // 对2号信号进行捕捉while(true){std::cout << "I am a process, pid: " << getpid() << std::endl;sleep(1);}return 0;
}

# 将3号和4号信号加入到阻塞表中,使用 sigaction 来捕获 SIGINT 信号,第一次捕获2号信号时,会执行我们自定义函数 handler,所以第一次捕获到2号信号时,pending 表中2号信号并不处于未决状态,因为已经在执行自定义函数了,然后会在handler中死循环获取 pending 表,所以我们后续不断发送2号,3号和4号信号时,都会被屏蔽,也就是阻塞住,那么 pending 表中这些信号就会因为阻塞处于未决状态。

http://www.dtcms.com/a/456697.html

相关文章:

  • Azure - 尝试创建并使用一下Azure AI Search
  • NtripShare GNSS接收机配置系统SPI读取村田SCL3300倾角数据
  • Python私教FastAPI+React构建Web应用02 什么是全栈Web应用
  • 开源安全管理平台wazuh-文件完整性监控FIM
  • 网站建设选超速云建站黄页88成立时间
  • 南通做网站ntwsd开发公司总工年终总结
  • VS Code文件监视排除设置详解
  • 3D坐标旋转公式
  • 《Git 从入门到进阶》教学大纲
  • linux网络服务+linux数据库5
  • 德山经济开发区建设局网站wordpress的数据库在哪里
  • P3808 AC 自动机(简单版)
  • C++----bitmap位图的使用
  • 单链表的应用02---算法中的暴力美学(第八讲)
  • 【RAG】优化query查询效果的几种处理
  • transformer详解(位置编码+attention+残差连接+全连接网络)
  • 已注册域名怎么做网站呢免费网站免费网站平台
  • 如何解决 pip install -r requirements.txt 约束文件 constraints.txt 仅允许固定版本(未锁定报错)问题
  • 【Camera】准备的一些Camera面试题——相机预览、拍照流程(经验尚欠,待补充)
  • CICD工具选型指南,GitLab cicd vs Arbess哪一款更好用?
  • 尉Lucene.Net 分词器选择指南:盘古分词 vs 结巴分词h
  • gitlab runner 安装
  • MySQL的OR条件查询不走索引及解决方案
  • 1688 店铺商品全量采集与智能分析:从接口调用到供应链数据挖掘
  • 淘宝商品详情采集方式,json数据返回
  • 【论文精度-1】 组合优化中的机器学习:方法论之旅(Yoshua Bengio, 2021)
  • 南京维露斯网站建设微信营销软件app
  • 从帧边界识别到数据编码:嵌入式通信协议与数据序列化方案深度对比
  • Quick SwiftObjective-C测试框架入门教程
  • GRM tools三大插件使用教程