前引:Linux 信号是进程间通信的核心机制,更是保障程序稳定性的关键 —— 当进程收到中断、终止等信号时,如何正确保存信号状态、避免丢失,又如何按预期执行处理逻辑,直接决定了程序的可靠性。本文将从内核底层逻辑出发,拆解信号保存的核心机制,结合实际代码案例,带你掌握 Linux 信号处理的完整流程与实操技巧!

目录

【一】信号的保存

(1)信号在内核的保存

(2)信号集

(3)信号集操作

(1)sigemptyset()

(2)sigfillset()

(3)sigaddset()

(4)sigdelset()

(5)sigismember()

(6)sigprocmask()

函数使用总结:

【二】二谈进程地址空间

【三】内核|用户态的切换

【四】信号的捕捉过程

(1)sigaction()

(2)信号自动屏蔽

(3)如何获取pending集

【五】可重入函数

【六】volatile关键字

【七】子进程的“遗嘱”信号


【一】信号的保存

在上篇我们已经学习了什么是信号、信号的产生,信号从产生到被处理不是立刻被执行的:

下面我们先来认识几个概念:

信号递达:实际处理信号的动作

信号末决:从产生到处理(递达)之间的过程

信号阻塞:进程可以选择阻塞某个信号,阻塞的信号会保持在末决状态(产生但是不执行),只                      有解决对该信号的阻塞,才会执行递达

信号忽略:区分“信号阻塞”(产生但是不执行),“忽略”属于执行处理动作的一种(选择忽略处理)

(1)信号在内核的保存

在内核中,信号其实通过三个指针被保存在task_struct中的:这涉及到三张表,我们逐一来看:

block:信号的标志位,对应状态信号阻塞状态(是否正常执行):0表示非阻塞,1表示阻塞

pending:信号的标志位,对应信号的接收:比如接收了2号信号(00000000.....00000010)

handler:信号的处理方式(默认处理、忽略处理、自定义处理),替换指针即替换了执行方法

当一个信号产生到执行,会根据三张表的查询来解决:pending—>block—>handler

(2)信号集

每个信号都有末决标志:非0即1,包括其中的阻塞状态也是0和1,对应该信号“有效”or“无效”

因此该状态的存储类型都可以表示为:sigset_t 类型

sigset_t:称为信号集,用来表示信号的一种状态,比如:

阻塞信号集:表示阻塞或者非阻塞状态;末决信号集:末决状态或者非末决状态

(3)信号集操作

我们可以操作下面的接口来实现控制 sigset_t 类型的变量,从而改变信号末决(过程):

#include <signal.h>
(1)sigemptyset()

原型:

int sigemptyset(sigset_t *set);

参数:指向 sigset_t 类型的指针(参数名自己设置)

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:代表要初始化的信号集

例如:

(2)sigfillset()

原型:

int sigfillset(sigset_t *set);

参数:指向 sigset_t 类型的指针

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:将信号集 set 初始化为全包含集合(可包含所有信号)

例如:

(3)sigaddset()

原型:

int sigaddset(sigset_t *set, int signo);

参数:

  • set:指向 sigset_t 类型的指针,代表要操作的信号集
  • signo:要添加的信号编号

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:将信号添加到信号集

例如:

(4)sigdelset()

原型:

int sigdelset(sigset_t *set, int signo);

参数:

  • set:指向 sigset_t 类型的指针
  • signo:要删除的信号编号

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:从信号集 set 中删除指定信号

(5)sigismember()

原型:

int sigismember(const sigset_t *set, int signo);

参数:

  • set:指向 const sigset_t 类型的指针
  • signo:要检查的信号编号

返回值:

  • 若信号 signo 在集合 set 中,返回 1
  • 若不在集合中,返回 0
  • 失败返回 -1,并设置 errno(如 signo 无效或 set 指针为空)

作用:检查指定信号 signo 是否为信号集 set 的成员

(6)sigprocmask()

原型:

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

参数:

