2012年8月21日 星期二

Linux 多執行緒應用中如何編寫安全的信號處理函數

1 、 可靠信號和不可靠信號

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

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

因此,早期unix下的不可靠信號主要指的是進程可能對信號做出錯誤的反應連同信號可能丟失。

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

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

信號值位於SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問題。Linux在支援新版本的信號安裝 函數sigation()連同信號發送函數sigqueue()的同時,仍然支援早期的signal()信號安裝函數,支援信號發送函數kill()。
注:不要有這樣的誤解:由sigqueue()發送、sigaction安裝的信號就是可靠的。事實上,可靠信號是指後來添加的新信號(信號值 位於SIGRTMIN及SIGRTMAX之間);不可靠信號是信號值小於SIGRTMIN的信號。信號的可靠和不可靠只和信號值有關,和信號的發送及安裝 函數無關。現在linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾 也不必再調用一次信號安裝函數。同時,由signal()安裝的即時信號支援排隊,同樣不會丟失。

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

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

Linux的信號的種類有60多種。可以用kill -l命令查看所有的信號,每個信號的含義如下:

  1. SIGHUP:當用戶退出shell時,由該shell啟動的所有進程將收到這個信號,預設動作為終止進程
  2. SIGINT:當使用者按下了<Ctrl+C>複合鍵時,使用者終端向正在運行中的由該終端啟動的程式發出此信號。默認動作為終止里程。
  3. SIGQUIT:當使用者按下<ctrl+\>複合鍵時產生該信號,使用者終端向正在運行中的由該終端啟動的程式發出些信號。預設動作為終止進程。
  4. SIGILL:CPU檢測到某進程執行了非法指令。預設動作為終止進程並產生core檔
  5. SIGTRAP:該信號由中斷點指令或其他 trap指令產生。默認動作為終止里程 並產生core檔。
  6. SIGABRT:調用abort函數時產生該信號。預設動作為終止進程並產生core檔。
  7. SIGBUS:非法訪問記憶體位址,包括記憶體對齊出錯,預設動作為終止進程並產生core檔。
  8. SIGFPE:在發生致命的運算錯誤時發出。不僅包括浮點運算錯誤,還包括溢出及除數為0等所有的演算法錯誤。預設動作為終止進程並產生core檔。
  9. SIGKILL:無條件終止進程。本信號不能被忽略,處理和阻塞。預設動作為終止進程。它向系統管理員提供了可以殺死任何進程的方法。
  10. SIGUSE1:使用者定義 的信號。即程式師可以在程式中定義並使用該信號。預設動作為終止進程。
  11. SIGSEGV:指示進程進行了無數記憶體訪問。預設動作為終止進程並產生core檔。
  12. SIGUSR2:這是另外一個使用者自訂信號 ,程式師可以在程式中定義 並使用該信號。預設動作為終止進程。1
  13. SIGPIPE:Broken pipe向一個沒有讀端的管道寫資料。預設動作為終止進程。
  14.  SIGALRM:計時器超時,超時的時間 由系統調用alarm設置。預設動作為終止進程。
  15. SIGTERM:程式結束信號,與SIGKILL不同的是,該信號可以被阻塞和終止。通常用來要示程式正常退出。執行shell命令Kill時,缺省產生這個信號。預設動作為終止進程。
  16. SIGCHLD:子進程結束時,父進程會收到這個信號。預設動作為忽略這個信號。
  17. SIGCONT:停止進程的執行。信號不能被忽略,處理和阻塞。預設動作為終止進程。
  18. SIGTTIN:停止進程的運行,但該信號可以被處理和忽略。按下<ctrl+z>複合鍵發出災個信號。預設動作為暫停進程。
  19. SIGTSTP:停止進程的運行,可該信號可以被處理可忽略。按下<ctrl+z>複合鍵時發出這個信號。預設動作為暫停進程。
  20. SIGTTOU:該信號類似於SIGTTIN,在後臺進程要向終端輸出資料時發生。預設動作為暫停進程。
  21. SIGURG:通訊端上有緊急資料時,向當前正在運行的進程發出些信號,報告有緊急資料到達。預設動作為忽略該信號。
  22. SIGXFSZ:進程執行時間超過了分配給該進程的CPU時間 ,系統產生該信號併發送給該進程。預設動作為終止進程。
  23. SIGXFSZ:超過文件的最大長度設置。預設動作為終止進程。
  24. SIGVTALRM:虛擬時鐘超時時產生該信號。類似於SIGALRM,但是該信號只計算該進程佔用CPU的使用時間。預設動作為終止進程。
  25. SGIPROF:類似於SIGVTALRM,它不公包括該進程佔用CPU時間還包括執行系統調用時間。預設動作為終止進程。
  26. SIGWINCH:視窗變化大小時發出。預設動作為忽略該信號。
  27. SIGIO:此信號向進程指示發出了一個非同步IO事件。默認動作為忽略。
  28. SIGPWR:關機。預設動作為終止進程。
  29. SIGSYS:無效的系統調用。預設動作為終止進程並產生core檔。
  30. SIGRTMIN~(64)SIGRTMAX:LINUX的即時信號,它們沒有固定的含義(可以由用戶自訂)。所有的即時信號的預設動作都為終止進程。


