1.信号概述

  • 信号是进程之间事件异步通知的一种方式,属于软中断。信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c ,会通过信号机制停止一个前台进程。

1.1 信号列表

  • 察看系统定义的信号列表
$ kill -l

在这里插入图片描述

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
  • 这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等,信号名都定义为正整数。
  • 编号1-31的是普通信号,34-64实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal
    在这里插入图片描述

2.信号处理

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号
  • 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL(9)和SIGSTOP(19)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。
  • 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理
  • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义(如上图所示)。

3.信号流程

在这里插入图片描述

  1. 实际执行信号的处理动作称为信号递达(Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)

从此开始往下的内容将分别从上图中从前往后介绍信号的整个流程。

4.信号产生

信号的产生主要有四种方式:

  1. 通过终端按键产生信号。
  2. 调用系统函数向进程发送信号。
  3. 由软件条件产生信号。
  4. 硬件异常产生信号。

4.1通过终端按键产生信号

  • Ctrl +c 是通过硬件终端的输入方式中断进程,它的本质也是通过系统向进程发送信号。在证明Ctrl c本质之前需要知道Ctrl c的本质是往前台进程发送信号,在一次会话中只允许打开一个前台进程。

在用代码证明以上结论时首先要了解以下函数。

4.1.2 signal()捕捉信号

  • signal的接口是捕获信号,对信号进行重定义捕捉到了信号才会触发这个函数

  • sighandler_t signal(int signum, sighandler_t handler);在这里插入图片描述

4.2 代码实现终端产生信号

在这里插入图片描述

  • 由上图可知ctrl+c暂停了前台进程,实际就是由终端按键发送的信号给前台进程crtl+c实际就是2号信号SIGINT。由如下代码证明:
    在这里插入图片描述
  • 由上图可得,ctrl+c产生2号信号,并结束前台进程,并且只能是前台进程,后台则不可以,如下代码证明:
    在这里插入图片描述
    总结:
  • Ctrl c只能给前台发送信号给前台进程,一个命令后面加&表示放到后台运行,这样shell不必等待进程结束就可以接收新的命令,启动新进程当进程被设置为后台进程时,我们在命令行输入的消息流会和后台进程的信息混合在一起,这是因为bash进程是在前台的,我们可以输入信息,但是显示器只有一个,两个进程同时使用,说明他是临界资源,而这个临界资源又没有被保护,因此它的数据会发生混乱
  • Shell可以同时运行一个前台进程和多个后台进程,只有前台进程才能接收到键盘输入的组合键信号
  • .前台进程可以随时接收一个组合键信号,证明了进程相对于信号是异步的。

由上可知ctrl+c是二号进程SIGINT的默认处理动作是终止进程,3号信号SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下:

4.3 Core Dump

  • 当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。

4.3.1ulimit命令

  • ulimit命令是查看或者设置当前用户或者进程使用资源的阀值。
    在这里插入图片描述
  • 由上图可知core fiel的大小是0,因此需要 ulimit -c +大小,对其进行大小的设置,才能生成对应的Core Dump文件。

4.3.2 Core Dump是一种事后调试

  • 生成的core文件是二进制文件,core文件是给个编译器看的,生成的core文件是为了事后调试(逐步逐过程为事前调试),类似在VS中编写代码时报错,并清晰指出代码的具体信息(什么错误,哪里出错)。

  • 代码图解示例
    在这里插入图片描述

  • Linux下用gdb调试器用来调试。用gdb调试该程序,要加上core文件就能定位出段错误的位置。
    在这里插入图片描述

  • 由上图可以看到,调试器已经帮我们将错误定位出来了,并且将原因及行号显示出来了。

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

4.4.1调用kill

在这里插入图片描述
pid:

  1. pid大于零时,pid是信号欲送往的进程的标识。
  2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
  3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
  4. pid小于-1时,信号将送往以-pid为组标识的进程。

sig:

  1. 准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。

返回值:
2. 成功执行时,返回0,失败返回-1。

4.4.2 调用rasize

