【Linux系统】信号机制拆解,透过内核三张表深入本质

文章目录

一、信号概念

我们之前提到过System V 信号量,请注意:信号和信号量完全没有关系。

信号,是外部或其他人或硬件给进程发送的一种异步的事件通知机制。

  • 异步:指多种事件,彼此不影响,同时发生
  • 事件:包括进程终止,异常,指令退出...

通过指令kill -l,可以查到系统中所有的信号种类:

在内核中,信号编号的和名字是用宏定义实现的,所以代码中使用时2和SIGINT是等价的。1 ~ 31信号是普通信号,34 ~ 64是实时信号,本文所谈内容只针对普通信号

最简单的信号产生方式是命令kill -信号编号 进程pid,给指定进程发送一个信号。

进程识别信号,是系统内置的。信号的处理方法,在信号产生之前一定是准备好了的!
信号的完整使用过程是:信号产生、信号保存、信号处理

二、信号处理

我们首先来讲信号处理的方法。

一个信号到来,进程有三种方法处理他:

  • 使用默认动作(大部分信号的默认是终止进程)
  • 忽略这个信号
  • 使用自定义动作

这些信号各自在什么条件下产生,默认的处理动作是什么,在man 7 signal中都能查到。

前两种方式很好理解,如2号信号的默认动作是终止进程,到来时,进程采用他的默认动作:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
    while (1)
    {
        cout << getpid() << endl;
        sleep(1);
    }
    return 0;
}

重点是自定义动作:
系统调用signal,可以更改当前进程对一个信号的处理动作!

他的第一个参数是想要改变的信号的编号,第二个参数是一个回调函数,即自定义动作!
这个自定义动作函数,必须有一个int参数,返回值为空。被调用时,参数就会自动传入这个信号编号!

第二个参数除了可以自己传,还有两个额外选项:SIG_IGN表示忽视这个信号,SIG_DFL表示采用默认动作。

signal函数的返回值也是一个函数指针,是这个信号的老的处理动作。

值得注意的是,signal函数只要在代码的开头调用一次就行了,整个进程都有效。

演示:

c 复制代码
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int sig)
{
    printf("接收到了%d号信号\n", sig);
}

int main()
{
    signal(2, handler);
    while (1)
    {
        printf("%d\n", getpid());
        sleep(1);
    }
    return 0;
}

可以发现,现在发送2号信号无法终止进程了,而是进程执行了自定义动作函数!

如果所有的信号都被自定义动作了,那我们该如何退出进程呢?
其实,9号和19号信号是无法被自定义动作的,称之为"管理员信号"。作用分别是强制终止进程、强制暂停进程。进程无法忽略这两个信号!

还有一个函数sigaction,和signal作用类似,它的操作粒度更细,用法也更多样。

三、信号产生

信号,有五种产生方式:

  • 命令kill产生:命令kill -信号编号 进程pid,给指定进程发送一个信号。最简单的一种。
  • 用键盘产生:按下ctrl c,表示发送2号信号;按下ctrl \,表示发送3号信号;按下ctrl z,表示发送19号信号,等等。
    2号和3号信号的默认动作是终止进程,19号信号的默认动作是暂停进程。
    当然,这种方式只能控制前台进程,只有前台进程能获取键盘的输入。

除了9号和19号,bash不对自己的任何信号响应,因为都自动忽略了。

  • 程序异常崩溃:如果你写下了除0、野指针等非法操作,就会发现程序运行到这里时就会崩溃终止。这是因为程序异常,导致收到了相关信号,如除0会发出8号信号,访问野指针会发出11号信号。
    本质原因是,硬件出错了!OS作为硬件的管理者,他就一定知道。而产生信号的方式有很多种,但最后只有OS能目标进程"写入"信号!

进程的task_struct中,会维护信号位图,OS向其发送信号本质就是写入信号位图!

  • 函数产生信号:

    • 函数kill

      这个系统调用很简单,第一个参数是目标进程,第二个参数是发送的信号。

    • 函数raise

      这个函数是向当前调用它的进程发送一个信号。其实等价于kill(getpid(), sig)。kill可以向任意进程发送信号,但raise是向自己发送信号。

    • 函数abort

      函数abort会向当前进程发送6号信号而使进程强制终止,6号信号可以被signal修改动作,但是执行自定义动作后还是会终止程序!

      abort通常用于终止进程,类似于exit,区别在于abort会进行核心转储(Core Dump),核心转储是程序异常终止时,OS将进程内存内容保存到磁盘的文件,用于事后调试分析崩溃原因。

  • 软件条件产生信号:软件条件产生的信号分类和举例有很多,如管道读端关闭时进程终止、alarm。
    alarm是一个用于设置闹钟的函数。它可以让内核在指定的秒数后,向当前进程发14号信号。

    它的参数是想要设置的秒数,返回值是上一个闹钟剩余时间。alarm(0)可以取消闹钟

四、信号保存

在这里,我们需要补充几个概念:

  • 实际处理信号的动作称为"信号递达"(Deliver)
  • 从信号产生到信号递达之间的状态,称为"信号未决"(Pending)
  • 信号可以被进程"阻塞"(Block)
  • 被阻塞的信号产生后将保持在未决状态,直到进程解除对其的阻塞,信号就会立刻递达!
  • 阻塞不等于忽略,只要信号被阻塞就不会递达,而忽略是信号递达后的一种处理动作!

在内核中,信号机制的核心是靠task_struct的三张表完成组织的!

  • block表,是一张31位的位图。位的编号是信号编号,位的内容表示该信号是否阻塞!
  • pending表,是一张31位的位图。位的编号是信号编号,位的内容表示是否收到该信号!
  • handler表,是一个函数指针数组,为信号动作表。数组下标+1为信号编号,SIG_DFL表示执行默认动作,SIG_IGN表示忽略动作!

假设2号信号的block位为1,即被阻塞。当2号信号产生时,pending表第2位会置为1,然后检查block表,如果被阻塞,此时就不能处理2号信号,只有解除阻塞后,才能完成信号递达,pending表第2位恢复为0。

可以在内核源码中看到,用于存储信号的位图是sigset_t类型,而本质是一个unsigned long 类型,我们称sigset_t为信号集,这个类型可以表示每个信号的"有效"和"无效"状态。



block表也叫做阻塞信号集,阻塞信号集也叫做信号屏蔽字(Signal Mask)

系统提供了一些函数用于操作sigset_t变量:

c 复制代码
#include <signal.h>
// 清空信号集set,信号全初始化为0
int sigemptyset(sigset_t *set);

// 填满信号集set,信号全设为为1
int sigfillset(sigset_t *set);

// 向信号集set中添加一个指定信号signum,将这一位信号设为1
int sigaddset(sigset_t *set, int signum);

// 从信号集set中删除一个指定信号signum,将这一位信号设为0
int sigdelset(sigset_t *set, int signum);

// 检测指定信号signum是否存在于信号集set中。
int sigismember(const sigset_t *set, int signum);

前四个函数都是成功返回0,失败返回-1。sigismember是布尔函数,若包含返回1,不包含返回0,失败返回-1。
在使用sigset_t变量前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

当然,也有相关的函数操作pending表和block表:

  • sigpending函数,用来读取当前进程的pending表:

    读取的结果会记录到参数set中,很简单

  • sigprocmask函数,可以读取或更改当前进程的block表:

    第一个参数为更改的方式选项,第二个参数是我们传入的set,第三个参数会记录老的block表。

    第一个参数有如下选项:

    • SIG_BLOCK:当设置为这个值时,表示新增信号屏蔽。set中的信号会添加到block表中
    • SIG_UNBLOCK:当设置为这个值时,表示解除信号屏蔽。set中的信号会从block表中删除
    • SIG_SETMASK:当设置为这个值时,block表会复制set中的全部信号状态

演示:

c 复制代码
#include <bits/types/sigset_t.h>
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int sig)
{
    printf("接收到了%d号信号\n", sig);
}

int main()
{
    signal(2, handler);
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set, 2);
    sigprocmask(SIG_BLOCK, &block_set, &old_set);
    int cnt  = 20;
    while (cnt--)
    {
        sigset_t pending_set;
        sigpending(&pending_set);

        // 打印出pending表的内容
        printf("pid:%d ", getpid());
        printf("当前pending表: ");
        for(int i = 31;i>=1;i--)
        {
            if(sigismember(&pending_set, i))
            {
                printf("1");
            }
            else
            {
                printf("0");
            }
        }
        printf("\n");

        if(cnt == 10)
        {
            // 这时我们解除2号信号的屏蔽
            sigprocmask(SIG_UNBLOCK, &block_set, NULL);
            printf("2号信号已解除屏蔽\n");
        }
        sleep(1);
    }
    return 0;
}
相关推荐
零涂毕业设计几秒前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
troyzhxu7 分钟前
列表查询的 GraphQL —— 一行代码终结你的 if-else 地狱!
java·springboot·graphql
tedcloud12311 分钟前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
kirs_ur22 分钟前
CXL(Compute Express Link)— 内存扩展的未来
服务器·数据库·性能优化
SakitamaX28 分钟前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.37 分钟前
Linux系统管理
linux·运维
孫治AllenSun1 小时前
【DataX】生产环境搭建DataX集群案例
java·开发语言·jvm
阿龙的工作记录1 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手1 小时前
Linux的基本指令
linux·运维·服务器
一拳一个呆瓜2 小时前
【STL】iostream 编程:检测提取操作产生的错误
c++·stl