linux-信号保存和处理

1. 信号其他相关常见概念

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

2. 在内核中的表示

每个信号都有两个标志位,分别表示阻塞(block)和未决(pending) ,这两个标志位本质上是保存收到的的信号的位图,比特位的位置表示的是第几号信号,比特位的内容表示是否阻塞。 还有一个函数指针表示处理动作

信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作

  • SIGINT信号产生过,但正在被阻塞 ,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞 ,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signum)
{
    std::cout << "signal: " << signum <<  std::endl;
    signal(2, SIG_DFL);
    std::cout << "恢复信号" << std::endl;
}
int main()
{
    signal(2, handler); //自定义信号
    //signal(2, SIG_IGN); //忽略信号

    while (true)
    {
        sleep(1);
        std::cout << "." << std::endl;
    }
    
    return 0;
}

3. 信号集操作函数

3.1 sigset

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

  • 在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。

  • 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。

cpp 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。

  • 注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1

sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

3.2 sigprocmask

在 Linux 中,sigprocmask 是用于 设置或检查进程的信号屏蔽字(信号阻塞集) 的系统调用。它用于 阻塞(屏蔽)或解除阻塞某些信号,以控制进程对特定信号的处理方式。

函数原型:

cpp 复制代码
#include <signal.h> 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数说明

- how:指定如何修改信号屏蔽字,假设当前的信号屏蔽字为mask。

cpp 复制代码
SIG_BLOCK:将 set 指定的信号 加入 到当前进程的信号屏蔽字中(即阻塞这些信号)。
mask = mask | set

SIG_UNBLOCK:从当前信号屏蔽字中 移除 set 指定的信号(即解除阻塞)。
mask = mask & ~set

SIG_SETMASK:用 set 指定的信号集 直接替换 进程当前的信号屏蔽字。
mask = set

- set:指定要修改的信号集。如果 NULL,则不改变当前信号屏蔽字,仅用于获取当前信号屏蔽字(结合 oldset 使用)。

- oldset:如果不为 NULL,则 sigprocmask 在修改信号屏蔽字前,会将进程当前的信号屏蔽字 保存oldset 中。

返回值

成功返回 0

失败返回 -1,并设置 errno 以指示错误原因(如 EINVAL 表示 how 参数无效)。

3.3 sigpending

在 Linux 中,sigpending 用于获取当前 进程未决信号集(pending signals),即那些已经产生但因被阻塞而未能递达的信号。

函数原型

cpp 复制代码
#include <signal.h> 
int sigpending(sigset_t *set);

参数说明

set:指向 sigset_t 类型的信号集变量,用于存储进程当前未决的信号集合

返回值

成功返回 0

失败返回 -1,并设置 errno 以指示错误原因(通常不会失败)。

产生1到9号命令

9号进程不可被捕捉,不可被屏蔽(SIGSTIOP(19)号进程也是)

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <vector>
#include <functional>
#include <unistd.h>

void PrintPend(sigset_t &pending)
{
    printf("我是一个进程,pid:%d, pending: ", getpid());
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}
void handler(int signum)
{
    std::cout << "递达" << signum << "号信号" << std::endl;
    std::cout << "######################" << std::endl;
    sigset_t pending;
    int n = sigpending(&pending);
    PrintPend(pending);
    std::cout << "######################" << std::endl;
}
int main()
{
    signal(2, handler);
    // 1. 屏蔽所有信号
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);

    sigaddset(&block, 2);
    int n = sigprocmask(SIG_SETMASK, &block, &oblock);
    int cnt = 0;
    while (true)
    {
        sleep(1);
        // 2. 获取pending信号集
        sigset_t pending;
        int m = sigpending(&pending);

        // 3. 打印
        PrintPend(pending);
        cnt++;
        if(cnt == 10)
        {
            std::cout << "解除对二号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK, &oblock, NULL);
        }
    }

    return 0;
}

由上面的结果可知,当我们准备递达的时候,要首先清空pending信号集中对应的位图。

4. 终止信号core vs term

在 Linux 中,终止信号(Termination Signals) 主要用于终止进程,而不同的终止信号可以导致进程以不同的方式退出。其中,coreterm 是两类不同的终止方式。

