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);
}
相关推荐
重生的黑客3 小时前
Linux 进程优先级、切换与调度:从孤儿进程到 O(1) 调度模型
linux·运维·服务器·进程优先级·nice
Urbano3 小时前
夹克与商务单西口袋工序自动化技改研究报告
运维·自动化
骑上单车去旅行4 小时前
MD5校验对比脚本
linux·服务器·windows
平生幻5 小时前
Linux 常用命令
linux
期待着20135 小时前
docker 安装 ,在centos7.9
运维·docker·容器
踏月的造梦星球5 小时前
DM8 DSC 单机双实例部署
运维·开发语言·数据库
MXsoft6186 小时前
平台化是智能运维发展的必然趋势吗?
运维
ShirleyWang0126 小时前
让headlamp控制台能访问
linux·服务器·python·k8s·k3s
liuccn6 小时前
Linux 存储系统:LVM 与直接分区
linux·运维
捷配链6 小时前
六层服务器板行业材料与工艺标准化选型指南
服务器·制造·pcb