• 【Linux操作系统】进程信号(二)


    3. 信号的保存

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

      • 自定义捕捉
      • 默认
      • 忽略
    • 信号从产生到递达之间的状态,称为信号未决(Pending)

      • 本质是这个信号被暂存在task_struct信号位图中,未决
    • 进程可以选择阻塞 (Block )某个信号

      • 本质是操作系统允许进程暂时屏蔽指定的信号

        • 该信号依旧是未决的

        • 该信号不会被递达,直到解除阻塞,方可递达

          递达中的忽略 跟 阻塞 有啥区别

          • 忽略是递达的一种方式
          • 阻塞是没有被递达,是一个独立的状态
    • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

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

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

    在这里插入图片描述

    SIG_DFL:默认 SIG_IGN:忽略

    在这里插入图片描述

    默认动作其实就是被强转的0,忽略动作为1

    pending位图保存的是已经收到但还没有被递达的信号,会在合适的时机执行handler中的方法,即完成递达

    block表本质上也是位图结构——uint32_t block;,比特位的位置代表信号的编号,比特位的内容代表信号是否被阻塞

    block位图称作阻塞位图,也叫信号屏蔽字,表示哪些信号不应该被递达,知道解除阻塞

    handler为函数指针数组,每个信号的编号就是该数组的下标

    3.2 sigset_t

    每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

    虽然sigset_t是一个位图结构,但是不同的操作系统实现是不一样的,不能让用户直接修改该变量,应该使用特定的函数

    sigset_t声明的变量同样也保存在用户栈上,与之前的int、double没有任何区别

    3.3 信号集操作函数

    sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的

    #include 
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
    • 函数sigfillset初始化set所指向的信号集,使系统支持的所有信号都加入到该集合内,表示该信号集的有效信号包括系统支持的所有信号
    • 在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
    • 前四个函数的返回值都是成功返回0,出错返回-1,sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

    3.4 sigprocmask

    调用函数sigprocmask可以读取或更改进程的信号屏蔽字(block表)

    在这里插入图片描述

    set:输入型参数,传入你想设置成的位图

    oldset:输出型参数,返回老的block位图

    how作用
    SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
    SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
    SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

    9号信号被称为管理员信号,不能被屏蔽也不能被自定义捕捉,必须永远遵守默认行为

    3.5 sigpending

    不会对pending位图做修改,而是单纯的获取进程的pending位图,修改只由操作系统完成

    在这里插入图片描述

    set:输出型参数,pending位图

    #include 
    #include 
    #include 
    
    void sig_show(sigset_t *sig)
    {
        int i;
        printf("current pending: ");
        for (i = 1; i < 31; i++) {
            if (sigismember(sig, i)) {
                printf("1");
            } else {
                printf("0");
            }
        }
        printf("\n");
    }
    
    void handler(int sig)
    {
        printf("%d号信号递达,处理完毕!\n", sig);
    }
    
    int main()
    {
        signal(2, handler);
    
        sigset_t iset, oset;
    
        sigemptyset(&iset);
        sigemptyset(&oset);
    
        sigaddset(&iset, 2);
    
        sigprocmask(SIG_SETMASK, &iset, &oset);
        
        sigset_t pending;
        int count = 0;
        while (1) {
            sigemptyset(&pending);
    
            sigpending(&pending);
    
            sig_show(&pending);
        
            sleep(1);
            
            count++;
            if (count == 5) {
                sigprocmask(SIG_SETMASK, &oset, NULL);
                printf("2号信号取消阻塞!\n");
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    在这里插入图片描述

    4. 信号的处理

    信号是被保存在进程的PCB中,pending位图里的,对其的处理分为:检测、递达(默认、忽略、自定义)

    当进程从内核态返回到用户态的时候,进行上面的检测并处理工作

    内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS代码的执行全都是在内核态

    用户态:就是用户代码和数据被访问或者执行的时候所处的状态,我们自己写的代码全都是在用户态执行的

    主要区别:主要在于权限

    感性的理解

    用户调用系统函数的的时候,除了进入函数,身份也会发生变化,用户身份变成了内核身份

    理性的认识

    用户的身份是以进程为代表的

    用户的数据和代码一定要被加载到内存,OS的数据和代码也一定要被加载到内存中的,这个工作由页表来完成

    地址空间中的用户空间由用户级页表负责映射到物理内存当中,不同进程对应的用户级页表各不相同,而内核空间由系统级页表——内核页表映射到物理内存当中,这个内核页表被所有进程共享

    进程具有了地址空间是能够看到用户和内核的所有内容的,但不一定能访问

    CPU内有寄存器保存了当前进程的状态

    • 用户态使用的是用户级页表,只能访问用户数据和代码

    • 内核态使用的是内核级页表,只能访问内核级的数据和代码

    进程之间无论如何切换,我们能够保障一定能找到同一个OS,因为每个进程都有3~4G的地址空间,而且使用同一张内核页表

    所谓的系统调用就是进程的身份转化成为内核,然后根据内核页表找到系统函数,执行就可以了

    在大部分情况下,实际OS都是可以在进程的上下文中直接运行的

    4.1 自定义捕捉

    默认和忽略都不需要再从内核态转到用户态,直接在内核态就可以完成

    在这里插入图片描述

    可以抽象为

    在这里插入图片描述

    内核为什么不直接执行用户的代码,而要切换到用户态来执行

    因为操作系统不相信任何人,操作系统身份特殊,不能直接执行用户的代码(OS权限太大)

    4.2 signal

    在这里插入图片描述

    typedef void (*sighandler_t)(int):为一个函数指针

    signum:信号对应的整型值

    handler:修改进程对信号的默认处理动作

    4.3 sigaction

    修改的是handler函数指针数组

    在这里插入图片描述

    act:输入型参数

    oact:输出型参数,待会老的信号处理方法,不需要置NULL

    • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号,若act指针非空,则根据act修改该信号的处理动作;若oact指针非空,则通过oact传出该信号原来的处理动作,act和oact指向sigaction结构体
    • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号,显然这也是一个回调函数,不是被main函数调用,而是被系统所调用

    sigaction结构体

    在这里插入图片描述

    当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,可以暂时将sa_flags设为0,sa_sigaction是实时信号的处理函数

    5. 可重入函数

    在这里插入图片描述

    在信号捕捉的时候再次插入了节点,这是就会导致有两个节点被同时插入了,head头指针只能指向一个节点,另一个节点就造成了内存泄漏

    • insert函数一旦重入,有可能出现问题,该函数被称为不可重入函数
    • insert函数一旦重入,不会出现问题,该函数被称为可重入函数
    • 我们学到的大部分函数,STL、boost库中的函数,大部分都是不可重入的

    6. volatile

    #include 
    #include 
    
    volatile int flag = 0;
    
    void handler(int signo)
    {
        flag = 1;
        printf("flag has changed to 1\n");
    }
    
    int main()
    {
        signal(2, handler);
    
        while (!flag);
    
        printf("exit success\n");
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    mytest:test.c 
    	gcc $^ -o $@ -O3 
    .PHONYE:clean
    clean:
    	rm -f mytest
    
    • 1
    • 2
    • 3
    • 4
    • 5

    加上O3优化之后,如果没有使用volatile修饰flag,会出现如下情况,发送2号信号时,flag的值好像并没有被改变

    在这里插入图片描述

    加上volatile之后

    在这里插入图片描述

    加上优化之后,编译器发现在main函数中并没有对flag进行修改,因此将其放到了寄存器中以提高程序执行效率,但这就相当于在CPU与内存之间建立了一道屏障,handler中修改flag的值是修改的内存中的,而在main函数中,while循环读取的是寄存器中的flag值,这就造成了上面的现象

    volatile的作用就是告诉编译器,不要对我这个变量做任何的优化,必需贯穿式的读取内存,不要读取中间缓冲区寄存器中的数据,即保存内存的可见性

    7. SIGCHLD

    子进程退出的时候会向父进程发送SIGCHLD信号,也就是说收到了SIGCHLD信号就知道子进程就退出了

    显式设置忽略子进程的SIGCHLD,子进程退出就不会变成僵尸进程,也就不需要我们去进程等待

    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
        // 显式设置忽略17号信号,当进程退出后自动释放僵尸进程,也就不需要我们去wait,如果我们不需要获取退出码的话
        // 只在Linux下有效
        signal(SIGCHLD, SIG_IGN);
    
        pid_t id = fork();
        if (id == 0) {
            // child
            int cnt = 5;
            while(cnt) {
                printf("I am child process: %d\n", getpid());
                sleep(1);
                cnt--;
            }
            exit(0);
        }
    
        // parent
        while(1);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述

  • 相关阅读:
    oracle、mysql、postgresql数据库的几种表关联方法
    零基础学Python的必备基础语法
    软件开发模型和软件开发模型的区别
    解决Python中使用requests库遇到的身份验证错误
    SPA项目开发之 分页+数据表格+分页
    【动手学深度学习-Pytorch版】BERT预测系列——用于预测的BERT数据集
    中断的分类、机理与嵌套:深入理解计算机系统的中断、陷入与异常
    octave 与 matlab
    Java中的基本数据类型
    16-SpringBoot 整合Druid数据源
  • 原文地址:https://blog.csdn.net/weixin_52665939/article/details/127840752