【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)

⭐️ 本篇博客要给大家介绍一些关于进程间通信的一些知识。其中包括信号是什么,如何产生的,信号如何保存,什么时候处理,如何捕捉信号等等一些问题,在今天的博客中,你都将找到答案。

目录

  • 信号概述
    • 认识信号
    • 查看信号
    • 信号常见处理方式
  • 产生信号
    • 通过按键产生
    • 通过系统调用
    • 通过软件条件产生
    • 通过硬件异常产生
  • 阻塞信号
    • 了解几个概念
    • 信号在内核图中的表示
    • 信号集及信号集操作函数
  • 捕捉信号
    • 捕捉过程的介绍
    • sigaction
  • 可重入函数
  • volatile
  • 总结


信号概述

认识信号

生活中的信号:
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能 “识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你在忙学习,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成 “在合适的时候去取”
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了。
而处理快递一般方式有三种:

  1. 执行默认动作(幸福的打开快递,使用商品)
  2. 执行自定义动作(快递是零食,你要送给你你的女朋友)
  3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)

快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

技术角度的信号:
根据我们之前所学的知识,用户通过按键盘按下Ctrl-C可以终止进程。键盘输入会产生一个硬件中断,被操作系统获取后,然后解释为信号,发送2号信号给进程,进程收到信号,然后终止进程。

实例演示:

#include 
#include 

int main(int argc, char *argv[])
{
	while (1){
		printf("I am a process,I am waitting signal\n");
		sleep(1);
	}
  return 0;
}

代码运行结果如下:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第1张图片
程序开始是正常运行,但是当我们按下Ctrl+C时,该进程受到2号信号,然后终止进程。下面我要给大家用代码验证一下Ctrl+C代表的是2号信号。
在验证这个事情之前,我先给大家介绍一个函数signal

功能: 对一个信号注册特定的处理动作(注册一个对信号的捕捉方式)
函数原型:

#include 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:

  • sig: 要注册的信号
  • handler: 处理动作,有三种:SIG_DFL(默认)SIG_IGN(忽略)自定义(函数指针)
    其中函数指针指向的函数有一个int类型的参数,无返回值,这个函数指针就是用户给信号自定义的处理动作,通过函数实现

实例演示:

#include 
#include 
#include 
#include 

int count = 0;

void handler(int signo)
{
    printf("catch a signal : %d\n", signo);
}

int main(int argc, char *argv[])
{
	// 注册一个对特定信号处理的动作或者说注册对一个信号的捕捉方式
	signal(2, handler);
	while(1){
	  printf("I am a process, I am running...\n");
	  sleep(1);
	}
	return 0;
}

代码运行结果如下:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第2张图片
从代码运行结果可以看出,Ctrl+C代表的是2号信号,且这个2号信号不再做终止进程的动作,而是打印了一句话。因为signal这个函数修改了2号信号的默认动作,让它执行自定义动作。
注意:

  1. Ctrl+C产生的信号只能发给前台进程。执行程序时在命令后面加上&可以把进程放到后台运行,后面可以通过一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。如果后期想把这个后台进程放到前天,可以通过fg指令,把这个后台进程放到前台
  2. Shell可以同时运行一个前台进程和任意多个后台进程(前台进程:后台进程=1:n),只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步)的

上面我们从生活角度和技术角度谈了一下信号的概念,其实二者是可以进行类比的,进程就是那个在学习的你,快递员就是操作系统,操作系统给进程发信号就相当于给你送快递,进程处理这个信号的动作就是你去取快递。

总结: 信号是进程之间事件通知的一种方式

查看信号

可以通过kill -l命令查看系统定义的信号列表:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第3张图片
可以看到的是,系统下总共有62个信号,1-31号属于普通信号,34-64属于实时信号。本篇博客只谈普通信号。
如果你想了解某个信号的产生条件和默认处理动作,可以通过指令man signal_id signal
在这里插入图片描述

信号常见处理方式

上面介绍了signal函数,这个函数可以更改处理信号的动作,有三种处理动作:

  1. 默认(default)
  2. 忽略(ignore)
  3. 自定义捕捉

后面还会介绍一个sigaction 函数,这也是一个对信号处理的函数,也有以上三个动作

产生信号

通过按键产生

前面我们介绍过了,通过按键Ctrl C可以发送2号信号(SIG_INT),默认处理动作是终止进程。还有可以通过按键按下Ctrl \,发送3号信号(SIG_QUIT),默认处理动作是终止进程并且Core Dump (在进程等待那里我们留下来这个问题,这里进行讨论).

