您现在的位置是:首页 >其他 >Linux——进程信号2网站首页其他

Linux——进程信号2

头发没有代码多 2024-06-17 10:13:47
简介Linux——进程信号2

阻塞信号

信号其他相关常见概念

  1. 实际执行信号的处理动作称为信号递达(Delivery)

  1. 信号从产生到递达之间的状态,称为信号未决(Pending)。

  1. 进程可以选择阻塞 (Block )某个信号。

  1. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

  1. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

  1. 信号是可以被屏蔽或阻塞的,进程可以阻塞或屏蔽信号

阻塞和忽略的区别:忽略属于已经递达了,已经对信号做了忽略处理。阻塞根本不会对信号进行递达,让信号一直在排队。

在内核中示意图

pending就是位图,代表的含义是对应的信号是否被接收,若接收到了为1,否则为0,handler是函数指针数组,数组的下标就是信号的编号,方框里填的是函数地址

注意这俩个宏用0和1,0是默认,1是忽略

当获取到信号之后,handler并不是直接调用对应的函数,而是先强制类型转换判断是否为0或1,如果是0或1就执行0或1的动作,如果强转后不是0或1,当执行handle表时,若信号被忽略,pending位图会由1清0才会调用对应的函数

block表和阻塞有关

block也是位图,该位图结构和pending一模一样,位图中的内容代表的含义是对应的信号是否被阻塞,0代表未被阻塞,1代表被阻塞。

一个信号被处理,操作系统是怎样的处理过程呢?

操作系统向目标进程发送信号,其实就是在修改pending位图,接下来检测pengding位图,看哪些比特位是1,若有一个比特位为1,并不是直接调用该信号的处理方法,而是去block表看看该信号是否阻塞,如果被阻塞了,则对该信号不做任何处理,如果未被阻塞,我们才调用handler,执行handler的处理方法。

pending->block->handler

sigset_t

这是一个位图结构,是操作系统提供的数据类型,这个位图不允许用户自己进行操作,OS给我们提供了对应操作位图的方法。用户是可以直接使用该类型的,和用内置类型还有自定义类型没任何的差别。而且这个类型需要对应的系统接口,来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或对象。

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来表示,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

block信号集:信号屏蔽字

信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set);//清空位图
int sigfillset(sigset_t *set);//位图全为1
int sigaddset (sigset_t *set, int signo);//添加特定信号到信号集
int sigdelset(sigset_t *set, int signo);//删除某信号
int sigismember(const sigset_t *set, int signo);//判定一个信号是否在该位图中

sigpending

参数是一个信号集,通过该函数可获取当前调用进程的pending信号集,返回值成功就是0,失败错误码被设置

sigprocmask

检查并且更改block信号集,返回值成功0,失败设置错误码

第一个参数代表用什么方式调用,第三个参数时输出型参数,返回老的信号屏蔽字。

小测试

  1. 如果我们对所有的信号都进行了自定义捕捉--我们是不是就写了一个不会被异常或用户杀掉的进程?不是

  1. 如果我们对所有的信号都进行block--我们是不是就写了一个不会被异常或用户杀掉的进程?不是

  1. 如果我们将2号信号block,并且不断的获取当前进程的pengding信号集,如果我们突然发送一个2号信号,我们就该肉眼看到pengding信号集中,有一个比特位由0变到1

我们发现9号信号不可被捕捉。a验证完毕

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
static void showPending(sigset_t& pending)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(&pending, sig))//sig信号是否在pending这个集合当中
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << std::endl;
}
int main()
{
 // 1. 定义信号集对象
    sigset_t bset, obset;
    sigset_t pending;
    // 2. 初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    // 3. 添加要进行屏蔽的信号
    sigaddset(&bset, 2 /*SIGINT*/);
    // 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
    int n = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(n == 0);
    (void)n;

    std::cout << "block 2 号信号成功...., pid: " << getpid() << std::endl;
    // 5. 重复打印当前进程的pending信号集
    int count = 0;
    while (true)
    {
        // 5.1 获取当前进程的pending信号集
        sigpending(&pending);
        // 5.2 显示pending信号集中的没有被递达的信号
        showPending(pending);
        sleep(1);
    }
    return 0;
}

此时我们可以看到2号信号已经被屏蔽

屏蔽2号信号,仅仅是修改了当前的pengding位图,将对应2号信号的比特位由0变为1,并不代表当前收到了2号信号,我们当前没2号信号,pengding位图中2号信号对应的比特位依旧是0,我们现在可以确定的是如果发送了2号信号,2号信号一定不会被递达,这个2号信号就永远保存在pending表当中

我们发送二号信号后,该二号信号一直在pengding表中,我们此时正在打印的正是penging表,此时频幕上会出现1

c验证完毕

在c的基础上,我们对2号信号进行恢复,再观察现象

我们看到pengding中2号信号对应的比特位没有从1变成0,而是直接终止了,这是因为默认情况下恢复对于2号信号block的时候确实会进行递达,但是2号信号的默认处理动作是终止进程,因此我们需要对2号信号进行捕捉。

此时我们可以观察到pending位图由0变成了1

我们可以看出先打印捕捉,然后打印解除,我们的预期是先打印解除,然后再打印捕捉。这是因为我们在写代码的时候把捕捉放到了前面,若想先解除再捕捉,我们把解除这句话放在捕捉前面即可。

