linux信号相关概念

信号引入

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

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

什么是信号?

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

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

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

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

如何产生信号?

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

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

#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的默认处理动作,所以当我们按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;
}

在这里插入图片描述

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

在这里插入图片描述

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

我们平常使用的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;
}

在这里插入图片描述

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

用户调用系统调用接口->执行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<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 = 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<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)函数。

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

  • 调用了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

但是当我们加上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;
}

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

在这里插入图片描述

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值