跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Unix/Linux 信号:原理、触发与响应机制实战

Unix/Linux 信号是操作系统用于进程间异步通知的机制。文章介绍了信号的基本概念、产生方式(键盘、kill、接口、异常、软件条件)、默认行为(终止、忽略、核心转储等)、发送与保存机制(位图、队列、未决/阻塞状态)。重点讲解了信号捕捉流程,包括用户态与内核态切换、sigaction 设置以及 volatile 关键字在信号处理中的重要性。最后补充了 SIGCHLD 信号的处理及子进程回收策略。

战神发布于 2026/3/16更新于 2026/10/2584 浏览
Unix/Linux 信号:原理、触发与响应机制实战

[图片]

信号的基本概念

在前言部分我们已经说到,信号是一种高效、及时的方式,用来传递信息、保证进程能够响应外部事件。下面主要介绍一些进程与信号的关联和概念。

  • 当一个进程接收到信号的时候,是允许不立即处理这个信号的;

因为有可能进程正在做更重要的事情,比如当一个进程正在向文件中写入数据,此次给他发送信号让他去做别的事,就有可能导致数据丢失,写入文件中的数据是不完整的。

  • 进程要能够识别信号,并且能处理信号;

每个进程都要有能力识别和处理信号,即使信号没有产生,也具备处理信号的能力,信号的处理能力是进程内置功能的一部分。

进程允许不立即处理信号,那么在这中间进程就必须有能力存储信号,在后面会详细讲解进程如何保存信号的。

[图片]

补充:前台进程:获取键盘输入的进程,Linux 中只允许有一个前台进程; 后台进程:没有获取键盘输入的进程,通过在执行可执行程序后面加 & 让可执行程序在后台运行。

当一个程序在后台运行的时候,我们使用 ctrl + c 就杀不掉了,不仅仅是 ctrl + c 所有通过键盘组合键的方式向后台进程发送信号都是无效的。

在 Linux 中通过 kill -l 可以查看操作系统中的所有信号,其中 1 - 31 是普通信号,运行不被立即处理,而 34 - 64 属于实时信号,产生后需要立即被处理:

[图片]

其中 ctrl + c 就是 2 号信号。

在操作系统中为我们提供了一个接口: sighandler_t signal(int signum , sighandler_t handler) 允许我们对信号的处理方法进行修改,即从定义信号的默认动作。

  1. 参数 1 表示要进行重定义的信号编号;
  2. 参数 2 的类型是 sighandler_t 是一个函数指针,typedef void (*__sighandler_t)(int); 无返回值,参数是 int 用来传信号编号。

当然该接口不能对 SIGKILL 和 SIGTSTP 进行捕捉。

通过该接口我们就可以对 2 号信号进行重定义,再使用 ctrl + c 看进程的反应。此处操作简单不再进行演示了。

我们在键盘上的输入是怎么被操作系统知道的???

操作系统读取 ctrl + c 信号的流程

CPU 不能直接与硬件进行数据交互,但是 CPU 在控制上可以与外设进行交互,也就是说 CPU 可以接收外设发送过来的信号。

键盘上的数据读取到内存中主要分 5 步:

以下是示意图:

[图片]

  1. 当我们在键盘上输入数据后,键盘会发送中断信号,将中断信号交给中断单元。中断单元是用来将接收到的信号进行排序,将优先级高的先发送给 CPU;
  2. 当 CPU 接收到键盘发送(通过 CPU 上的针脚进行接收,实际上就是高低电压)过来的中断信号之后,CPU 知道键盘上有数据要进行读取。所以 CPU 要找读取键盘的方法;
  3. 在内核中有一个中断向量表,就是指针数组,指向各个外设的交互方法,键盘向 CPU 发送的中断信号中包含中断号,该中断号就指引 CPU 执行中断向量表中的哪一个方法;
  4. CPU 拿着中断号,执行读取键盘数据的方法,将键盘输入的数据读取到内存中;
  5. 对于读取上来的数据,操作系统将进行检查,发现是 ctrl + c 组合键,就会向前台进程发送 2 号信号。

信号的产生于我们自己代码的运行是异步的,两个互不相干,进程不知道什么时候会有信号,同样信号也不知道进程现在处于什么情况;

信号是进程间异步通知的一种方式,属于软中断.

信号的处理方式可以分为三类:

  1. 默认动作,执行操作系统的默认动作;
  2. 自定义动作,通过 接口来自定义动作;
