Linux--信号

信号入门

生活角度的信号

​ 你在网上买了很多商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该如何处理快递,也就是你能识别快递。当快递到了楼下,也收到了快递到来的通知,但是突然你有点事,需要过几分钟之后才能取快递。也就是说取快递的行为并不是一定要立即执行。在收到通知到拿到快递期间,是有一个事件窗口的,在这段时间,你并没有拿到快递,但是本质上是你记住了“有一个快递要去取”。当时间合适的时候,顺利拿到快递之后,就要处理快递了。而处理快递一般方式有三种:1. 执行默认动作(拆开快递)、2. 执行自定义动作(这是送给别人的东西,直接拿去送) 3. 忽略快递(快递拿上来之后,放到一边)

技术应用角度的信号
  1. 用户输入命令,在shell下启动一个前台进程
    • 用户按Ctrl c ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号发送给目标前台进程
    • 前台进程收到型号,引起进程退出
注意
  1. ctrl C产生的信号只能发给前台进程。一个命令后面加一个&可以把进程放到后台运行。这样shell不必等待进程结束就可以接收新的命令,启动新的进程
  2. shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接受到像ctrl c这种控制键产生的信号
信号概念

信号是进程之间事件异步通知的一种方式,属于软中断

使用kill -l命令可以查看系统定义的信号列表

在这里插入图片描述

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2,编号34以上的是实时信号

信号处理常见的方式
  1. 忽略此信号
  2. 执行该信号的默认处理动作
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号

产生信号

信号的产生方式有很多种

1. 通过终端按键产生信号

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

Core Dump

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常时因为有bug,比如非法访问导致段段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程Resource(这个信息保存在PCB中)。默认是不允许产生core文件的。

  • 使用ulimit -a可以查看所有进程的Resource limit
  • 使用ulimit -c 1024可以允许core文件最大为1024k

在这里插入图片描述

在这里插入图片描述

使用core dump进行事后调试
#include<stdio.h>
#include<stdlib.h>
int main()
{
    //  一个数除以0是错误的
    int a = 10 / 0;
    return 0;
}

生成core文件

在这里插入图片描述

进行事后调试

在这里插入图片描述

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

首先在后台执行死循环程序,然后用kill命令给他发送SIGEGV信号

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 int main()
  4 {
  5   // 这里写一个死循环       
  6   while(1)
  7   {
  8    
  9   }
 10   return 0;
 11 }

在这里插入图片描述

程序运行起来的进程收到了信号,终止了进程,并且生成了core文件

在这里插入图片描述

kill命令是调用kill函数实现的,kill函数可以给以指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发送信号)

#include<signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1
3. 由软件产生信号
#include<unistd.h>
unsigned int alarm(unsigned int seconds);

用途:
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送`SIGALRM`信号,该信号的默认处理动作是终止当前进程
返回值:
这个函数的返回值是0或者以前设定的闹钟还余下的秒数
4. 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后向当前进程发送适当的信号。例如,当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释 为SIGSEGV信号发送给进程

信号捕捉
1 #include<stdio.h>  
  2 #include<signal.h>  
  3   
  4 void handler(int sig)  
  5 {  
  6   printf("this is %d signal\n", sig);  
  7 }  
  8   
  9 int main()  
 10 {
 11   // 捕捉2号信号,采用自定的处理方式
 12   signal(2, handler);
 13   while(1);     
 14   return 0;
 15 }

在这里插入图片描述

阻塞信号

1. 信号与其他相关常见概念
  • 实际执行信号得处理动作称为信号递达
  • 信号从产生到递达之间得状态,称为信号未决
  • 进程可以选择阻塞某个信号
  • 被阻塞得信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作
  • 注意阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
在内核中的表示