進程對信號的回應

 進程能夠通過三種方式來響應一個信號:

  1. 忽略信號,即對信號不做任何處理,其中,有兩個信號不能忽略:SIGKILL及SIGSTOP; 
  2. 捕獲信號。定義信號處理函數,當信號發生時,執行相應的處理函數;
  3. 執行預設操作,Linux對每種信號都規定了預設操作,周詳情況請參考 [2]連同其他資料。注意,進程對即時信號的預設反應是進程終止。

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

發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()連同abort()。
1、kill()
#include
#include
int kill(pid_t pid,int signo)
參數pid的值 信號的接收進程
pid>0 進程ID為pid的進程
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。
2、raise()
#include
int raise(int signo)
3、sigqueue()
#include
#include
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()發送非即時信號時,仍然不支援排隊,即在信號處理函數執行過程中到來的任何相同信號,都被合併為一個信號。
4、alarm()
#include
unsigned int alarm(unsigned int seconds)
專門為SIGALRM信號而設,在指定的時間seconds秒後,將向進程本身發送SIGALRM信號,又稱為鬧鐘時間。進程調用alarm後,任何以前的alarm()調用都將無效。假如參數seconds為零,那麼進程內將不再包含任何鬧鐘時間。
5、setitimer()
#include
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。
6、abort()
#include
void abort(void);
向進程發送SIGABORT信號,預設情況下進程會異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進程配置為阻塞信號,調用abort()後,SIGABORT仍然能被進程接收。該函數無返回值。

