Linux —— 信号(3)

时间:2024-05-08 11:06:25

Linux —— 信号(3)

  • Core dump
  • 为什么core默认是被关闭的
  • 阻塞信号
    • 信号其他相关常见概念
    • 信号递达
    • 信号未决
    • 信号阻塞
    • 两者的区别
    • 信号的结构
  • 信号集操作函数
  • 一个简单使用例子
  • sigpending的使用例子

我们今天接着来了解信号:

Core dump

大家不知道有没有留意过,如果有 / 0 的错误,一般会报这个错误:

#include <stdio.h>  
#include <stdlib.h>  
#include <signal.h>  
#include <unistd.h>  
#include <cstring>
#include<iostream>

int main()
{
    int b = 10;
    int a = b / 0;

    return 0;
}

在这里插入图片描述
我们可以在7号手册里查看signal的一些信息:
在这里插入图片描述
action这一栏,如果有core的话,一般会触发Core dump(核心转储)。

Core dump,中文可译作“核心转储”或通俗地称为“吐核”,是在计算机科学中一个术语,特指当一个程序异常终止(由于错误、异常或收到特定信号)时,操作系统捕捉该程序当时的内存状态,并将其保存到一个文件中,这个文件即称为core dump文件。此操作允许开发者事后分析程序崩溃的原因。

Core dump文件包含但不限于以下信息:

  • 进程执行时的内存内容,即程序崩溃瞬间的“内存快照”。
  • 寄存器状态,包括程序计数器、栈指针等,这对于重建程序执行流程至关重要。
  • 内存管理信息,帮助理解程序如何使用分配的内存。
  • 其他处理器和操作系统相关状态信息。

这些信息对于调试非常宝贵,因为它们可以帮助程序员确定导致程序失败的确切原因,尤其是在那些难以复现的错误场景中。通过使用如GDB(GNU Debugger)这样的调试工具,开发者可以加载core dump文件和相应的可执行文件,逐步检查程序崩溃时的状态,进而定位和修复错误。

我们可以用ulimit -a查看我们关于core dump的设置:
在这里插入图片描述这里的core file设置为0,表明在触发时不会生成core file,如果我们想让它生成core file文件,我们要对其进行设置:

ulimit -c 大小

这样可以临时在当前会话中开辟core file的文件大小,这里注意一下,这只是临时设置,如果重新登录,就会重新归0。

在这里插入图片描述
这个时候运行程序:
在这里插入图片描述
在比较新的Centos中,core 文件会存放在这个路径下:
在这里插入图片描述我们看看下面有哪些文件:
在这里插入图片描述
得到了一个zst的压缩文件,我们解压一下:
在这里插入图片描述
然后我们开启gdb:
在这里插入图片描述然后我们执行以下命令:

core-file 文件名

在这里插入图片描述
我们可以从这些信息中得到:
在这里插入图片描述

为什么core默认是被关闭的

core文件中有错误的详细信息,为啥默认是关闭的呢?

Core files,默认关闭的主要原因有以下几点:

  1. 安全性考虑:Core文件可能包含敏感信息,如程序运行时的内存数据,这可能包括密码、密钥或是其他重要数据。如果这些信息落入不当之手,可能会造成安全风险。因此,默认关闭可以减少潜在的信息泄露。
  2. 磁盘空间占用:Core文件可能非常大,特别是对于消耗大量内存的应用程序来说。如果不加限制地生成,可能会迅速消耗系统磁盘空间,影响系统的正常运行和其他程序的功能。
  3. 性能考量:频繁生成Core文件会对系统性能产生一定影响,尤其是在高负载或资源紧张的服务器环境中,额外的I/O操作来写入这些大文件可能会成为瓶颈。
  4. 默认保守策略:系统管理员通常倾向于采取较为保守的配置策略,避免不必要的麻烦。只有当特定需求出现时,比如在开发和调试环境中,才会选择性地开启Core文件生成,以便于问题诊断。
  5. 用户意识:不是所有用户都了解如何正确地使用和分析Core文件,对于非技术用户来说,这些文件可能是无用且占空间的。因此,默认关闭可以避免给普通用户带来困惑。

尽管如此,对于开发者和系统管理员来说,Core文件是一个强大的调试工具,能够在程序崩溃时提供宝贵的诊断信息。因此,在开发和测试环境中,通常会根据需要配置开启Core文件生成,并合理设定其大小和存放位置,以平衡调试便利性和系统管理的需求。

阻塞信号

信号其他相关常见概念

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

