Linux操作系统进程信号

Posted Ricky_0528

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 <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所指向的信号集,使系统支持的所有信号都加入到该集合内,表示该信号集的有效信号包括系统支持的所有信号
  • 在使用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 <stdio.h>
#include <signal.h>
#include <unistd.h>

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;

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 <stdio.h>
#include <signal.h>

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;

mytest:test.c 
	gcc $^ -o $@ -O3 
.PHONYE:clean
clean:
	rm -f mytest

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

加上volatile之后

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

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

7. SIGCHLD

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

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

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

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;

以上是关于Linux操作系统进程信号的主要内容,如果未能解决你的问题,请参考以下文章

linux下 进程信号量和线程信号量的区别和联系是啥

Linux操作系统进程信号

Linux信号+再谈进程地址空间

Linux系统的进程管理

Linux操作系统 - 信号

Linux进程间通信---信号量