Advertisement

linux信号相关概念

阅读量:

signal

  • 信号引入

    • 请解释什么是信号?
    • 如何实现信号的发送?
      • 通过按下特定的按键触发控制单元发送相应的控制信息

      • 系统函数被调用以发送相应的控制信息到目标进程中

      • 系统通信流程的具体步骤如下:

        • 检测接收到的目标地址
        • 根据目标地址触发相应的系统函数
        • 系统函数被成功调用后立即返回执行下一个操作
      • 由软件条件产生信号

        • 软件发送信号的流程:
      • 硬件异常产生信号

        • 硬件异常的流程:
  • 提交(Deliver)、等待(Pending)及拒绝(Block)的状态概念

    • 在内核中的信号项表示图

      • 用于表示信号集的操作指令
      • 注意事项
    • 信号捕捉

      • 捕捉信号的时机:
    • 可重入函数

    • volatile

信号引入

在编写代码时,在Linux系统中我们常用的方法是按住Ctrl+C组合键。

在这里插入图片描述

实际上,在操作系统中传递中断信号是一种机制,在日常操作中并不自觉地频繁使用这一机制。

如何理解组合键转化为信号的过程?
操作系统通过解析机制处理组合键,并定位当前正在运行的任务列表。接着,在后台状态下执行指定的操作后会暂停当前任务并将其提升为前台状态。随后系统会根据该任务的状态信息确定对应的任务状态信息,并将其记录在该任务的状态位图中;随后,在完成当前操作后立即向系统发送相应的控制信号。最终,在完成所有步骤后会将该控制信号直接写入到目标进程的状态位图结构中(操作系统直接修改其PCB中的相关位图信息)。

什么是信号?

在进程间实现事件的异步通知是一种称为信号的方式,并将其归类为软中断。(不管进程如何运行,在任何情况下都可以利用信号来通知它们执行相应的操作)

使用kill -l 命令,可查看linux下的信号。(131是常用的信号,3464是实时信号)

在这里插入图片描述

每个信号都包含一个标识以及一个宏名,在开发环境中通常会将这些关键信息存储在头文件如signal.h中。

在这里插入图片描述

通常处理信号时会遇到以下几种基本的操作流程:第一种是主动忽略此信号;第二种是按照既定程序自动触发相应的反应;第三种是重新定义默认的操作路径(这种机制通常称为捕获(catch)一个事件)。其中系统默认将这些操作路由至内核层面完成,在用户态则由特定程序接手完成相关操作

如何产生信号?

我们先认识一个,可以修改信号处理动作的函数:signal()

在这里插入图片描述

第一个参数为信号,并支持填写宏定义和数字。
第二个参数为回调函数(callback function),用于指定用户希望执行的操作(operation)。该操作通常包括预设的一系列动作(predefined actions)以及允许用户自定义逻辑(custom logic)。

复制代码
    #include<iostream>
    #include <unistd.h>
    #include<signal.h>
    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;
    }

举例来说,在优化SIGINT的默认处理动作后,在按下Ctrl+C时,进程并未立即停止而启动了自己定义的功能。

在这里插入图片描述

通过按键产生信号

默认情况下,SIGINT会触发进程终止行为,而SIGQUIT不仅会导致进程终止,此外还会生成核心日志记录

您知道Core Dump是什么吗?在进程发生异常终止时,系统会将该进程所使用的用户控制单元内存数据全部保存到磁盘上,默认情况下以'core'为文件名存储。随后,在事件后分析时可查阅生成的core文件以确定错误原因。需要注意的是,默认情况下禁止生成core文件以防止包含敏感信息如密码等潜在风险,在开发调试阶段可以通过ulimit命令临时解除此限制以允许核心文件生成。每个进程所能产生的核心文件大小受限于该进程所配置的资源限制信息(相关信息记录于Process Control Block中)。由于核心数据可能包含重要信息如密码等敏感内容,默认情况下不允许其被保存或分析。在实际应用中需谨慎处理此类操作以确保系统的安全性