这里我们来介绍一下信号递达和信号未决

信号递达

信号递达(Signal Delivery)指的是操作系统将一个已产生的信号正式通知给目标进程的过程。当一个信号被发送给一个进程,操作系统负责确保这个信号最终被该进程知晓并采取相应的行动。这一过程包括几个关键步骤:

  1. 信号产生:信号可能由硬件异常(如除零错误)、软件请求(如通过kill系统调用)、终端输入(如Ctrl+C)或其他进程操作产生。
  2. 检查信号屏蔽:在信号尝试递达之前,操作系统会检查目标进程的信号屏蔽字(signal mask)。如果该信号被进程阻塞(即包含在信号屏蔽字中),则信号不会立即递达,而是保持未决状态,直到进程解除对该信号的阻塞。
  3. 安排递达:如果信号未被阻塞,操作系统会选择合适的时机将信号递交给进程。这个时机通常是进程当前指令执行完毕,或者在某些可中断点。
  4. 处理信号:信号递达后,进程可以选择如何响应:
  • 默认动作:如果没有为信号安装自定义的处理函数,操作系统会执行该信号的默认动作,比如终止进程(如SIGINT, Ctrl+C)、忽略(如SIGCHLD)或暂停进程(如SIGSTOP)。
  • 自定义处理函数:如果进程之前通过sigactionsignal系统调用注册了信号处理器,则当信号递达时,会调用该处理器函数。在信号处理函数执行期间,可能会有其他信号的递达被临时阻塞,以避免并发处理信号导致的复杂情况。
  1. 信号处理完成:信号处理结束后,进程恢复正常的执行流程,除非信号导致了进程终止或产生了其他不可逆的状态变化。

信号递达机制是操作系统提供的一种进程间通信和异常处理的方式,对于编写健壮的、能够优雅处理外部事件的程序至关重要。

信号未决

信号未决(Signal Pending)是指在计算机操作系统中,尤其是Linux系统环境下,一个信号已经产生但尚未被进程处理的状态。当一个信号被发送给一个进程,但该进程目前因为某些原因不能立即处理这个信号时,这个信号就会处于未决状态。

信号未决主要发生在以下几种情况:

  1. 信号阻塞:进程通过设置信号屏蔽字(signal mask)来阻塞某些信号。当被阻塞的信号产生时,它不会立即被递送到进程,而是保持在未决状态,直到进程解除对该信号的阻塞。
  2. 信号处理:如果进程正在处理另一个信号,新产生的信号可能会等待当前信号处理完成后再进行处理,这时新信号也是未决的。
  3. 特定时机:在某些特定执行点,如系统调用的不可中断点,即使信号没有被显式阻塞,也可能暂时保持未决状态,直到该操作完成。

信号未决状态是暂时的,它确保了信号在适当的时候被处理,而不是在进程执行某些关键操作时打断进程,从而保护了程序的稳定性和数据的一致性。进程可以通过调用如sigpending这样的系统调用来查询当前有哪些信号处于未决状态。

简单点来说阻塞的意思就是一直让信号处于未决状态
下面我们来介绍信号阻塞:

信号阻塞

信号阻塞是操作系统中一种管理信号(signals)的机制,尤其在Unix和类Unix系统如Linux中较为常见。这一机制允许进程暂时阻止(或“屏蔽”)某些信号的递达,直到进程准备好处理它们。以下是信号阻塞概念的核心要点:

  1. 信号(Signals):信号是操作系统用来通知进程发生了某种事件的机制。这些事件可能包括用户请求终止进程(如Ctrl+C产生的SIGINT)、硬件异常、软件条件(如除零错误)等。
  2. 信号屏蔽字(Signal Mask):每个进程都有一个信号屏蔽字,这是一个数据结构,通常是一个位图,其中每个位对应一个可能的信号。如果某个信号对应的位被设置(通常是1),则表示该信号当前被阻塞,即系统不会立即将该信号递交给进程处理。
  3. 阻塞行为:当一个被阻塞的信号产生时,它不会立即影响进程的执行。相反,该信号进入一个“未决状态”,等待进程解除对它的阻塞。这意味着进程可以继续执行而不受该信号的即时干扰,直到它准备好处理这些信号。
  4. 解除阻塞:进程可以通过调用如sigprocmask的系统调用来修改其信号屏蔽字,从而解除对某些信号的阻塞。一旦解除阻塞,任何处于未决状态的相应信号将立即或按先进先出的顺序递达给进程进行处理。
  5. 与信号忽略和默认处理的区别
  • 忽略:如果进程配置了忽略某个信号,那么即使信号未被阻塞,当信号到达时也会被直接丢弃,不会有任何处理动作。
  • 阻塞:信号被阻塞时,它不会被立即处理,而是等待解除阻塞后再递达。
  1. 应用场景:信号阻塞常用于保护关键代码段,确保在执行某些不允许被中断的操作(如资源锁定、数据结构更新等)时,不会因为外部信号的干扰而引发错误或不一致的状态。