信號安裝
假如進程要處理某一信號,那麼就要在進程中安裝該信號。安裝信號主要用來確定信號值及進程針對該信號值的動作之間的映射關係,即進程將要處理哪個信號;該信號被傳遞給進程時,將執行何種操作。
linux主要有兩個函數實現信號的安裝:signal()、sigaction()。其中signal()在可靠信號系統調用的基礎上實現, 是庫函數。他只有兩個參數,不支援信號傳遞資訊,主要是用於前32種非即時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現: sys_signal連同sys_rt_sigaction),有三個參數,支援信號傳遞資訊,主要用來和 sigqueue() 系統調用配合使用,當然,sigaction()同樣支援非即時信號的安裝。sigaction()優於signal()主要體現在支援信號帶有參數。
1、signal()
#include
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。
2、sigaction()
#include
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函數用於改變進程接收到特定信號後的行為。該函數的第一個參數為信號的值,能夠為除SIGKILL及SIGSTOP外的任何一 個特定有效的信號(為這兩個信號定義自己的處理函數,將導致信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指標,在結構 sigaction的實例中,指定了對特定信號的處理,能夠為空,進程會以缺省方式對信號處理;第三個參數oldact指向的物件用來保存原來對相應信號 的處理,可指定oldact為NULL。假如把第二、第三個參數都設為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時,該數 據結構中的資料就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就能夠讓信號傳遞一些附加資訊。信號能夠傳遞資訊對程式研發是很有意義 的。
信號參數的傳遞過程可圖示如下:

3、sa_mask指定在信號處理程式執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或SA_NOMASK標誌位元。
注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程中由sa_mask指定的信號才被阻塞。
4、sa_flags中包含了許多標誌位元,包括剛剛提到的SA_NODEFER及SA_NOMASK標誌位元。另一個比較重要的標誌位元是 SA_SIGINFO,當設定了該標誌位元時,表示信號附帶的參數能夠被傳遞到信號處理函數中,因此,應該為sigaction結構中的 sa_sigaction指定處理函數,而不應該為sa_handler指定信號處理函數,否則,配置該標誌變得毫無意義。即使為 sa_sigaction指定了信號處理函數,假如不配置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過來的資料,在信號處理函數中對這些信 息的訪問都將導致段錯誤(Segmentation fault)。
注:很多文獻在闡述該標誌位元時都認為,假如配置了該標誌位元,就必須定義三參數信號處理函數。實際不是這樣的,驗證方法很簡單:自己實現一個單一 參數信號處理函數,並在程式中配置該標誌位元,能夠察看程式的運行結果。實際上,能夠把該標誌位元看成信號是否傳遞參數的開關,假如配置該位元,則傳遞參數;否 則,不傳遞參數。
Linux信號阻塞和信號未決
1. 信號遮罩——被阻塞的信號集
  每個進程都有一個用來描述哪些信號傳送來將被阻塞的信號集,如果某種信號在某個進程的阻塞信號集中,則傳送到該進程的此種信號將會被阻塞。當前被進程阻塞的信號集也叫信號遮罩,類型為sigset_t。每個進程都有自己 的信號遮罩,且創建子進程時,子進程會繼承父進程的信號遮罩。
2. 信號阻塞和忽略的區別
  阻塞的概念與忽略信號是不同的:作業系統在信號被進程解除阻塞之前不會將信號傳遞出去,被阻塞的信號也不會影響進程的行為,信號只是暫時被阻止傳遞;當進程忽略一個信號時,信號會被傳遞出去,但進程將信號丟棄。
3. 信號集的操作
  信號集可以由以下幾個函數操作:
  int sigemptyset(sigset_t *set); //清空信號集
  int sigfillset(sigset_t *set); //將所有信號填充進set中
  int sigaddset(sigset_t *set, int signum); //往set中添加信號signum
  int sigdelset(sigset_t *set, int signum); //從set中移除信號signum
  int sigismember(const sigset_t *set, int signum); //判斷signnum是不是包含在set中,在返回1,不在返回0
  初始化往往可以用sigemptyset()將信號集清空,再用sigaddset()向信號集中添加信號;或者可以使用sigfillset()將所有信號添加到信號集,再用sigdelset()將某信號從中刪除掉。
4. sigprocmask()介紹
  可以使用函數sigprocmask()來檢查或者修改進程的信號遮罩。函數資訊如下:
  #include <signal.h>
  int sigprocmask ( int how, const sigset_t *restrict set,
  sigset_t *restrict old );
  參數how 是一個整數,說明信號遮罩的修改方式:
  SIG_BLOCK --- 將set指向的信號集中的信號添加到當前阻塞信號集中;
  SIG_UNBLOCK --- 從當前阻塞信號集中移除set指向的信號集中的信號;
  SIG_SETMASK --- 指定set所指向的信號集為當前阻塞信號集。
  此外,如果參數set 為NULL, 說明不需要修改,如果old 為NULL,sigprocmask會將修改之前的信號集放在*old 之中返回。
5.sigaction()回顧
  在前面有用過sigaction()函數:
  include <signal.h>
  int sigaction(int signum,const struct sigaction *act,
  const struct sigaction *oldact);
  該函數是用於註冊一個信號處理函數。參數結構體sigaction與函數同名,具體資訊如下:
  struct sigaction {
  void (*sa_handler)(int); //老類型的信號處理函數指標
  void (*sa_sigaction)(int, siginfo_t *, void *);//新類型的信號處理函數指標
  sigset_t sa_mask; //將要被阻塞的信號集合
  int sa_flags; //信號處理方式遮罩
  void (*sa_restorer)(void); //保留
  }
  5.1 sa_handler:一個函數指標,用於指向原型為void handler(int)的信號處理函數位址(老類型的信號處理函數);
  5.2 sa_sigaction:也是一個函數指標,用於指向原型為:
  void handler(int (新類型的信號處理函數);
  三個參數的含義為:
  iSignNum:傳入的信號
  pSignInfo:與該信號相關的一些資訊,它是個結構體
  pReserved:保留,現沒用
   5.3 sa_handler和sa_sigaction只應該有一個生效,如果想採用老的信號處理機制,就應該讓sa_handler指向正確的信號處理函數; 否則應該讓sa_sigaction指向正確的信號處理函數,並且讓欄位sa_flags包含SA_SIGINFO選項。
  5.4 sa_mask是一個包含信號集合的結構體,該結構體內的信號表示在進行信號處理時,將要被阻塞的信號。該信號集可以用前面標題3提到的5個函數來進行操作。
  5.5 欄位sa_flags是一組遮罩的合成值,指示信號處理時所應該採取的一些行為,各遮罩的含義為:
  (1)SA_RESETHAND ---處理完畢要捕捉的信號後,將自動撤銷信號處理函數的註冊,即必須再重新註冊信號處理函數,才能繼續處理接下來產生的信號。
  (2)SA_NODEFER ---在處理信號時,如果又發生了其它的信號,則立即進入其它信號的處理,等其它信號處理完畢後,再繼續處理當前的信號,即遞規地處理。如果sa_flags包含了該遮罩,則結構體sigaction的sa_mask將無效;
  (3)SA_RESTART--- 如果在發生信號時,程式正阻塞在某個系統調用,例如調用read()函數,則在處理完畢信號後,接著從阻塞的系統返回。該遮罩符合普通的程式處理流程,所以一般來說,應該設置該遮罩,否則信號處理完後,阻塞的系統調用將會返回失敗;
  (4)SA_SIGINFO ---指示結構體的信號處理函數指標是哪個有效,如果sa_flags包含該遮罩,則sa_sigactiion指針有效,否則是sa_handler指針有效。
  需要注意的是:
   函數sigprocmask是全程阻塞,在sigprocmask中設置了阻塞集合後,被阻塞的信號將不能再被信號處理函數捕捉,直到重新設置阻塞信號 集合。而在sigaction()註冊信號處理函數時,選擇阻塞的信號集只是在處理捕捉的信號時,才對指定的其他信號進行阻塞。
6、信號未決
sigpending(sigset_t *set))獲得當前已遞送到進程,卻被阻塞的任何信號,在set指向的信號集中返回結果。
sigsuspend(const sigset_t *mask))用於在接收到某個信號之前, 臨時用mask替換進程的信號遮罩, 並暫停進程執行,直到收到信號為止。sigsuspend 返回後將恢復調用之前的信號遮罩。信號處理函數完成後,進程將繼續執行。該系統調用始終返回-1,並將errno配置為EINTR。

