Linux——淺析信號處理
信號及其處理
信號處理是Unix和LInux系統為了響應某些狀況而產生的事件,通常內核產生信號,進程收到信號後采取相應的動作。
例如當我們想強制結束一個程序的時候,我們通常會給它發送一個信號,然後該進程會捕捉到信號,緊接著該進程執行一定操作後最終被終止掉。不僅僅如此,通常下面幾種情況
①鍵盤事件(ctrl+c、ctrl+\)
②訪問非法內存
③硬件故障(如算術運算執行除以0操作)
④ 環境切換
都會有信號的產生,而對這些產生的信號是需要讓進程來處理的,進而信號也被作為進程間通信或修改行為的一種方式,是可明確地由一個進程發送給另一個進程的。一般當一個信號的產生時,我們把它叫作信號生
然後來認識一下這些信號,可用 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——淺析信號處理