第一个参数:SIG_BLOCK:将 set 中的信号添加到当前屏蔽集

                      SIG_UNBLOCK:将set中的信号从当前屏蔽集移除

                      SIG_SETMASK:将当前屏蔽集直接设置为set的值

第二个参数:指向 sigset_t 类型的指针。若为 NULL,则 how 的操作被忽略,仅用于 “获取旧屏蔽                       集”

第三个参数:指向 sigset_t 类型的指针,用于保存 “修改前的旧屏蔽集”。若为 NULL,则不保存旧                        屏蔽集

返回值:

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

作用:sigprocmask 直接修改进程 task_struct 中的 block 集合,从而控制信号的 “阻塞状态”

例如:

函数使用总结:

这些函数看起来很杂乱,但是它们的功能其实都是在执行一件事:改变当前进程的 block 信号集

(1)(2)(3)(4)(5):你自己创建一个信号集,将你想统一操作的信号放在信号集

(6):根据选项选择统一屏蔽/解决屏蔽信号集里面的信号

             它的第三个参数:用来一键恢复修改之前的信号集,目前简单场景用不到~

【二】二谈进程地址空间

如果有50个完全不同进程,那么可能有50个页表,对应50个该进程在内存的分布空间,而我们可以看到上面有1G的内核空间,那对应50个内核代码和数据吗?内核空间也有自己的内核级页表

答案:内核的代码和数据只有一份,被50个进程重复使用,因为里面是系统调用的相关的方法

                                         

我们看下面的进程地址空间与、页表、内存的大概联系图:

操作系统来做软硬件的管理者,那么操作系统本身也是软件资源,谁让它一直跑的?

时钟中断是由硬件定时器(如 CPU 的时钟芯片)周期性触发的中断信号,类似操作系统的心脏

操作系统内核会不断执行 “等待时钟中断 → 处理中断 → 回到等待” 的循环~

【三】内核|用户态的切换

在这里需要两个概念储备:然后我们直接看表:

用户态:允许你访问用户的代码和数据

内核态:允许你访问操作系统的代码和数据

状态的切换是需要每次更改CPU中ecs寄存器的标志位:00(用户态)-><-11(内核态)

(1)用户执行系统调用,就进入了内核态->(2)

(2)完成系统调用之后准备返回用户态,此时发送信号检测,如果进程中有信号,且信号的block标志位对应1,说明该信号不用管,直接进入(3);否则执行该信号:默认执行方法就执行完返回用户态直接进入(3);自定义信号执行方法需要返回用户态执行用户指定的方法,然后进入内核态,再返回用户态直接进入(3)

(3)开始下一轮系统调用

因此:信号不是立刻被执行的,有一个检测的过程

【四】信号的捕捉过程

(1)sigaction()

我们来看一个函数:(signal()+sigprocmask()==sigaction()【狗头】)

#include <signal.h> 
int sigaction(int signo, const struct sigaction *act, struct sigaction 
*oact);

参数:

第一个参数:要操作的信号编号

第二个参数:指向 struct sigaction 结构体的指针,用于设置新的信号处理规则(如果为 NULL,则只获取当前规则,不修改)

第三个参数:指向 struct sigaction 结构体的指针,用于保存旧的信号处理规则(如果为 NULL,则不保存)

struct sigaction:

struct sigaction {
    void (*sa_handler)(int);          // 简单信号处理函数(参数为信号编号)
    void (*sa_sigaction)(int, siginfo_t *, void *);  // 高级处理函数(带更多信息)
    sigset_t sa_mask;                 // 处理信号时需要阻塞的信号集
    int sa_flags;                     // 控制信号处理行为的标志
    void (*sa_restorer)(void);        // 已废弃(无需关注)
};

返回值:成功返回0;失败返回-1,设置errno

作用:为指定的信号(比如 SIGINT 中断信号、SIGTERM 终止信号)设置新的处理规则,或获取当             前已有的处理规则

使用说明:一般对于刚创建的结构体可以使用 memset()接口来置空成员