計時器與信號

睡眠函數
Linux下有兩個睡眠函數,原型為:
#include <unistd.h>
 unsigned int sleep(unsigned int seconds);
 void usleep(unsigned long usec);
函數sleep讓進程睡眠seconds秒,函數usleep讓進程睡眠usec毫秒。
sleep睡眠函數內部是用信號機制進行處理的,用到的函數有:
 #include <unistd.h>
 unsigned int alarm(unsigned int seconds); //告知自身進程,要進程在seconds秒後自動產生一個//SIGALRM的信號,
 int pause(void); //將自身進程掛起,直到有信號發生時才從pause返回

示例:模擬睡眠3秒:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void SignHandler(int iSignNo)
{
printf("signal:%d\n",iSignNo);
}
int main()
{
signal(SIGALRM,SignHandler);
alarm(3);
printf("Before pause().\n");
pause();
printf("After pause().\n");
return 0;
}
注意:因為sleep在內部是用alarm實現的,所以在程式中最好不要sleep與alarm混用,以免造成混亂。

時鐘處理
Linux為每個進程維護3個計時器,分別是真實計時器、虛擬計時器和實用計時器。
真實計時器計算的是程式運行的實際時間;
虛擬計時器計算的是程式運行在使用者態時所消耗的時間(可認為是實際時間減掉(系統調用和程式睡眠所消耗)的時間);
實用計時器計算的是程式處於使用者態和處於內核態所消耗的時間之和。
例如:有一程式運行,在使用者態運行了5秒,在內核態運行了6秒,還睡眠了7秒,則真實計算器計算的結果是18秒,虛擬計時器計算的是5秒,實用計時器計算的是11秒。
用指定的初始間隔和重複間隔時間為進程設定好一個計時器後,該計時器就會定時地向進程發送時鐘信號。3個計時器發送的時鐘信號分別為:SIGALRM,SIGVTALRM和SIGPROF。
用到的函數與資料結構:
#include <sys/time.h>
//獲取計時器的設置
//which指定哪個計時器,可選項為ITIMER_REAL(真實計時器)、ITIMER_VITUAL(虛擬計時器、ITIMER_PROF(實用計時器))
//value為一結構體的傳出參數,用於傳出該計時器的初始間隔時間和重複間隔時間
//如果成功,返回0,否則-1
int getitimer(int which, struct itimerval *value);
//設置計時器
//which指定哪個計時器,可選項為ITIMER_REAL(真實計時器)、ITIMER_VITUAL(虛擬計時器、ITIMER_PROF(實用計時器))
//value為一結構體的傳入參數,指定該計時器的初始間隔時間和重複間隔時間
//ovalue為一結構體傳出參數,用於傳出以前的計時器時間設置。
//如果成功,返回0,否則-1
int setitimer(int which, const struct itimerval *value, struct itimer val *ovalue);

