2011年12月20日 星期二

Linux環境進程間通信(二): 信號(上)

一、信號及信號來源

信號本質

信號是在軟體層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個插斷要求可以說是一樣的。信號是非同步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什麼時候到達。

信號是進程間通信機制中唯一的非同步通信機制,可以看作是非同步通知,通知接收信號的進程有哪些事情發生了。信號機制經過POSIX即時擴展後,功能更加強大,除了基本通知功能外,還可以傳遞附加資訊。

信號來源

信號事件的發生有兩個來源:硬體來源(比如我們按下了鍵盤或者其它硬體故障);軟體來源,最常用發送信號的系統函數是kill, raise, alarmsetitimer以及sigqueue函數,軟體來源還包括一些非法運算等操作。



二、信號的種類

可以從兩個不同的分類角度對信號進行分類:(1)可靠性方面:可靠信號與不可靠信號;(2)與時間的關係上:即時信號與非即時信號。在《Linux環境進程間通信(一):管道及有名管道》的附1中列出了系統所支援的所有信號。

1、可靠信號與不可靠信號

"不可靠信號"

Linux信號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的信號機制比較簡單和原始,後來在實踐中暴露出一些問題,因此,把那些建立在早期機制上的信號叫做"不可靠信號",信號值小於SIGRTMIN(Red hat 7.2中,SIGRTMIN=32SIGRTMAX=63)的信號都是不可靠信號。這就是"不可靠信號"的來源。它的主要問題是:

  • 進程每次處理信號後,就將對信號的回應設置為預設動作。在某些情況下,將導致對信號的錯誤處理;因此,用戶如果不希望這樣的操作,那麼就要在信號處理函數結尾再一次調用signal(),重新安裝該信號。
  • 信號可能丟失,後面將對此詳細闡述。 
    因此,早期unix下的不可靠信號主要指的是進程可能對信號做出錯誤的反應以及信號可能丟失。

Linux支援不可靠信號,但是對不可靠信號機制做了改進:在調用完信號處理函數後,不必重新調用該信號的安裝函數(信號安裝函數是在可靠機制上的實現)。因此,Linux下的不可靠信號問題主要指的是信號可能丟失。

"可靠信號"

隨著時間的發展,實踐證明了有必要對信號的原始機制加以改進和擴充。所以,後來出現的各種Unix版本分別在這方面進行了研究,力圖實現"可靠信號"。由於原來定義的信號已有許多應用,不好再做改動,最終只好又新增加了一些信號,並在一開始就把它們定義為可靠信號,這些信號支援排隊,不會丟失。同時,信號的發送和安裝也出現了新版本:信號發送函數sigqueue()及信號安裝函數sigaction()POSIX.4對可靠信號機制做了標準化。但是,POSIX只對可靠信號機制應具有的功能以及信號機制的對外介面做了標準化,對信號機制的實現沒有作具體的規定。

信號值位於SIGRTMINSIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問題。Linux在支援新版本的信號安裝函數sigation()以及信號發送函數sigqueue()的同時,仍然支援早期的signal()信號安裝函數,支援信號發送函數kill()

注:不要有這樣的誤解:由sigqueue()發送、sigaction安裝的信號就是可靠的。事實上,可靠信號是指後來添加的新信號(信號值位於SIGRTMINSIGRTMAX之間);不可靠信號是信號值小於SIGRTMIN的信號。信號的可靠與不可靠只與信號值有關,與信號的發送及安裝函數無關。目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾也不必再調用一次信號安裝函數。同時,由signal()安裝的即時信號支援排隊,同樣不會丟失。

對於目前linux的兩個信號安裝函數:signal()sigaction()來說,它們都不能把SIGRTMIN以前的信號變成可靠信號(都不支援排隊,仍有可能丟失,仍然是不可靠信號),而且對SIGRTMIN以後的信號都支援排隊。這兩個函數的最大區別在於,經過sigaction安裝的信號都能傳遞資訊給信號處理函數(對所有信號這一點都成立),而經過signal安裝的信號卻不能向信號處理函數傳遞資訊。對於信號發送函數來說也是一樣的。

