信号的基本概念、信号的产生以及阻塞信号

一、信号的基本概念

1.引入

计算机中常见的信号:(1) ⽤户输⼊命令,在Shell下启动⼀个前台进程;

(2)⽤户按下Ctrl-C/Ctrl-Z等,这个键盘输⼊产⽣⼀个硬件中断。如此类的组合键等被操作系统解释为信号(注意,Ctrl-C产⽣的信号只能发给前台进程。 );

(3)如果CPU当前正在执⾏这个进程的代码,则该进程的⽤户空间代码暂停执⾏,CPU从⽤ 户态 切换到内核态处理硬件中断。(硬件异常产生的信号)

(4)终端驱动程序将Ctrl-C解释成⼀个SIGINT信号,记在该进程的PCB中。(也可以说发送了 ⼀ 个SIGINT信号给该进程)

(5)当某个时刻要从内核返回到该进程的⽤户空间代码继续执⾏之前,⾸先处理PCB中记录的信号,发现有⼀个SIGINT信号待处理,⽽这个信号的默认处理动作是终⽌进程,以直接终⽌进程⽽不再返回它的⽤户空间代码执⾏。
./a.out & 像这样的⼀个命令 后⾯加个&可以放到后台运⾏,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

2.基本理解

信号(signal)是Linux进程间通信的一种机制,全称为软中断信号,也被称为软中断。信号本质上是在软件层次上对硬件中断机制的一种模拟。它提供了一种处理异步事件的方法,也是进程间惟一的异步通信方式。体现为操作系统修改了目标进程的PCB内容,即为对其发送了信号。信号的来源有多种方式,前面的引入部分已经其实就已经提出了,下面做以总结:(1)硬件方式

a.当用户在终端上按下某键时,将产生信号。如按下<Ctral + C>组合键后将产生一个SIGINT信号。

b.硬件异常产生信号:除数据、无效的存储访问等。这些事件通常由硬件(如:CPU)检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时正在进行的程序。

程序示例:

 

(2) 软件方式

c.用户在终端下调用kill命令向进程发送任务信号。

d.进程调用kill或sigqueue函数发送信号。

e.当检测到某种软件条件已经具备时发出信号,如由alarm或settimer设置的定时器超时时将生成SIGALRM信号。

3.信号的种类

⽤kill -l命令可以察看系统定义的信号列表:

每个信号都有⼀个编号和⼀个宏定义名称,定义在signal.h中,例如其中有定义#define SIGINT 2。其中1~31号信号为普通信号,34~64为实时信号,在Linux中没有33和32这两个信号。这里只研究普通信号,上面的普通信号的含义如下:

(1) SIGHUP:当用户退出Shell时,由该Shell启的发所有进程都退接收到这个信号,默认动作为终止进程。

(2) SIGINT:用户按下<Ctrl + C>组合键时,用户端时向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。

(3) SIGQUIT:当用户按下<Ctrl + />组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程并产生core文件。

(4) SIGILL :CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件。

(5) SIGTRAP:该信号由断点指令或其他trap指令产生。默认动作为终止进程并产生core文件。

(6) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。

(7) SIGBUS:非法访问内存地址,包括内存地址对齐(alignment)出错,默认动作为终止进程并产生core文件。

(8) SIGFPE:在发生致命的算术错误时产生。不仅包括浮点运行错误,还包括溢出及除数为0等所有的算术错误。默认动作为终止进程并产生core文件。

(9) SIGKILL:无条件终止进程。本信号不能被忽略、处理和阻塞。默认动作为终止进程。它向系统管理员提供了一种可以杀死任何进程的方法。

(10) SIGUSR1:用户定义的信号,即程序可以在程序中定义并使用该信号。默认动作为终止进程。

(11) SIGSEGV:指示进程进行了无效的内存访问。默认动作为终止进程并使用该信号。默认动作为终止进程。

(12) SIGUSR2:这是另外一个用户定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

(13) SIGPIPE:Broken pipe:向一个没有读端的管道写数据。默认动作为终止进程。

(14) SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。

(15) SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是,该信号可以被阻塞和处理。通常用来要求程序正常退出。执行Shell命令kill时,缺少产生这个信号。默认动作为终止进程。

(17) SIGCHLD:子程序结束时,父进程会收到这个信号。默认动作为忽略该信号。

(18) SIGCONT:让一个暂停的进程继续执行。

(19) SIGSTOP:停止(stopped)进程的执行。注意它和SIGTERM以及SIGINT的区别:该进程还未结束,只是暂停执行。本信号不能被忽略、处理和阻塞。默认作为暂停进程。

(20) SIGTSTP:停止进程的动作,但该信号可以被处理和忽略。按下<Ctrl + Z>组合键时发出该信号。默认动作为暂停进程。

