• linux信号相关概念


    信号引入

    我们在linux编写代码时,如果想提前结束一个进程,通常我们会按ctrl+c组合键:
    在这里插入图片描述
    其实这就是想OS传递了一个中断进程的信号,我们平常就有意无意的在使用它!

    • 如何理解组合键变成信号呢?‘
      OS解释组合键->查找进程列表->前台运行的进程->OS写入对应的信号到进程内部的位图结构中(OS直接修改进程PCB的位图结构)。

    什么是信号?

    • 信号是进程之间事件异步通知的一种方式,属于软中断。(进程无论怎么运行,我们都能使用信号来通知他们执行动作)。

    • 使用kill -l 命令,可查看linux下的信号。(1~31是常用的信号,34~64是实时信号)
      在这里插入图片描述

    • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,如下图: 在这里插入图片描述

    • 对于信号,一般有三种处理函数:忽略此信号、执行该信号的默认处理动作、修改信号的默认处理动作(本来在OS内核执行的默认动作,被切换到用户态执行用户自己的函数,这种方式被称为Catch一个信号)。

    如何产生信号?

    我们先认识一个,可以修改信号处理动作的函数:signal()
    在这里插入图片描述

    第一个参数是信号,可填宏定义可填数字。
    第二个参数是回调函数,用于定义用户想要执行的动作。

    #include
    #include 
    #include
    using namespace std;
    
    void catchSignal(int signum)
    {
        cout<<"我收到了一个信号,正在处理:"<<signal<<" Pid:"<<getpid()<<endl;
    }
    
    
    int main()
    {
        int i = 0;
        signal(SIGINT, catchSignal);
        while(1)
        {
            sleep(1);
            cout<<"这是一个死循环"<<++i<<endl;
        }
    
        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

    该例子我们修改了SIGINT的默认处理动作,所以当我们按Crtl+C的时候,进程并没有中止,而是执行了自己定义的函数。
    在这里插入图片描述

    通过按键产生信号

    SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

    • 什么是Core Dump?
      当一个进程要异常终止时,可以选择把进程的用户控件内存数据全部保存到磁盘上,文件名通常是core,这就叫Core Dump。然后事后可以检查core文件,查看错误原因,这叫Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认不允许产生core文件,因为core可能包含密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制。

    ulimit -c 1024
    修改shell进程的Resource Limit,允许core文件最大为1024K
    ulimit -a
    查看相关信息

    在这里插入图片描述
    在这里插入图片描述
    只要动作是action的都会产生core文件:
    在这里插入图片描述

    下面演示如何产生core 文件:设置了一段除0的代码

    int main()
    {
        int i = 10;
    
        while(1)
        {
            sleep(1);
            i/=0;
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    如何使用core文件呢?使用gdb调试命令:
    这样就可以直接根据core文件,定位出错误的地方了。

    在这里插入图片描述

    调用系统函数向进程发信号

    我们平常使用的kill命令,其实就是调用的系统kill函数:
    在这里插入图片描述
    这个函数的功能就是给指定的进程发送信号:
    在这里插入图片描述


    raise函数:
    自己给自己发信号,成功返回0,错误返回-1;

    int main()
    {
        int i = 10;
    
        while(1)
        {
            sleep(1);
            raise(SIGSEGV);  //段错误信号
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述


    abort函数:

    void abort(void)
    使当前进程接收到信号而终止;

    int main()
    {
        int i = 10;
    
        while(1)
        {
            sleep(1);
            abort();   //什么也不填,相当与exit
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    系统调用函数发送信号的流程:

    用户调用系统调用接口->执行OS对用的系统调用代码->OS提取参数,或者设置特定数值->OS向目标进程写信号->修改对应进程的信号标记位(PCB里)->进程后续执行对应的处理动作

    由软件条件产生信号

    在这里插入图片描述
    该函数可以理解为设置一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程(也可以通过修改默认处理动作,去完成用户的需求)

    软件发送信号的流程:

    OS先识别到某种软件条件触发或者不满足->OS构建信号,发送给指定的进程。

    硬件异常产生信号

    CPU除0错误,访问非法内存地址,都是硬件异常。

    硬件异常的流程:

    除0错误:
    1.CPU在计算时,发现状态寄存器的溢出标记位是1->OS系统识别出有溢出问题,立即找到谁在运行这个程序->OS给这个进程发送信号,进程会在合适的时候,进行处理。
    2. 出现硬件异常,进程一定会退出吗?不一定!默认是退出,但是我们即使不退出,也做不了什么。
    3. 为什么会死循环?如果你把除0的默认动作改了之后,溢出标志位就一直是1(没有人改它),所以会一直执行你改正的动作。

    指针越界问题:
    4. 指针必须通过地址找到目标位置
    5. 而语言层面的地址,是虚拟地址
    6. 将虚拟地址转化成物理地址需要(页表+MMU内存管理单元)
    7. 如果是野指针,越界->非法地址->MMU转化的时候,OS一定会报错!

    Deliver、Pending、Block概念

    • 信号递达(Deliver):执行信号的处理动作
    • 信号未决(Pending):还没有响应的信号
    • 阻塞(Block):阻塞某个信号
    • 阻塞和忽略是不同的,阻塞是未处理,忽略是处理动作是忽略。
      被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

    信号在内核表示示意图

    在这里插入图片描述

    信号产生会修改PCB(pcb有指向信号数据结构的指针),Block、pending都是位图结构,handler是信号相应的动作

    • block表示的位图为是否阻塞该信号、pending表示接收到该信号。
    • 信号处理流程:OS->pending->block(如果被阻塞了就不处理呢)否则就进入handler处理。

    sigset_t

    该类型是系统的位图变量(用来描述上面的位图结构),并且OS提供了对它的操作函数。

    信号集操作函数

    #include
    //set是信号集[]
    int sigemptyset(sigset_t *set);   //把set都置为0
    int sigfillset(sigset_t *set);     //把set都置为1
    int sigaddset(sigset_t *set, int signo);      //把signo 数字的信号 置为1
    int sigdelset(sigset_t * set, int signo);  //删除signo ,位图置为0
    int sigismember(sigset_t *set, int signo);  //该信号集有效信号是否有signo,有就返回1,没有返回0,出错返回-1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    前四个函数成功返回0,出错返回-1;


    功能:读取或更改进程的信号屏蔽字(阻塞信号集)
    int sigprocmask (int how, const sigset_t *set, sigset_t *oset);
    返回值:成功返回0,出错返回-1.

    如果oset是非空,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都非空,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

    • SIG_BLOCK:set包含了希望添加到当前信号屏蔽字的信号,相当于mask = mask|set
    • SIG_UNBLOCK:set包含希望解除阻塞的信号,相当于mask = mask&~set
    • SIG_SETMASK:设置当前信号屏蔽字为set指向的值, 相当于mask = set

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


    读取当前进程pending位图信息,通过set传出
    int sigpending(sigset_t *set);
    调用成功返回0,调用失败返回-1

    注意

    如果我们对所有信号都进行block,是不是就可以写出一个无法杀死的进程了?
    不对,比如说9号信号是无法被屏蔽的!

    信号捕捉

    捕捉信号的时机:

    在这里插入图片描述

    解释:因为信号相关字段在PCB中,所以信号的检测是一定会在内核态进行。主程序遇到异常后,OS要转到内核态处理异常,当处理完准备返回用户态的时候,此时,进行信号的处理,检测信号是否被屏蔽,未屏蔽再中断,回到用户态,执行信号处理函数,然后再返回内核态,接着被中断的位置继续返回用户态,执行函数。


    signal.h
    功能:读取和修改与指定信号相关联的处理动作(更高级的signal)
    int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

    • signo:指定的信号编号
    • 若act非空,则根据act修改该信号的处理动作
    • 若oact非空,则将原来的信号处理动作,保留在此。
    • 成功返回0,出错返回-1

    其中该结构体我们只关心画圈圈的两个参数,其他不用管。
    在这里插入图片描述

    下面的例子屏蔽了2号信号。并获得了2号信号的默认动作。

    
    //makefile
    mytext:mytext.cc
    	g++ -o $@ $^ -std=c++11 -fpermissive
    .PHONY:clean
    clean:
    	rm -f mytext
    
    //ytext.cc
    #include
    #include
    #include 
    using namespace std;
    
    void handler(int signum)
    {
        cout<<"处理信号:"<<signum<<endl;
    }
    
    
    int main()
    {
        // cout<<"hello world"<
    
    
        //内核从数据类型,用户栈定义
        struct sigaction act, oact;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        act.sa_handler = handler;
    
        //设置到当前进程的PCB中
        sigaction(2, &act,&oact);
    
        cout<<"默认处理动作oact:"<<(int)(oact.sa_handler)<<endl;
    
        while(1) sleep(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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    可重入函数

    简单理解说,就是多个进程可同时进入的函数,并且多次运行的结果唯一,此函数就是可重入函数。反之如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。

    如果一个函数符合以下条件之一则是不可重入的:

    • 调用了malloc或free,因为malloc也是用全局链表管理堆的。
    • 调用了标准IO库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

    volatile

    volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
    我们有如下代码逻辑,全局flag,当收到信号时,全局flag变成1,进程结束。

    int flag = 0;
    void handler(int sig)
    {
        printf("change flag 0 to 1\n");
        flag = 1;
    }
    
    int main()
    {
        signal(2, handler);
        while(!flag);
        printf("process quiit normal\n");
        return 0;
    }
    
    
    //makefile
    mytext:mytext.cc
    	g++ -o $@ $^ -std=c++11 
    .PHONY:clean
    clean:
    	rm -f mytext
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    但是当我们加上O2优化的时候,此时进程就不退出了!
    在这里插入图片描述
    这是为什么呢?(因为编译器把代码优化了)
    优化情况下,键入 CTRL-C,2号信号被捕捉,执行自定义动作,修改 flag=1,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显,while循环检查的flag并不是内存中最新的flag,这就存在了数据二异性的问题。while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?需要 volatile。

    volatile int flag = 0;   //volatile 防止编译器优化!
    void handler(int sig)
    {
        printf("change flag 0 to 1\n");
        flag = 1;
    }
    
    int main()
    {
        signal(2, handler);
        while(!flag);
        printf("process quiit normal\n");
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    加了volatile后,问题就被解决了。

    在这里插入图片描述

  • 相关阅读:
    MicroStation二次开发问题记录(1):打开项目时自动加载dll文件
    linux同步搭建多台服务器
    支持 equals 相等的对象(可重复对象)作为 WeakHashMap 的 Key
    MySQL高效分页-mybatis插件PageHelper改进
    undefined /swagger/v1/swagger.json错误解决
    自动驾驶系列—图像到IPM:深入解析IMP投影变换技术
    Nacos详解
    图像风格迁移的发展历程
    大秒杀系统设计
    FileInputStream和FileOutputStream
  • 原文地址:https://blog.csdn.net/weixin_45153969/article/details/137740239