综上所述,信号阻塞是进程控制信号处理时机的一种策略,有助于提高程序的稳定性和可靠性。

两者的区别

信号阻塞和信号未决是信号处理机制中的两个关键概念,它们在Linux和其他类Unix系统中扮演着重要角色,但代表了不同的状态和操作:

信号阻塞(Signal Blocking):

  • 定义: 信号阻塞是一种主动的控制机制,通过设置进程的信号屏蔽字(通常使用sigprocmask函数),使得某些信号在产生后不会立即递交给进程进行处理。这相当于为进程设置了一个过滤器,暂时搁置特定信号的处理。
  • 作用: 主要用于保护关键代码段,防止信号中断导致的不一致性或错误。例如,在进行文件系统操作或资源锁的管理时,避免因信号中断而导致数据损坏或死锁。
  • 状态: 信号阻塞是一个动态的开关动作,可以随时开启或关闭。当信号被阻塞时,它仍然可以被内核生成,但不会送达进程,直到解除阻塞。

信号未决(Signal Pending):

  • 定义: 信号未决是一种状态描述,指的是一个或多个信号已经生成但尚未被进程处理的情况。当信号由于阻塞或其他原因(如正在处理另一个信号)不能立即递达时,就会处于未决状态。
  • 作用: 表明有信号等待处理,一旦进程准备好(比如从信号阻塞状态恢复),这些未决信号将会被依次递达并执行相应的处理动作。
  • 状态: 信号未决是一个中间状态,反映了信号从生成到实际处理之间的时间差。信号未决状态由内核维护,并且对于用户空间进程来说通常是透明的,尽管可以通过如sigpending系统调用来查询当前进程的未决信号集。

总结:

信号阻塞是一种预防措施用于控制何时允许信号影响进程,而信号未决描述的是信号已经被生成但还未被进程实际处理的情景信号阻塞导致信号进入未决状态,解除阻塞后,未决信号会被递达处理。这两个概念共同构成了信号处理机制中关于信号何时及如何被响应的核心逻辑。

信号的结构

加上了阻塞之后,我们信号大致就分为三个板块:

  1. 是否阻塞
  2. 是否未决
  3. 信号
    在这里插入图片描述

对于阻塞和未决我们还有几点要注意:

  1. 未决不一定会阻塞(可能只是单纯的没时间处理)
  2. 阻塞一定未决(阻塞的精髓在于根本就没收到信号)
  3. 忽略不是阻塞(忽略是接收到信号之后所做的决定,而阻塞是根本没收到信号)

信号集操作函数

信号集操作函数在Linux系统编程中用于管理和处理进程接收到的信号。这些函数允许程序注册对特定信号的处理方式、屏蔽(阻止)或解除屏蔽某些信号,以及查询信号的当前状态。以下是几个常用信号集操作函数的简要说明:

  1. sigemptyset, sigfillset, sigaddset, sigdelset, sigismember:
  • sigemptyset(sigset_t *set): 初始化信号集set,清空所有信号位,使得信号集不包含任何信号。
  • sigfillset(sigset_t *set): 将信号集set中的所有信号位设置为1,意味着信号集包含所有可能的信号。
  • sigaddset(sigset_t *set, int signum): 向信号集set中添加指定的信号signum
  • sigdelset(sigset_t *set, int signum): 从信号集set中移除指定的信号signum
  • sigismember(const sigset_t *set, int signum): 检查信号signum是否在信号集set中,如果是返回非0值,否则返回0。
  1. sigprocmask:
  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset): 用于改变当前进程的信号屏蔽字。how参数指定了操作方式(SIG_BLOCK, SIG_UNBLOCK, 或SIG_SETMASK),set指定了新的信号屏蔽字,oldset(如果非NULL)用来保存之前的信号屏蔽字。
    在这里插入图片描述
  1. sigpending:
  • int sigpending(sigset_t *set): 查询当前进程的未决信号集,即将要递达给进程但因被阻塞而暂未处理的信号。结果保存在set指向的信号集中。
  1. sigaction:
  • int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact): 更高级的信号处理函数,用于改变进程对指定信号signum的行为。act指定了新的信号处理动作,oldact(如果非NULL)用于保存先前的信号处理动作。