ulimit -c 1024
限制shell进程对资源的限制
设置core文件大小不超过1024K
获取相关信息

在这里插入图片描述
在这里插入图片描述

只要动作是action的都会产生core文件:

在这里插入图片描述

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

复制代码
    int main()
    {
    int i = 10;
    
    while(1)
    {
        sleep(1);
        i/=0;
    }
    
    return 0;
    }
在这里插入图片描述

通过核心文件实现功能的方法是什么?运行gdb调试命令后,从而可以直接通过核心文件来定位错误。

在这里插入图片描述

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

我们平常使用的kill命令,其实就是调用的系统kill函数:

在这里插入图片描述

这个函数的功能就是给指定的进程发送信号:

在这里插入图片描述

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

复制代码
    int main()
    {
    int i = 10;
    
    while(1)
    {
        sleep(1);
        raise(SIGSEGV);  //段错误信号
    }
    
    return 0;
    }
在这里插入图片描述

abort函数:

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

复制代码
    int main()
    {
    int i = 10;
    
    while(1)
    {
        sleep(1);
        abort();   //什么也不填,相当与exit
    }
    
    return 0;
    }
在这里插入图片描述

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

由用户发起对操作系统提供的API进行请求,并由操作系统接收并执行相应的操作序列;根据需求获取相关参数信息或设定固定数值值;将相关事件状态标记传递给目标进程;随后该目标进程按照预先约定的操作流程完成相应的处理逻辑;具体操作包括更新该进程在虚拟地址空间管理结构中的标志字段(PCB里),以反映其当前的状态特征;在此基础上完成相应任务处理流程

由软件条件产生信号

在这里插入图片描述

该函数的功能类似于设置一个定时闹钟装置,在指定的时间段内向当前进程发送 SIGALRM 信号。其作用相当于让内核在设定的时间间隔向当前进程发送 SIGALRM 通知,并且通常情况下会立即终止当前进程运行状态;但同时也允许用户自定义处理方式以满足特定需求

软件发送信号的流程:

当系统检测到某种软件条件触发或未满足时->系统将生成一个信号并发送给指定的进程

硬件异常产生信号

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

硬件异常的流程:

当CPU执行计算操作时,在状态寄存器中检测到溢出标记位处于高电平状态(值为1),随后操作系统识别该现象为异常事件,并迅速定位并通知相关进程进行处理。一旦出现硬件异常状况,在正常情况下程序会自动终止执行以避免潜在问题。然而操作系统通常会优先选择让程序终止以恢复稳定状态。程序为何陷入死循环?这是因为如果将除零操作的默认行为修改后,默认情况下该标志位始终保持高位(值为1),导致程序无法正常退出而陷入无限循环。

4. 指针必须借助地址来定位到目标位置。
5. 在语言层面的层面上所使用的称谓是虚拟地址。
6. 要将虚拟地址转换为物理内存引用需使用页表以及MMU内存管理单元。
7. 当野指针发生越界时会指向非法内存区域,在由MMU进行转换的过程中操作系统必然会出现错误。

Deliver、Pending、Block概念

  • 信号递达(Deliver):对信号进行处理操作
  • 未响应的信号(Pending):尚未响应的信号
  • 阻塞(Block):阻止该信号的处理
  • 阻塞与忽略存在区别:在系统中区分两种行为模式
  • 当一个被阻塞的信号产生时(即发生),它会暂时处于未响应的状态(即Pending状态)
  • 直到该进程解除对该信号的阻塞行为后才会进行递达操作。

信号在内核表示示意图

在这里插入图片描述

当信号产生时会导致PCB(pcb具有指向其所属信号数据结构的指针)发生变化,在此过程中Block和pending均属于位图数据结构,并且handler负责处理相关信号

  • block位图指示是否阻塞了该信号;pending标志表示接收到该信号。
  • 在信号处理流程中:OS状态会先进入pending状态;接着进入block状态(若被阻塞则不会继续处理),否则会调用handler函数进行处理。

sigset_t