struct itimerval {
struct timeval it_interval; /* next value */ //重複間隔
struct timeval it_value;/* current value */ //初始間隔
};
struct timeval {
long tv_sec;/* seconds *///時間的秒數部分
long tv_usec; /* microseconds */ //時間的微秒部分
};
示例:啟用真實計時器的進行時鐘處理
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
void TimeInt2Obj(int imSecond,timeval *ptVal)
{
ptVal->tv_sec=imSecond/1000;
ptVal->tv_usec=(imSecond%1000)*1000;
}
void SignHandler(int SignNo)
{
printf("Clock\n");
}
int main()
{
signal(SIGALRM,SignHandler);
itimerval tval;
TimeInt2Obj(1,&tval.it_value); //設初始間隔為1毫秒,注意不要為0
TimeInt2Obj(1500,&tval.it_interval); //設置以後的重複間隔為1500毫秒
setitimer(ITIMER_REAL,&tval,NULL);
while(getchar()!=EOF);
return 0;
}

信號生命週期
從信號發送到信號處理函數的執行完畢
對於一個完整的信號生命週期(從信號發送到相應的處理函數執行完畢)來說,能夠分為三個重要的階段,這三個階段由四個重要事件來刻畫:信號誕生;信號在進程中註冊完畢;信號在進程中的登出完畢;信號處理函數執行完畢。相鄰兩個事件的時間間隔構成信號生命週期的一個階段。

