信号
什么是信号?
我们可以从实际生活中来解释信号,像红绿灯就是一个信号,我们可以通过它发出的哪个信号去做相对应的事情。
比如我们可以写一个简单的死循环程序:
运行时我们按下ctrl+c就可以中断这个死循环程序。我们可以将ctrl+c解释成一个SIGINT信号,可以说给这个进程发送了一个SIGINT信号使这个进程结束。
我们可以用kill -l查看系统定义的信号列表:
可以看到一共有62种信号,1-31 是普通信号,34-64 是实时信号。我们现在只学习普通信号。
上面我们用ctrl+c可以中断死循环程序,具体实现是怎样呢?
我们运行一个前台进程,通过按下ctrl+c,这个键盘输入产生一个硬件中断。
如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
终端驱动程序将ctrl+c解释成一个SIGINT信号,记在该进程的PCB中。
当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,
然后根据这个信号的处理方式处理信号。
其实发送一个信号给一个进程就是在修改这个进程PCB中记录信号的结构体(一般我们可以认为这个结构体为位图)。
就是操作系统给进程发送信号,进程接收信号然后内核处理信号。
那么如何产生信号呢?接下来我们看几种产生信号的方法:
1:用户在终端按下某些键时,中断驱动程序会发送信号给前台进程。例如ctrl+c产生SIGINT信号,ctrl+\产生SIGOUT信号,ctrl+z产生SIGTSTP信号。
2:硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向前台进程发送适当的信号。
3:一个进程调用kill(2)函数可以发送信号给另一个进程。
4:软件条件产生。
内核处理信号的三种方式:
1:忽略该信号。
2:执行该信号的默认处理动作。大多数信号的默认处理动作都是终止进程。
3:用户自定义信号处理函数,也被称之为捕捉信号。
在终端下产生信号
上面提过大多数信号的默认处理动作都是终止进程,ctrl+\会产生一个SIGOUT信号而它的默认处理动作是终止进程并且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
接下来我们用之前写的死循环程序验证一下:
调用系统函数向进程发送信号
在后台运行死循环程序,然后用kill命令给它发SIGSEGV信号。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
raise函数可以给当前进程发送指定的信号。
abort函数使当前进程接受到信号并异常终止。
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
软件条件下产生信号
SIGPIPE信号就是一个由软件条件产生的信号,在学习管道时提到当俩个进程一方在写,另一方却不读时,当管道满了时会产生
SIGPIPE信号终止执行写端的进程。而今天我们要学习的是另一个由软件条件产生的信号SIGALRM和alarm函数。
这个函数的返回值是0,或者是以前设定的闹钟时间还剩下的秒数。如果seconds值为0,表示取消以前的闹钟,函数的返回值任然
是以前设定的闹钟时间还余下的秒数。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟还余
下的秒数。
运行这个程序之后一秒之内不停数数,1秒钟到了就被signal信号终止。
以上是产生信号的几种方法,信号有三个阶段:产生阶段,在PCB保留阶段,处理阶段。接下来我们要看几种状态。
实际执行信号的处理动作称为信号递达;信号从产生到递达之间的状态,称为信号未决(pending);进程可以选择阻塞
(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞之后就不会被递达,而忽略是递达之后可选择的一种处理动作。
信号在内核中的表示示意图为:
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的因此,未决和阻
塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞
信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状
态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
接下来介绍几个信号集操作函数:
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使
用者的角度是不必关心的,使用者只能调用以下函数来操作sigset t变量,而不应该对它的内部数据做任何解释,⽐比如⽤用printf直
接打印sigset_t变量是没有意义的。
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰示该信号集不包含任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使⽤用sigset_ t类型的变量之前,⼀一定要调 ⽤用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化
sigset_t变量之后就可以在调⽤用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是⼀一个布尔函数,⽤用于判断⼀一个信号集的有效信号中是否包含某种信
号,若包含则返回1,不包含则返回0,出错返回-1。
调用sigprocmask函数可以读取或更改进程的信号屏蔽字。
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指
示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset⾥里,然后根据set和how参数更改信号屏蔽字。
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
sigpending函数可以读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错返回-1。
写一个小程序验证一下上面的几个函数:
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4
5 void printsigset(sigset_t *set)
6 {
7 int i = 0;
8 for(;i<32;i++){
9 if(sigismember(set,i)){
10 printf("1");
11 }
12 else{
13 printf("0");
14 }
15 fflush(stdout);
16 }
17 printf("\n");
18 }
19 int main()
20 {
21 sigset_t s,p;
22 sigemptyset(&s);
23 sigaddset(&s,SIGINT);
24 sigprocmask(SIG_BLOCK,&s,NULL);
25 while(1){
26 sigpending(&p);
27 printsigset(&p);
28 sleep(1);
29 }
30 return 0;
31 }
程序运行起来之后,会把每个信号的未决状态打印一遍,我们阻塞了SIGINT信号,下面我们看看运行结果:
可以看到我们运行时按下ctrl+c不会退出,但会改变SIGINT信号的未决状态。没有阻塞SIGQUIT信号,按下ctrl+\会退出。
捕捉信号
信号处理的第三种方式是捕捉信号,那么内核是如何实现信号的捕捉的呢?
捕捉信号使信号处理的一种方式,那我们要知道一个概念:进程是从内核态返回到用户态时进行信号处理。而信号处理的动作如
果是自定义,那我们就实现了信号捕捉。可以用一张图让我们理解起来更直观一些:
上图就是捕捉信号的具体过程,我们可以形象的将其记忆为一个无穷大的符号的上半部穿过了一根横线,横线上面是用户态,下
面是内核态。
sigaction函数可以读取和修改与指定信号相关联的处理动作。调⽤用成功则返回0,出错则返回- 1。
signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理
动作。act和oact指向sigaction结构体:将sahandler赋值为常数SIGIGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执
行系统默认动作,赋值为⼀一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,
可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是
被main函数调用,而是被系统所调用。
pause函数使调用进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处
理动作是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno
设置为EINTR, 所以pause只有出错的返回值。
我们用上面的函数模拟实现一个mysleep函数:
1 #include<stdio.h>
2 #include<signal.h>
3
4 void sig_alarm(int signal)
5 {
6 //自定义处理
7 }
8 unsigned int mysleep(unsigned int time)
9 {
10 struct sigaction new,old;
11 sigset_t newmask,oldmask,suspmask;
12 unsigned int unslep = 0;
13 new.sa_handler = sig_alarm;
14 new.sa_flags = 0;
15 sigemptyset(&new.sa_mask);
16 sigaction(SIGALRM,&new,&old);
17 sigemptyset(&newmask);
18 sigaddset(&newmask,SIGALRM);
19 sigprocmask(SIGALRM,&newmask,&oldmask);
20 alarm(time);
21 suspmask = oldmask;
22 sigdelset(&suspmask,SIGALRM);
23 sigsuspend(&suspmask);
24 unslep = alarm(0);
25 sigaction(SIGALRM,&old,NULL);
26 sigprocmask(SIG_SETMASK,&oldmask,NULL);
27 return unslep;
28 }
29 int main()
30 {
31 while(1){
32 mysleep(5);
33 printf("5 seconds passerd\n");
34 }
35 return 0;
36 }
其中有个函数sigsuspend,这个函数本质和pause是一个作用,但它将“解除信号屏蔽”和“挂起等待信号”这两步合并成了一
个原子操作。解决了竟态条件的问题。什么是竟态条件呢?意思就是我们在写程序时如果考虑不周,就可能由于时序问题而导致
错误,这叫做竟态条件。时序问题:系统运行的时序不像我门所设想的那样,出现更高优先级的进程在任何时候都有可能发生。
具体内容可以自行查一下。
接下来我们看上面那个程序的运行结果:
每隔5秒就会打印我们想要输出的东西,和系统的sleep函数效果一样。
以上就是信号的部分内容了,还有一些内容后续会补上。