core 终止信号会导致进程异常退出,并在当前路径或指定目录下生成一个 core dump 文件。进程在退出前,其内存中的核心数据会被拷贝到磁盘,形成 core dump,这一过程称为核心转储(Core Dump)

term 终止信号(如 SIGTERMSIGINT)则仅会使进程退出,不会生成 core dump 文件。

不过在云服务器上,core dump的功能是默认关闭的,我们可以通过ulimit命令查看、修改 。

cpp 复制代码
ulimit -a:查看当前用户的所有资源限制。
ulimit -c:查看 core dump 文件的大小限制(单位:blocks,0 表示不生成 core dump)。

设置core dump大小为40960

cpp 复制代码
int main()
{
    printf("hello, henu\n");
    printf("hello, henu\n");
    printf("hello, henu\n");
    printf("hello, henu\n");
    printf("hello, henu\n");
    printf("hello, henu\n");
    int a = 10;
    a /= 0;
    printf("hello, henu\n");
    printf("hello, henu\n");
    printf("hello, henu\n");
    return 0;
}

gdb 中使用 core-file core 命令加载 core dump 文件,可以直接查看导致程序崩溃的代码行。

正常终止

当进程正常终止时,会设置一个"退出状态"。在Linux中,进程通过 exit 系列函数(如 exit()_exit())来设置退出状态。该状态通常是一个整数值,父进程可以通过 wait()waitpid() 函数获取子进程的退出状态,从而判断子进程的执行结果。

退出状态 :在正常终止时,进程的退出状态通常是 0,表示成功执行;如果退出状态是非零值,则表示程序执行过程中出现了错误。图中的"正常终止"右侧的8位设置为 0,这表明这些位不用于存储信号信息。

被信号所杀

当进程由于接收到某个信号而终止时,退出状态包含有关信号的信息。

终止信号编号:图中的右侧8位存储了导致进程终止的信号编号。例如:

cpp 复制代码
SIGINT(信号编号2):通常是由用户按下 Ctrl+C 触发的终止信号。
SIGTERM(信号编号15):用于请求进程正常终止的信号。
SIGKILL(信号编号9):强制终止进程,不能被捕获或忽略。

core dump标志 :如果进程因为信号终止,并且系统设置了生成 core dump 文件的标志(通常为 1),系统将保存进程终止时的内存镜像和寄存器状态。这些信息对开发者调试程序异常终止问题非常有帮助。图中提到的"core dump标志"表示这一点。

未使用区域:图中提到的左侧"未用"区域表明,在信号终止的情况下,该区域未用于存储与进程终止相关的其他信息。因为信号终止本身会填充信号编号和相关信息,而这些"未用"区域不需要额外的数据。

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(2);
        printf("hello bit\n");
        printf("hello bit\n");
        printf("hello bit\n");
        printf("hello bit\n");
        printf("hello bit\n");
        int a = 10;
        a /= 0;
        printf("hello bit\n");

        exit(1);
    }
    int status = 0;
    waitpid(id, &status, 0);
    printf("signal: %d, exit code: %d, core dump: %d\n",
           (status & 0x7F), (status >> 8) & 0xFF, (status >> 7) & 0x1);
}
相关推荐
熙曦Sakura7 分钟前
【Linux网络】TCP全连接队列
linux·网络·tcp/ip
脚比路长30 分钟前
win11 安装 wsl ubuntu 18.04后换源失败!
linux
菜鸟康1 小时前
Linux——CMake的快速入门上手和保姆级使用介绍、一键执行shell脚本
linux·运维·服务器
卷卷的小趴菜学编程1 小时前
Linux系统之----基础IO
linux·运维·服务器·文件·fopen·文件操作符·位图传递标志位
码上飞扬2 小时前
Java大师成长计划之第22天:Spring Cloud微服务架构
java·运维·云计算
陈苏同学2 小时前
[已解决] VS Code / Cursor / Trae 的 PowerShell 终端 conda activate 进不去环境的常见问题
linux·windows·conda
我不是秃头sheep2 小时前
Ubuntu 安装 Docker(镜像加速)完整教程
linux·ubuntu·docker
重启就好2 小时前
【Ansible】之inventory主机清单
运维·ansible
xmweisi022 小时前
【华为】现场配置OSPF
服务器·华为·华为认证·hcie·hcip·ospf·it培训
靡樊2 小时前
网络基础概念
linux·服务器·网络·c++·学习