rasie可以给当前进程发送指定的信号(自己给自己发送信号)。
在这里插入图片描述
代码示例:在这里插入图片描述

4.4.3 调用abort

  1. abort使当前进程收到信号而异常终止,给自己发生6号信号。
    在这里插入图片描述
    代码类比上面的rasize()函数。

4.4.4 函数总结

  1. raise()可以给自己发送任意信号。
  2. abort()默认给自己发送的是6号信号,6号信号可以被捕捉,但是还是会被终止,而9号信号直接不可以被捕捉
  3. 这么多信号肯定要有信号不能被捕捉,因为如果都可以被捕捉,病毒可以将所有信号捕捉更改掉,系统就瘫痪了,因此需要9号信号不能被捕捉,即系统始终拥有对进程的终止能力。
    在这里插入图片描述
    在这里插入图片描述

4.5 由软件条件产生信号

#include <unistd.h>
 unsigned int alarm(unsigned int seconds);
  1. alarm()会在时间到了以后发送(14号)SIGALRM信号
    在这里插入图片描述
    代码示例:
    在这里插入图片描述

4.6硬件异常产生信号

  1. 硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为(8号)SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为(11号)SIGSEGV信号发送给进程。
    在这里插入图片描述

5.信号的保存与发送

  1. 进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
    注意:阻塞和忽略是不同,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

5.1 信号的发送过程

  1. PCB进程控制块中函数有信号屏蔽状态字和信号未决状态字,还有是否忽略标志。
  2. 向进程发送SIGINT,内核首先判断信号屏蔽状态字是否阻塞,若阻塞,信号未决状态字(pending)相应位制成1;若阻塞解除,信号未决状态字(pending)相应位制成0;表示信号可以抵达了。
  3. block状态字用户可以读写,pending状态字用户只能读;这是信号设计机制。

6. 信号在内核中的示意图

在这里插入图片描述
上图中:

  • block表和pending表都是用位图来表示信号的状态,block表的0/1表示为是哪个信号?是否被阻塞?pending表的0/1代表 是谁?是否是未决?普通信号用位图表示(容易丢失),而后面的实时信号则是用链表表示(被管理起来,且实时性较强,不易丢失)。

block表:代表产生的信号是否要被阻塞(屏蔽);
pending表:代表是否有产生的信号处于未决状态,当信号由未决状态变到递达时,相应的位图由1变为0;
handler表:储存函数指针的数组,代表了信号的处理方式(默认,忽略,自定义)。

6.1 sigset_t信号集

  • 每个信号的阻塞或未决都是由一个比特位来表示的,不是0就是1,因此未决和阻塞标志可以使用一样数据类型sigset_t来进行存储。sigset_t被称为信号集表示每个信号是有效还是无效
  • 阻塞状态中,有效、无效表示是否被阻塞,阻塞信号集(block表)也被叫做当前进程的信号屏蔽字
  • 未决状态中,有效、无效表示信号是否处于未决状态

6.2 信号集操作函数

#include <signal.h>  
int sigemptyset(sigset_t *set); //把信号集清零;(64bit/8=8字节)  
int sigfillset(sigset_t *set);  //把信号集64bit全部置为1  
int sigaddset(sigset_t *set, int signo);    //根据signo,把信号集中的对应位置成1  
int sigdelset(sigset_t *set, int signo);    //根据signo,把信号集中的对应位置成0  
int sigismember(const sigset_t *set, int signo);    //判断signo是否在信号集中 

6.3 block表系统调用接口sigprocmask()

在这里插入图片描述

6.4读取pending表当前进程未决信号集sigpending()

  • 读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
    在这里插入图片描述

6.5 函数使用代码展示

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

void show_pending(sigset_t *pending)
{
  int sig = 1;
  for(; sig<=31; sig++){
    if(sigismember(pending, sig)){
      printf("1");
    }
    else 
    {
      printf("0");
    }
  }
  printf("\n");
}

void handler(int sig)
{
  printf("get a sig: %d\n", sig);
}

