信号的概念
为了理解信号,先从我们最熟悉的场景说起:
1. ⽤户输⼊命令,在Shell下启动⼀个前台进程。
2. ⽤户按下Ctrl-C,这个键盘输⼊产⽣⼀个硬件中断。
3. 如果CPU当前正在执⾏这个进程的代码,则该进程的⽤户空间代码暂停执⾏,CPU从⽤户态 切换到内核态处理硬件中断。
4. 终端驱动程序将Ctrl-C解释成⼀个SIGINT信号,记在该进程的PCB中(也可以说发送了⼀ 个SIGINT信号给该进程)。
5. 当某个时刻要从内核返回到该进程的⽤户空间代码继续执⾏之前,⾸先处理PCB中记录的信号,发现有⼀个SIGINT信号待处理,⽽这个信号的默认处理动作是终⽌进程,所以直接终⽌进程⽽不再返回它的⽤户空间代码执⾏。
需要注意的是:键盘上的组合键形成的信号只能用于前台进程
目前来说,前台进程随时随地都可以收到一个信号,因为你在这个进程运行的任何时刻,你都可以出入“Ctrl-C”终止该进程。也就是说,信号对于进程来说是异步的。
对于信号的种类的和多少,可以再Linux中断输入“kill -l”显示。
每个信号都是一个宏定义,具体的宏值为多少请百度。
信号的产生
产生信号的方法有四种:
1. 对于前台进程来说,通过键盘的组合键就可以;
2. 计算机的软/硬件中断或异常。
硬件异常产⽣信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令,CPU的运算单元会产⽣异常,内核将这个异常解释 为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址,,MMU会产⽣异常,内核 将这个异常解释为SIGSEGV信号发送给进程。
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
int i=0;
int ret=10/i;
return 0;
}
[dlm@localhost temp]$
3. signal函数,但严格来说signal并不是一个产生信号的函数,而是一个改变信号默认动作的函数,signal函数可以对于指定的信号更改他的默认动作来达到自定义信号动作的效果。
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include "mysleep.h"
void Handler(int i)
{
printf("get a signal:%d \n",i);
}
int main()
{
int i=1;
for(;i<32;i++){
signal(i,Handler);
mysleep(1);
}
return 0;
}
4. kill函数,kill函数可以对一个指定的进程发送一个指定的信号。
//kill函数
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
if( kill( atoi(argv[1]), atoi(argv[2]) )<0 ){
perror("kill");
exit(1);
}else{
printf("send No.%d sig to %d proc success\n",atoi(argv[2]),atoi(argv[1]));
}
return 0;
}
- 后台运行一个死循环进程
然后运行kill函数
信号的处理
而操作系统对发来信号的处理操作有三种:
1. 忽略此信号。
2. 执⾏该信号的默认处理动作。
3. 提供⼀个信号处理函数,要求内核在处理该信号时切换到⽤户态执⾏这个处理函数,这种⽅式称为捕捉(Catch)⼀个信号。我们上面的第三条其实就是捕捉函数。
软件如何产⽣信号
其实,库里已经为我们提供了函数,我们以alarm举例。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 这个函数的作用是在seconds秒后给本进程发送一个SIGALRM信号,而该信号的默认动作为终止该进程。
返回值:
这个函数的返回值是0或者是以前设定的闹钟时间还余下 的秒数。打个⽐⽅,某⼈要⼩睡⼀觉,设定闹钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡 ⼀会⼉,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
#include <stdio.h>
#include <sys/types.h>
int main()
{
int seconds=5;
printf("pid:%d\n",getpid());
int ret=alarm(seconds);
while(1);
printf("pid:%d\n",getpid());
return 0;
}
- 5秒钟之后因为闹钟信号来临,终止该进程。
信号的结构
信号并不是很简单的进程给操作系统发信号或操作系统给进程发信号,然后调用系统调用解决问题那么简单的。
实际执⾏信号的处理动作称为信号递达(Delivery),信号从产⽣到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block )某个信号。被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后 可选的⼀种处理动作。
信号的产生是有操纵系统发出的,发给进程。每个进程的PCB中都有三个“信号表”,分别为block、pending、handler。
需要注意的是:这三个表都是使用位图实现的。而且操作系统中,很多都是以位图实现的,思考一下为什么呢?
其中:block时信号屏蔽字,意味着只要这个位图中对应位有效后,对应的信号就被屏蔽,无法正常递达。
pending表的代表的意思是,信号我已经收到,但是还没有递达到正确位置。
handler表代表的意思是每个信号对应的处理方式。假如我们捕捉信号,自定义信号处理动作,则对应的handler表就会发生变化。
信号在计算机中可以用下图简单说明:
需要注意的是,即使pending中已有这个信号,但是block中已经屏蔽该信号,那该信号以无法正常递达。
两种信号的递达
如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?POSIX.1允许系统递送该信号⼀次或多次。Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。本章不讨论实时信号。从上图来看,每个信号只有⼀ 个bit的未决标志,⾮0即1,不记录该信号产⽣了多少次,阻塞标志也是这样表⽰的。因此,未决和阻塞标志可以⽤相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表⽰每个信号的“有效”或“⽆效”状态,在阻塞信号集中“有效”和“⽆效”的含义是该信号是否被阻塞,⽽在未决信号集中“有效”和“⽆效”的含义是该信号是否处于未决状态。下⼀节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这⾥的“屏蔽”应该理解为阻塞⽽不是忽略。
几个操作信号集的函数
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 1
- 2
返回值:若成功则为0,若出错则为-1
how参数的含义
#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);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰该信号集的有效信号包括系统⽀持的所有信号。注意,在使⽤sigset_t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以 在调⽤sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
使用这几个函数来试试对信号的了解
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void printsigset(sigset_t* sig)
{
int i=1;
for(;i<32;i++){
if( sigismember(sig, i) ){//check No."i" signal
printf("%d ",1);
}else{
printf("%d ",0);
}
}
printf("\n");
}
int main()
{
sigset_t a,b;
sigemptyset(&a);//init
sigemptyset(&b);
sigaddset(&a,SIGINT);//add SIGINT to a
if( sigprocmask(SIG_BLOCK,&a,NULL)<0 ){//block SIGINT signal ,mean CTRL-C can't end this peocess
perror("sigprocmask");
exit(1);
}
while(1){
sigpending(&b);//get b'pending table
printsigset(&b);//print now pending
sleep(1);
}
return 0;
}
可以发现,原来没有一个信号位有效,但是我使用组合键Ctrl+C后,2号信号就变为有效了,并且之前使用sigprocmask函数屏蔽了2号信号,也就是SIGINT,所以Ctrl+C没有终止程序。
信号的捕捉
如果信号的处理动作是⽤户⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。由于信号处理函数的代码是在⽤户空间的,处理过程⽐较复杂,举例如下:
1. ⽤户程序注册了SIGQUIT信号的处理函数sighandler。
2. 当前正在执⾏main函数,这时发⽣中断或异常切换到内核态。
3. 在中断处理完毕后要返回⽤户态的main函数之前检查到有信号SIGQUIT递达。
4. 内核决定返回⽤户态后不是恢复main函数的上下⽂继续执⾏,⽽是执⾏sighandler函数,sighandler和main函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是 两个独⽴的控制流程。
5. sighandler函数返回后⾃动执⾏特殊的系统调⽤sigreturn再次进⼊内核态。
6. 如果没有新的信号要递达,这次再返回⽤户态就是恢复main函数的上下⽂继续执⾏了。
可以用下图表示:
信号的捕捉的代码,上面已经实现了。