#include <string.h>

void *memset(void *s, int c, size_t n);

参数说明:
s:指向要操作的内存区域的指针(对于结构体,就是结构体变量的地址,用 & 取地址)
c:要设置的值(通常传 0,表示清零)
n:要设置的内存字节数(对于结构体,就是 sizeof(结构体变量) 或 sizeof(结构体类型))
(2)信号自动屏蔽

说明:当一个信号正在执行执行方法时,系统会自动将该信号屏蔽,避免多次出现该信号,在执               行方法完成时,再解除对该信号的自动屏蔽。如果你想在执行某个信号方法时,额外屏蔽               其它信号,可以使用 sa_flags 标志位(这是sigaction()的成员,一般带选项设值)

(3)如何获取pending集
#include <signal.h>

int sigpending(sigset_t *set);

参数:指向一个 sigset_t 变量,函数成功时自动将当前未决的信号集填充到 set 中

未决信号的特点:

  • 已被发送(如通过 kill() 或系统事件产生)。
  • 处于阻塞状态(在 block 信号集中),因此未被处理。
  • 一旦信号从 block 集中移除(解除阻塞),会立即被处理
#include <signal.h>

int sigismember(const sigset_t *set, int signum);

参数:

   set:指向要检查的信号集(sigset_t 类型)

   signum:要检查的信号编号(按对应标志位检查)

    for(int i=32;i>0;i--)
    {
        d = sigismember(&set, i);
        if(d==1)
        {
            std::cout<<"1";
        }
        else
        {
            std::cout<<"0";
        }
    }

返回值:

  • 若信号 signum 在 set 中:返回 1
  • 若信号 signum 不在 set 中:返回 0
  • 失败(如 signum 无效):返回 -1,并设置 errno=EINVAL

【五】可重入函数

简单理解:一个工程没有执行完立刻执行另外一个工程,此时容易发生事故。例如:

如果⼀个函数符合以下条件之⼀则是不可重⼊的:

(1)调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的

(2)调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构

【六】volatile关键字

我们看下面的代码:flag为全局变量;如果出现2号信号会执行自定义方法;执行while的条件是真

#include <stdio.h> 
#include <signal.h> 

int flag = 0; 

void handler(int sig) 
{ 

 printf("chage flag 0 to 1\n"); 
 flag = 1; 

} 

int main() 
{ 

 signal(2, handler); 
 while(!flag); 
 printf("process quit normal\n"); 
 return 0; 

} 

代码情况:当出现2号信号flag=1,而main里面循环的条件是为真(即一直循环),但是当你按下Ctrl+C,循环也一直就行不退出,按理说:flag会被设为1,不进入循环

理解:内存读写比寄存器慢得多,编译器会尽量把变量 “缓存” 到寄存器中。编译器会分析 “谁会修改flag”。在main函数内部,没有任何代码修改flag;而编译器默认不会跨函数分析 “其他函数是否修改了全局变量”,因此就需要volatile去修饰变量,告诉编译器该变量需要从内存读取

解释:编译器为了提高程序性能,会对代码进行优化(比如将变量缓存到寄存器,减少内存访问次数)。但如果变量的值可能被程序外部的因素修改(如多线程、硬件、信号等),这种优化就会导致程序逻辑错误。volatile 就像给变量贴了个 “警示牌”:每次用它都得去内存里查最新值

【七】子进程的“遗嘱”信号

如下直接出结论:

子进程在退出的时候会给父进程发生17号信号

而waitpid是可以在信号执行函数中回收子进程的

如果你想要子进程在退出的时候不捕获信号:signal (SIGCHLD, SIG_IGN)

那么内核会在子进程终止后直接清理其进程控制块(PCB),无需父进程调用 wait/waitpid

如果你想让子进程变成僵尸进程:不捕获也不忽略:signal(SIGCHLD, SIG_DFL)

那么内核会在子进程终止后不会清理其进程控制块(PCB)

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