2、即時信號與非即時信號

早期Unix系統只定義了32種信號,Ret hat7.2支援64種信號,編號0-63(SIGRTMIN=31SIGRTMAX=63),將來可能進一步增加,這需要得到內核的支援。前32種信號已經有了預定義值,每個信號有了確定的用途及含義,並且每種信號都有各自的缺省動作。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的預設反應就是進程終止。後32個信號表示即時信號,等同於前面闡述的可靠信號。這保證了發送的多個即時信號都被接收。即時信號是POSIX標準的一部分,可用于應用進程。

非即時信號都不支援排隊,都是不可靠信號;即時信號都支援排隊,都是可靠信號。



三、進程對信號的回應

進程可以通過三種方式來回應一個信號:(1)忽略信號,即對信號不做任何處理,其中,有兩個信號不能忽略:SIGKILLSIGSTOP;(2)捕捉信號。定義信號處理函數,當信號發生時,執行相應的處理函數;(3)執行缺省操作,Linux對每種信號都規定了預設操作,詳細情況請參考[2]以及其它資料。注意,進程對即時信號的缺省反應是進程終止。

Linux究竟採用上述三種方式的哪一個來回應信號,取決於傳遞給相應API函數的參數。



四、信號的發送

發送信號的主要函數有:kill()raise() sigqueue()alarm()setitimer()以及abort()

1kill() 
#include <sys/types.h>
 
#include <signal.h>
 
int kill(pid_t pid,int signo)
 

參數pid的值
信號的接收進程
pid>0
進程IDpid的進程
pid=0
同一個進程組的進程
pid<0 pid!=-1
進程組ID -pid的所有進程
pid=-1
除發送進程自身外,所有進程ID大於1的進程

Sinno是信號值,當為0時(即空信號),實際不發送任何信號,但照常進行錯誤檢查,因此,可用於檢查目標進程是否存在,以及當前進程是否具有向目標發送信號的許可權(root許可權的進程可以向任何進程發送信號,非root許可權的進程只能向屬於同一個session或者同一個使用者的進程發送信號)。

Kill()最常用于pid>0時的信號發送,調用成功返回 0 否則,返回 -1 注:對於pid<0時的情況,對於哪些進程將接受信號,各種版本說法不一,其實很簡單,參閱內核源碼kernal/signal.c即可,上表中的規則是參考red hat 7.2

2raise() 
#include <signal.h>
 
int raise(int signo)
 
向進程本身發送信號,參數為即將發送的信號值。調用成功返回 0;否則,返回 -1

3sigqueue() 
#include <sys/types.h>
 
#include <signal.h>
 
int sigqueue(pid_t pid, int sig, const union sigval val)
 
調用成功返回 0;否則,返回 -1

sigqueue()是比較新的發送信號系統調用,主要是針對即時信號提出的(當然也支持前32種),支援信號帶有參數,與函數sigaction()配合使用。

sigqueue的第一個參數是指定接收信號的進程ID,第二個參數確定即將發送的信號,第三個參數是一個聯合資料結構union sigval,指定了信號傳遞的參數,即通常所說的4位元組值。

            typedef union sigval {
                        int  sival_int;
                        void *sival_ptr;
            }sigval_t;




sigqueue()kill()傳遞了更多的附加資訊,但sigqueue()只能向一個進程發送信號,而不能發送信號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用於檢查pid的有效性以及當前進程是否有許可權向目標進程發送信號。

在調用sigqueue時,sigval_t指定的資訊會拷貝到3參數信號處理函數(3參數信號處理函數指的是信號處理函數由sigaction安裝,並設定了sa_sigaction指標,稍後將闡述)的siginfo_t結構中,這樣信號處理函數就可以處理這些資訊了。由於sigqueue系統調用支援發送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。

