1. 程式人生 > >Linux——淺析信號處理

Linux——淺析信號處理

第一個 鍵盤 自定義函數 什麽 nbsp 程序 連接 執行 class

信號及其處理


信號處理是Unix和LInux系統為了響應某些狀況而產生的事件,通常內核產生信號,進程收到信號後采取相應的動作

例如當我們想強制結束一個程序的時候,我們通常會給它發送一個信號,然後該進程會捕捉到信號,緊接著該進程執行一定操作後最終被終止掉。不僅僅如此,通常下面幾種情況

  鍵盤事件(ctrl+c、ctrl+\

  ②訪問非法內存 

  ③硬件故障(如算術運算執行除以0操作

  ④ 環境切換

都會有信號的產生,而對這些產生的信號是需要讓進程來處理的,進而信號也被作為進程間通信或修改行為的一種方式,是可明確地由一個進程發送給另一個進程的。一般當一個信號的產生時,我們把它叫作信號生

,對一個信號接收到叫信號捕獲。關於信號的捕獲例子是比較多的,這裏列舉平時可能經常遇到的幾個,其它可自行查詢(~v~雖然比較多)

然後來認識一下這些信號,可用  kill -l查看

技術分享圖片

平常最常用的信號

技術分享圖片

其它信號作用大致如下(前面數字代表信號編號),

 2.ctrl + c 進程終止信號 中斷方式終止掉進程
 3. ctrl + \  退出信號,發送 SIGQUIT 信號給前臺進程組中的所有進程,終止前臺進程並生成 core 文件
 6.異常退出信號    像abort退出等
 7.總線與進程虛擬地址空間未成功連接信號
 8.浮點數異常錯誤信號
 9.終止進程信號,與kill命令一起,可用來強行殺死進程 如kill-SIGKILL pid(註意,它不可被捕獲)
 11 段錯誤信號
 13.管道破裂信號
 14 鬧鐘信號
 17 子進程返回給父進程的信號
 19 進程暫停信號 但是它不可以被捕獲(和9號信號一樣,比較特殊)
 20 發送 SIGTSTP 信號給前臺進程組中的所有進程,常用於掛起一個進程。相當於ctrl+z
 23 處理緊急數據信號, 某些數據較為緊急,可使其優先傳輸。
 29 異步IO信號
 32~33號用作多線程使用,不讓用戶使用; 編號34之後的信號,是沒有限制的,可讓我們自己開發使用

針對上面這麽多信號,那對這些產生的信號進行何種處理?

可通過 man 7 signal命令查看處理的方式,通常進程對收到的信號的處理有以下3種方式

   默認處理方式

  ② 忽略

    對到來的信號,不做出反應 但SIGKILL SIGSTOP不能被忽略)

  ③ 捕獲並處理

   對到來的信號,執行我們自己寫的代碼 但是註意SIGKILL 和SIGTOP不能捕獲

好,接下來便看看信號處理的一些具體的例子

對信號的操作


(1)註冊信號

註冊信號實際是對信號進行三種處理操作,用於告訴當前進程對接收到信號後該去執行什麽動作

具體用signal函數來進行操作,它原型如下

頭文件:#include <signal.h>

   typedef void (*sighandler_t)(int);
   sighandler_t signal(int signum, sighandler_t handler);

    //void(* signal(int signum, void (*handler)(int)))(int)

功能:開始獲取信號值為signum的信號,如果獲取到該信號,則開始執行handler指向的函數(典型回調函數)
返回值: 調用成功返回原本的信號處理函數指針,失敗返回 SIGERR
SIGERR的宏為 #define SIG_IGN ((sighandler_t)-1)

參數:

   signum:指明了所要處理的信號類型,它可以取除了SIGKILL和SIGSTOP外的任何一種信號。

   sighandler_t:描述了與信號關聯的動作,它可以取以下三種值,如下表:

技術分享圖片

註:上面這幾個信號可傳入作signal函數第二個參數,因為它們雖是整型但進行了強轉。

這裏可以來個例子看看

//比如驗證SIG_IGN信號,這樣ctrl c就起不了作用了
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <signal.h>
 
 int main( void)
 {
     signal( SIGINT, SIG_IGN);
 
     for(int i =0; i< 20; ++i)
     {
         printf( "玩不死我!\n");
         sleep(1);
     }
     return 0;
 }

結果如下:

技術分享圖片

再看另一個

//驗證自定義信號,這樣ctrl c 就會去執行 handler 函數

 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <signal.h>
 
 void handler(int s)
 { 
     printf( "呃,我被搞死了。SIGNAL =%d\n", s);
     exit(1);
 }   
 int main( void)
 { 
     __sighandler_t ret;
     ret = signal( SIGINT, handler);
     
     for(int i =0; i< 20; ++i)
     { 
         printf( "玩不死我!\n");
         sleep(1);
     }   
     return 0;
 } 

結果驗證:

技術分享圖片

上面是幾個簡單的信號處理,其實信號是異步實現的,當信號到達時,系統會保存當前進程的運行環境,轉去執行信號處理函數,當信號處理函數執行完畢,然後再恢復現場,繼續往後執行(就好像中斷處理一樣)。

(2)給進程發送信號

前面的提到的signal函數對收到的信號進行了處理,同時我們也可主動給進程發送信號。具體可以有兩種方式,第一個就是用shell命令的方式

  kill -信號值 pid

一般可以用jobs 去查看有哪些後臺進程;而若要將執行程序以後臺方式運行,則可在後面加上 &符號(.如/a.out &)。時,如果當你再用ctrl c想要將該進程終止時 已經無法成功了,因為ctrl + c只能發給前臺進程,結束的是前臺進程。 這個時候可以用fg +%numid將後臺進程調到前臺進程(註意:numid是作業號,不是進程pid),這樣便可使用ctrl c了。同時,如果想要將前臺執行進程轉去後臺暫停掉可使用 Ctrl + Z命令

好,然後第二種給進程發送信號還可以通過函數的方式

原型:int kill(int pid, int signum)

功能:用該函數給進程id為pid的進程發送一個信號值為signum的信號
返回值
成功返回0,失敗返回-1
參數解釋
signum:信號值,即信號編號
pid:進程id,它可以取以下四種值,如下表: 

技術分享圖片

順道提一下進程組

進程組中通常有若幹個進程。 它們可以是用管道連接的進程,可以是fork創建出來的父子進程;這些都屬於同一個進程組

來例子繼續應用一下~

 /*************************************************************************
   > File Name: 3.c
   > Author: tp
   > Mail: 
   > Created Time: Tue 08 May 2018 08:55:26 PM CST
  ************************************************************************/
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <signal.h>
 void handler( int s) //自定義函數,驗證能否接收信號 
 {   
     printf( "信號收到!recv_SIG=%d\n", s);
 }
 int main( void)
 {
     signal(SIGUSR1, handler);
     pid_t pid = fork( );
     if( pid == 0)
     {
         sleep(1);
         kill(getppid(), SIGUSR1); //給父進程發送一個自定義信號SIGUSR1,該信號常用於接收發信號
         exit( 0);
     }
     else
     {
         int i =0;
         while( 1)
         {
             printf( "%d 我執行子進程\n", i++);
             sleep(1);  //返回>0 表示還剩多少時間允許其被信號打斷
         }
     }
     return 0;
 } 

技術分享圖片

除此之外還有兩個函數可以發送信號,可以了解一下

1.int rasie (int signum);  //給自己發送信號
返回值:成功返回0;失敗:返回-1

2.int killpig(int gid,int signum); // 給進程組發送信號
返回值:-1,並把error值設為EINTR

同時我們還可以暫停進程,直到進程被信號打斷

int pause(void)函數 值得註意的是它暫停時,會讓出cpu,不像while(1)循環

信號的分類

前面,我們列舉了這麽多信號,它們大致可分為①可靠信號 ②不可靠信號 ③實時信號 ④非實時信號,這樣4種信號

不可靠信號:編號為1~31 的信號都是不可靠的信號。由於linux的信號繼承自早期的UNIX 信號,所以這些不可靠信號也或多或少也繼承了UNIX信號的缺陷即,

  * 信號處理函數完畢,信號會恢復成按默認處理方式處理(不過現在liunx已經將其改進)

  * 會出現信號丟失的現象,原因就是此類信號不排隊,並且此種情況暫時還沒辦法解決

可靠信號34 - 64號信號為可靠的信號。 它不會出現信號丟失,支持排隊,信號處理函數執行完畢,不會恢復成缺省的處理方式

實時信號:就是可靠信號(字面意義上感覺實時信號好像要比非實時信號要快,其實不然)

非實時信號:其實就是不可靠信號

(3)SIGALRM信號

這個SIGALRM信號(編號14)在平常的應用中比較廣泛。常常用alarm函數來發出SIGALRM信號,來用作報警處理,這個信號也是一個很有用的信號,用它可以來實現一些比較有意思的東西。當然要使用它,還得先來看看這個alarm函數,它原型是

技術分享圖片

功能:
    當規定的seconds時間到了,給當前進程發送一個SIGALRM信號
返回值:
    成功:如果調用此alarm()前,進程已經設置了鬧鐘時間,則返回上一個鬧鐘時間的剩余時間,否則返回0。失敗就返回-1
參數解釋:
 如果second > 0:當seconds秒後,觸發SIGALRM信號
 如果seconds = 0: 表示清除SIGALRM信號

稍稍應用一下

 /*************************************************************************
   > File Name: alarm.c
   > Author: tp
   > Mail: 
   > Created Time: Tue 08 May 2018 09:15:53 PM CST
  ************************************************************************/
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <signal.h>
 
 void handler( int s)
 {
     printf( "\n很可惜,時間到了!\n");
     exit( 1);
 }
 int main( void)
 {
     char buff[ 100]={ };
     printf( "輸入字符串:");
 
     signal(SIGALRM, handler); //收到SIGALRM信號時,執行handler函數
     alarm( 3);   //設置3秒的警報時間,時間一到便發出SIGALRM信號
     scanf("%s", buff);
     alarm(0);  //清除鬧鐘
     printf( "收到:%s\n", buff);
     while( 1)
     {
         printf( "6 ");
         fflush( stdout);
         sleep( 1);
     }
     return 0;
 }

技術分享圖片

信號阻塞


實際執行信號的處理動作稱為信號抵達,信號從產生到抵達之間的狀態,稱為信號未決。進程可以選擇阻塞某個信號, 被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞後才執行後續的抵達動作。 信號阻塞和上面所述的信號忽略是不相同的。信號只要被阻塞時,它就不會抵達;而信號忽略則是在抵達之後可選的一種處理動作。 同時,每個信號都有兩個標誌位來分別表示阻塞(block) 和 未決(pendin)。還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標誌,直到信號抵達才消除該標誌. 技術分享圖片 如上圖的SIGHUP信號未阻塞也未產生過,當它抵達時執行默認處理動作。 而SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前是不能忽略這個信號的,因為進程仍有機會在改變處理動作之後再解除阻塞,然後接著來處理。SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。 如果在進程在阻塞某信號時,該信號產生過多次,Liunx這樣實現的:常規信號在抵達之前產生多次只計一次,而實時信號在遞達之前 產生多個信號可以依次放在一個隊列裏。 每個信號只有一個bit的未決標誌,非0既1,這個地方不記錄該信號產生了多少次。同樣,阻塞標誌也是這樣表示的。因此呢,未決和阻塞標誌可以用相同的數據類型sigset_t來存儲,sigset_t為信號集,這個類型可以表示每個信號的"有效"或"無效“狀態,在阻塞信號集中"有效"和"無效"的含義是該信號是否被阻塞,而在未決信號集中類似。 阻塞信號集也叫做當前進程的信號屏蔽字. 主要的信號集操作函數
sigset_t類型對於每種信號用一個bit表示 "有效"或者"無效" 
頭文件:#include<signal.h>

①int sigemptyset(sigset_t *set);
//初始化set所指向的信號集,使其中所有信號的對應的bit清零,表示該信號集不包含任何有效信號.

②int sigfillset(sigset_t *set);
//初始化set所指向的信號集,使其中所有信號的對應bit置位,表示該信號機的有效信號包括系統支持的所有信號.

③int sigaddset(sigset_t *set,int signo);
//在該信號集中添加某種有效信號.

④int sigdelset(sigset_t *set,int signo);
//在該信號集中刪除某種有效信號

⑤int sigismemeber(const sigset_t *set,int signo);
//是一個布爾函數,用於判斷一個信號集的有效信號中是否包含某種信號,若包含賊返回1,不包含則返回0,出錯返回-1

⑥int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
//讀取或更改進程的信號屏蔽字(阻塞信號集)如果成功返回0 失敗返回-1

⑦int sigpending(sigset_t *set);
//讀取當前進程的未決信號集,通過set參數傳出,調用成功則返回0,出錯則返回-1.

一個比較經典的例子:

 /*************************************************************************
   > File Name: set.c
   > Author: tp
   > Mail: 
   > Created Time: Thu 10 May 2018 05:38:50 PM CST
  ************************************************************************/
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <signal.h>
 
 void printsigset( sigset_t *set)
 {
     int i = 0;
     for(  ; i<32; i++) {
         if( sigismember( set,i) )//判定指定信號是否在目標集合中  
             putchar( ‘1‘);
         else
             putchar( ‘0‘);
     }
     puts(" ");
 }
 
 int main( )
 {
     sigset_t s ,p;  //定義信號集對象 
     sigemptyset( &s) ; //清空進行初始化 
     sigaddset( &s, SIGINT) ;
     sigprocmask( SIG_BLOCK, &s, NULL); //設置阻塞信號集  
     while( 1)
     {
         sigpending( &p) ; //獲取未決信號集 
         printsigset( &p) ;
         sleep( 1) ;
     }
     return 0;
 }

這個程序的大概意思就是我們阻塞一個信號集,讓它一直處於未決狀態,並把它裏面的信號編號顯示出來,比如中途我們加入了一個ctrl+c, 後面信號集裏面就會出現這個信號,然後他們還是一直處於未決狀態。

技術分享圖片

特別提醒的是如果一個信號被進程阻塞,它就不會傳遞給進程,但會停留在待處理狀態,當進程解除對待處理信號的阻塞時,待處理信號就會立刻被處理。

Linux——淺析信號處理