signal()
  • 忽略。
    • signal 可以对信号的动作进行执行,如果第二个参数是函数指针,就对自定义捕捉信号的动作;
    • 如果第二个参数是 SIG_IGN 表示忽略该信号;SIG_DFL 表示执行默认动作。

    信号的产生

    信号的产生方式可以分为 5 种:

    1. 键盘组合键;
    2. kill 命令;
    3. 系统接口/库函数;
    4. 异常;
    5. 软件条件。

    下面一一介绍。

    键盘组合键

    常见的键盘组合键有:ctrl + c 表示第二个信号 SIGINT 中断信号,ctrl + \ 第三个信号 SIGQUIT 退出进程,并生成核心转储文件,后面进行解释,ctrl + z 第 19 信号 SIGSTOP 暂停进程。

    关于键盘组合键的方式向进程发送信号,要求进程必须是前台进程。

    kill 命令

    在 Linux 操作系统中可以通过 kill + -信号编号 + 进程 PID 的方式向进程发送信号,可以通过 ps - axj 和 grep 命令来获取进程的 PID,只要知道进程的 PID 就可以向进程发送信号。

    kill 的使用很简单,此处不再过多赘述。

    系统接口/库函数

    在操作系统中提供了一些接口允许我们通过进程发送信号。

    int kill(pid_t pid , int sig):

    1. 参数一:向指定进程 ID 发送信号;
    2. 参数二:向进程发送 sig 信号;
    3. 返回值,0 表示发送成功,-1 表示失败。

    int raise(int sig):该接口于上一个对比,没有了进程的 PID,只能向调用该接口的进程发送信号。

    void abort(void): abort() 会向当前进程发送 SIGABRT 信号(编号为 6)。进程收到该信号后的默认行为是:

    • 终止进程;
    • 生成核心转储文件(core dump)(记录进程终止时的内存状态,用于调试 无法被忽略或阻止 与 SIGINT 等信号不同,abort() 触发的终止行为很难被完全阻止:
    • 即使程序注册了 SIGABRT 的自定义处理函数,abort() 仍可能在处理函数返回后继续终止进程(具体行为可能因系统而异,但通常会强制退出);
    • 唯一例外是在 SIGABRT 处理函数中调用 _exit() 或 exit() 主动控制退出方式,但这会绕过核心转储。

    异常

    我们编写的进程崩溃大多数都是因为程序出现了异常。

    我们在编码过程中一定有过除零和访问空指针导致程序崩溃的情况,其对应的信号分别是 SIGFPE 和 SIGSEGV。

    如果对异常信号进行捕捉会怎么样: 通过以下代码进行测试以下:

    void signal_handler(int sig){ std::cout << "Divide by zero signal caught" << std::endl;}
    int main(){
    // 对 8 号信号进行重定义
    signal(8, signal_handler);
    while(1){
    int a = 1;
    int b = 0;
    int c = a / b;
    sleep(1);
    }
    return 0;
    }
    

    现象:

    [图片]

    根据上面的现象可以看出,确实信号被捕捉了,但是好像一直都在被调用,这是为什么呢?

    下面将介绍一异常产生的过程。

    我们的代码时 CPU 在执行,操作系统时如何知道代码中有这些非法操作???

    此处先以除零为例:

    在 CPU 中有一群状态寄存器,可以理解为一个位图,通过 0,1 表示不同的状态; 其中有一个标志位是状态表示为溢出标志位,当一个数很大溢出时,该位置就会被标记,由 0 变成 1;

    因此除零异常的流程如下:

    1. 代码中出现/0 操作,导致得到的值很大出现溢出,CPU 中的溢出标志位被标记,CPU 出现硬件异常;
    2. 操作系统能够管理软硬件,所以自然也就知道 CPU 出现了异常,操作系统就会根据异常情况向进程发送信号,从而进程做出相应的反应。

    CPU 中的标志位被修改,会不会影响其他进程???

    不会,CPU 上的数据属于进程的上下文,当一个进程从 CPU 上拿下来的时候,也会带走该进程的上下文,来让下一次再放到 CPU 上执行时,知道从哪里继续,以及进程的状态情况。

    野指针异常与除零错误不太一样:

    • 当 CPU 访问资源的时候,要拿着虚拟地址在页表上在物理地址的位置,如果发现虚拟地址没有对应的物理地址,就会发生缺页中断,CPU 中同样也有一个寄存器专门存放没有对应的虚拟地址,此时操作系统会判断该虚拟地址对应的数据是还没有加载到内存中,还是非法访问。

    当操作系统认为该指针正在进行非法访问,同样也会向进程发送对应的信号。

    如何解释,当对异常信号进行捕捉后,没有崩溃,而是一直调用重定义的方法???

    当 CPU 运行一个进程时,如果该进程出现异常,CPU 就会出现硬件异常,在 CPU 中关于进程上下文的寄存器中就会标记该异常,操作系统识别异常后,向进程发送信号,进行捕捉后,并没有崩溃,而是继续在 CPU 上运行,此时 CPU 还处于硬件异常,操作系统继续向进程发送信号,循环往复。

    捕获信号的目的是什么???

    捕捉信号的目的并不是让我们在程序运行时对异常信号进行解决,而是让我们知道代码中有错误,要进行修正。

    软件条件

    异常不一定都是硬件 CPU 产生的,一些软件也有可能产生异常。

    最常见的软件异常有:管道异常 SIGPIPE,当一个管道的读端关闭后,写端进程就会收到该异常,导致写端进程被终止。

    闹钟异常 SIGALRM:

    在操作系统中有一个接口:unsigned int alarm(unsigned int seconds)

    • 调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终⽌当前进程。
    • 这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。

    打个比方,某人要小睡⼀觉,设定闹钟为 30 分钟之后响,20 分钟后被⼈吵醒了,还想多睡⼀会儿,于是重新设定闹钟为 15 分钟之后响,'以前设定的闹钟时间还余下的时间'就是 10 分钟。如果 seconds 值为 0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

    通过闹钟异常,我们可以实现一个代码,简单验证以下 IO 的速度:

    第一段代码是这样的:

    int main(){
    long long count = 0;
    alarm(1);
    while(1){ std::cout << "Count: " << count << std::endl; count++;}
    return 0;
    }
    

    第二段代码是这样的:

    long long count = 0;
    void signal_handler(int sig){ std::cout << "Count: " << count << std::endl; exit(0);}
    int main(){
    signal(SIGALRM , signal_handler);
    alarm(1);
    while(1) count++;
    return 0;
    }
    

    两个代码中闹钟都设置为 1,第一组代码中 IO 的次数比第二组更多,看最后两个 count 相差多少,即进入循环的次数相差多少。

    第一组:

    [图片]

    第二组:

    [图片]

    可以看到两者的循环次数相差很多,所以 IO 效率极低。

    我们也可以自定义闹钟异常,如果在闹钟异常的处理方法中,再加上一个下一次的定时闹钟,就可以实现重复闹钟,让闹钟每个固定时间响一次。

    在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如 alarm 函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的 SIGPIPE 信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。

    简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号生成。

    信号的默认行为种类

    通过 man 7 signal 可以查看信号的不同默认行为:

    [图片]

    1. Term
    • 含义:Terminate(终止)。
    • 默认动作:收到信号后,直接终止进程(无核心转储)。
    • SIGINT(Ctrl+C,终端中断)、SIGTERM(普通终止请求,可被捕获)。
    1. Ign
    • 含义:Ignore(忽略)。
    • 默认动作:收到信号后,完全忽略,进程无任何反应。
    • SIGHUP(部分守护进程会忽略'挂起'信号); SIGCHLD(父进程可忽略,让系统自动回收子进程资源)。
    1. Core
    • 含义:Terminate + Core Dump(终止 + 核心转储)。
    • 默认动作:
      1. 终止进程;
      2. 生成核心转储文件(core dump)(记录进程终止时的内存、寄存器状态,用于调试)。
    • SIGSEGV(段错误,如空指针访问)、SIGFPE(算术错误,如除零)、SIGABRT(abort() 调用触发)。
    1. Stop
    • 含义:Stop(暂停)。
    • 默认动作:让进程进入**'停止状态'**(无法继续运行,可通过 ps 看到状态为 T)。
    • SIGSTOP(强制暂停,不可捕获 / 忽略,用于系统级控制); SIGTSTP(Ctrl+Z,交互型暂停,可捕获,常用于终端程序暂停)。
    1. Cont
    • 含义:Continue(继续)。
    • 默认动作:如果进程当前处于**'停止状态'(如被 Stop 类信号暂停),则恢复进程运行**。
    • SIGCONT(唯一作用是恢复暂停的进程)。

    在上面有两个好像很类似:Core 和 Term 都是终止进程,有什么区别呢?

    Core 在退出之前,操作系统将进程在用户空间类的数据 dump(转储)到进程当前目录 (磁盘) 上形成 core.pid 文件。

    [图片]

    在进程退出的信息中,也有 core dump 标志位,记录文件终止收到信号的类型,是不是 Core 类型。

    为什么要进行 core dump 核心存储???

    进程异常终止通常是因为有 Bug,比如非法内存访问导致段错误,事后可以用调试器检查 core 文件以查清错误原因,这叫做 Post-mortem Debug(事后调试)。

    在 gdb 调试的时候,将核心转储文件调入进去就可以看到是哪个位置出现异常,下面以除 0 为例:

    [图片]

    如果你使用的是云服务器,默认 core dump 功能是关闭的,可以通过 ulimit -a 来查看,因为云服务器上运行的大多都是公司的服务端,如果服务端挂了,会自动重启,如果一个程序一直启动,一直挂,就会导致磁盘中存储大量的 core.pid 无效文件。

    如果你希望在云服务器上打开这一功能,可以使用 ulimit -c 指令进行打开。

    [图片]

    信号的发送

    操作系统向进程发送信号实际上是向进程得 task_struct 结构体进行发送的。

    在进程的结构体中,有一个位图,每个位置表示不同的信号,其中使用 0,1 表示信号是否产生。 因此,所谓的发信号,本质就是操作系统改变了进程 task_struct 中的信号位图对应的比特位。

    • 操作系统是进程的管理者,只有它有资格修改进程中的属性。

    因为操作系统中位图只有 0 和 1,因此当一个进程接收到一个普通信号后,将对应的比特位置为 1 后,再发送相同的普通信号也会有不有反应,知道该信号被处理后,再发送信号才有反应。

    实时信号的管理与普通信号不一样,实时信号被发送后会被立即处理,实时信号是通过队列来进行维护的,队列中存储信号属性的结构体,实时信号发送几次就必须被处理几次,

    信号的保存

    进程收到信号后,可以不立即处理,还需要结合进程自己的情况判断什么时候进行处理,因此在此期间信号必须能够被保存。

    信号的几个概念:

    • 执行信号的处理动作叫信号递达 Delivery;
    • 信号从产生到递达之间的状态,叫做信号未决 Pending;
    • 进程可以阻塞某一种信号,当接收到被阻塞的信号后,该信号将一直处于信号未决状态,不会被处理,知道进程解开阻塞;
    • 注意:阻塞和忽略是不一样的行为,被阻塞的信号不会被递达,但是被忽略的信号会被递达,只不过对该信号的处理方法是忽略。

    在一个进程的 task_struct 的结构体中,必须能够存储那些信号是阻塞的,那些信号是未决的,以及每个信号的处理方法:

    struct task_struct {
    struct sighand_struct *sighand; // 每个信号对应的处理方法
    sigset_t blocked; // 存储被阻塞的信号
    struct sigpending pending; // 存储处于未决的信号
    }
    

    首先先看一下 struct sighand_struct 结构体:

    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;
    };
    typedef void (*__sighandler_t)(int);
    struct __new_sigaction {
    __sighandler_t sa_handler; // 信号对应的处理方法
    unsigned long sa_flags;
    __sigrestore_t sa_restorer; /* not used by Linux/SPARC yet */
    __new_sigset_t sa_mask;
    };
    

    sigset_t blocked 的结构就比较简单,就是用一个数组来模拟位图:

    sigset_t blocked;
    typedef struct {
    unsigned long sig[_NSIG_WORDS];
    } sigset_t;
    

    上面的 sigset_t 被称为信号集,阻塞信号集也被称为信号屏蔽字。

    struct sigpending pending 使用一个链表将所有处于未决的信号连接起来:

    struct sigpending pending;
    struct sigpending {
    struct list_head list;
    sigset_t signal; // 存储没有被处于未决的信号
    };
    

    以下是上面对应的示意图:

    [图片]

    用户如何对这些表进行操作,尤其是 block 表

    信号集操作函数

    操作系统中提供了信号集结构体 sigset_t,我们对所有信号表的修改都需要通过该结构体:

    1. int sigemptyset(sigset_t *set),将信号集全部置为 0;
    2. int sigfillset(sigset_t *set),将信号集全部置为 1;
    3. int sigaddset(sigset_t *set , int signo),向信号集中添加一个信号,即将对应的信号置为 1;
    4. int sigdelset(sigset_t *set , int signo),从信号集中删除一个信号,即将对应的信号置为 0;
    5. int sigismember(sigset_t *set , int signo),在信号集中判断一个特定信号是否存在。

    以上所有接口的返回值都是 0 表示成功,-1 表示失败; 而 sigset_t 都是要进行操作信号集的地址,信号集要自己进行定义; int signo 表示信号编号。

    最终我们将自己希望的信号集设置好后,还要将其设置进进程的 task_struct 结构体中:

    int sigprocmask(int how , const sigset_t *set, sigset_t *oset):

    1. 返回值 0 成功,-1 失败;
    2. 第一个参数,选项,选择如何进行操作:SIG_BLOCK新增屏蔽字,相当于 mask |= set;SIG_UNBLOCK删除屏蔽字,相当于 mask &= set;SIG_SETMASK设置当前信号屏蔽字为 set,相当于 mask = set;
    3. 第三个参数,是一个输出型参数,记录修改之前的 block 表,如果后续需要恢复就可以直接找到。

    还有一个接口,可以让我们直接获取进程中的 pending 表:

    int sigpending(sigset_t *set),参数是输出型参数,将 pending 从进程中带出来。

    以上接口就这些,下面进行简单实验:将所有的信号设置为阻塞,重复打印进程中的 pending 表,观察 pending 表的变化。

    代码如下:

    // 打印未处理的信号集
    void PrintPending(sigset_t &set){
    for(int i = 1; i < 32; i++){
    std::cout << (sigismember(&set, i)?"1":"0");
    }
    std::cout << std::endl;
    }
    int main(){
    sigset_t set;
    sigfillset(&set); // 将信号集全部置为 1
    // 此时我们在栈上定义了一个 sigset_t,但是并没有将其传递给进程中,也就是说进程中并没有进行设置
    // 将信号集设置进进程中
    sigprocmask(SIG_SETMASK,&set,nullptr); // 阻塞信号集中信号
    sigset_t sig_pending;
    while(1){
    sigpending(&sig_pending); // 获取当前进程中未处理的信号集
    PrintPending(sig_pending);
    sleep(1);
    }
    return 0;
    }
    

    下面看实验现象:

    [图片]

    信号的捕捉

    进程会在时机合适的时候,对信号进行处理,什么时候叫做时机合适???

    答案:操作系统会在进程从内核态回到用户态的时候对信号进行检查,并对信号进行处理。 关于这一句话的含义及步骤,以下将进行详细讲解。

    我们都是到,在进程地址空间中,只有 0-3G 属于用户,而 3-4G 是属于操作系统的,所以这也就意味着每个进程的地址空间中,都有操作系统的代码和数据地址,可以访问操作系统的代码和数据。

    进程不能直接访问操作系统的代码和数据,操作系统不信任任何人,因此必须要有身份标识,才能进行访问,也就是进程必须处于内核态才行。

    当进程调用系统调用时,就需要进程从用户态转变为内核态,才能指向内核中的方法实现。而这一转化由操作系统来完成,操作系统负责进行进程的'身份'切换。

    不同的进程的代码和数据不一样,因此不同的进程用户空间时不一样的,但是所有进程都由一个操作系统进行管理,因此所有进程的内核空间中的代码和数据是一摸一样的。

    • 通过将操作系统的代码和数据映射到进程地址空间中,使得代码在执行的时候可以直接进行跳转来执行操作系统的代码,获取操作系统的数据。

    操作系统是软硬件的管理者,操作系统可以指挥进程的调度,那么操作系统也是一个软件呀,操作系统的运行机制是什么,换句话说操作系统怎么知道要调度哪一个进程,要调用哪一个方法???

    操作系统的内核运行在内核态(拥有最高权限),其执行依赖于硬件触发的'事件',这些事件会主动'唤醒'内核进行处理:

    • 中断(Interrupt):外部硬件(如键盘、鼠标、磁盘、网络卡)完成操作后,会向 CPU 发送中断信号,迫使 CPU 暂停当前任务,转而去执行内核中对应的'中断处理程序'。像前面讲的键盘输入 ctrl + c。
    • 异常(Exception):进程在运行中出现错误(如除以零、访问无效内存、系统调用)时,会触发 CPU 的异常机制,强制切换到内核态执行异常处理程序。
    • 时钟中断:CPU 内部的定时器会定期(如每 10ms)发送时钟中断,内核会利用这个事件触发'进程调度'—— 暂停当前运行的进程,根据调度算法选择下一个要运行的进程,实现多任务切换。

    操作系统本质就是一个死循环,操作系统会按预设逻辑进行工作:

    • 等待硬件事件(中断 / 异常),事件发生后执行相应的处理逻辑(如调度、IO 处理、资源分配),处理完后继续等待下一个事件;一直这样循环往复。

    此处以时钟中断为例:

    1. 当 CPU 接收到时钟中断时,就会到中断向量表中执行相应的方法;
    2. 此时操作系统被唤醒,先将进程从 CPU 上拿下来,根据调度算法,决定将哪一个进程放到 CPU 上进行执行,实现进程'并发'运行。

    上述的硬件中断信号和时钟中断都属于硬件中断,程序异常通常属于软件中断;

    以上这些情况都会导致进程从用户态进入内核态,CPU 在内核中执行相应的中断处理方法。

    CPU 中有ecs 寄存器,用来表示 CPU 上正在运行的进程处于用户态还是内核态。

    现在我们就可以真实了解进程是如何对信号进行处理的:

    先贴一张示意图:

    [图片]

    以下是具体的步骤流程: 0. 进程执行自己的代码;

    1. 遇到中断,异常,系统调用等,要进入内核,执行内核的相应方法;
    2. 进入内核,执行相应方法;
    3. 方法执行完毕,准备返回用户态之前,对信号的 pending 表,block 表进行检查,看是否有信号需要进行处理;
    4. 有信号要处理,如果信号的处理方式是忽略就不做处理;如果是默认处理方法,就直接在内核中调用默认处理方法;如果用户对信号进行捕捉了,要执行用户自定义的方法,此时因为自定义的方法在用户空间中,所以要先从内核态回到用户态执行相应的方法,执行完后,再回到内核态,看是否还有信号没有处理,继续处理;
    5. 所有信号处理完毕,从内核态回到用户态中,继续从原来代码后面位置执行。

    以上就是信号被处理的全流程。

    信号被处理完后,需要将 pending 表对应位置从 1 置为 0,那么是在信号处理完后置 0,还是在要执行信号处理前置为 0???

    答案:在要执行捕捉方法之前,先置为 0,再执行相应的方法。

    如果我对信号进行自定义捕捉,如果在执行捕捉方法时,我又再次发送这一个信号,此时 pending 表的对应位置又置为了 1,那么此时不就有可能导致在调用信号处理方法时,再次检查到该信号,再次调用该信号的捕捉方法,最终导致 handler 方法被循环调用???

    是的,这种情况有可能发生,因此操作系统在调用一个信号的处理方法的时候,会将该信号的 block 表对应位置置为 1,表示阻塞该信号,即使信号再次被发送也不会被执行,最等到信号的处理方法执行完后,才将 block 表对应位置置回 0。这样就防止了同一个信号被多次递交。

    在操作系统中提供了一个接口:允许我们设置在调用信号处理方法时,屏蔽那些信号。

    int sigaction(int signo , const struct sigactoin* act , struct sigactoin* oact)

    其中 struct sigaction 是操作系统提供的一个结构体:

    struct sigaction {
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    sigset_t sa_mask; /* mask last for extensibility */
    };
    
    1. sa_handler 表示函数指针,表示捕捉信号后,对应的处理方法;
    2. sa_mask 表示要额外阻塞的信号集.

    补充

    我们日常在进行开发的过程中,有些函数是不可以重复进入的,就不能同时有两个控制流在同一个函数中进行执行,如以下这个例子:

    [图片]

    当我们在进行 node1 节点插入的时候,还没有对头指针进行改变之前,因为某些原因导致,进程要去执行信号的处理方法,进入到内核中调用信号对应的处理方法,而该方法中也对链表进行了修改,此时就会导致不可预料的结果。如上图所示,node2 节点最后插入失败。

    • 像上例这样,insert 函数被不同的控制流调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数,这称为重⼊,insert 函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为 不可重入函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重入 (Reentrant) 函数。

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

    • 调⽤了 malloc 或 free,因为 malloc 也是⽤全局链表来管理堆的。
    • 调⽤了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。

    volatile 关键字

    volatile 关键字用来保证内存可见性。

    当一个数据在正常执行流中。不会被修改,只会被读取,此时编译器为了更快的访问到该数据,就会将数据放到寄存器中。

    但是如果因为一些原因,导致数据被修改,而 CPU 依旧使用寄存器中原来的数据,而不是重新在内存中进行读取就会导致,数据读取错误。

    int flag = 1;
    void signal_handler(int sig){
    flag = 0;
    std::cout << "flag changed to 0" << std::endl;
    }
    int main(){
    signal(2, signal_handler); // 设置信号处理函数
    while(flag);
    std::cout << "flag is 0, exit" << std::endl;
    return 0;
    }
    

    可以通过执行以上代码,看如果发送二号信号进程是否退出了:

    [图片]

    可以看到通过 ctrl + c, flag 的值确实改变了,但是编译器是从寄存器中拿的 flag 值,因此一直拿到的都是 0;

    如果我们希望每一次在使用这个变量的时候,都去内存中拿,可以通过在该变量前面加上 volatile 关键字;同样如果我们希望一个变量放到寄存器中让获取更快,可以使用 register 关键字。

    编译器也有优化等级,不同的优化等级,编译器处理的优化的成不不同:O0 , O1 , O2 , O3… O0 表示优化最高,后面优化逐渐下降。

    SIGCHLD 信号

    子进程在退出前会向父进程发送 SIGCHLD 信号,告诉父进程代码执行完了,要退出了。

    所以在进行子进程回收的时候,可以对 SIGCHLD 进行捕捉,来进行进程等待,获取子进程的退出信息。

    在前面我们谈到在进行 SGICHLD 捕捉的时候,会将该信号加入到阻塞中,如果此时有多个子进程都退出,都发送了 SGICHLD 信号,但是我们在 pending 表中只能设置一个,就会导致其他有些子进程没有被回收。

    为了解决上面的问题,在进行信号等待的时候使用轮询式的等待方式,而不是只等待一个子进程,一次性等待多个子进程即可。

    void signal_handler(int signo){
    while(waitpid(-1, nullptr, WNOHANG) > 0){ ; }
    }
    

    我们同样也可以通过 signal 将 SIGCHLD 信号设置为忽略,此时子进程退出时就会直接退出,父进程不用进行回收了,同样父进程也就获取不了子进程的退出信息。

    目录

    1. 操作系统读取 ctrl + c 信号的流程
    2. 信号的产生
    3. 键盘组合键
    4. kill 命令
    5. 系统接口/库函数
    6. 异常
    7. 软件条件
    8. 信号的默认行为种类
    9. 信号的发送
    10. 信号的保存
    11. 信号集操作函数
    12. 信号的捕捉
    13. 补充
    14. volatile 关键字
    15. SIGCHLD 信号

    更多推荐文章

    查看全部
    • ChatTTS 开源语音模型部署与使用指南
    • 利用 AI 大模型从多模态数据获取业务洞察的实践
    • DeepSeek-V3 开源:671B 参数 MoE 模型性能解析
    • Java 项目构建与管理:Maven 核心实战指南
    • jQuery 4.0 正式发布:重构核心,拥抱现代 Web
    • PostgreSQL 利用动态 SQL 实现自动按月创建表分区
    • 飞算 JavaAI 工具重塑 Java 开发体验测评
    • Android 组件化与插件化架构详解与实践指南
    • VMware vSAN 7.0 使用 RVC 将闪存设备标记为容量设备
    • AI 时代产品核心功能落地全流程管控方法
    • AnythingLLM 集成 Whisper 实战:构建高效语音转文本方案
    • 文心一言大模型本地部署与微调实战
    • 火影忍者主题网页设计与实现
    • AIGC 在现代教育技术中的应用实践
    • 深度解析 KBQA 常用数据集:WebQSP 与 CWQ
    • PyCharm 与 GitHub Copilot 配置指南:学生认证与账户安全
    • 顺序文件的基本概念与查找算法
    • SQL Server 2008 R2 安装教程及常见错误解决
    • 秋叶绘世 Stable Diffusion 整合包与 ComfyUI 启动器说明
    • OpenClaw 开源 AI 助手安装与配置指南

    相关免费在线工具

    • Base64 字符串编码/解码

      将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

    • Base64 文件转换器

      将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

    • Markdown转HTML

      将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

    • HTML转Markdown

      将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

    • JSON 压缩

      通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

    • JSON美化和格式化

      将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online