下面闡述四個事件的實際意義:
1. 信號"誕生"。信號的誕生指的是觸發信號的事件發生(如檢測到硬體異常、計時器超時連同調用信號發送函數kill()或sigqueue()等)。
2. 信號在目標進程中"註冊";進程的task_struct結構中有關於本進程中未決信號的資料成員:
3. struct sigpending pending:
4. struct sigpending{
5. struct sigqueue *head, **tail;
6. sigset_t signal;
7. };
第三個成員是進程中任何未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之為"未決信號資訊鏈")的首尾,資訊鏈中的每個sigqueue結構刻畫一個特定信號所攜帶的資訊,並指向下一個sigqueue結構:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信號在進程中註冊指的就是信號值加入到進程的未決信號集中(sigpending結構的第二個成員sigset_t signal),並且信號所攜帶的資訊被保留到未決信號資訊鏈的某個sigqueue結構中。只要信號在進程的未決信號集中,表明進程已知道這些信號的 存在,但還沒來得及處理,或該信號被進程阻塞。
注:
當一個即時信號發送給一個進程時,不管該信號是否已在進程中註冊,都會被再註冊一次,因此,信號不會丟失,因此,即時信號又叫做"可靠信號"。這意味著 同一個即時信號能夠在同一個進程的未決信號資訊鏈中佔有多個sigqueue結構(進程每收到一個即時信號,都會為他分配一個結構來登記該信號資訊,並把 該結構添加在未決信號鏈尾,即任何誕生的即時信號都會在目標進程中註冊);
當一個非即時信號發送給一個進程時,假如該信號已在進程中註冊,則該信號將被丟棄,造成信號丟失。因此,非即時信號又叫做"不可靠信號"。這 意味著同一個非即時信號在進程的未決信號資訊鏈中,至多佔有一個sigqueue結構(一個非即時信號誕生後,(1)、假如發現相同的信號已在目標結構 中註冊,則不再註冊,對於進程來說,相當於不知道本次信號發生,信號丟失;(2)、假如進程的未決信號中沒有相同信號,則在進程中註冊自己)。
8. 信號在進程中的登出。在目標進程執行過程中,會檢測是否有信號等待處理(每次從系統空間返回到使用者空間時都做這樣的檢查)。假如存在未決信號等待處理且該 信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中佔有的結構卸掉。是否將信號從進程未決信號集中刪除對於即時和非即時信號 是不同的。對於非即時信號來說,由於在未決信號資訊鏈中最多只佔用一個sigqueue結構,因此該結構被釋放後,應該把信號在進程未決信號集中刪除(信 號註銷完畢);而對於即時信號來說,可能在未決信號資訊鏈中佔用多個sigqueue結構,因此應該針對佔用sigqueue結構的數目區別對待:假如只 佔用一個sigqueue結構(進程只收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號登出完畢)。否則,不應該在進程的未決信號集中刪除 該信號(信號登出完畢)。
進程在執行信號相應處理函數之前,首先要把信號在進程中登出。
9. 信號生命終止。進程登出信號後,立即執行相應的信號處理函數,執行完畢後,信號的本次發送對進程的影響完全結束。
注:
1)信號註冊和否,和發送信號的函數(如kill()或sigqueue()等)連同信號安裝函數(signal()及sigaction()) 無關,只和信號值有關(信號值小於SIGRTMIN的信號最多只註冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被 註冊)。
2)在信號被登出到相應的信號處理函數執行完畢這段時間內,假如進程又收到同一信號多次,則對即時信號來說,每一次都會在進程中註冊;而對於非即時信號來說,無論收到多少次信號,都會視為只收到一個信號,只在進程中註冊一次。
信號程式設計注意事項
1. 防止不該丟失的信號丟失。假如對八中所提到的信號生命週期理解深刻的話,很容易知道信號會不會丟失,連同在哪裡丟失。
2. 程式的可攜性
考慮到程式的可攜性,應該儘量採用POSIX信號函數,POSIX信號函數主要分為兩類:
o POSIX 1003.1信號函數: Kill()、sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、 sigismember()、sigpending()、sigprocmask()、sigsuspend()。
o POSIX 1003.1b信號函數。POSIX 1003.1b在信號的即時性方面對POSIX 1003.1做了擴展,包括以下三個函數: sigqueue()、sigtimedwait()、sigwaitinfo()。其中,sigqueue主要針對信號發送,而 sigtimedwait及sigwaitinfo()主要用於取代sigsuspend()函數,後面有相應實例。
o #include
o int sigwaitinfo(sigset_t *set, siginfo_t *info).
該函數和sigsuspend()類似,阻塞一個進程直到特定信號發生,但信號到來時不執行信號處理函數,而是返回信號值。因此為了避免執行相應的信號處理函數,必須在調用該函數前,使進程遮罩掉set指向的信號,因此調用該函數的典型代碼是:
sigset_t newmask;
int rcvd_sig;
siginfo_t info;