注:sigqueue()發送非即時信號時,第三個參數包含的資訊仍然能夠傳遞給信號處理函數; sigqueue()發送非即時信號時,仍然不支援排隊,即在信號處理函數執行過程中到來的所有相同信號,都被合併為一個信號。

4alarm() 
#include <unistd.h>
 
unsigned int alarm(unsigned int seconds)
 
專門為SIGALRM信號而設,在指定的時間seconds秒後,將向進程本身發送SIGALRM信號,又稱為鬧鐘時間。進程調用alarm後,任何以前的alarm()調用都將無效。如果參數seconds為零,那麼進程內將不再包含任何鬧鐘時間。 
返回值,如果調用alarm()前,進程中已經設置了鬧鐘時間,則返回上一個鬧鐘時間的剩餘時間,否則返回0

5setitimer() 
#include <sys/time.h>
 
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
 
setitimer()
alarm功能強大,支援3種類型的計時器:

  • ITIMER_REAL 設定絕對時間;經過指定的時間後,內核將發送SIGALRM信號給本進程;
  • ITIMER_VIRTUAL 設定程式執行時間;經過指定的時間後,內核將發送SIGVTALRM信號給本進程;
  • ITIMER_PROF 設定進程執行以及內核因本進程而消耗的時間和,經過指定的時間後,內核將發送ITIMER_VIRTUAL信號給本進程;

Setitimer()第一個參數which指定計時器類型(上面三種之一);第二個參數是結構itimerval的一個實例,結構itimerval形式見附錄1。第三個參數可不做處理。

Setitimer()調用成功返回0,否則返回-1

6abort() 
#include <stdlib.h>
 
void abort(void);

向進程發送SIGABORT信號,預設情況下進程會異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進程設置為阻塞信號,調用abort()後,SIGABORT仍然能被進程接收。該函數無返回值。



五、信號的安裝(設置信號關聯動作)

如果進程要處理某一信號,那麼就要在進程中安裝該信號。安裝信號主要用來確定信號值及進程針對該信號值的動作之間的映射關係,即進程將要處理哪個信號;該信號被傳遞給進程時,將執行何種操作。

linux主要有兩個函數實現信號的安裝:signal()sigaction()。其中signal()在可靠信號系統調用的基礎上實現, 是庫函數。它只有兩個參數,不支援信號傳遞資訊,主要是用於前32種非即時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支援信號傳遞資訊,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支援非即時信號的安裝。sigaction()優於signal()主要體現在支援信號帶有參數。

1signal() 
#include <signal.h>
 
void (*signal(int signum, void (*handler))(int)))(int);
 
如果該函數原型不容易理解的話,可以參考下面的分解方式來理解: 
typedef void (*sighandler_t)(int)
 
sighandler_t signal(int signum, sighandler_t handler));
 
第一個參數指定信號的值,第二個參數指定針對前面信號值的處理,可以忽略該信號(參數設為SIG_IGN);可以採用系統預設方式處理信號(參數設為SIG_DFL);也可以自己實現處理方式(參數指定一個函數位址) 
如果signal()調用成功,返回最後一次為安裝信號signum而調用signal()時的handler值;失敗則返回SIG_ERR