我们发现貌似没有一个接口可以用来设置pending位图,但是我们是可以获取的sigpending,因为所有的信号发送方式,都是修改pending位图的过程,所以我们不需要特定的接口来修改pending位图

当捕捉方法执行完后,pending位图由1置0,便于下一次捕捉。

 i=1; id=$(pidof mysignal); while [ $i -le 31 ]; do echo "i: $i, id: $id"; let i++;sleep 1; done
i=1; id=$(pidof mysignal); while [ $i -le 31 ]; do kill -$i $id; let i++;sleep 1; done

当我们发到9号信号的时候,进程被终止,说明9号信号不能被进行屏蔽或阻塞,9号信号不可被捕捉和屏蔽b验证完毕

当发到19号信号时,进程也会被终止,我们可以看到20号信号也没被block,但我们的进程并未终止,20号信号可能被忽略了。

信号处理

信号相关的数据字段都是在进程PCB内部,PCB内部属于内核范畴,要检测信号或检测当前信号是否被屏蔽一定在内核状态,当执行代码的状态叫用户态。

处理信号在内核态中,从内核态返回用户态的时候进行信号的检测和处理。

用户态是一个受管控的状态,如受访问权限的约束。

内核态是一个操作系统执行自己代码的一个状态,具备非常高的优先级,基本不受任何资源的约束和权限的管控。

为什么会进入内核态?

一般进行系统调用会进入内核态,当有缺陷陷阱异常等也会进入内核态。汇编语句中有int 80这条语句内置在了系统调用函数中,当执行这条语句后,我们便会进入内核态

用户级页表每个进程都有,而且不一样,因为进程具有独立性,每个进程都有3-4G的地址空间给内核用。

内核级页表可以被所有进程看到。内核级页表可以把内核地址空间中的数据映射到物理内存

软件层面:如我们要调用OPEN接口,OPEN相关的代码在内核地址空间当中,调用OPEN的时候跳转到内核地址空间,然后经过页表查找到对应的物理内存。

当执行进程切换的代码时:当进程时间片到了,操作系统底层硬件发送时钟中断,由于当前进程还在CPU上,操作系统在CPU中找到当前正在执行的进程,然后通过该进程的地址空间找到对应的切换进程的函数,然后在进程的上下文中进行切换,因此能直接访问该进程在CPU中的临时数据,然后所有的临时数据被压倒PCB当中,进而把进程放下去,然后选择一个进程再上来,操作系统继续使用下一个进程3-4G的地址空间和内核级页表,然后恢复上下文代码和数据,然后把上一个进程恢复上来。

内核是在所有进程的地址空间上下文数据跑的。

我们执行OS的代码依靠的是处于内核态还是用户态。

硬件层面:CPU寄存器有俩套,其中一套可见,另一套不可见(CPU自用),其中CR3寄存器中用比特位表示当前CPU的执行权限,如1表示内核态,3表示用户态。当执行int 80后,CPU内寄存器状态由内核态改为用户态

信号捕捉

  1. 当正在执行用户代码时,可能会遇到一些情况,而导致陷入内核,列入:进程的系统调用或进程时间片到了,此时会发生进程调度,会在当前进程的上下文执行调度函数。在执行调度函数之前会陷入内核。即时间片到了->陷入内核->执行调度函数,当陷入内核之后OS去查找原因,找为什么会陷入内核,当OS调度进程的时候,在调度期间进程可能会收到信号,即被调度。

  1. 当从用户态进入内核态,我们一定是在完成某种行为如打开文件或读写网络等等,当我们把所对应的工作做完了,OS就准备给我们从内核态返回到用户态,返回到曾经被中断的地方继续向后执行。

  1. 当返回的时候,OS会顺手处理信号,先检测当前进程所对应的pending位图,如果全为0,直接返回,若有1,再检测block位图,之后再根据block确定是否执行handle。当执行handle表时,若信号被忽略,pending位图会由1清0,然后直接返回继续执行剩下的代码。若是默认,默认大多数是终止,即不调度进程,把进程的PCB页表全部释放掉。我们之前的_exit就是进程终止,但这种终止方式不会返回用户态。系统中有一些系统调用会在进程终止之前,帮我们返回用户态,之后执行特定的方法。在进程终止的时候,帮我们执行设定好的用户返回调用。

有一些信号不会退出,如暂停,当某个进程收到暂停信号后,OS会把该进程放到等待队列里,然后重新选择进程去调度,所以该进程进入了暂停状态。

当我们检测到信号要被捕捉,要执行对应的信号捕捉方法时,我们身份是内核即操作系统,而且这个状态能执行用户设置好的handler方法。

OS能做到帮用户执行对应的handler方法,但是它不愿意,也不想。若以操作系统内核态的身份去执行handler,handler中若有非法操作,则会引起大麻烦。OS不相信任何人,因此不能用内核态执行用户的代码。因此执行handler方法时,需要从内核态变成用户态。

  1. 当我们把信号处理完毕,此时需将pending中的比特位由1置0,此时需要进入内核态修改,而且同时要返回用户陷入内核的地方,将曾经的代码继续向后执行(这里需要在内核态中将函数的栈帧结构等恢复,然后跳回用户态继续向后执行后续代码),这里从用户态到内核态的系统接口是sys_sigreturn。

横线上面是用户态,下面是内核态,蓝色圆圈代表状态的切换,箭头代表从某个状态跳转到另一个状态。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。