sigemptyset(&newmask);
sigaddset(&newmask, SIGRTMIN);
sigprocmask(SIG_BLOCK, &newmask, NULL);
rcvd_sig = sigwaitinfo(&newmask, &info)
if (rcvd_sig == -1) {
..
}
調用成功返回信號值,否則返回-1。sigtimedwait()功能相似,只但是增加了一個進程等待的時間。
3. 程式的穩定性。
為了增強程式的穩定性,在信號處理函數中應使用可重入函數。
信號處理程式中應當使用可再入(可重入)函數(注:所謂可重入函數是指一個能夠被多個任務調用的過程,任務在調用時不必擔心資料是否會出錯)。因為進程在 收到信號後,就將跳轉到信號處理函數去接著執行。假如信號處理函數中使用了不可重入函數,那麼信號處理函數可能會修改原來進程中不應該被修改的資料,這樣 進程從信號處理函數中返回接著執行時,可能會出現不可預料的後果。不可再入函數在信號處理函數中被視為不安全函數。
滿足下列條件的函數多數是不可再入的:(1)使用靜態的資料結構,如getlogin(),gmtime(),getgrgid(), getgrnam(),getpwuid()連同getpwnam()等等;(2)函數實現時,調用了malloc()或free()函數;(3)實現 時使用了標準I/O函數的。The Open Group視下列函數為可再入的:
_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、 cfsetospeed()、chdir()、chmod()、chown()、close()、creat()、dup()、dup2()、 execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、 geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、 kill()、link()、lseek()、mkdir()、mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、setgid ()、setpgid()、setsid()、setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、 sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、 stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、 tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。
即使信號處理函數使用的都是"安全函數",同樣要注意進入處理函數時,首先要保存errno的值,結束時,再恢復原值。因為,信號處理過程中,errno 值隨時可能被改變。另外,longjmp()連同siglongjmp()沒有被列為可再入函數,因為不能確保緊接著兩個函數的其他調用是安全的。
信號應用實例
信號的安裝(配置信號關聯動作)
linux下的信號應用並沒有想像的那麼恐怖,程式員所要做的最多只有三件事情:
1. 安裝信號(推薦使用sigaction());
2. 實現三參數信號處理函數,handler(int signal,struct siginfo *info, void *);
3. 發送信號,推薦使用sigqueue()。
實際上,對有些信號來說,只要安裝信號就足夠了(信號處理方式採用缺省或忽略)。其他可能要做的無非是和信號集相關的幾種操作。
實例一:信號發送及處理
實現一個信號接收程式sigreceive(其中信號安裝由sigaction())。
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
int sig;
sig=atoi(argv[1]);

sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
act.sa_sigaction=new_op;

if(sigaction(sig,&act,NULL) < 0)
{
printf("install sigal error\n");
}

