Linux学习笔记-----进程信号

一、信号概念

1.信号是进程之间事件异步通知的一种方式,属于软中断。
2.在一个bash(终端)中,只允许有一个前台进程。
3.所有信号都必须经过操作系统的手发出。
4.用kill-l可以查看系统定义的信号列表。
5.普通信号可以发送多次,但是只进入一次,所以有可能丢失。
6.实时信号实时性比较强,来了立即处理,信号不会丢失,在系统层面采用链表进行管理。
在这里插入图片描述

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

二、信号的产生

信号有四种产生方式,通过终端按键产生信号、调用系统函数向进程发信号、由软件条件产生信号、硬件异常产生信号。这四种最终由操作系统向目标位置中写信号来进行发送。

(一)通过终端按键产生信号

Core Dump

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做CoreDump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortemDebug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。

1.首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:$ ulimit -c 1024。
在这里插入图片描述
2.然后写一个死循环程序,前台运行这个程序,然后在终端键入Ctrl-C( 不行)或Ctrl-\(可以)
在这里插入图片描述
3.ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。
(二)调用系统函数向进程发信号

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。

1.在程序中执行死循环程序,然后用kill命令给它发2号信号,
在这里插入图片描述
2.发送信号后
在这里插入图片描述
3.可以看到这个进程已经停止。
在这里插入图片描述
(三)由软件条件产生信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
在这里插入图片描述

这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。

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

三、信号的保存

首先信号有两种状态信号未决和递达。
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
信号保存通过三张表来进行保存,分别是block、pending、handle。他们在内核中的表示示意图如下。
在这里插入图片描述
(一)信号的阻塞(block位图)

1.进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2.当一个信号被阻塞时,block表的该信号的标志位被置为1,如上图2号信号和3号信号被阻塞1号未被阻塞。
3.block位图也叫信号屏蔽字
(二)未决信号集(pending位图)
第二张表则表示该信号有没有产生过,如果产生了,将该标志位置为1,如上图,1、3信号没有产生,即未递达,2号信号已产生,即已递达。
(三)handler位图
分为默认、忽略、用户自定义函数,singal就是自定义函数,即产生一个信号捕捉到后,系统是默认还是忽略还是自定义。
(四)sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
通过sigset_t设置的自定义类型在栈上,它并没有被设置进操作系统,还在用户空间,只有通过sigprocmask设置后才可以进入操作系统中。

通俗来说就是一种位图数据类型,用来保存信号的一种结构,类似于整型用int字符用char,保存信号就用sigset_t。
(五)信号集操作函数
1.要修改位图,由硬件或用户使用 sigset_t设置,让操作系统对进程进行操作来修改一个信号的状态,所有对信号操作都不能使用按位与按位或操作对位图进行修改,因为不同操作系统信号结构不一样,不一定是一个整数。应使用以下几个函数进行设置。

#include <signal.h>
位图清空即初始化
int sigemptyset(sigset_t *set);
将所指向的所有信号集置位
int sigfillset(sigset_t *set);
往定义的位图结构中增加一个信号
int sigaddset (sigset_t *set, int signo);
在定义的位图结构中删除所指向的信号
int sigdelset(sigset_t *set, int signo);
测试参数signum 代表的信号是否已加入至参数set信号集里。
int sigismember(constsigset_t *set, int signo);

注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
2.sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
sigprocmask作用是进行增删查改block信号集,本质就是对信号屏蔽字进行修改设置和查询。
在这里插入图片描述
3.sigpending
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
实例:
1.阻塞2号信号和40号信号, 分别给进程发送5次2号信号和5次40号信号,观察结果。

  1 #include<stdio.h>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 void show_pending(sigset_t *pending)
  5 {
  6   int sig=1;
  7   for(;sig<=40;sig++)
  8   {
  9     if(sigismember(pending,sig))
 10     {
 11       printf("1");
 12     }
 13     else 
 14     {
 15       printf("0");
 16     }
 17   }
 18   printf("\n");
 19 }
 20 int main()
 21 {
 22   sigset_t pending;
 23   sigset_t block1,block2,oblock1,oblock2;
 24   sigemptyset(&block1);
 25   sigemptyset(&block2);
 26   sigemptyset(&oblock1);
 27   sigemptyset(&oblock2);
 28   sigaddset(&block1,2);
 29   sigaddset(&block2,40);
 30 
 31   sigprocmask(SIG_BLOCK,&block1,&oblock1);
 32   sigprocmask(SIG_BLOCK,&block2,&oblock2);
 33   while(1)
 34   {
 35     sigemptyset(&pending);
 36     sigpending(&pending); 
 37     show_pending(&pending);
 38     sleep(1);
 39   }
 40                                                                                                                                                                          
 41   return 0;
 42 }          

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

五、信号的捕捉

1.sigaction函数

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

实例:自定义处理函数名称为“sigcb”, 在sigcb当中完成打印信号值。

  1 #include<stdio.h>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 
  5 void sigcb(int sig)
  6 {
  7 
  8   printf("catch a sig: %d\n",sig);
  9 }
 10 
 11 int main()
 12 {
 13 
 14   struct sigaction act;
 15   struct sigaction oldact;
 16 
 17   act.sa_handler=sigcb;
 18   sigaction(2,&act,&oldact);
 19   while(1)
 20   {
 21     printf("i am is running\n");
 22     sleep(1);
 23   }                                                                                                                                             
 24 
 25   printf("the process is end\n");
 26 
 27   return 0;
 28 }

在这里插入图片描述
2.signal函数

#include <signal.h>
int signal(参数1 ,参数2 );
参数1:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l查看(共64个)。其实这些信号时系统定义的宏。
参数2:我们处理的方式(是系统默认还是忽略还是捕获)
实例:自定义处理函数名称为“sigcb”, 在sigcb当中完成打印信号值。

1 #include<stdio.h>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 
  5 void sigcb(int sig)
  6 {
  7 
  8   printf("catch a sig: %d\n",sig);
  9 }
 10 
 11 int main()
 12 {
 13 
 14   signal(2,sigcb);
 15   while(1)
 16   {
 17     printf("i am is running\n");
 18     sleep(1);
 19   }
 20   printf("the process is end\n");
 21   return 0;
 22 }                

在这里插入图片描述

六、volatile函数

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

举个例子比如我们定义一个全局变量q=0;当我们发送信号的时候在信号函数中将q改成1,此时内存中q的值就为1,当我们进行O2优化时,将q=0直接优化到寄存器中,这样每次cpu进行处理时会优先看到q=0,当再次发送信号时,虽然q被更改成1,但是cpu还是只认为q=1,加上volatile后相当于cpu只认内存的值,而不去管寄存器的值。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ishao97

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

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

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

打赏作者

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

抵扣说明:

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

余额充值