这些函数允许程序以细粒度控制对信号的响应,增强了程序处理异步事件的能力和灵活性。在多线程环境中,可能还需要考虑使用线程特定的信号掩码函数,如pthread_sigmask

一个简单使用例子

下面的这段代码阻塞了2号信号,从表面来看好像从没接受到一样:

int main()
{
   sigset_t set,oset;  //信号集

   //初始化信号集
   sigemptyset(&set); //最新信号集
   sigemptyset(&oset); //老信号集

   //向当前进程的信号集添加信号
   sigaddset(&set,SIGINT);
   

    //设置阻塞
    sigprocmask(SIG_BLOCK,&set,&oset); //向set设置阻塞信号,在这之前,备份到oset中
   
   while(true)
   {
     std::cout << "process is running... pid :" << getpid() << std::endl; 
     sleep(1);
   }

}

在这里插入图片描述
为了表明确实阻塞了,我们可以设计一个计数器,等到计数器为5时,解除阻塞:

void signal_handler(int signum)
{
    std::cout << "signum: "<< signum << std::endl;
    //exit(0);
}


int main()
{
   signal(2,signal_handler); //设置自定义行为
    
   sigset_t set,oset;  //信号集

   //初始化信号集
   sigemptyset(&set); //最新信号集
   sigemptyset(&oset); //老信号集

   //向当前进程的信号集添加信号
   sigaddset(&set,SIGINT);
   

    //设置阻塞
    sigprocmask(SIG_BLOCK,&set,&oset); //向set设置阻塞信号,在这之前,备份到oset中
   
   int cnt = 0;
   while(true)
   {
     std::cout << "process is running... pid :" << getpid() << std::endl; 
     if(cnt == 5)
     {
        sigprocmask(SIG_UNBLOCK,&set,&oset); //解除阻塞信号
        kill(getpid(),2);
     }
     cnt++;
     sleep(1);
   }

}

在这里插入图片描述

sigpending的使用例子

sigpending的作用是查看当前的信号是否还是在阻塞状态,是为1,不是为0。下面这个例子我们先阻塞2号信号5秒,然后解除2号信号,看相应的位数变化:

#include<iostream>
#include<signal.h>
#include <unistd.h>
#include <stdlib.h>

// 信号处理函数
void signal_handler(int signum)
{
    std::cout << "signum: "<< signum<< std::endl;
    //exit(0);
}

// 打印信号集中的信号
void PrintSign(const sigset_t &pending)
{
    for(int i = 1 ; i <= 31 ; i++)
    {
        if(sigismember(&pending,i))
        {
            std::cout << "1";
        }
        else
        {
            std::cout<<"0";
        }
    }
    std::cout<<"\n";
}

int main()
{
   // 设置自定义信号处理函数
   signal(2,signal_handler);
    
   sigset_t set,oset;  // 信号集

   // 初始化信号集
   sigemptyset(&set); // 最新信号集
   sigemptyset(&oset); // 老信号集

   // 向当前进程的信号集添加信号
   sigaddset(&set,SIGINT);
   
   // 设置阻塞信号
   sigprocmask(SIG_BLOCK,&set,&oset); // 向set设置阻塞信号,在这之前,备份到oset中
   
   int cnt = 0;
   sigset_t pending;
   while(true)
   {
        if(cnt == 5)
        {
            // 解除阻塞信号
            sigprocmask(SIG_UNBLOCK,&set,&oset);
            // 发送信号
            kill(getpid(),2);
        }

        cnt++;
        // 发送信号
        kill(getpid(),2);
        // 获取当前进程的挂起信号集
        sigpending(&pending);
        // 打印信号集中的信号
        PrintSign(pending);
        // 暂停1秒
        sleep(1);
    }

}

在这里插入图片描述

这段代码主要演示了如何使用信号处理函数、信号集和信号阻塞。程序中设置了一个自定义的信号处理函数signal_handler,当接收到信号2时,会调用该函数。在main函数中,首先初始化信号集,然后向信号集中添加信号SIGINT。接着,使用sigprocmask函数设置阻塞信号。在循环中,当cnt等于5时,解除阻塞信号并发送信号2。在每次循环中,发送信号2,获取当前进程的挂起信号集,并打印信号集中的信号。最后,暂停1秒。