[linux仓库]信号保存[进程信号·肆]

🌟 各位看官好,我是!****

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux的信号保存,明白进程是如何识别信号及相关函数。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

信号保存

对信号产生有了一定的理解后,就可以从时间维度上讲解信号保存的话题

信号相关概念

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

信号是否被阻塞在内核中是用位图进行表示:pending位图表示是否收到信号,block位图表示信号是否被屏蔽.

进程如何识别信号

信号在内核中的表示示意图:

  • 每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
  • SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是用户⾃定义函数sighandler。

这表明什么呢?信号是否收到信号,是否阻塞,用什么方法进行处理.都是围绕这张表进行的啊!

结论:围绕这信号,进程能识别信号,本质是三张表:block表pending表handler表 --> 由理论转实操: 都是对这三张表的操作.

bash 复制代码
struct task_struct {
...
    /* signal handlers */
    sigset_t blocked
    struct sigpending pending;
...
}

struct sigpending {
    struct list_head list;
    sigset_t signal;
}

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

struct sighand_struct {
    atomic_t count;
    struct k_sigaction action[_NSIG]; // #define _NSIG 64
    spinlock_t siglock;
};

sigset_t

从上图来看,每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, , 这个类型可以表⽰每个信号的"有效"或"无效"状态, 在阻塞信号集中"有效"和"⽆效"的含义是该信号是否被阻塞, ⽽在未决信号集中"有 效"和"⽆效"的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的"屏蔽"。

bash 复制代码
    // 用户层面设置位图
    sigset_t block, oblock;

sigprocmask

int sigprocmask(int how, const sigset_t *_Nullable restrict set, sigset_t *_Nullable restrict oldset);

该函数主要用来检查和修改block位图

  • how:指示如何修改
  • set : 输入型参数 --> 若为非空指针,更该进程的信号屏蔽字
  • oldset : 输出型参数 --> 若为非空指针,返回老图,方便后悔操作

如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后 根据set和how参数更改信号屏蔽字。

  • 返回值:若成功则为0,若出错则为-1

假设当前信号屏蔽字为mask,下面为how参数可选值

如果调用sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀个信号递达。

因此,对这三张表的操作,可以通过sigprocmask对block位图进行修改,对pending可以通过进程对pending位图进行修改,sigpending函数可以修改pending位图(如下介绍),signal函数可以设置三种处理行为.

sigpending

信号集操作函数

sigset_t类型对于每种信号⽤⼀个bit表⽰"有效"或"⽆效"状态, ⾄于这个类型内部如何存储这些bit则依赖于系统实现, 从使⽤者的⻆度是不必关⼼的, 使⽤者只能调⽤以下函数来操作sigset_ t变量.

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置位,表⽰ 该信号集的有效信号包括系统⽀持的所有信号。
  • 注意,在使⽤sigset_ t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处 于确定的 状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1.

这里提出几层理解:

  • 问题1:屏蔽所有信号 ? 9号信号不可被捕捉,不可被屏蔽 -- done
  • 问题2:如果解除对2号的屏蔽,我也要看到pending位图 1 -> 0 -- done
  • 问题3:2号信号被递达,pending 位图由1置为0,是在递达前还是递达后置为0

为了验证问题2,这里写了一段程序:先对2号信号进行屏蔽,接着我通过kill指令向该进程发送2号信号,应该观察到pending位图由0置1,等待一段时间让该进程对2号信号解除屏蔽,此时信号会被递达,应该观察到pending位图的2号信号由1重新置为0,必要的时候可以加上自定义捕捉查看是否捕捉到2号信号.

为了验证问题3,可以这样处理,如果是递达后处理2号信号时,在自定义捕捉方法中应一直为1,如果为0则证明是递达前.

bash 复制代码
void PrintPending(sigset_t &pending)
{
    std::cout << "[pid: " << getpid() << "] " << "sigpending list: ";
    // 右->左, 低->高 , 0000 0000
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << "\r\n";
}

void handler(int signo)
{
    std::cout << "我获取到了: " << signo << " 信号" << std::endl;
    // 不要让进程终止
    // 在对2号信号捕捉的代码中,获取pending && 打印??
    sigset_t pending;
    sigemptyset(&pending);
    // 2.1 获取当前进程的pending信号集
    sigpending(&pending);

    // 2.2 不断打印所有的pending信号集中的信号
    std::cout << "###########################" << std::endl;
    PrintPending(pending);
    std::cout << "###########################" << std::endl;
}

int main()
{
    // 0.设置2号信号的处理动作,不要让他终止
    signal(2, handler);

    // 1.屏蔽2号信号
    // 1.1 用户层面设置位图
    sigset_t block, oblock;
    sigemptyset(&block);  // 进行初始化
    sigemptyset(&oblock); // 是输出型,也可以不初始化

    sigaddset(&block, SIGINT); // 这里的时候,我们有没有设置当前进程的信号屏蔽字?没有!

    // 1.2将栈上位图设置到内核的信号屏蔽字-->block图
    sigprocmask(SIG_SETMASK, &block, &oblock);

    int cnt = 10;

    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 2.1获取当前进程的pending信号集
        sigpending(&pending);
        // 2.2不断打印所有pending信号集中的信号
        PrintPending(pending);

        // 验证取消block,看信号是否递达
        cnt--;
        if (cnt == 0)
        {
            // 解除对2号的屏蔽
            std::cout << "解除对2号的屏蔽啦!" << std::endl;
            // 取消block
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }

        sleep(1);
    }

    return 0;
}

总结

本文介绍了Linux系统中信号保存的相关概念和操作。信号从产生到递达之间存在未决状态,进程可通过阻塞位图屏蔽信号。内核使用三张表(block、pending、handler)管理信号状态。文章详细讲解了如何通过sigprocmask函数修改block位图,以及使用sigpending、signal等函数操作信号集。通过实验程序验证了信号屏蔽、解除和递达过程中pending位图的变化,说明信号处理的核心是对这三张表的操作。关键点包括:信号递达前后pending位图的变化、9号信号的特殊性,以及如何通过编程验证信号处理流程。

相关推荐
Sirens.1 天前
Java核心概念:抽象类、接口、Object类深度剖析
java·开发语言·github
獭.獭.1 天前
Linux -- 线程概念
linux·线程·进程·多级页表·缺页异常
望获linux1 天前
【实时Linux实战系列】使用 u-trace 或 a-trace 进行用户态应用剖析
java·linux·前端·网络·数据库·elasticsearch·操作系统
dessler1 天前
Elasticsearch(ES)-Logstash
linux·运维·elasticsearch
lht6319356121 天前
Ubuntu Server系统安装谷歌浏览器
linux·运维·ubuntu
程序员阿鹏1 天前
49.字母异位词分组
java·开发语言·leetcode
JanelSirry1 天前
DevOps是什么,有什么作用,一般用来干嘛
linux·运维·devops
---学无止境---1 天前
Linux中控制台初始化console_init函数的实现
linux
望获linux1 天前
【实时Linux实战系列】FPGA 与实时 Linux 的协同设计
大数据·linux·服务器·网络·数据库·fpga开发·操作系统
Yurko131 天前
【C语言】基本语法结构(上篇)
c语言·开发语言·学习