Linux信号处理
这两天一直在看Linux 中信号的知识,看完了也应该总结总结了.当然以下结论可能还有不足之处,有的话尽管言明,大家共同学习学习.
其实在现实生活中我们都知道信号是什么意思,在生活中很简单很容易理解,信号嘛就是一个信息的意思,比如说我们古代打仗的时候,兵将什么时候开始向前冲,什么时候开始奋战,当指挥部发出一个"命令"的时候,这个命令可能是吹号角,也可能是放烟火等等,再如现在,我们接到一个电话,某个朋友拜托我们去干一件事,那我们接完电话后就可能立即去办事情了,这些,"号角","烟火","电话"都是我们收到的"信号",而后面的动作就是我们收到信号后要做的动作.其实计算机也是一样的,它的内部有好多进程在执行,如果一个进程想让另一个进程办一件事情,那么它也可以通过发信号来通知另一个进程,这时候,信号在进程间的通信就起到了一定的作用,接下来我们具体看看信号的来源,种类,以及进程对信号的响应. www.zhishiwu.com
其实在计算机中信号就相当于是软中断,,它提供了一种处理异步事件的方法,也是进程键唯一的异步通信方式,我们知道异步就是说,根本没有时间规定,你(一个进程)现在可以处理你想干的事情,而当检测到有信号的时候再转去处理信号,也就是说这种信号的发生是随机的.
信号的来源有两种方式:硬件方式和软件方式.
信号的种类有好多中,在Linux下面,我们在终端通过使用命令 kill -l 便可以查看Linux系统支持的全部信号,至于信号的含义,这里不再赘述,有兴趣的读者可以翻阅相关的资料了解.我们也可以在头文件<signal.h>中查看这些信号.
而进程对信号有三种处理方式:捕捉信号,忽略信号,按照系统默认方式处理.
信号的捕捉和处理:Linux 系统对信号的处理主要由signal和sigaction函数来处理.
1.signal函数:用来设置进程在接收到信号时的动作,我们在shell下面输入man signal可获取该函数的原型.
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
signal会根据signum指定的信号编号来设置该信号的处理函数,当指定的信号到达时,就会转到参数handler指定的参数执行,如果参数handler不是函数指针,则必须是常数SIG_INT(忽略该信号)或者是SIG_DFL(采用默认方式处理信号).若handler是一个函数指针,它所指向的函数的类型是sighandler_t,,即它所指向函数有一个int型的参数,且返回值类型为void.函数执行成功返回以前的信号处理函数指针,当有错误发生时返回SIG_ERR(即-1).
下面通过具体的例子看一个signal函数的用法:
#include<stdio.h> www.zhishiwu.com
#include<signal.h>
/*信号处理函数*/
void handler_sigint(int signo)
{
printf("recv SIGINT/n");
}
int main()
{
/*安装信号处理函数*/
signal(SIGINT,handler_sigint);
while(1);
return ;
}
当我们在终端中运行该程序后,按下Ctrl+C键,则会出现recv SIGINT,继续按键继续出现recv SIGINT,但是当我们按下Ctrl+/后则退出了,我们按下的Ctrl+/键就相当与进程收到了SIGQUIT这个信号,而这个信号的作用就是让进程终止,那么如果我们忽略这个信号呢,则程序就会永远的死在那里了,退不出来了,如改为如下所示:
#include<stdio.h>
#include<signal.h>
void handler_sigint(int signo)
{
printf("recv SIGINT/n");
} www.zhishiwu.com
int main()
{
signal(SIGINT,handler_sigint);
signal(SIGQUIT,SIG_INT);//忽略该信号,此时Ctrl+/已不再起作用了.
while(1);
return ;
}
上面为什么会退出呢,就是因为进程没有对信号SIGQUIT 做处理,那么它就按照默认的方式走,即结束进程.
接下来我们看sigaction函数:同样man sigaction一下你会看到原型:
#include<signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)
signum与上面的相同,即可以是SIGKILL和SIGSTOP以外的任意信号,而act是一个结构体指针,如果act不是空指针,则为signum设置心的信号处理函数,如果oldact不是空指针,则旧的处理函数将被存储在oldact中.
接下来看一个sigaction的例子:
#include<stdio.h>
#include<signal.h>
int temp=0;
void handler_sigint(int signo)
{
printf("recv SIGINT/n");
sleep(5);
temp+=1;
printf("the value of temp is :%d/n",temp);
printf("in handler_sigint,after sleep/n");
}
int main()
{
struct sigaction act;
act.sa_handler=handler_sigint;
act.sa_flags=SA_NOMASK;//意思是在处理次信号前允许此信号再次传递,相当于中断嵌套
sigaction(SIGINT,&act,NULL);
while(1);
return 0;
} www.zhishiwu.com
运行结果如下:
hfm@hfm-Lenovo-KL6B-KL6C-Z475:~$ gcc -o sigaction sigaction.c
hfm@hfm-Lenovo-KL6B-KL6C-Z475:~$ ./sigaction
^Crecv SIGINT
^Crecv SIGINT
^Crecv SIGINT
^Crecv SIGINT
the value of temp is :1
in handler_sigint,after sleep
the value of temp is :2
in handler_sigint,after sleep
the value of temp is :3
in handler_sigint,after sleep
the value of temp is :4
in handler_sigint,after sleep
注意我上面提到的中断嵌套的含义,当我们按下Ctrl+c的时候,发出SIGINT信号,打印,然后休眠5秒,但是在这5秒之内呢,你又按下了Ctrl+c,于是从sleep函数处嵌套调用信号处理函数handler_sigint,5秒之后呢,函数打印出temp的值,又返回到sleep处继续执行你刚按下的第二次Ctrl+c,一次类推执行完四次之后,temp的值也增加到了4,于是程序返回到住函数处继续执行,基本上的执行过程就是这样的啦!
信号处理函数的发送主要由函数:kill,faise,sigqueue,alarm,setitimer,abort来完成.
通过man命令可以查看kill函数的原型:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
pid顾名思义就是进程号了,而sig是发送信号的编号,我们要注意,只有root权限的进程才能向其他任一进程发送信号,而非root权限的进程只能向同一组或同一用户的进程发送信号.
看一个简单模拟kill命令的例子:kill.c
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/types.h>
int main(int argc,char **argv)
{ www.zhishiwu.com
int i,j;
int signum=SIGTERM;
pid_t pid;
if(argc!=2&&argc!=4){
printf("Usage:./kill<-s signum>[pid]/n");
exit(0);
}
for(i=1;i<argc;i++){
if(!strcmp(argv[i],"-s")){
signum=argv[i+1];
break;
}
}
if(argc==2){
pid=atoi(argv[1]);
}else{
for(j=1;j<argc;j++){
if(j!=i&&j!=i+1){
pid=atoi(argv[j]);
break;
}
}
}
if(kill(pid,signum)<0){
perror("kill");
exit(0);
}
return 0;
}
信号SIGINT 的编号在所有的Linux系统中都为2.
函数raise比较简单,用来给调用它的进程发送信号:原型:int faise(int sig);参数sig表示要发送的信号编号,成功返回0,失败返回非0值.
sigqueue函数:它是一个比较新的发送信号函数,通过man命令可以查看原型.该函数不仅可以向进程发送信号,还可以给进程发送数据,其原型为:
int sigqueue(pid_t pid,int sig,const union sigval value);
下面来看一个例子:send_data_signo.c:利用信号传递数据,本程序发送数据
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
int main(int argc,char **argv)
{
union sigval value;
int signum=SIGTERM;
pid_t pid;
int i; www.zhishiwu.com
value.sival_int=0;
if(argc!=3&&argc!=5&&argc!=7){
printf("./send_data_signo <-d data> <-s signum> [-p] [data]/n");
exit(1);
}
/*从命令行解析出信号编号,PID 以及待传送的数据*/
for(i=1;i<argc;i++){
if(!strcmp(argv[i],"-d")){
value.sival_int=atoi(argv[i+1]);
continue;
}
if(!strcmp(argv[i],"-s")){
signum=atoi(argv[i+1]);
continue;
}
if(!strcmp(argv[i],"-p")){
pid=atoi(argv[i+1]);
continue;
}
}
/*利用sigqueue函数来发送信号*/
if(sigqueue(pid,signum,value)<0){
perror("sigqueue");
exit(1);
}
return 0;
}
这个例子是:利用信号传递数据,本程序接收数据
#include<stdio.h>
#include<signal.h>
void handler_sigint(int signo,siginfo_t *siginfo,void *pvoid)
{
printf("recv SIGINT,the data value is : %d/n",siginfo->si_int);
}
int main()
{
struct sigaction act;
act.sa_sigaction=handler_sigint;
act.sa_flags=SA_SIGINFO;//指定使用三参数的信号处理函数
sigaction(SIGINT,&act,NULL);
while(1); www.zhishiwu.com
return 0;
}
为了容易写,我把发送数据的进程的名字名为b.c,接收进程的名字命为c.c
运行上面的程序结果如下所示:
hfm@hfm-Lenovo-KL6B-KL6C-Z475:~$ ./c &
[1] 4229
hfm@hfm-Lenovo-KL6B-KL6C-Z475:~$ ./b -s 2 -d 100 -p 4229
recv SIGINT,the data value is : 100
alarm函数:可以用来设置定时器,定时器超时将产生SIGALRM信号给调用进程,在shell下面可以看原型:
unsigned int alarm(unsigned int seconds);参数seconds表示设定的秒数,经过seconds后,内核将给调用该函数的进程发送一个SIGALRM 信号,如果seconds为0将不再发送信号,最新一次调用该函数将取消之前一次的设定.alarm只设定为发送一次信号,如果要多次发送,就要对alarm进行多次调用.如果之前已经调用过alarm函数,则返回之前设置的定时器剩余时间,否则如果之前没有设置过定时器,则返回0.
下面通过一个模拟网络ping的例子来看一下alarm函数的用法:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void send_ip()
{
printf("send a icmp packet/n");
}
void recv_ip()
{
while(1);
}
void handler_sigint(int signo)
{
send_ip();
alarm(2);
}
int main()
{
signal(SIGALRM,handler_sigint);
raise(SIGALRM);//触发一个SIGALRM信号给本进程
recv_ip();
return 0;
}
运行结果如下所示:
hfm@hfm-Lenovo-KL6B-KL6C-Z475:~$ gcc -o m m.c
hfm@hfm-Lenovo-KL6B-KL6C-Z475:~$ ./m
send a icmp packet www.zhishiwu.com
send a icmp packet
send a icmp packet
send a icmp packet
send a icmp packet
send a icmp packet
send a icmp packet
send a icmp packet
send a icmp packet
..............
我们可以看到每当2秒过后就用raise()函数出发一个SIGALRM信号给本进程,实现ping程序的定时发包功能.同时我们也可以想象一下网络里面的路由器是怎么发送报文段的,它也是有时间规定的.我们可以通过设定alarm()函数中的值来设定.其实还有一个函数也是用来设定定时器的,它比alarm具有更多的功能,即setitimer()函数,它的原型如下:
int setitimer(int which,const struct itimerval *value,struct itimerval *ovalue);有兴趣的读者可以下去自己看一下.
void abort(void)函数用来向进程发送SIGABRT信号来终止进程,如果进程设置了SIGABRT被阻塞或忽略,abort()将覆盖这种设置.
目前所掌握的就这些了,以后接触到要再做研究,有待于更深层次的研究.
作者 hfm_honey