讨论Core Dump:
Core Dump是什么?

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。我们可以通过使用gdb调试查看core文件查看进程退出的原因,这也叫事后调试

Core Dump使用演示:
一个进程允许产生多大的core文件取决于进程的Resource Limit,这个信息报错在PCB中。默认情况下,不允许产生core文件,因为core文件中比较大(容易浪费资源),包含用户密码等敏感信息,不安全。我们可以通过命令ulimit -a 查看系统允许我们产生多大的core文件
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第4张图片

可以发现的是,系统是不允许产生这个core文件的,但是我们可以通过命令ulimit -c size 修改,允许产生size大小的core文件
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第5张图片
此时我们编写一个程序:

#include 

int main()
{
	printf("waiting signal...\n");
	while(1);
	return 0;
}

代码运行结果如下: 运行起来后,用Ctrl \信号终止进程并参数core文件
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第6张图片
查看目录下的文件,会发现多了一个core文件,可以看到的是,这个文件很大,这也是系统不允许我们产生这个文件的一个原因

【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第7张图片
此时通过gdb调试器打开这个程序,然后通过指令core-file长core文件的错误信息,就可以发现这个进程是被收到3号信号如何退出的
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第8张图片

通过系统调用

之前我们介绍过kill -9可以发送9号信号杀死进程,同样地,后台进程也可以被干掉。
下面介绍三个系统函数:

  1. kill

功能: 给任意进程发送任意信号

#include 
int kill(pid_t pid, int sig); 

参数:

  • pid:进程pid
  • sig:要发送的信号

返回值: 成功返回0,失败返回-1

实例演示:

#include 
#include 

void handler(int signo)
{
  printf("catch a signal : %d\n", signo);
}

int main(int argc, char *argv[])
{
  signal(9, handler); 
  if (argc == 3){
    // 给指定的进程发送指定的信号
    kill(atoi(argv[1]), atoi(argv[2]));
  }
  return 0;
}

代码运行结果如下: 运行一个后台进程,然后通过mytest程序加参数的方式发生9号信号杀死后台sleep进程
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第9张图片

  1. raise:

功能: 给进程自己发送信号

#include   
int raise(int sig);  

参数:

  • sig:要发送的信号

返回值: 成功返回0,失败返回-1
和kill比较:

 raise函数相当于kill(getpid(), sig)  

实例演示:

#include 
#include 

void handler(int signo)
{
  printf("catch a signal : %d\n", signo);
}

int main()
{
  signal(2, handler);
  while(1){
     raise(2);
     sleep(1);
  }
  return 0;
}

代码运行结果如下: 可以看到的是,该进程不断给自己发送2号进程
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第10张图片

  1. abort

功能: 使用当前进程收到信号而异常终止(发送6号信号)
函数原型:

#include   
void abort(void); 

实例演示:

#include 
#include 
#include 

void handler(int signo)
{
  printf("catch a signal : %d\n", signo);
}

int main()
{
  signal(6, handler);
  while(1){
     abort();
  }
  return 0;
}

代码运行结果如下:

【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第11张图片
可以看出,即使我们对6号信号的处理动作进行了修改,但是这个信号还是把该进程终止了,这就说明了abort的函数永远是成功的。
在这里插入图片描述

通过软件条件产生

在上一篇博客介绍过,管道如果读端不读了,存储系统会发生SIGPIPE 信号给写端进程,终止进程。这个信号就是由一种软件条件产生的,这里再介绍一种由软件条件产生的信号SIGALRM(时钟信号)
先说alarm函数:

功能: 设定一个闹钟,操作系统会在闹钟到了时送SIGALRM 信号给进程,默认处理动作是终止进程
函数原型:

#include  
unsigned alarm(unsigned seconds); 

参数:

  • second:设置时间,单位是s

返回值: 0或者此前设定的闹钟时间还余下的秒数

实例演示:
实例1:

#include 
#include 
#include 

int main()
{
  // 由软件条件产生信号  alarm函数和SIGPIPE
  alarm(5);

  while (1){
    printf("count:%d\n", ++count);
    sleep(1);
  }
  return 0;
}

代码运行结果如下: 5s后,闹钟到了,发生闹钟信号终止进程
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第12张图片
实例2: 看下面两段代码
代码1: 不断打印count的数

#include 
#include 
#include 
#include 

int count = 0;

int main()
{
  alarm(1);
  while (1){
    printf("count:%d\n", ++count);
  }
  return 0;
}

代码2: 最后打印

#include 
#include 
#include 
#include 

int count = 0;

void handler(int signo)
{
  printf("count:%d\n", count);
  exit(0);
}

int main()
{
  
  // 由软件条件产生信号  alarm函数和SIGPIPE
  signal(14, handler);
  alarm(1);
  while (1){
    count++;
  }
  return 0;
}

代码运行结果:
代码1:
在这里插入图片描述
代码2:
在这里插入图片描述
对比两个程序最后运行的结果,同样是1s,程序1的count最后加到了2w,但是程序2的count且加到了5亿多。二者是1w倍的关系。相差这么大时什么原因导致的呢?

但是是IO,因为程序1每加1次都在打印,但是程序2只是最后一次才打印,所以程序1在运行的过程中不断的在进行IO操作,IO操作其实是很慢的。得出结论:一个体系结构中,IO是影响程序运行效率的最大一方面

通过硬件异常产生

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
这里给大家介绍两个硬件异常:CPU产生异常MMU产生异常

CPU产生异常 发生除零错误,CPU运行单元会产生异常,内核将这个异常解释为信号,最后OS发送SIGFPE信号给进程

实例演示:

#include 
#include 
#include 
#include 


int main()
{
  
  // 由软件条件产生信号  alarm函数和SIGPIPE
  // CPU运算单元产生异常,内核将这个异常处理为SIGFPE信号发送给进程
  int a = 10;
  int b = 0;
  printf("%d", a/b); 
  return 0;
}

代码运行结果如下:
在这里插入图片描述
MMU产生异常: 当进程访问非法地址时,mmu想通过页表映射来将虚拟转换为物理地址,此时发现页表中不存在该虚拟地址,此时会产生异常,然后OS将异常解释为SIGSEGV信号,然后发送给进程
实例演示:

#include 
#include 
#include 
#include 


int main()
{
  // MMU硬件产生异常,内核将这个异常处理为SIGSEGV信号发送给进程
  signal(11, handler);
  int* p = NULL;
  printf("%d\n", *p);
  return 0;
}

代码运行结果如下:
在这里插入图片描述

阻塞信号

了解几个概念

  • 实际执行信号的处理动作称为信号递达
  • 信号递达的三种方式:默认、忽略和自定义捕捉
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意: 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

信号在内核图中的表示

OS发生信号给一个进程,此信号不是立即被处理的,那么这个时间窗口中,信号就需要被记录保存下来,那么信号是如何在内核中保存和表示的呢?
先看下面的图(有三张表):
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第13张图片
说明: 上面有三种表,分别是信号阻塞位图block表,信号未决位图pending表,信号处理动作handler表

  • block表:每个信号对应1位,如果该位为1,那么代表该信号被阻塞,为0代表不被阻塞
  • pending表:如果该位为1,代表收到该信号,处于未决状态,为0代表还没收到该信号或者收到信号已经被递达了
  • handler表:代表对该信号处理动作,前面说过有三种,默认、忽略和自定义捕捉,其中自定义捕捉就是用户自定义的函数。handler表本质其实是函数指针数组,存放的是用户自定义函数的指针

分析图中几个信号:

  • 1号信号未阻塞也未产生过,但它递达时执行默认处理动作。
  • 2号信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • 3信号未产生过,一旦产生3信号将被阻塞,它的处理动作是用户自定义函数handler1。

总结:

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?

POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

回答几个问题:

  1. 所有信号的产生都要由OS来进行执行,这是为什么?

信号的产生涉及到软硬件,且OS是软硬件资源的管理者,还是进程的管理者。

  1. 进程在没有收到信号的时候,能否知道自己应该如何对合法信号进行处理呢?

答案是能知道的。每个进程都可以通过task_struct找到表示信号的三张表。此时该进程的pending表中哪些信号对应的那一位比特位是为0的,且进程能够查看block表知道如果收到该信号是否需要阻塞,可以查看handler表知道对该信号的处理动作。

  1. OS如何发生信号?

OS给某一个进程发送了某一个信号后,OS会找到信号在进程中pending表对应的那一位比特位,然后把那一位比特位由0置1,这样OS就完成了信号发送的过程。

信号集及信号集操作函数

sigset_t: 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,也被定义为一种数据类型。这个类型可以表示每个信号状态处于何种状态(是否被阻塞,是否处于未决状态)。阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数: sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释
注意: 对应sigset类型的变量,我们不可以直接使用位操作来进行操作,而是一个严格实现系统给我们提供的库函数来对这个类型的变量进行操作

下面是信号集操作函数的原型:

#include 
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
  • sigemptyset: 初始化set指向的信号集,将所有比特位置0
  • sigfillset: 初始化set指向的信号集,将所有比特位置1
  • sigaddset: 把set指向的信号集中signum信号对应的比特位置1
  • sigdelset: 把set指向的信号集中signum信号对应的比特位置0
  • sigismember: 判断signum信号是否存在set指向的信号集中(本质是信号判断对应位是否为1)

注意: 在实现这些函数之前,需要使用sigemptysetsigfillset对信号集进行初始化。前四个函数的返回值是成功返回0,失败返回-1。最后一个函数的返回值是真返回1,假返回-1

阻塞信号集操作函数——sigprocmask:

功能: 读取或更改进程的信号屏蔽字
函数原型:

#include  
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

参数:

  • how:三个选项

    • SIG_BLOCK:把set中的信号屏蔽字添加到进程的信号屏蔽字中,mask = mask|set
    • SIG_UNBLOCK:把set中的信号屏蔽字在进程信号屏蔽字的那些去掉,mask = mask&~set
    • SIG_SETMASK:设置当前进程的信号屏蔽字为set,mask = set
  • set:如果为非空指针,则根据how参数更改进程的信号屏蔽字

  • oset:如果为非空指针,将进程原来的信号屏蔽字备份六种oset中
    返回值: 成功返回0,失败返回-1

未决信号集操作函数——sigpending:

功能: 读取进程的未决信号集
函数原型:

#include  
int sigpending(sigset_t *set);

参数:

  • set:读取当前进程的信号屏蔽字到set指向的信号屏蔽中

返回值: 成功返回0,失败返回-1

实例演示:
实例1: 把进程中信号屏蔽字2号信号进行阻塞,然后隔1s对未决信号集进行打印,观察现象

#include 
#include 
#include 

void PrintPending(sigset_t* pend)
{
  int i = 0;
  for (i = 1; i < 32; ++i)
  {
    if (sigismember(pend, i)){
      printf("1");
    }
    else{
      printf("0");
    }
  }
  printf("\n");
}

int main()
{
  sigset_t set, oset;
  sigset_t pending;
  // 使用系统函数对信号集进行初始化
  sigemptyset(&set);
  sigemptyset(&oset);
  sigemptyset(&pending);

  // 阻塞2号信号
  // 先用系统函数对set信号集进行设置
  sigaddset(&set, 2);
  // 使用sigprocmask函数更改进程的信号屏蔽字
  // 第一个参数,三个选项:SIG_BLOCK(mask |= set) SIG_UNBLOCK(mask &= ~set) SIG_SETMASK(mask = set)
  sigprocmask(SIG_BLOCK, &set, &oset);
  
  int flag = 1; // 表示已经阻塞2号信号
  int count = 0;
  while (1){
    // 使用sigpending函数获取pending信号集
    sigpending(&pending);
    // 打印pending位图
    PrintPending(&pending);
    sleep(1);
  }
  return 0;
}

代码运行结果如下: 可以看到,进程收到2号信号时,且该信号被阻塞,处于未决状态,未决信号集中2号信号对应的比特位由0置1
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第14张图片
实例2: 将上面的代码进行修改,进行运行10s后,我们将信号屏蔽字中2号信号解除屏蔽

int count = 0;
while (1){
	// 使用sigpending函数获取pending信号集
	sigpending(&pending);
	// 打印pending位图
	PrintPending(&pending);
	if (++count == 10){
		// 两种方法都可以
		sigprocmask(SIG_UNBLOCK, &set, &oset);
		//sigprocmask(SIG_SETMASK, &oset, NULL);
	}
sleep(1);
}

代码运行结果如下: 可以看出的是,2号信号解除阻塞后,信号被递达了,进程终止
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第15张图片

捕捉信号

捕捉过程的介绍

先思考一个问题:信号是什么时候被进程处理的?

首先,不是立即被处理的。而是在合适的时候,这个合适的时候,具体指的是进程从用户态切换回内核态时进行处理

这句话如何理解,什么是用户态?什么是内核态?

  • 用户态: 处于⽤户态的 CPU 只能受限的访问内存,用户的代码,并且不允许访问外围设备,权限比较低
  • 内核态: 处于内核态的 CPU 可以访问任意的数据,包括外围设备,⽐如⽹卡、硬盘等,权限比较高

注意: 操作系统中有一个cr寄存器来记录当前进程处于何种状态

进程空间分为用户空间和内核空间。此前我们介绍的页表都是用户级页表,其实还有内核级页表。进程的用户空间是通过用户级页表映射到物理内存上,内核空间是通过内核级页表映射到物理内存上,如下面简图所示:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第16张图片

进程有不同的用户空间,但是只有一个内核空间,不同进程的用户空间的代码和数据是不一样的,但是内核空间的代码和数据是一样的。
上面这些主要是想说:进程处于用户态访问的是用户空间的代码和数据,进程处于内核态,访问的是内核空间的代码和数据。
下面给演示信号捕捉的整个过程:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第17张图片

从上面的图可以看出,进程是在返回用户态之前对信号进行检测,检测pending位图,根据信号处理动作,来对信号进行处理。这个处理动作是在内核态返回用户态后进行执行的,所以这里也就回答了开始提出的那一个问题了。

我们还需要注意的是,如果信号处理动作是用户自定义的函数,那么信号捕捉的整个过程如下:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第18张图片

sigaction

这里还有介绍一个函数sigaction,这个也是一个对指定信号的执行动作进行特殊处理的函数

功能: 可以读取和修改与指定信号相关联的处理动作
函数原型:

#include 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

  • signum: 要操作的信号

  • act:一个结构体

    • sa_handler:SIG_DFT、SIG_IGN和handler(用户自定义处理函数)
    • sa_sigaction:实时信号处理的函数,我们不关心
    • sa_mask:一个信号屏蔽字,里面有需要额外屏蔽的的信号
    • sa_flags:包含一下选项,这里我们给0
    • sa_restorer:我们这里不使用
      结构体如下:
struct sigaction {
	void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  • act:如果不为空,根据act修改信号处理动作
  • oact: 如果不为空,备份原来的信号处理动作给oact

返回值: 成功返回0,失败返回-1

实例演示:

#include 
#include 
#include 

void handler(int signo)
{
  printf("catch a signal: %d\n", signo);
}

int main()
{
  struct sigaction act, oact;

  act.sa_flags = 0;// 选项 设置为0
  sigfillset(&act.sa_mask);
  act.sa_handler = handler;
  // 对2号信号修改处理动作
  sigaction(2, &act, &oact);
  while (1){
    raise(2);
    sleep(1);
  }
  return 0;
}

代码运行结果如下:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第19张图片

可重入函数

先看下面一段代码:

#include 
#include 

int a = 10;

void SelfAdd(int n)
{
	a = a + n;
	a = a + n;
}

void handler(int signo)
{
	SelfAdd(signo);
}
int main()
{
	signal(2, handler);
	SlefAdd(2);
	printf("%d\n", a);
	return 0;
}

上面我写了一个比较简单的代码,我们慢慢分析,当我们在主函数中执行调用SelfAdd时,进入该函数,执行完函数中int a = a + n这句代码后,a变成了12,此时收到2号信号,发生中断
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第20张图片
最后打印a结果是18,其实正常调用该函数的话,打印的应该是18。
像上面这样的因为重入导致结果错乱的函数就叫做不可重入函数。其中a是一个全局变量。如果一个函数值访问自己的局部变量或参数,那么这样的函数就叫做可重入函数。

思考一个问题:为什么两个不同的控制流程调用同一个函数,访问同一个局部变量或参数不会造成错乱?

在多线程中,每个线程虽然是资源共享,但是他们的栈却是独有的,所以说局部变量不会造成错乱

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

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

volatile

先看一段代码:

#include 
#include 
#include 

int flag = 1;
void handler(int signo)
{
  flag = 0;
  printf("flag changs from 1 to 0\n");
}

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

  while (flag);

  printf("running here...\n");
  return 0;
}

代码运行结果如下: 此时代码看上去没有什么问题
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第21张图片
此时我们使用gcc带上优化(-O2)进行编译,如下:
在这里插入图片描述
优化后,执行代码结果如下:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第22张图片
此时按下Ctrl C 程序并没有往下指向,其实是因为flag的值没有发生改变,因为编译器优化,会把flag这个变量放入到寄存器中,handler指向流只会把内存中的flag变为1,但是flag在寄存器中的值并没有改变,但while检测flag是检测寄存器中flag值,所以这下循环是不会退出的。
这时解决办法就是使用volatile关键字,报错内存的可见性。
volatile: 保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量
的任何操作,都必须在真实的内存中进行操作
在这里插入图片描述

加上这个关键字后,代码运行结果如下:
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第23张图片

总结

今天的内容就先介绍到这里了,喜欢的话欢迎点赞、支持和关注~
【Linux篇】第十二篇——信号(概念+信号的产生+阻塞信号+捕捉信号)_第24张图片

你可能感兴趣的:(Linux,Linux,信号)