2sigaction() 
#include <signal.h>
 
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函數用於改變進程接收到特定信號後的行為。該函數的第一個參數為信號的值,可以為除SIGKILLSIGSTOP外的任何一個特定有效的信號(為這兩個信號定義自己的處理函數,將導致信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指標,在結構sigaction的實例中,指定了對特定信號的處理,可以為空,進程會以缺省方式對信號處理;第三個參數oldact指向的物件用來保存原來對相應信號的處理,可指定oldactNULL。如果把第二、第三個參數都設為NULL,那麼該函數可用於檢查信號的有效性。

第二個參數最為重要,其中包含了對指定信號的處理、信號所傳遞的資訊、信號處理函數執行過程中應遮罩掉哪些函數等等。

sigaction結構定義如下:

 struct sigaction {
          union{
            __sighandler_t _sa_handler;
            void (*_sa_sigaction)(int,struct siginfo *, void *)
            }_u
                     sigset_t sa_mask
                    unsigned long sa_flags
                  void (*sa_restorer)(void)
                  }
                                                            




其中,sa_restorer,已過時,POSIX不支援它,不應再被使用。

1、聯合資料結構中的兩個元素_sa_handler以及*_sa_sigaction指定信號關聯函數,即使用者指定的信號處理函數。除了可以是使用者自訂的處理函數外,還可以為SIG_DFL(採用缺省的處理方式),也可以為SIG_IGN(忽略信號)。

2、由_sa_handler指定的處理函數只有一個參數,即信號值,所以信號不能傳遞除信號值之外的任何資訊;由_sa_sigaction是指定的信號處理函數帶有三個參數,是為即時信號而設的(當然同樣支援非即時信號),它指定一個3參數信號處理函數。第一個參數為信號值,第三個參數沒有使用(posix沒有規範使用該參數的標準),第二個參數是指向siginfo_t結構的指標,結構中包含信號攜帶的資料值,參數所指向的結構如下:

 siginfo_t {
                  int      si_signo;  /* 信號值,對所有信號有意義*/
                  int      si_errno;  /* errno值,對所有信號有意義*/
                  int      si_code;   /* 信號產生的原因,對所有信號有意義*/
        union{          /* 聯合資料結構,不同成員適應不同信號 */ 
          //確保分配足夠大的存儲空間
          int _pad[SI_PAD_SIZE];
          //SIGKILL有意義的結構
          struct{
              ...
              }...
            ... ...
            ... ...         
          //SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構
              struct{
              ...
              }...
            ... ...
            }
      }
                                    




注:為了更便於閱讀,在說明問題時常把該結構表示為附錄2所表示的形式。

siginfo_t結構中的聯合資料成員確保該結構適應所有的信號,比如對於即時信號來說,則實際採用下面的結構形式:

            typedef struct {
                        int si_signo;
                        int si_errno;                                   
                        int si_code;                                    
                        union sigval si_value; 
                        } siginfo_t;
                       




結構的第四個域同樣為一個聯合資料結構:

            union sigval {
                        int sival_int;                      
                        void *sival_ptr;       
                        }




採用聯合資料結構,說明siginfo_t結構中的si_value要麼持有一個4位元組的整數值,要麼持有一個指標,這就構成了與信號相關的資料。在信號的處理函數中,包含這樣的信號相關資料指標,但沒有規定具體如何對這些資料進行操作,操作方法應該由程式開發人員根據具體任務事先約定。

前面在討論系統調用sigqueue發送信號時,sigqueue的第三個參數就是sigval聯合資料結構,當調用sigqueue時,該資料結構中的資料就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就可以讓信號傳遞一些附加資訊。信號可以傳遞資訊對程式開發是非常有意義的。

信號參數的傳遞過程可圖示如下:


  


3sa_mask指定在信號處理常式執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標誌位元。

注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程中由sa_mask指定的信號才被阻塞。

4sa_flags中包含了許多標誌位元,包括剛剛提到的SA_NODEFERSA_NOMASK標誌位元。另一個比較重要的標誌位元是SA_SIGINFO,當設定了該標誌位元時,表示信號附帶的參數可以被傳遞到信號處理函數中,因此,應該為sigaction結構中的sa_sigaction指定處理函數,而不應該為sa_handler指定信號處理函數,否則,設置該標誌變得毫無意義。即使為sa_sigaction指定了信號處理函數,如果不設置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過來的資料,在信號處理函數中對這些資訊的訪問都將導致段錯誤(Segmentation fault)。

注:很多文獻在闡述該標誌位元時都認為,如果設置了該標誌位元,就必須定義三參數信號處理函數。實際不是這樣的,驗證方法很簡單:自己實現一個單一參數信號處理函數,並在程式中設置該標誌位元,可以察看程式的運行結果。實際上,可以把該標誌位元看成信號是否傳遞參數的開關,如果設置該位元,則傳遞參數;否則,不傳遞參數。


六、信號集及信號集操作函數:

信號集被定義為一種資料類型:

            typedef struct {
                                     unsigned long sig[_NSIG_WORDS]
                                     } sigset_t




信號集用來描述信號的集合,linux所支援的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關函數配合使用。下面是為信號集操作定義的相關函數:

            #include <signal.h>
int sigemptyset(sigset_t *set)
int sigfillset(sigset_t *set)
int sigaddset(sigset_t *set, int signum)
int sigdelset(sigset_t *set, int signum)
int sigismember(const sigset_t *set, int signum)
sigemptyset(sigset_t *set)初始化由set指定的信號集,信號集裡面的所有信號被清空;
sigfillset(sigset_t *set)調用該函數後,set指向的信號集中將包含linux支援的64種信號;
sigaddset(sigset_t *set, int signum)set指向的信號集中加入signum信號;
sigdelset(sigset_t *set, int signum)set指向的信號集中刪除signum信號;
sigismember(const sigset_t *set, int signum)判定信號signum是否在set指向的信號集中。




七、信號阻塞與信號未決:

每個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的所有信號在遞送到進程後都將被阻塞。下面是與信號阻塞相關的幾個函數:

#include <signal.h>
int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset))
int sigpending(sigset_t *set));
int sigsuspend(const sigset_t *mask))




sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種:

參數how
進程當前信號集
SIG_BLOCK
在進程當前阻塞信號集中添加set指向信號集中的信號
SIG_UNBLOCK
如果進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞
SIG_SETMASK
更新進程阻塞信號集為set指向的信號集

sigpending(sigset_t *set))獲得當前已遞送到進程,卻被阻塞的所有信號,在set指向的信號集中返回結果。

sigsuspend(const sigset_t *mask))用於在接收到某個信號之前, 臨時用mask替換進程的信號遮罩, 並暫停進程執行,直到收到信號為止。sigsuspend 返回後將恢復調用之前的信號遮罩。信號處理函數完成後,進程將繼續執行。該系統調用始終返回-1,並將errno設置為EINTR

附錄1:結構itimerval

            struct itimerval {
                struct timeval it_interval; /* next value */
                struct timeval it_value;    /* current value */
            };
            struct timeval {
                long tv_sec;                /* seconds */
                long tv_usec;               /* microseconds */
            };




附錄2:三參數信號處理函數中第二個參數的說明性描述:

siginfo_t {
int      si_signo;  /* 信號值,對所有信號有意義*/
int      si_errno;  /* errno值,對所有信號有意義*/
int      si_code;   /* 信號產生的原因,對所有信號有意義*/
pid_t    si_pid;    /* 發送信號的進程ID,kill(2),即時信號以及SIGCHLD有意義 */
uid_t    si_uid;    /* 發送信號進程的真實使用者ID,對kill(2),即時信號以及SIGCHLD有意義 */
int      si_status; /* 退出狀態,對SIGCHLD有意義*/
clock_t  si_utime;  /* 用戶消耗的時間,對SIGCHLD有意義 */
clock_t  si_stime;  /* 內核消耗的時間,對SIGCHLD有意義 */
sigval_t si_value;  /* 信號值,對所有即時有意義,是一個聯合資料結構,
                          /*可以為一個整數(由si_int標示,也可以為一個指標,由si_ptr標示)*/
           
void *   si_addr;   /* 觸發fault的記憶體位址,對SIGILL,SIGFPE,SIGSEGV,SIGBUS 信號有意義*/
int      si_band;   /* SIGPOLL信號有意義 */
int      si_fd;     /* SIGPOLL信號有意義 */
}




實際上,除了前三個元素外,其他元素組織在一個聯合結構中,在聯合資料結構中,又根據不同的信號組織成不同的結構。注釋中提到的對某種信號有意義指的是,在該信號的處理函數中可以訪問這些域來獲得與信號相關的有意義的資訊,只不過特定信號只對特定資訊感興趣而已。


沒有留言: