1. 程式人生 > >淺談Linux中的訊號處理機制(一)

淺談Linux中的訊號處理機制(一)

     有好些日子沒有寫部落格了,自己想想還是不要荒廢了時間,寫點兒東西記錄自己的成長還是百利無一害的。今天是9月17號,暑假在某家遊戲公司實習了一段時間,做的事情是在Windows上用c++寫一些遊戲英雄技能的邏輯實現。雖然時間不算長,但是也算學了一點東西,對團隊專案開發流程也有了一個直觀的感受,專案裡c++11新特性也有用到不少,特別是lambda表示式,STL的一些容器和演算法也終於有了可以實踐的地方。由於自己比較喜歡Linux C,也就沒有做留下的打算,現在回到了學校,好好複習一段時間,準備一下校招吧。如果有朋友有工作機會的話,不妨可以推薦一下我O(∩_∩)O,我的EMAIL:[email protected]

。貌似有點扯遠了,好久不寫東西了。

訊號的基本概念

  談起Linux程式設計signal是非常重要的一塊內容。關於signal,Linux 的 manual有很詳細的介紹。具體:man 7 signal .我就不浪費篇幅貼出來了。

  訊號被認為是一種軟體中斷(區別於硬體中斷)。訊號機制提供了一種在單程序/執行緒 下處理非同步事件的方法。具體過程是當程序執行到某處,接受到一個訊號,保留“現場”,響應訊號(注意這裡的響應是一種巨集觀意義上的響應,對訊號的忽略(SIG_IGN)也被以為是一種響應,後面會詳細談到訊號響應的方式。),在返回到剛剛儲存的地方繼續執行。我製作了一張GIF或許可以清晰的體現這樣的處理方式:

      產生訊號的條件有很多,某些組合鍵(CTRL+C、CTRL+\,CTRL+Z等),kill命令,kill系統呼叫以及由核心產生的某些訊號(如核心檢測到段錯誤、管道破裂等)。值得注意的是當我們傳送訊號時受到許可權的限制,傳送一個訊號到另一個沒有許可權的程序是不合法的(關於許可權的規則會在之後的部落格總結)。訊號的種類非常多,都以SIG+名字的形式命名的巨集,通常都有實際意義和用法具體可查閱manual。有些常見的訊號是需要熟記的如SIGINT,SIGCHLD,SIGIO等等。在編寫程式的時候,我們最好用訊號的巨集的形式,這樣可讀性更好。那麼如何“響應”訊號呢?

訊號處理的介面之一 signal()

      對於大部分的訊號,Linux系統都有預設的處理方式。而大部分預設的處理方式是終止程式並轉儲core檔案。要處理訊號,Linux系統處理訊號的介面有兩個sigaction(),signal(),較簡單的是signal()函式,其形式如下:

 typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

      siganl()函式有兩個引數其中有一個int的引數便是要處理的訊號,諸如SIGINT的巨集。另一個引數型別為sighandler_t的函式指標,handler指標對應的函式我們稱之為:訊號處理函式(signal-handler function)。可見signal()的第二個引數是一個訊號處理函式,返回值也是一個訊號處理函式,失敗返回巨集SIG_ERR(SIGKILL和SIGSTOP的預設行為分別是殺死和停止一個程序,任何試圖改變這兩個訊號的處理方式的行為都將返回錯誤)。這樣經典形式的函式在Linux上我們經常會經常碰到。signal()函式的作用就是建立一個signum訊號的處理函式(establish a signal handler function)。通俗一點來說就是當signum訊號到來時,程序會儲存當前的程序棧,轉去執行siganl()中指定的handler函式。之前提到過,訊號的響應方式有多種,因此handler不僅可以是一個函式指標也可以是ISO C為我們定義的巨集:SIG_IGN,SIG_DEL,和他們的名字一樣SIG_IGN是忽略這個訊號,SIG_DEL是保持這個訊號的預設處理方式(預設處理方式也可以可以是SIG_IGN ,比較繞,但是合理)。前文提到的三個巨集定義分別如下(/usr/include/bits/signum.h):

#define SIG_ERR ((__sighandler_t) -1) 
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

      下面我寫一個小的DEMO演示一下如何寫一個訊號處理函式:

#include <signal.h>
#include <stdio.h>

void sigdemo(int sig)
{
    printf("Receive a signal:%s\n",strsignal(sig));
}

int main()
{
    if(signal(SIGINT,sigdemo) == SIG_ERR)
  {
    perror("signal()");
    return ;
  }
    printf("Main started.\n");
    pause();//wait a signal.
}

可以看到,我在main函式中並沒有主動呼叫sigdemo函式,可是執行程式後,當我們在中斷按下CTRL+C時(傳送SIGINT訊號,巨集對應的值是2),出現了這樣的結果:

[baixiang ~$]a.out
 Main started.
 ^CReceive a signal:2

可見sigdemo函式得到了執行,其引數sig便是接受到的訊號的值。要將訊號的值,轉換為其意義string.h中提供了一個函式char* strsignal(int sig), 基本上看到該函式原型就知道這個函式怎麼用了,在此我就不再浪費篇幅贅述了。

傳送訊號

     上文我們通過使用CTRL+C組合鍵傳送訊號SIGINT給當前的程序。但是這種方法只能傳送少部分訊號且並不適用所有的程序比如後臺程序和守護程序。守護程序不必說,連終端都沒有。互動shell (interactive shell)在啟動一個後臺程序的時候,會自動把中斷和退出訊號設定為忽略,關於這點我在網上看到一篇不錯的部落格:http://hongjiang.info/shell-script-background-process-ignore-sigint/ 。這樣的情況下就無法使用快捷鍵的方式了。這裡我介紹幾種其他的傳送訊號的方式。

     首先是shell命令kill其用法如下:

kill [-s signal|-p] [-q sigval] [-a] [--] pid...

     -s  signal  signal可以是諸如SIGINT,SIGQUIT之類的巨集,亦可以是1,2,3...這樣的值,可以隨意使用,你開心就好。

     -q queue  sigval是值,可以伴隨訊號傳遞,但是這裡只可以是一個integer,在程序中可以使用sigaction()接收到這個值,與之對應的是另一個函式sigqueue()。這裡先不詳細介紹,下文會談到。

     pid就是目標程序的程序id,可以是一個或者多個。但是傳送訊號時,要確保你所使用的使用者是具有傳送訊號到目標程序的許可權的。

     kill的選項遠不止這些,但是通常這些已經夠用了。如有興趣請自行 “man 1 kill”檢視。

     和shell命令kill有一個同名的系統呼叫kill(),其原型是這樣的:

int kill(pid_t pid, int sig);

     相信看了上邊的shell指令,這個函式的用法就一目瞭然了吧。pid是目標程序的pid,sig是要傳送的訊號。和其他函式一樣它也是成功返回0,失敗-1。然而真的這麼簡單嗎?事實上不是。pid這個引數在這裡大有學問。它的取值不僅僅可以是程序id,它甚至可以是負的。如果你對linux下程式設計熟悉的話,這樣的用法肯定接觸過,獲取訊息佇列時使用的msgrcv()函式,其中的msgtype引數也具有類似的用法。當然扯遠了。

pid>0 此時正式最普通的一種情況,pid是要目標程序的pid。

    pid=0  那麼kill()會將訊號傳送給呼叫程序同組的所有程序,也包括他自己。

    pid=-1 那麼訊號將被髮送至所有它具有許可權傳送訊號的每一個程序(init程序和呼叫程序除外)。

    pid<-1 訊號會發送sig訊號到組id等於該pid絕對值的程序組中的每一個程序

如果pid在以上四種情況之外,無法匹配到目標程序,那麼就會返回-1,errno被設定為ESRCH。當沒有許可權傳送時kill()也將失敗返回-1,errno會被設定為EPERM。關於linux上許可權是如何作用的細節,我爭取再後面的部落格總結一下。

     與kill()類似的還有一個函式killpg(),用法簡單多了,也不浪費篇幅了,檢視manual就能搞定。

     最後一個傳送訊號的函式是raise(),它只接受一個引數signal,然後把該訊號傳遞給呼叫程序:

int raise(int sig);//成功返回0,失敗返回-1

    由於這個函式不需要引用程序ID,它是被納入C99標準的函式。

    除了這幾種產生訊號的shell命令和函式之外還有一些情況下可以產生訊號,比如alarm(),settimer()之類的一些與時間相關的函式,以及一些常見的軟硬體錯誤都會產生訊號。詳細談這些貌似就有點淡化主題了,扯遠了。

不可靠訊號與可靠訊號的語義

      訊號的可靠與不可靠主要體現在兩個方面:

  • 對於不可靠訊號,程序每次處理訊號後,都會將訊號的處理方式設定為預設動作。而對於可靠訊號,它的處理函式執行以後,對該訊號的處理方式不會發生變化。
  • 訊號可能會丟失。

     由於Linux訊號機制基本上從早期的UNIX系統上的訊號機制移植過來的,所以Linux仍舊支援這些早期的不可靠訊號。但是Linux也對不可靠訊號做了(上面兩點區別的第一小點)改進,即不可靠訊號處理方式,不會在處理函式執行後變成預設方式。所以,在Linux上對於不可靠訊號與可靠訊號的區別就在於是否支援排隊。

     關於訊號是否會丟失,我們看這樣兩段程式碼,首先是rcv.c:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void fun(int sig)
{
    printf("Recvive a signal:%s\n",strsignal(sig));
}

int main()
{
    if(signal(SIGINT,fun) == SIG_ERR)
        perror("signal");
    printf("%d\n",getpid());
    while(1)
       pause();
}

     這段程式先安裝SIGINT的訊號處理函式fun,fun函式只是列印訊號資訊。之後打印出程序id同時死迴圈等待訊號。

另一段程式是send.c:

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc,char** argv)
{
    pid_t pid = (pid_t)atol(argv[1]);
    printf("%d\n",pid);
    int i = 0;
    for(;i<500;++i)
        kill(pid,SIGINT);
}

send程式從終端接收一個引數即目標程序的PID,然後向其傳送500次SIGINT訊號。下面我們分別把rcv.c,send.c編譯成rcv和send。首先執行rcv,打印出了程序pid 20273然後,我們在開一個終端執行send 20273,觀察到這樣的結果:

send明明發送了500個SIGINT訊號,而rcv中只接受處理了13個SIGINT訊號,這是怎麼回事兒呢?明天再接著寫下去,今天太晚了,戰鬥力不足有點犯困了。