int main()
{
  signal(2, handler);

  sigset_t pending;
  
  sigset_t block,oblock;
  sigemptyset(&block);
  sigemptyset(&oblock);
  
  sigaddset(&block, 2);//阻塞信号
  
  sigprocmask(SIG_SETMASK, &block, &oblock);//屏蔽进程的信号集
  
  int count = 0;
  
  while(1){
    sigemptyset(&pending);//初始化
    sigpending(&pending);//未决信号集
    show_pending(&pending);//显示未决信号
    sleep(1);
    count++;
    if(count == 10){
      printf("recover sig mask!\n");
      sigprocmask(SIG_SETMASK, &oblock, NULL);//10s时解除屏蔽
    }
  }
   
  return 0;
}

在这里插入图片描述

7.捕捉信号

  • 操作系统向进程发出信号,进程并不是立即执行信号的,而是在合适的时候,这个合适的时候是信号被递达的时候一个信号递达,是在内核态切换回用户态时,进行信号的相关检测。
    在这里插入图片描述
    上图很好的说明了信号捕捉时用户态和内核态的切换(用户处理信号最好的时机是程序从内核态切换至用户态的时候),下面就上图的一系列操作作以解释说明:
  1. 用户程序注册了SIGQUIT信号的处理函数sighandler(自定义信号处理函数)。
  2. 当前正在执行main函数,这里发生中断、异常或者系统调用切换至内核态
  3. 中断处理完毕后要返回用户态的main函数之前,检查到有信号SIGQUIT递达
  4. 内核决定返回用户态后不是恢复main函数的上下文信息继续执行,而是执行sighandler函数,sighandler函数和main函数使用不同的堆栈空间,两者之间不存在调用和被调用的关系,属于两个独立的控制流程。
  5. sighandler函数返回后自动执行特殊的系统调用调用return再次进入内核态
  6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续向下执行。

处理自定义信号返回到用户态是因为,内核态权限最高,且内核态很重要,如果当前自定义信号是非法的,而用内核态去处理难免会出现一些未知的问题

8.信号捕捉函数 sigaction

  • sigaction与signal是以样的,都是信号捕捉函数,只是sigaction的功能更加的丰富
    在这里插入图片描述
    代码示例
void handler(int sig)
{
  printf("you alread lost\n");
}
int main()
{
  struct sigaction act,oldact;
  act.sa_handler=handler;
  sigaction(2,&act,&oldact);
  while(1);
  printf("end process\n");
}

在这里插入图片描述

9.可重入函数

  • 在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果有一个函数不幸被设计成为这样:那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果这样的函数是不安全的函数,也叫不可重入函数。
  • 重入即表示重复进入首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是purecode(纯代码)可重入可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括 static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。

保证函数的可重入性的方法:

1)在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量)。
2)对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。

满足下列条件的函数多数是不可重入(不安全)的:

1)函数体内使用了静态的数据结构。
2)函数体内调用了malloc() 或者 free() 函数。
3)函数体内调用了标准 I/O 函数。

Linux常见的可重入函数:
在这里插入图片描述

10.volatile关键字

  • volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。

  • volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

11.SIGCHLD信号(17号)

  • 子进程退出,父进程可以通过阻塞或非阻塞的方式等待子进程结束,然后清理资源,但是不管是那种方式,程序的实现都是比较复杂的有什么办法可以让父进程不用等待,又不会产生僵尸进程呢?

可以让子进程退出时,给父进程发送一个信号,父进程只需要在信号执行函数中调用wait函数清理资源,这样父进程就不必去检测子进程是否退出的问题了

  1. 进程终止时,会给父进程发送SIGCHLD信号(17号信号)
  2. 父进程如果通过signal或sigaction设置忽略这个信号子进程终止时自动释放自己的资源,父进程不必再等待子进程(linux下可用,不保证其它系统上可用),或者自定义SIGCHLD信号处理动作,调用wait等资源释放函数,也可以完成子进程资源的释放。
Logo

更多推荐