(21) SIGTTIN:当后台进程要从用户终端读数据时,该终端中的所有进程会收到SIGTTIN信号。默认动作为暂停进程。

(22) SIGTTOU:该信号类似于SIGTIN,在后台进程要向终端输出数据时产生。默认动作为暂停进程。

(23) SIGURG:套接字(socket)上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达。默认动作为忽略该信号。

(24) SIGXCPU:进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。

(25) SIGXFSZ:超过文件最大长度的限制。默认动作为yl终止进程并产生core文件。

(26) SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是它只计算该进程占有用的CPU时间。默认动作为终止进程。

(27) SIGPROF:类似于SIGVTALRM,它不仅包括该进程占用的CPU时间还抱括执行系统调用的时间。默认动作为终止进程。

(28) SIGWINCH:窗口大小改变时发出。默认动作为忽略该信号。

(29) SIGIO:此信号向进程指示发出一个异步IO事件。默认动作为忽略。

(30) SIGPWR:关机。默认动作为终止进程。

(31) SIGRTMIN~SIGRTMAX:Linux的实时信号,它没有固定的含义(或者说可以由用户自由使用)。注意,Linux线程机制使用了前3个实时信号。所有的实时信号的默认动作都是终止进程。

这些信号各⾃在什么条件下产⽣,默认的处理动作是什么,signal(7) 中都有 详细说明:
man 7 signal

在Linux系统中,信号的可靠性是指信号是否会丢失,或者说该信号是否支持排除。SIGHUP( 1 ) ~ SIGSYS( 31 )之间的信号都是继承自UNIX系统是不可靠信号。Linux系统根据POSIX标准定义了SIGRTMIN(34) ~ SIGRTMAX(64)之间的信号,它们都是可靠信号,也称为实时信号。9号信号不可被捕捉/修改。位图中比特位32位与信号编号对应,其内容表示是否收到信号,1为收到信号,0为没有收到信号。

4.信号处理

对于信号,可选的处理动作有以下三种:
(1) 忽略此信号。
(2)执⾏该信号的默认处理动作。
(3)提供⼀个信号处理函数,要求内核在处理该信号时切换到⽤户态执⾏这个处理函数,这种⽅式称为捕捉(Catch)⼀个信号。

二、信号产生

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

SIGINT的默认处理动作是终⽌进程,SIGQUIT的默认处理动作是终⽌进程并且Core Dump,⾸先解释什么是Core Dump。当⼀个进程要异常终⽌时,可以选择把进程的⽤户空间内存数据全部 保存到磁盘上,⽂件名通常是core,这叫做Core Dump。进程异常终⽌通常是因为有Bug,⽐如⾮法内存访问导致段错误,事后可以⽤调试器检查core件以查清错误原因,这叫做Post-mortem Debug(事后调试) 。⼀个进程允许产⽣多⼤的core⽂件取决于进程的Resource Limit(这个信息保存 在PCB)。默认是不允许产⽣core⽂件的,因为core⽂件中可能包含⽤户密码等敏感信息,不安全。在开发调试阶段可以⽤ulimit命令改变这个限制,允许产⽣core⽂件。
Core Dump,即核心转储,进程异常退出之前,将其内存中的有效数据以文件形式存贮到内存中,方便事后进行GDB调试和定位。

⾸先⽤ulimit命令改变Shell进程的Resource Limit,允许core⽂件最⼤为1024K:

1 #include<stdio.h>
2 int main() //除0异常
3 {
4      a = 5;
5      a /= 0;
6      return 0;
7 }


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

⾸先在后台执⾏死循环程序,然后⽤kill命令给它发SIGSEGV信号。

13224signal进程的id。之所以要再次回车才显⽰Segmentation fault,是因为在13224
程终⽌掉 之前已经回到了Shell提⽰符等待⽤户输⼊下⼀条命令,Shell不希望 Segmentation fault信息和⽤ 户的输⼊交错在⼀起,所以等⽤户输⼊命令之后才显⽰。指定某种信号的kill命令可以有多种写 法,上⾯的命令还可以写成kill -SIGSEGV 13224kill -11 13224, 11 是信号SIGSEGV的编号。以往遇 到的段错误都是由⾮法内存访问产⽣的,⽽这个程序本⾝没错,给它发SIGSEGV也能产⽣段错误。 kill命令是调⽤kill函数实现的。 kill函数可以给⼀个指定的进程发送指定的信号。r a i s e函数可 以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)
#include <signal.h>

int kill(pid_t pid, int signo);

int raise(int signo);
这两个函数都是成功返回0,错误返回-1 。

abort函数使当前进程接收到信号⽽异常终⽌。
#include <stdlib.h>