该类型属于系统的bitmap数据结构(用于表示上述bitmap布局),并且由OS提供相应的操作函数用于处理该bitmap数据结构。

信号集操作函数

复制代码
    #include<signal.h>
    //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;

前四个函数成功返回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 |= set
  • SIG_UNBLOCK:所有希望解除阻塞作用的信号都被指定在set中,请注意此操作对应于mask &= ~set
  • SIG_SETMASK:请将当前信号屏蔽字设置为指定的值,请注意此操作对应于mask = set

当调用sigprocmask成功解除对当前若干个未被处理的信号阻塞时,在sigprocmask返回之前至少需要确保其中一个信号已提交。


获取当前进程pending位图信息经由set传出
int sigpending(sigset_t *set);
调用成功返回零

注意

如果我们将所有的信号全部实施block操作,是否就能生成一个难以被终止的进程呢? 实际上并非如此,请注意,在这种情况下(例如),9号信号是无法被屏蔽的。

信号捕捉

捕捉信号的时机:

在这里插入图片描述

由于PCB中的信号相关字段存在,因此该类信号的检测必然会在内核态完成。当主程序发生异常时,操作系统会切换至内核态来处理异常,待完成操作准备切换回用户态时,立即开始对信号进行检测,判断该信号是否被屏蔽,若未被屏蔽则立即立即立即立即立即立即立即立即立即立即立即立即立即立即立即将其打断并切换回主程序状态并完成相应的处理任务。接着会再次切换回主程序状态并执行相应的操作


signal.h
功能:注:该函数用于获取与指定信号相关的处理信息并进行相应的更新操作。
int processAction(int typeID, const struct sigaction *act, struct sigaction *oact);

参数:
typeID - 信号类型标识符
act - 操作描述符结构体实例(包含当前信号状态等细节)
oact - 操作描述符结构体目标实例

返回:
int Process - 返回值表示操作是否成功完成

  • signo:指定的一个信号编号。
  • 如果act不为空,则会根据act对该信号的动作进行相应的设置。
  • 如果oat不为空,则会保留原有的该信号的动作。
  • 成功则返回0;如果出现错误则返回-1。

其中该结构体我们只关心画圈圈的两个参数,其他不用管。

在这里插入图片描述

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

复制代码
    //makefile
    mytext:mytext.cc
    	g++ -o $@ $^ -std=c++11 -fpermissive
    .PHONY:clean
    clean:
    	rm -f mytext
    
    //ytext.cc
    #include<iostream>
    #include<signal.h>
    #include <unistd.h>
    using namespace std;
    
    void handler(int signum)
    {
    cout<<"处理信号:"<<signum<<endl;
    }
    
    
    int main()
    {
    // cout<<"hello world"<<endl;
    
    
    //内核从数据类型,用户栈定义
    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;
    
    }

可重入函数

直观地说,在允许多个进程同时调用该功能的同时,并且其运行结果具有唯一性的情况下,则称该功能为可重入(Reentrant)功能;反之,在这种情况下(即当一个功能仅访问自身的局部变量或参数时),我们称其为可重入(Reentrant)功能

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

  • 使用了内存分配或内存释放函数;这是因为内存分配函数同样依赖于全局内存管理架构。
    • 使用了标准输入输出库函数;许多标准I/O实现都采用非重入方式管理全局数据结构。

volatile

volatile的作用:保证内存可见性,在编译器端对被此关键字修饰的变量实施保护措施,不允许对其做任何优化处理,其任何操作都必须直接作用于实际存在的内存单元。

复制代码
    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

但是当我们加上O2优化的时候,此时进程就不退出了!

在这里插入图片描述

那么为什么会这样?(因为编译器对代码进行了优化处理) 在优化状态下,在按下 CTRL+C 键时捕获到第2个信号并触发特定自定义动作后将 flag字段设置为1 然而 while条件仍然满足 并使进程继续运行;然而显然该操作已经改变了 flag字段的值 而 while循环仅会检查当前的 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;
    }

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

在这里插入图片描述

全部评论 (0)

还没有任何评论哟~