小工具知识网

当前位置: 首页 > 行业知识 > 【Linux】信号保存与信号捕捉处理

行业知识

【Linux】信号保存与信号捕捉处理

2026-11-05 11:02:09 来源:互联网转载

信号保存与信号捕捉

  • 一、信号保存
    • 1. 信号的发送
    • 2. 理解信号保存
      • (1)信号保存原因
      • (2)信号保存概念
      • 3. 信号保存系统接口
        • (1)sigset_t
        • (2)sigprocmask()
        • (3)sigpending()
        • (4)signal()
        • (5)测试系统接口
        • 二、信号捕捉处理
          • 1. 信号的处理
          • 2. 理解用户态和内核态
          • 3. 信号的捕捉
          • 4. 系统调用
            • (1)sigaction()
            • (2)pending 表的置0顺序
            • (3)struct sigaction 中的 sa_mask 字段
            • 三、信号扩展
              • 1. 可重入函数
              • 2. volatile
              • 3. SIGCHLD 信号

                一、信号保存

                1. 信号的发送

                那么在学习信号保存之前,我们先了解一下信号的发送,我们知道普通信号一共有31个,如下:

                但是这个31就非常特殊,对于普通信号而言,对于进程而言,自己有还是没有收到哪一个信号。实际上,我们发送信号是给进程发,具体点就是给进程的 PCB 发,所以 task_struct 中必定有维护信号的字段,那么在 task_struct 中其实只需要维护一个整数即可,因为一个整数有 32 个比特位!而我们忽略第一位,从第2位开始到第32位一共31个比特位,就分别表示31种信号!也就是说,用0、1来描述信号,用位图管理普通信号!

                所以,

                1. 比特位的内容是0还是1,表明是否收到;
                2. 比特位的位置(第几个),表示信号的编号;
                3. 所谓的发送信号,本质就是操作系统去修改 task_struct 的信号位图对应的比特位;

                那么为什么必须是操作系统向进程PCB中写入呢?因为操作系统是进程的管理者,只有它有资格才能修改 task_struct 内部的属性!所以这就是为什么只有操作系统才能有资格给进程发信号!

                2. 理解信号保存

                (1)信号保存原因

                信号为什么要保存呢?因为进程收到信号之后,可能不会立即处理这个信号,可能正在处理更重要的事情,所以信号不会被处理,就要有一个时间窗口,所以信号就要被保存。

                (2)信号保存概念

                • 实际执行信号的处理动作称为信号递达(Delivery);
                • 信号从产生到递达之间的状态,称为信号未决(Pending);
                • 进程可以选择阻塞 (Block )某个信号;
                • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;
                • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

                  所以进程的 task_struct 中不仅要保存信号的状态,还要保存信号的阻塞状态;而且信号的范围是 1~31,每一种信号都要有自己的一种处理方法,所以在 task_struct 中还要为每一个信号维护一张 handler 表,这张表是函数指针数组,比如数组中的内容是 typedef void (*handler_t)(int); 类型,数组名为 handler_t handler[31];,所以它就是一个函数指针数组,里面放的就是指向方法的地址,而下标就是信号的编号!那么当我们捕捉对应信号后自定义的方法,就将我们的方法的地址填入对应的位置即可!如下图:

                  而上面的 pending 表就是一个位图,表示信号未决的状态;

                  那么 block 表也是一个位图,1表示被阻塞,0表示未阻塞。一旦阻塞了某个信号,在该信号没有被解除阻塞之前,即便收到了该信号,对应的信号也不会被操作系统进行递达。

                  所以 pending 表记录当前进程是否收到了信号以及收到了哪些信号;block 表记录特定信号是否被屏蔽;handler 表记录每种信号的处理方法。

                  所以对于某个信号,对应上面的三张表中,我们应该横向看,是否被屏蔽、是否收到、对应方法。所以我们对于信号的学习,无论给我们提供多少系统接口,都是围绕这三张表获取或者修改。

                  3. 信号保存系统接口

                  上面的两张表中,block 和 pending 是两张位图,也就是两个整数,我们当然可以用位操作去修改,但是整数都是32个比特位,而如果当操作系统想要扩展这两张位图的时候,一个整型就放不下了,所以操作系统给我们提供了一种用户层的类型,这个类型可以直接设置进操作系统的位图里。

                  而且上面的三张表都属于操作系统的内核数据结构,它不允许用户直接修改这三张表,所以操作系统必须给我们提供系统调用修改这三张表。我们要获取的 pending 表和 block 表都是位图,这就注定了要在用户空间到内核空间,内核空间到用户空间进行来回拷贝,所以数据拷贝时就要在系统调用接口的参数上设置输入输出型参数。

                  所以操作系统给我们提供了一种经过封装的数据类型,来获取内核中的位图,就是 sigset_t.

                  (1)sigset_t

                  sigset_t 其实就是一个位图结构,我们称为信号集。因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

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

                  #include int sigemptyset(sigset_t *set);// 清空信号集int sigfillset(sigset_t *set);//  将整个位图置1int sigaddset (sigset_t *set, int signo); // 向指定信号集中添加指定信号int sigdelset(sigset_t *set, int signo);// 在指定信号集中去掉指定信号int sigismember(const sigset_t *set, int signo);// 判断指定信号是否在信号集中

                  (2)sigprocmask()

                  调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集),接口如下:

                  int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

                  其中第一个参数代表我们需要设置还是获取 block 表,有三个选项,如下,它们之间是并列关系,只能三选一:

                  • SIG_BLOCK:set 包含了我们希望添加到当前信号屏蔽字(block表)的信号,相当于 mask = mask | set;
                  • SIG_UNBLOCK:set 包含了我们希望从当前信号屏蔽字(block表)中解除阻塞的信号,相当于 mask = mask & ~set;
                  • SIG_SETMASK:设置当前信号屏蔽字(block表)为 set 所指向的值,相当于 mask = set;

                    第二个参数就是我们当前设置的信号集,它是一个输入型参数;第三个参数是一个输出型参数,当我们对进程的 block 表做修改的时候,在改之前,系统会将改之前的表通过 oldset 保存起来。那么修改之后,然后我们就能通过 oldset 拿到上一次的 block 表。 如果不需要的话可以设为 nullptr.

                    返回值则是成功返回0;失败返回-1.

                    (3)sigpending()

                    读取当前进程的未决信号集,通过 set 参数传出。调用成功则返回0,出错则返回-1;接口如下:

                    int sigpending(sigset_t *set);

                    对于 sigpending 函数的唯一参数,它是一个输出型参数,就是调用进程所对应的 pending 表带出来。

                    (4)signal()

                    signal() 接口我们早就接触过了,它就是用来修改 handler 表的,接口如下:

                    typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);

                    (5)测试系统接口

                    下面我们写一个这样的代码:先将2号信号阻塞,然后打印出 pending 表,我们初始化的时候应该是全0的,然后我们给进程发送2号信号,因为2号信号被阻塞了,所以 pending 表中2号信号所对应的比特位在没有被解除阻塞前一直都是1的,然后我们打印 pending 表出来观察,是否如此。代码如下:

                    void Print(sigset_t sigset){    for(int i = 31; i >= 1; i--)    {        if(sigismember(&sigset, i))        {            cout 

上一篇:【课程设计/毕业设计】python外卖点餐管理系统源码+开发文档

下一篇:怎么破解apk的验证服务器?