while(1)
{
sleep(2);
printf("wait for the signal\n");
}
}
void new_op(int signum,siginfo_t *info,void *myact)
{
printf("receive signal %d", signum);
sleep(5);
}
說明,命令列參數為信號值,後臺運行sigreceive signo &,可獲得該進程的ID,假設為pid,然後再另一終端上運行kill -s signo pid驗證信號的發送接收及處理。同時,可驗證信號的排隊問題。
注:能夠用sigqueue實現一個命令列信號發送程式sigqueuesend
實例二:信號傳遞附加資訊
主要包括兩個實例:
1. 向進程本身發送信號,並傳遞指標參數;
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
union sigval mysigval;
 int i;
 int sig;
 pid_t pid;
 char data[10];
 memset(data,0,sizeof(data));
 for(i=0;i < 5;i )
 data[i]='2';
 mysigval.sival_ptr=data;

 sig=atoi(argv[1]);
 pid=getpid();

 sigemptyset(&act.sa_mask);
 act.sa_sigaction=new_op;//三參數信號處理函數
 act.sa_flags=SA_SIGINFO;//資訊傳遞開關
 if(sigaction(sig,&act,NULL) < 0)
 {
 printf("install sigal error\n");
 }
 while(1)
 {
 sleep(2);
 printf("wait for the signal\n");
 sigqueue(pid,sig,mysigval);//向本進程發送信號,並傳遞附加資訊
 }

 }

 void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實現
 {
 int i;
 for(i=0;i<10;i )
 {
 printf("%c\n ",(*( (char*)((*info).si_ptr) i)));
 }
 printf("handle signal %d over;",signum);
 }
這個例子中,信號實現了附加資訊的傳遞,信號究竟如何對這些資訊進行處理則取決於具體的應用。
2、 不同進程間傳遞整型參數:把1中的信號發送和接收放在兩個程式中,並且在發送過程中傳遞整型參數。
信號接收程式:
#include
#include
#include
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
int sig;
pid_t pid;

pid=getpid();
sig=atoi(argv[1]);

sigemptyset(&act.sa_mask);
act.sa_sigaction=new_op;
act.sa_flags=SA_SIGINFO;
if(sigaction(sig,&act,NULL)<0)
{
printf("install sigal error\n");
}
while(1)
{
sleep(2);
printf("wait for the signal\n");
}

}
void new_op(int signum,siginfo_t *info,void *myact)
{
printf("the int value is %d \n",info->si_int);
}
信號發送程式:命令列第二個參數為信號值,第三個參數為接收進程ID。
main(int argc,char**argv)
{
pid_t pid;
int signum;
union sigval mysigval;

signum=atoi(argv[1]);
pid=(pid_t)atoi(argv[2]);
mysigval.sival_int=8;//不代表具體含義,只用於說明問題

if(sigqueue(pid,signum,mysigval)==-1)
printf("send error\n");
sleep(2);
}
實例三:信號阻塞及信號集操作
#include "signal.h"
#include "unistd.h"
static void my_op(int);
main()
{
sigset_t new_mask,old_mask,pending_mask;
struct sigaction act;

sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
act.sa_sigaction=(void*)my_op;
if(sigaction(SIGRTMIN 10,&act,NULL))
printf("install signal SIGRTMIN 10 error\n");

sigemptyset(&new_mask);
sigaddset(&new_mask,SIGRTMIN 10);
if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))
printf("block signal SIGRTMIN 10 error\n");

sleep(10);
printf("now begin to get pending mask and unblock SIGRTMIN 10\n");
if(sigpending(&pending_mask)<0)
printf("get pending mask error\n");
if(sigismember(&pending_mask,SIGRTMIN 10))
printf("signal SIGRTMIN 10 is pending\n");

if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)
printf("unblock signal error\n");
printf("signal unblocked\n");

sleep(10);
}
static void my_op(int signum)
{
printf("receive signal %d \n",signum);
}

編譯該程式,並以後臺方式運行。在另一終端向該進程發送信號(運行kill -s 42 pid,SIGRTMIN 10為42),查看結果能夠看出幾個關鍵函數的運行機制,信號集相關操作比較簡單。
注:在上面幾個實例中,使用了printf()函數,只是作為診斷工具,pringf()函數是不可重入的,不應在信號處理函數中使用。
用sigqueue實現的命令列信號發送程式sigqueuesend,命令列第二個參數是發送的信號值,第三個參數是接收該信號的進程ID,能夠配合實例一使用:
int main(int argc,char**argv)
{
pid_t pid;
int sig;
sig=atoi(argv[1]);
pid=atoi(argv[2]);
sigqueue(pid,sig,NULL);
sleep(2);
}

沒有留言: