Linux --- 进程信号

一.信号入门

1.生活应用的信号
街道上的红绿灯就是一个信号。
下课的铃声
古代的狼烟
2技术应用角度的信号
(1)用户输入命令,在shell下启动一个前台进程

  • 用户按ctrl + c,这个键盘输入产生了一个硬件信号,被os获取,解释成信号,发送给目标前台进程
  • 前台进程因为受到信号,从而引起进程退出。

3.注意

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

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

  • (1)每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
  • (2)编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

4.同步和异步
同步的原理:由文件(驱动程序)向应用程序发送信号,应用程序再调用信号函数。

应用层:首先需要应用程序绑定一个信号函数,当收到某个信号后,执行该函数;然后需要将该进程和某个设备文件绑定,这样设备文件可读写时,才知道给那个进程发信号。

**区别:**我们在读写设备文件时,可以采用同步或异步机制,同步机制下,需要我们的应用程序自己不断去查询设备文件是否可读写,属于主动出击;而采用异步机制时,应用程序是被动接受信号,当设备文件可读写时,由设备驱动程序给应用程序发信号,应用程序然后才去读写设备文件,属于被动接受。

同步和异步实现,都是靠对应的设备驱动程序。

5.信号处理常见方式概览
(sigaction函数稍后详细介绍),可选的处理动作有以下三种:

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

进程是通过位图向进程发信号的。
操作系统可以修改位图的比特位完成向目标进程发信号的动作。

1.通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一 下。

  • (1)Core Dump
    首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许 产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许 产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024
  1 #include<iostream>
  2 #include<unistd.h>
  3 #include<signal.h>
  4 #include<cstdio>
  5 
  6 void handler(int signo)
  7 {
  8     printf("get a sig: %d\n", signo);
  9 }
 10 
 11 int main()
 12 {
 13     signal(SIGINT, handler);
 14     while(1)
 15     {
 16         printf("I am running .....\n");
 17         sleep(1);
 18     }
 19 }

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

  • (1)kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号,
  • (2)raise函数可以给当前进程发送指定的信号(自己给自己发信号)
#include <signal.h> 
int kill(pid_t pid, int signo); 
int raise(int signo); 
//这两个函数都是成功返回0,错误返回-1。
 
  • (3)abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h> 
void abort(void); 
//就像exit函数一样,abort函数总是会成功的,所以没有返回值

3.由软件条件产生的信号

#include <unistd.h> 
unsigned int alarm(unsigned int seconds); 
//调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 
//该信号的默认处理动作是终止当前进程。 
//1秒钟打印出多少数据
 10 int main()
 11 {
 12     int count = 0;
 13     alarm(1);
 14     while(1)
 15     {
 16         printf("data is:%d\n", count++);
 17     }
 18 }
//1秒中捕获数据,只打印一次
  1 #include<iostream>
  2 #include<unistd.h>
  3 #include<signal.h>
  4 #include<cstdio>
  5 #include<signal.h>
  6 #include<stdlib.h>
  7 
  8 int count = 0;
  9 
 10 void handler(int sig)
 11 {
 12     printf("count is : %d\n", count);
 13     exit(1);
 14 }
 15 int main()
 16 {
 17     signal(14,handler);
 18     alarm(1);
 19     while(1)
 20     {
 21         count++;
 22     }
 23     return 0;
 24 }

4.硬件异常产生信号

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

三.阻塞信号

1.概念

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

在这里插入图片描述
2.在内核中的表示

  • (1)每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子 中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • (2)SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • (3)SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。本章不讨论实时信号。

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

4.信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做 任何解释,比如用printf直接打印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);
int sigismember(const sigset_t *set, int signo);

  • (1)函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • (2)函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
  • (3)注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • (4)这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
//调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
 #include <signal.h> 
 int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
 //返回值:若成功则为0,若出错则为-1 
  1 #include<iostream>
  2 #include<unistd.h>
  3 #include<signal.h>
  4 #include<cstdio>
  5 #include<signal.h>
  6 #include<stdlib.h>
  7 
  8 
  9 void handler(int signo)
 10 {
 11     printf("sig is : %d\n", signo);
 12 }
 13 int main()
 14 {
 15     signal(2,handler);//对2号信号进行handler方法操作
 16     sigset_t set, oset;//定义信号集对象
 17     sigemptyset(&set);//清空初始化信号集对象
 18     sigaddset(&set,2);//将2号信号添加到信号集中
 19 
 20     sigprocmask(SIG_BLOCK,&set,&oset);//设置阻塞信号集,阻塞2号信号
 21 
 22     int flag = 0;
 23     sigset_t pi;
 24     while(1)
 25     {
 26         sigpending(&pi);//读取当前进程的未决信号集
 27         int i = 1;
 28         for(;i <= 31; i++)
 29         {
 30             if(sigismember(&pi, i))//判断信号集中是否包含2号信号
 31             {
 32                 printf("1 ");
 33             }
 34             else
 35             {
 36                 printf("0");
 37             }
 38         }
 39         printf("\n");
 40         sleep(1);
 41         flag++;
 42 
 43         if(flag > 10)
 44         {
 45             sigprocmask(SIG_SETMASK, &oset, NULL);//取消二号信号集,二号信号应答
 46         }
 47     }
 48     return 0;
 49 }
四.捕捉信号

信号的捕捉
在这里插入图片描述
1.内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码 是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返 回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。

2.volatile
作用:保持内存的可见性,防止编译器做优化,把相关变量优化到寄存器当中,进而覆盖了内存当中的数据表现。
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

volatile int i=10;
int a = i;

// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值