void abort(void);
就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。

 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 #include<signal.h>
 4 void myhandler(int sig)
 5 {
 6      printf("sig is : %d",sig);
 7 }
 8 int main()
 9 {
10      int i = 0;
11      for(;i<32;i++)
12      {
13           signal(i, myhandler);
14      }
15      abort();
16      return 0;
17 }

3.由软件条件产⽣信号

SIGPIPE是⼀种由软件条件产⽣的信号,管道中已经介绍过了。主要介绍 alarm函数 和SIGALRM信号。

#include <unistd.h> unsigned int alarm(unsigned int seconds);
调⽤alarm函数可以设定⼀个闹钟,也就是告诉内核在seconds秒之后给当前进程发 SIGALRM信号, 该信号的默认处理动作是终⽌当前进程。 这个函数的返回值是0或者是以前设定的闹钟时间还余下 的秒数。打个⽐⽅,某⼈要⼩睡⼀觉,设定闹钟为30分钟之后响,20钟后被⼈吵醒了,还想多睡 ⼀会⼉,于是重新设定闹钟为15分钟之后响,以前设定的闹钟时间还余下的时间就是10分钟。如果 s e c o n d s值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹时间还余下的秒数。然是以前设定的闹钟时间还余下的秒数然是以前设定的闹钟时间还余下的秒数然是以前设定的闹钟时间还余下的秒数

 例 alarm

三、阻塞信号

1.信号在内核中的表⽰
以上我们讨论了信号产⽣(Generation )的各种原因,实际执⾏信号的处理动作称为信号递(Delivery),信号从产⽣到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block )某个信号。被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才 执⾏递达的动作。 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后 可选的⼀种处理动作。信号在内核中的表⽰可以看作是这样的:
达之后 可选的⼀种处理动作。信号在内核中的表⽰可以看作是这样的:

每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。 在上图的例⼦中
1. SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
2. SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
3. SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤户⾃定义函数sighandler

如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?POSIX.1 允许系统递送该信号⼀次或多次。 Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。 本章不讨论实时信号。从上图来看,每个信号只有⼀ 个bit的未决标志,01 ,不记录该信号产⽣了多少次,阻塞标志也是这样表⽰。因此,未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, s i g s e t _ t称为信号集,这个类型可以表⽰每个信号的有效⽆效状态,阻塞信号集有效⽆效的含义是该信号是否被阻塞,⽽在未决信号集有效⽆效的含义是该信号是否处于未决状态。⼀节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这⾥的“屏蔽”应该理解为阻塞⽽不是忽略。

2.信号集操作函数
sigset_t类型对于每种信号⽤⼀个bit表⽰有效⽆效状态,⾄于这个类型内部如何存储这 些bit则依赖于系统实现,从使⽤者的⾓度是不必关⼼的,使⽤者只能调⽤以下函数来操 作sigset_t变量,⽽不应该对它的内部数据做任何解释,⽐如⽤printf直接打印sigset_t量是没 有意义的。

1 #include <signal.h>
2 int sigemptyset(sigset_t *set); //初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含 任何有效信号。
3 int sigfillset(sigset_t *set);  //初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰ 该信号集的有效信号包括系统⽀持的所有信号。
4 // 注意,在使⽤sigset_t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
5 int sigaddset(sigset_t *set, int signo);  //初始化sigset_t变量之后就可以 在调⽤sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
6 int sigdelset(sigset_t *set, int signo);
7 int sigismember(const sigset_t *set, int signo);  //sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含某种 信号,若包含则返回1,
8 不包含则返回0,出错返回-1 。

 前面四个函数都是成功返回0,出错返回-1 。

3.sigprocmask
调⽤函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1。如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指,则 更改进程的信号屏蔽字,参数how指⽰如何更改。如果osetset都是⾮空指针,则先将原来的信号 屏蔽字备份到oset,然后根据sethow参数更改号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

如果调⽤ s i g p r o c m a s k解除了对当前若⼲个未决信号的阻塞,则在 s i g p r o c m a s k返回前,少将其中 ⼀个信号递达。

4.sigpending
#include <signal.h>
int sigpending(sigset_t *set);
sigpending读取当前进程的未决信号集,通过set参数传出。调⽤成功则返回0,出错则返回-1

 下面的程序测试信号屏蔽与解除并递达:

 输出结果:

程序运⾏时,每秒钟把各信号的未决状态打印⼀遍,由于我们阻塞了SIGINT信号,Ctrl-C将会使SIGINT信号处于未决状态,Ctrl-\仍然可以终⽌程序,因为SIGQUIT信号没有阻塞。

posted @ 2017-06-14 14:59 滴巴戈 阅读(...) 评论(...) 编辑 收藏