回到上面,当我们收到快递的时候,我们有很多种选择,可以马上去拿,可以让快递小哥放到一个地方过一会去拿,也可以直接 拒收。为什么我们可以等一一段时间再去处理快递的事情呢?本质上,是因为我们记住了这样一件事,我们有一个快递在等我们去处理。同理,信号的处理也是一样,可以立即处理,也可以等一会再去处理,本质上是操作系统记住有这样一个信号在等待处理。所以在操作系统中一定有一种结构用来存放这些信号

在这里插入图片描述

也就是说,在内核中有这样三张表,用来存放信号的状态

  • block表:本质上是位图结构,把它想像成为一个二进制数,位置对应的就是信号的编号,对应位置上的数据就代表信号是否被阻塞,如果对应比特位上是1,就表示该信号被阻塞,信号不会被递达
  • pending表:代表OS是否受到该信号,同样是位图结构。比特位的位置代表的就是哪一个信号,比特位的内容(0, 1),代表的就是是否收到了信号
  • handler表:就是对信号的处理方式,有三种,默认处理(SIG_DFL),忽略该信号(SIG_IGN),还有一种方式就是自定义的方式处理信号
sigset_t

从上图看来,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决阻塞标志可以用相同的数据烈性sigset_t来存储。sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态

信号集操作函数

sigset_t类型对于每种信号用一个比特位表示有效或无效,至于这个类型内部是如何存储这些bit的则依赖于系统实现。使用者也只能调用系统接口来操作sigset_t变量

#include<signal.h>
int sigemptyset(sigset_t *set);
/*
作用:将由set指定的信号集初始化为空,并排除改集合的所有信号
*/

int sigfillset(sigset_t *set);
/*
 作用:将set指定的信号集初始化为full,包含所有信号
*/

int sigaddset(sigset_t *set, int signum);
/*
作用:向集合中添加信号
*/
int sigdelset(sigset_t *set, int signum);
/*
作用:向集合中删除信号
*/
int sigismember(const sigset_t *set, int signum);
/*
作用:测试某种信号是否在集合中
*/

int sigpending(sigset_t *set);
/*
作用:不对pending位图做修改,而只是单纯的获取进程pending位图
*/

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
/*
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字(block位图),参数how指示该如何修改,如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前信号屏蔽字为mask
*/
SIG_BLOCK: set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK:  set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK:  设置当前信号屏蔽字为set所指向的值,相当于mask=set

举个例子

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

void show_pending(sigset_t set)
{
  int i = 0;
  for(i =0 ; i < 32; ++i)
  {
    int ret = sigismember(&set, i);
    if(ret == 1)
    {
      printf("%d",1);
    }else{
      printf("%d",0);
    }
  }
  printf("\n");
}

void handler(int signo)
{
  printf("收到%d信号\n", signo);
}


int main()
{
  // 定义一个sigset_t变量
  signal(2,handler);
  sigset_t set, pending, oset;

  // 向set中添加2号信号
  sigaddset(&set, 2);
  // 阻塞掉2号信号
  sigprocmask(SIG_BLOCK,&set,&oset);
  int count = 0;
  while(1)
  {
     // 初始化set为空
     sigemptyset(&pending);
     // 查看pending位图
     sigpending(&pending);
     show_pending(pending);
     sleep(1);
     ++count;
     if(count == 20)
     {
       // 恢复对2号信号的阻塞
       sigprocmask(SIG_UNBLOCK, &set, NULL);
       printf("恢复2号信号的,可以被递达\n");
     }
  }
  return 0;
}

在这里插入图片描述

捕捉信号

要理解清楚信号是怎么被捕捉的需要明白两个概念:内核态用户态

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

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

在这里插入图片描述

我们知道,每一个进程PCB都有一个结构用来存放地址空间,地址空间分为两个大部分,1个G的内核空间3个G的用户空间,这两个空间分别有自己的页表映射到物理内存中,用户几页表是每个进程 独有的一张表,而系统级页表是所有进程公用的一张页表。

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

在这里插入图片描述

信号的处理

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_yiyi_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值