Linux信號處理
這兩天一直在看Linux 中信號的知識,看完了也應該總結總結了.當然以下結論可能還有不足之處,有的話盡管言明,大家共同學習學習.
其實在現實生活中我們都知道信號是什麼意思,在生活中很簡單很容易理解,信號嘛就是一個信息的意思,比如說我們古代打仗的時候,兵將什麼時候開始向前沖,什麼時候開始奮戰,當指揮部發出一個"命令"的時候,這個命令可能是吹號角,也可能是放煙火等等,再如現在,我們接到一個電話,某個朋友拜托我們去干一件事,那我們接完電話後就可能立即去辦事情了,這些,"號角","煙火","電話"都是我們收到的"信號",而後面的動作就是我們收到信號後要做的動作.其實計算機也是一樣的,它的內部有好多進程在執行,如果一個進程想讓另一個進程辦一件事情,那麼它也可以通過發信號來通知另一個進程,這時候,信號在進程間的通信就起到了一定的作用,接下來我們具體看看信號的來源,種類,以及進程對信號的響應. www.2cto.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.2cto.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.2cto.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.2cto.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.2cto.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.2cto.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.2cto.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.2cto.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