程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> (八) 一起學 Unix 環境高級編程 (APUE) 之 信號,高級編程apue

(八) 一起學 Unix 環境高級編程 (APUE) 之 信號,高級編程apue

編輯:關於C語言

(八) 一起學 Unix 環境高級編程 (APUE) 之 信號,高級編程apue


.

.

.

.

.

目錄

(一) 一起學 Unix 環境高級編程 (APUE) 之 標准IO

(二) 一起學 Unix 環境高級編程 (APUE) 之 文件 IO

(三) 一起學 Unix 環境高級編程 (APUE) 之 文件和目錄

(四) 一起學 Unix 環境高級編程 (APUE) 之 系統數據文件和信息

(五) 一起學 Unix 環境高級編程 (APUE) 之 進程環境

(六) 一起學 Unix 環境高級編程 (APUE) 之 進程控制

(七) 一起學 Unix 環境高級編程 (APUE) 之 進程關系 和 守護進程

(八) 一起學 Unix 環境高級編程 (APUE) 之 信號

(九) 一起學 Unix 環境高級編程 (APUE) 之 線程

(十) 一起學 Unix 環境高級編程 (APUE) 之 線程控制

 

 

從信號這章開始情況就開始變得復雜了,所以從這篇開始後面的博文大家一天可能就無法學習完畢了,大家可以把每篇博文分為兩天到三天的時間來學習,要給自己充足的時間練習才能掌握這些內容。

所以一定不要手懶,想要學會唯一的辦法就是多寫多練習。

如果前面的東西你還沒有掌握,那麼從這部分開始到後面的內容就先不要看了,一定要回去把前面的部分掌握了再開始看後面的內容。

因為 APUE 的東西是前後呼應的,如果前面沒有學扎實,後面有些東西是不太容易理解的。

說實話前面的內容幾乎都沒有難度,只有從信號開始才算是有一點難度了,APUE 的東西多寫多練很容易掌握的,所以不要害怕。

 

到目前為止,之前寫的程序還沒有一個是異步運行的,全部都是同步運行的。

在第一篇博文中我們就介紹過了,Linux 環境中的並發可以分為 多進程+信號 和 多線程兩種,信號屬於初級異步,多線程屬於強烈異步。

在實際項目中信號和多線程基本不會一塊兒使用,要麼使用 多進程+信號 的形式,要麼采用多線程的形式。

同步程序的特點是程序的執行流程、分支都是明確的。

異步事件的特點:事件到來的時間不確定,到來之後產生的結果是不確定的。比如在俄羅斯方塊游戲中需要異步接收用戶的方向控制輸入,你永遠無法知道用戶什麼時候按下方向鍵,以及按下哪個方向鍵。

 

異步事件的獲取方式通常只有兩種:查詢法,通知法

 

假如我們使用一個煙霧傳感器監測庫房中是否發生了火災,火災的到來的時間就是一種異步事件。

我們可以通過兩種方式獲取是否發生了火災:

1)查詢法:傳感器將狀態寫到一個位圖當中,我們不停的查詢位圖的狀態來得到傳感器的最新監測結果。

2)通知法:當檢測到火災時傳感器推送一個消息給我們,這樣我們就不用不停的查詢位圖了。

 

那麼什麼情況使用查詢法更好,什麼情況使用通知法更好呢?

異步事件到來的頻率比較的情況考慮使用查詢法,因為撞到異步事件到來的概率比較高。

異步事件到來的頻率比較稀疏的情況考慮通知法,因為比較經濟實惠。

所有的通知法都需要配合一個監聽機制才行。否則比如你在垂釣,放下一個魚竿之後你就走了,就算魚上鉤了你也不可能知道。

 

即使計算機中沒有連接任何外部硬件設備,內核每秒鐘也會發生成百上千個中斷來打斷正在運行的程序。

時間片調度其實就是通過中斷打斷程序的執行,把時間片耗盡的進程移動到隊列中等待。所以任何一個進程在執行的過程中都是磕磕絆絆的不斷被打斷的,程序在任何地方都可能被打斷,唯獨一條機器指令是無法被打斷的(機器指令是原子的)。

比如你在執行一句 printf("Hello World!\n"); 的時候,看似是很流暢的打印出來了,但是執行過程中已經被打斷很多次了。

所以在單核 CPU 上其實是不存在真正意義上的異步的,你感受到的異步無非就是時間片切換給你帶來的錯覺。你以為你邊聽音樂邊寫程序,這兩件事是同時進行的嗎?其實內核在快速的不斷的打斷其中一個程序,然後再讓另一個程序運行一會兒,如此往復,給你一種兩件事情在同時發生的錯覺。

 

1.信號概述

信號不是中斷,中斷只能由硬件產生,信號是模擬硬件中斷的原理在軟件層面上進行的實現。

 

可以使用 kill(1) 命令向其它進程查看或發送信號。

>$ kill -l
 1) SIGHUP          2) SIGINT          3) SIGQUIT         4) SIGILL          5) SIGTRAP
 6) SIGABRT         7) SIGBUS          8) SIGFPE          9) SIGKILL        10) SIGUSR1
11) SIGSEGV        12) SIGUSR2        13) SIGPIPE        14) SIGALRM        15) SIGTERM
16) SIGSTKFLT      17) SIGCHLD        18) SIGCONT        19) SIGSTOP        20) SIGTSTP
21) SIGTTIN        22) SIGTTOU        23) SIGURG         24) SIGXCPU        25) SIGXFSZ
26) SIGVTALRM      27) SIGPROF        28) SIGWINCH       29) SIGIO          30) SIGPWR
31) SIGSYS         34) SIGRTMIN       35) SIGRTMIN+1     36) SIGRTMIN+2     37) SIGRTMIN+3
38) SIGRTMIN+4     39) SIGRTMIN+5     40) SIGRTMIN+6     41) SIGRTMIN+7     42) SIGRTMIN+8
43) SIGRTMIN+9     44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9     56) SIGRTMAX-8     57) SIGRTMAX-7
58) SIGRTMAX-6     59) SIGRTMAX-5     60) SIGRTMAX-4     61) SIGRTMAX-3     62) SIGRTMAX-2
63) SIGRTMAX-1     64) SIGRTMAX
>$

 

其中 1 - 31 是標准信號,34 - 64 是實時信號。我們下面討論的內容如果沒有特殊說明則都是針對標准信號。

信號有五種不同的默認行為:終止、終止+core、忽略、停止進程、繼續。

 core 文件就是程序在崩潰時由操作系統為它生成的內存現場映像和調試信息,主要是用來調試程序的,可以使用 ulimit(1) 命令設置允許生成的 core 文件的最大大小。

1)終止:使程序異常結束。還記得我們在前面的博文中提到的程序的 3 種異常終止情況嗎?其中被信號殺死就是異常終止的一種。

2)終止+core:殺死進程,並為其產生一個 core dump 文件,可以使用這個 core dump 文件獲得程序被殺死的原因。

3)忽略:程序會忽略該信號,不作出任何響應。

4)停止進程:將運行中的程序中斷。被停止的進程就像被下了一個斷點一樣,停止運行並不會再被調度,直到收到繼續運行的信號。當按下 Ctrl+Z 時就會將一個正在運行的前台進程停止,其實就是向這個進程發送了一個 SIGTSTP 信號。

5)繼續:使被停止的進程繼續運行。只有 SIGCONT 信號具有這項功能。

這裡介紹下常用的標准信號,但是有時間所有的信號都要仔細的看(見《APUE》第三版 P252 - P256)。

信號 默認動作 說明 SIGABRT 終止+core 調用 abort(3) 函數會向自己發送該信號使程序異常終止,通常在程序自殺時使用。 SIGALRM 終止

調用 alarm(2) 或 setitimer(2) 定時器超時時向自身發送的信號。
setitimer(2) 設置 which 參數的值為 ITIMER_REAL 時,超時後會發送此信號。

SIGCHLD(某些平台是 SIGCLD) 忽略

當子進程狀態改變系統會將該信號發送給其父進程。
狀態改變是指由運行狀態改變為暫停狀態、由暫停狀態改變為運行狀態、由運行狀態改變為終止狀態等等。

SIGHUP 終止 如果終端接口檢測到鏈接斷開則將此信號發送給該終端的控制進程,通常會話首進程就是該終端的控制進程。 SIGINT 終止 

當用戶按下中斷鍵(Ctrl+C)時,終端驅動程序產生此信號並發送給前台進程組中的每一個進程。
大家經常使用 Ctrl + C 來殺死進程,這回知道是什麼原理了吧?

SIGPROF 終止   setitimer(2) 設置 which 參數的值為 ITIMER_PROF 時,超時後會發送此信號。 SIGQUIT 終止+core 

當用戶在終端上按下退出鍵(Ctrl+\)時,終端驅動程序產生此信號並發送給前台進程組中的所有進程。

該信號與 SIGINT 的區別是,在終止進程的同時為它生成 core dump 文件。

SIGTERM 終止  使用 kill(1) 命令發送信號時,如果不指定具體的信號,則默認發送該信號。 SIGUSR1 終止  用戶自定義的信號。
有童鞋說不明白什麼是用戶自定義的信號,
其實所謂自定義的信號就是系統不賦予它什麼特殊的意義,你想用它來做什麼都行,
根據你的程序邏輯為它定義好相應的信號處理函數就行了。 SIGUSR2 終止  另一個用戶自定義的信號,作用同上。 SIGVTALRM 終止    setitimer(2) 設置 which 參數的值為 ITIMER_VIRTUAL 時,超時後會發送此信號。

表1 常見的標准信號 

2. signal(2)

 1 signal - ANSI C signal handling
 2 
 3 #include <signal.h>
 4 
 5 /* man 手冊中定義的寫法 */
 6 
 7 typedef void (*sighandler_t)(int);
 8 
 9 sighandler_t signal(int signum, sighandler_t handler);
10 
11 /* APUE 課本上的寫法 */
12 void (*signal (int signo, void (*func) (int))) (int);

signal(2) 函數的作用是為某個信號注冊一個信號處理函數。

課本上的寫法比 man 手冊中的寫法更好,因為 sighandler_t 這個名字純屬手冊捏造出來的,如果某一天標准庫發布了一個函數的名字恰巧也叫 sighandler_t,那麼手冊就出問題了,這是C 語言名空間管理不善導致的。

參數列表:

  singno:1 - 31 是標准信號,34 - 64 是實時信號,當然也可以使用 kill(1) -l 所列出的宏名;

  func:收到信號時的處理行為,也就是信號處理函數;也可以使用 SIG_DEF 和  SIG_IGN 兩個宏來替代。SIG_DEF 表示使用信號的默認處理行為。SIG_IGN 表示忽略該信號。

返回值:原來的信號處理函數。有時候我們在定義自己的信號處理函數之前會把原來的信號處理函數保存下來,這樣當我們的庫使用完之後需要還原原來注冊的信號處理函數,避免因為調用了我們的庫而導致別人的庫失效的問題。

我們先來看下面的代碼:

 1 #include <stdio.h>
 2 #include <signal.h>
 3 #include <unistd.h>
 4 
 5 static void handler (int s)
 6 {
 7         write(1, "!", 1);
 8 }
 9 
10 int main(void)
11 {
12         int i = 0;
13 
14         signal(SIGINT, handler);
15 
16         for (i = 0; i < 10; i++)
17         {
18                 write(1, "*", 1);
19                 sleep(1);
20         }
21 
22         return 0;
23 }

 

這個程序運行起來之後,每秒鐘會打印一個星號(*),當按下 Ctrl+C 時會打印一個感歎號(!),直到 10 秒鐘後程序退出,下面是不停的按 Ctrl+C 的運行結果。

>$ gcc -Wall signal.c
>$ time ./a.out 
*^C!*^C!*^C!*^C!*^C!*^C!*^C!*^C!*^C!*^C!
real    0m1.656s
user    0m0.000s
sys    0m0.002s
>$

 

通過 time(1) 命令可以測試出來,程序並沒有持續 10 秒鐘才結束,這是因為信號會打斷阻塞的系統調用,也就是說 SIGINT 這個信號打斷了 sleep(3)。

比如使用 read(2) 函數讀取一個設備的時候,當設備中沒有充足的數據供讀取時,read(2) 函數會進入阻塞等待數據的狀態,這時候如果收到了一個信號就會打斷阻塞中的 read(2) 函數,它會設置 EINTR 的 errno。所以收到函數報錯的時候往往需要判斷一下是否被信號打斷了,如果是被信號打斷的,還要重新再執行一次。

 

3.競爭

當學習了信號之後,我們的程序中就出現異步的情況了,只要是異步的程序就可能會出現競爭,先來了解下什麼是競爭。

競爭:一個十字路口沒有紅綠燈,兩輛不同方向駛來車可能會發生碰撞,而且碰撞可能很嚴重也可能很輕微。當安裝上紅綠燈之後就相當於增加了一個協議,如果沒有這個協議的限制,大家就可以隨意的使用公共資源了,你在十字路口中間跳廣場舞也可以。所以為了避免競爭帶來的後果,我們會使用一些協議來避免競爭的發生。

當然,避免競爭的辦法我們後面會討論。

 

4.不可靠的信號

很多人看到了不可靠的信號這一章節,就認為因為額信號會丟失所以是不可靠的,其實這麼理解是不對的,不可靠的信號是指信號的行為不可靠。

信號的處理就好比現在 LZ 正在寫這篇博文,忽然來了一個電話,於是打斷了手頭的工作,先接電話去了。

信號處理函數的執行現場不是程序員布置的,而是內核布置的,因為程序中不會有調用信號處理函數的地方。

同一個信號處理函數的執行現場會被布置在同一個地方,所以當一次信號處理函數未執行完成時再次觸發了相同的信號,信號處理函數發生了第二次調用,則第一次調用的執行現場會被覆蓋。

 

5.可重入函數

函數重入乍一看上去像是遞歸,但又是有區別的,遞歸調用的現場是程序員布置的,而重入是在一個函數執行未結束時再次發生了調用並且進入了同一個函數現場。

重入時函數會發生錯誤的函數稱為“不可重入函數”,重入不會出現錯誤的函數叫做“可重入函數”。

所有的系統調用都是可重入函數,所以信號處理函數中可以放心的使用系統調用。但並不是說所有的非系統調用都是不可重入的。

man 手冊所有的函數中如果有一個同名的帶 _r 後綴的函數,那麼不帶 _r 後綴的函數是不可重入的函數,而帶 _r 後綴的函數是可重入的函數。比如下面這兩個常見的函數:

 1 strerror,  strerror_r - return string describing error number
 2 
 3 #include <string.h>
 4 
 5 char *strerror(int errnum);
 6 
 7 int strerror_r(int errnum, char *buf, size_t buflen);
 8             /* XSI-compliant */
 9 
10 char *strerror_r(int errnum, char *buf, size_t buflen);
11             /* GNU-specific */

 

6.可靠信號術語和語義

這是信號這章比較重要的內容,通過這個我們來了解信號在 Linux 系統中是如何實現的。

 

圖1 標准信號的處理過程

 

呼,累死了,這麼簡單的一張圖竟然畫了一個小時。。下面且聽 LZ 解釋上圖的內容。

mask 和 padding 位圖是一一對應的,它們用於反映當前進程信號的狀態。每一位代表了一個標准信號。

mask 位圖用於記錄哪些信號可以響應。1 表示該信號可以響應,0 表示該信號不可響應(會被忽略)。

padding 位圖用於記錄收到了哪些信號。1 表示收到了該信號,0 表示沒有收到該信號。

前面說過了,程序在執行的過程中會被內核打斷無數次,也就是說程序被打斷後要停止手頭的工作,進入一個隊列排隊等待再次被調度才能繼續工作。

當進程獲得調度機會後,從內核態返回到用戶態之前要做很多事情,其中一件事就是將 mask 位圖和 padding 位圖進行 & 運算,當計算的結果不為 0 時就需要調用相應的信號處理函數或執行信號的默認動作。

這就是 Linux 的信號處理機制,從這個機制中,我們可以總結出幾個信號的特點:

1)如果想要屏蔽某個信號,只需將對應的 mask 位 置為 0 即可。這樣當程序從內核態返回用戶態進行 mask & padding 時,該信號位的計算結果一定為 0。

2)信號從收到到響應是存在延遲的,一般最長延遲 10 毫秒。因為只有程序被打斷並且重新被調度的時候才有機會發現收到了信號,所以當我們向一個程序按下 Ctrl+C 時程序並沒有立即掛掉,只不過這個時間非常短暫我們一般情況下感覺不到而已,我們自己以為程序是立即掛掉了。其實想要實驗也很容易,寫一個死循環不斷打印一個字符,然後在它跑起來的時候按下 Ctrl+C,你會發現並不是打印了 ^C 之後程序會立即停止,而是繼續打印了一些字符之後才停止。

3)當一個信號沒有被處理時,無論再次接受到多少個相同的信號都只能保留一個,因為 padding 是位圖,位圖的特點就是只能保留最後一次的狀態。這一點說的就是標准信號會丟失的特點,如果想要不丟失信號就只能使用實時信號了。

4)信號處理函數輕易不允許使用 longjmp(3) 進行跨函數跳轉。因為處理信號之前系統會把 mask 對應的位設置為 0 來避免信號處理函數重入,當信號處理完成之後系統會把對應的 mask 位設置為 1 恢復進程對該信號的響應能力。如果進行了長跳轉系統就不會恢復 mask 位圖了,也就再也無法收到該信號了。其實這個圖只是一個草圖,信號實際上是線程級別的(這個我們在後面講到線程的時候會詳細討論),所以即使 mask 位圖在處理前被置為 0,依然有可能出現重入的現象,因為無法保證兄弟線程也同步屏蔽了相應的位。

5)信號處理函數的執行時間越短越好,因為信號處理函數是在用戶態執行的,在它的執行過程中也會不停的被內核打斷,所以如果信號處理函數執行的時間過長會使情況變得復雜。

6)信號的響應是嵌套執行的。就是說假設進程先收到了 SIGINT 信號,當它的信號處理函數還沒有執行完畢時又收到了另一個信號 SIGQUIT,那麼當進程從內核態返回到用戶態時會優先執行 SIGQUIT 的信號處理函數,等 SIGQUIT 的信號處理函數執行完畢後再回到 SIGINT 信號處理函數上次被打斷時的地方繼續執行,函數調用棧看上去就像在 SIGINT 的信號處理函數中調用了 SIGQUIT 的信號處理函數一樣。這也是上面所說的為什麼信號處理函數的執行時間要越短越好,要盡量避免這種復雜的情況發生。

7)如果同時到來多個優先級差不多的信號,無法保證優先響應哪個信號,它們的響應沒有嚴格意義上的順序。除非是收到了優先級較高的信號,系統會保證高優先級的先被處理。

 

7. kill(2)

1 kill - send a signal to a process or a group of processes
2 
3 #include <signal.h>
4 
5 int kill(pid_t pid, int sig);

kill(2) 函數的作用是將指定的信號(sig)發送給指定的進程(pid)。

大家一看到 kill 就覺得有殺死進程的意味,其實未必如此,kill(3) 也負責給進程發送各種信號。

參數列表:

  pid:接收信號的進程 ID。可填的內容詳見下表:

值 說明 >0 接收信號的進程 ID。 ==0 發送信號給當前進程所在進程組的所有進程。 ==-1

發送信號給當前進程有權向它們發送信號的所有進程,1 號 init 進程除外。

相當於一個全局廣播信號,發送這種信號一般只有 1 號 init 會做,比如在關機的時候 init 進程會發送全局廣播信號通知大家該結束了。

<-1 將 pid 的絕對值作為組 ID,給這個組中所有的進程發送信號。

表2 kill(2) 函數 pid 參數的取值

  sig:要發送的信號,可以使用 kill(1) -l 所列出的信號。如果 sig 是 0 會執行所有的錯誤檢查,但並不真正發送信號。所以通常使用 0 值檢查一個進程是否仍然存在,如果該進程不存在則返回 -1 並將 errno 設置為 ESRCH。需要注意的是這種檢查並不原子,當 kill(2) 返回測試結果的時候也許被測試的進程已經終止了。當然也可以測試當前進程是否對目標進程有權限發送信號,如果 errno 為 EPERM 表示被測試的進程存在但當前進程無權限訪問。

返回值:成功為 0,失敗為 -1,並設置 errno。

 

8. pause(3P)

1 pause - suspend the thread until a signal is received
2 
3 #include <unistd.h>
4 
5 int pause(void);

專門用於阻塞當前進程,等待一個信號來打斷它。

 

9. alarm(3P)

1 alarm - schedule an alarm signal
2 
3 #include <unistd.h>
4 
5 unsigned alarm(unsigned seconds);

 指定  seconds 秒,發送一個 SIGALRM 信號給自己。

seconds 為 0 的時候,表示取消這個定時器,並且新設置的值會覆蓋上次設置的值。所以當程序中出現了多個對 alarm(3P) 的調用時,計時是不准確的。

注意,SIGALRM 信號默認動作是殺死進程。

 

我們來看看代碼 count_alarm.c、count_time.c,哪個效率更高。

 1 /* count_alarm.c */
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4 #include <unistd.h>
 5 #include <signal.h>
 6 
 7 long long count = 0;
 8 static volatile int flag = 1;
 9 
10 void alarm_handler (int s)
11 {
12     flag = 0;
13 }
14 
15 int main (void)
16 {
17     signal(SIGALRM, alarm_handler);
18     alarm(5);
19 
20     flag = 1;
21 
22     while (flag)
23     {
24         count++;
25     }
26 
27     printf("%lld\n", count);
28 
29     return 0;
30 }

 

 1 /* count_time.c */
 2 #include <stdio.h>
 3 #include <time.h>
 4 
 5 int main (void)
 6 {
 7     long long count = 0;
 8     time_t t;
 9 
10     t = time(NULL) + 5;
11 
12     while (time(NULL) < t) {
13         count++;
14     }
15 
16     printf("%lld\n", count);
17 
18     return 0;
19 }

 

編譯運行:

>$ make count_alarm count_time
cc     count_alarm.c   -o count_alarm
cc     count_time.c   -o count_time
>$ time ./count_alarm
2374311494

real    0m5.004s
user    0m4.780s
sys    0m0.194s
>$ time ./count_time
2139947

real    0m4.152s
user    0m4.116s
sys    0m0.021s
>$

 

通過執行結果可以看出來,alarm(3P) 的方式和 time(2) 的方式執行效率竟然差了 1000 多倍,當然這個簡單的測試精度是不高的。

上面的代碼通過 gcc count_alarm.c -O1 優化之後就無法正確執行了。

我們先把 count_alarm.c 編譯成匯編代碼再討論它為什麼被優化之後無法正確執行了。

優化前:

 1 >$ gcc -S count_alarm.c -o count_alarm.S
 2 >$ vim count_alarm.S
 3     ; ...... 省略不相關代碼
 4 
 5 alarm_handler:
 6 .LFB0:
 7     .cfi_startproc
 8     pushq    %rbp
 9     .cfi_def_cfa_offset 16
10     .cfi_offset 6, -16
11     movq    %rsp, %rbp
12     .cfi_def_cfa_register 6
13     movl    %edi, -4(%rbp)
14     movl    $0, flag(%rip)   ; 修改 flag 的值
15     leave
16     .cfi_def_cfa 7, 8
17     ret
18     .cfi_endproc
19 
20     ; ...... 省略不相關代碼
21 
22 main:
23 .LFB1:
24     .cfi_startproc
25     pushq    %rbp
26     .cfi_def_cfa_offset 16
27     .cfi_offset 6, -16
28     movq    %rsp, %rbp
29     .cfi_def_cfa_register 6
30     movl    $alarm_handler, %esi
31     movl    $14, %edi
32     call    signal
33     movl    $5, %edi
34     call    alarm
35     movl    $1, flag(%rip)
36     jmp    .L4
37 .L5:
38     movq    count(%rip), %rax
39     addq    $1, %rax
40     movq    %rax, count(%rip)
41 .L4:
42     movl    flag(%rip), %eax
43     testl    %eax, %eax        ; 每次循環會檢測 flag 的值是否改變
44     jne    .L5
45     movq    count(%rip), %rdx
46     movl    $.LC0, %eax
47     movq    %rdx, %rsi
48     movq    %rax, %rdi
49     movl    $0, %eax
50     call    printf
51     movl    $0, %eax
52     leave
53     .cfi_def_cfa 7, 8
54     ret
55     .cfi_endproc
56 
57     ; ...... 省略不相關代碼

 

優化後:

 1 >$ gcc -S count_alarm.c -O1 -o count_alarm1.S
 2 >$ vim count_alarm1.S
 3     ; ...... 省略不相關代碼
 4 
 5 alarm_handler:
 6 .LFB21:
 7     .cfi_startproc
 8     movl    $0, flag(%rip)    ; 修改 flag 的值
 9     ret
10     .cfi_endproc
11 
12     ; ...... 省略不相關代碼
13 
14 main:
15 .LFB22:
16     .cfi_startproc
17     subq    $8, %rsp
18     .cfi_def_cfa_offset 16
19     movl    $alarm_handler, %esi
20     movl    $14, %edi
21     call    signal
22     movl    $5, %edi
23     call    alarm
24     movl    $1, flag(%rip)
25 .L4:
26     jmp    .L4                ; 變成了死循環
27     .cfi_endproc
28 
29     ; ...... 省略不相關代碼

 

從上面的代碼不難看出,優化時編譯器認為 flag 的值一直沒有改變,所以直接把 flag 的值拿過來作為循環條件了,每次循環的時候不再從 flag 變量所在的內存位置取值了。

為了避免編譯器犯這種錯誤,我們需要把 flag 定義成 volatile 變量,volatile 關鍵字表示一定要到變量定義的位置取變量的值,而不要輕信曾經拿到的值。

 

10.流量控制

播放音樂和電影的時候都要按照播放的速率讀取文件,而不能像 cat(1) 命令一樣,直接將交給它的文件用最快的速度讀取出來,否則你聽到的音樂就轉瞬即逝了。

我們先通過一個栗子了解下什麼是流量控制:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <fcntl.h>
 4 #include <errno.h>
 5 #include <signal.h>
 6 
 7 #include <sys/types.h>
 8 #include <sys/stat.h>
 9 
10 #define BUFSIZE    10
11 
12 static volatile int loop = 0;
13 
14 static void alarm_handler (int s)
15 {
16     alarm(1);
17     loop = 0;
18 }
19 
20 int main (int argc, char **argv)
21 {
22     int fd = -1;
23     char buf[BUFSIZE] = "";
24     ssize_t readsize = -1;
25     ssize_t writesize = -1;
26     size_t off = 0;
27 
28     if (argc < 2)
29     {
30         fprintf(stderr, "Usage %s <filepath>\n", argv[0]);
31         return 1;
32     }
33 
34     do {
35         fd = open(argv[1], O_RDONLY);
36         if (fd < 0) {
37             if (EINTR != errno) {
38                 perror("open()");
39                 goto e_open;
40             }
41         }
42     } while (fd < 0);
43 
44     loop = 1;
45     signal(SIGALRM, alarm_handler);
46     alarm(1);
47 
48     while(1) {
49         // while (loop); // 忙等
50         // 非忙等
51         while (loop) {
52             pause();
53         }
54         loop = 1;
55 
56         while ((readsize = read(fd, buf, BUFSIZE)) < 0) {
57             if (readsize < 0) {
58                 if (EINTR == errno) {
59                     continue;
60                 }
61                 perror("read()");
62                 goto e_read;
63             }
64         }
65         if (!readsize) {
66             break;
67         }
68 
69         off = 0;
70         do {
71             writesize = write(1, buf + off, readsize);
72             off += writesize;
73             readsize -= writesize;
74         } while (readsize > 0);
75     }
76 
77     close(fd);
78 
79     return 0;
80 
81 e_read:
82     close(fd);
83 e_open:
84     return 1;
85 }

 

程序的運行結果我就不貼出來了,各位童鞋一定要在自己的電腦上運行一下(為了加強練習,最好不要直接復制代碼)。

等你運行完了上面的代碼就可以繼續往下看了,否則你會不知道我在說什麼。

前面文件 IO 的部分我們做過一個栗子 mycp,用來模仿 cp(1) 命令。這次我們把它修改為 mycat,用來模仿 cat(1) 命令,並且是慢慢的 cat,每秒鐘輸出 10 個字節的數據。

這個流控方案就是漏桶:當沒有數據可讀的時候就是閒著,並沒有積攢權限,所以當數據再次可讀的時候它的速率不會變。

我們前面提到過,stream 這種東西並非像小河流水一樣是非常均勻的潺潺細流,而是要麼沒有數據,要麼一下子來一大坨。如果用漏桶處理這種情況速度會非常慢,那麼有沒有什麼更好的流控方案呢?當然有,用令牌桶來處理就可以很好的解決這種流量激增的情況。

令牌桶閒著的時候在積攢權限,所以實際使用時令牌同比漏桶用得更普遍。

具體要用哪種桶需要根據實際需求來決定,比如在線聽音樂的時候網速不好,不能等數據來了的時候用最快的速度把之前積攢了權限的數據一下子都播放出來,應當還保持原來的速率播放,所以這時候選擇漏桶就更加合適了。

下面我們重構一下上面的漏桶流控代碼,把它改成令牌桶的實現:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <fcntl.h>
 4 #include <errno.h>
 5 #include <signal.h>
 6 
 7 #include <sys/types.h>
 8 #include <sys/stat.h>
 9 
10 #define BUFSIZE     10   // 流量速率
11 #define MAXTOKEN    1024 // 令牌上限
12 
13 static volatile int token = 0; // 積攢的令牌數量
14 
15 static void alarm_handler (int s)
16 {
17     alarm(1);
18     if (token < MAXTOKEN) {
19         token++; // 每秒鐘增加令牌
20     }
21 }
22 
23 int main (int argc, char **argv)
24 {
25     int fd = -1;
26     char buf[BUFSIZE] = "";
27     ssize_t readsize = -1;
28     ssize_t writesize = -1;
29     size_t off = 0;
30 
31     if (argc < 2)
32     {
33         fprintf(stderr, "Usage %s <filepath>\n", argv[0]);
34         return 1;
35     }
36 
37     do {
38         fd = open(argv[1], O_RDONLY);
39         if (fd < 0) {
40             if (EINTR != errno) {
41                 perror("open()");
42                 goto e_open;
43             }
44         }
45     } while (fd < 0);
46 
47     signal(SIGALRM, alarm_handler);
48     alarm(1);
49 
50     while(1) {
51         while (token <= 0) { // 如果令牌數量不足則等待添加令牌
52             pause(); // 因為添加令牌是通過信號實現的,所以可以使用 pause(2) 實現非忙等(通知法)
53         }
54         token--; // 每次讀取 BUFSIZE 個字節的數據時要扣減令牌
55 
56         while ((readsize = read(fd, buf, BUFSIZE)) < 0) {
57             if (readsize < 0) {
58                 if (EINTR == errno) {
59                     continue;
60                 }
61                 perror("read()");
62                 goto e_read;
63             }
64         }
65         if (!readsize) {
66             break;
67         }
68 
69         off = 0;
70         do {
71             writesize = write(1, buf + off, readsize);
72             off += writesize;
73             readsize -= writesize;
74         } while (readsize > 0);
75     }
76 
77     close(fd);
78 
79     return 0;
80 
81 e_read:
82     close(fd);
83 e_open:
84     return 1;
85 }

 當然這只是一個簡單的令牌桶的雛形,不過已經足以讓我們了解令牌桶的工作原理了。

 令牌桶的三要素:令牌、令牌上限、流量速率(CPS)。

從上面的代碼可以看出來:SIGALRM 的回調函數負責向令牌桶中添加令牌,而每次讀取數據之前要先檢查令牌的剩余數量。如果令牌充足則扣減令牌後開始讀取數據,如果令牌數量不足則阻塞等待 SIGALRM 回調函數向令牌桶中補充令牌。

設計令牌上限是為了防止令牌桶溢出,通常沒必要讓令牌無限制的上漲。

 

11. getitimer(3P) 和 setitimer(3P) 函數

1 getitimer, setitimer - get or set value of an interval timer
2 
3 #include <sys/time.h>
4 
5 int getitimer(int which, struct itimerval *curr_value);
6 int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

setitimer(2) 函數可以用來替代 alarm(2) 函數。

setitimer(2) 函數主要有兩點比 alarm(2) 函數更好:

  1)setitimer(2) 函數可以使用精度更高的微秒為計時單位;

  2)從 it_interval 賦值給 it_value 是采用原子操作的。

setitimer(2) 直接可以構成一個類似 alarm(2) 鏈的執行結構。也就是說當 it_value 的值被遞減為 0 時會發送一個信號給當前進程,並且自動將 it_interval 的值賦給 it_value 使計時重新開始。

參數列表:

which:使用不同的時間,並發送不同的信號;詳見下表(其實在 表1 中我們也提到它們了)

which 可選宏值 對應的信號 ITIMER_PROF SIGPROF ITIMER_REAL SIGALRM ITIMER_VIRTUAL SIGVTALRM

表3 which 與對應的信號

  new_value:新的定時器周期;這個結構體的定義可以見下面的說明。

  old_value:由該函數回填以前設定的定時器周期,不需要保存可以設置為 NULL;

1 struct itimerval {
2     struct timeval it_interval; /* next value */
3     struct timeval it_value;    /* current value */
4 };
5 
6 struct timeval {
7     time_t      tv_sec;         /* seconds */
8     suseconds_t tv_usec;        /* microseconds */
9 };

遞減的是 it_value 的值,當 it_value 被遞減為 0 的時候將 it_interval 的值 原子化 的賦給 it_value。

tv_sec 表示以秒為單位;tv_usec 表示以微秒為單位。使用一種計時方式時,另一種必須設置為 0。

 

 

12.信號集

信號集就是一種能表示一組信號的數據類型,一般都是用在批量設置信號掩碼時使用。

信號集使用 sigset_t 類型表示,有一組函數可以操作它。

 1 sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations
 2 
 3 #include <signal.h>
 4 
 5 int sigemptyset(sigset_t *set);
 6 
 7 int sigfillset(sigset_t *set);
 8 
 9 int sigaddset(sigset_t *set, int signum);
10 
11 int sigdelset(sigset_t *set, int signum);
12 
13 int sigismember(const sigset_t *set, int signum);

 

這一組函數的作用無非就是對信號集中的信號進行曾刪改差,這裡 LZ 就不再贅述了,具體的用法各位可以自行查閱 man 手冊。

 

13.sigprocmask(2)

1 sigprocmask - examine and change blocked signals
2 
3 #include <signal.h>
4 
5 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

前面我們提到過我們可以人為的干擾信號 mask 位圖,唯一的途徑就是通過這個函數實現。但是 padding 位圖是無法人為干擾的。

我們不能保證信號什麼時候來,使用這個函數的目的就是為了讓我們來決定什麼時候響應信號。

參數列表:

how:指定如何來干擾 mask 位圖,可以使用下表中三個宏中的任何一個來指定;

宏 含義 SIG_BLOCK 將當前進程的信號屏蔽字和 set 信號集中的信號全部屏蔽,也就是將它們的 mask 位設置為 0 SIG_UNBLOCK 將 set 信號集中與當前信號屏蔽字重疊的信號解除屏蔽,也就是將它們的 mask 位設置為 1 SIG_SETMASK 將 set 信號集中的信號 mask 位設置為 0,其它的信號全部恢復為 1

表4 干擾 mask 位圖的方式

  set:需要被干擾 mask 位圖的信號集;

  oldset:由該函數回填之前被干擾的信號集。

使用這個函數,我們來重構上面那個打印星號和感歎號的程序,新需求是這樣的:

每行打印 5 個星號,然後停止。期間如果收到了 SIGINT 信號不會立即響應,而是等待本行打印結束後再響應,並且在收到信號之後再打印下一行。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <signal.h>
 4 #include <unistd.h>
 5 
 6 static void int_handler(int s)
 7 {
 8     write(1,"!",1);
 9 }
10 
11 int main()
12 {
13     sigset_t set,oset,saveset;
14     int i,j;
15 
16     signal(SIGINT,int_handler);
17 
18     sigemptyset(&set);
19     sigaddset(&set,SIGINT);    
20 
21     sigprocmask(SIG_UNBLOCK,&set,&saveset);
22 
23     sigprocmask(SIG_BLOCK,&set,&oset);
24     for(j = 0 ; j < 10000; j++)
25     {
26         for(i = 0 ; i < 5; i++)
27         {
28             write(1,"*",1);
29             sleep(1);
30         }
31         write(1,"\n",1);
32 
33         // 相當於下面三行的原子操作
34         sigsuspend(&oset);
35     /*
36         sigset_t tmpset;
37         sigprocmask(SIG_SETMASK,&oset,&tmpset);
38         pause();
39         sigprocmask(SIG_SETMASK,&tmpset,NULL);
40     */
41     }
42 
43     sigprocmask(SIG_SETMASK,&saveset,NULL);
44 
45     exit(0);
46 }
47     

 

大致的實現思路是:開始打印每行星號之前先屏蔽信號,當打印完成之後再恢復信號,然後等待被信號打斷,再重新屏蔽信號,打印星號。

但是在測試的時候會發現,這樣只能實現當一行信號打印完畢時可以停住,然後按下 Ctrl+C 發送信號,可以繼續打印下一行。但是當一行沒有打印完成時就按 Ctrl+C 發送信號,下一行會在行首打印感歎號,但是卻並不繼續開始打印星號。

這是什麼原因導致的呢?其實仔細分析一下信號的處理過程就明白了,在開始打印星號之前我們屏蔽了信號的 mask 位,當接收到信號時對應的 padding 位被置1,由於 mask 位是 0 所以程序不會響應信號。當星號打印完成時 mask 位被置為 1,程序會再次看到信號,所以會打印感歎號並進入 pause 狀態等待被信號打斷,所以程序只打印了一個感歎號卻沒有繼續打印星號。

歸根結底還是因為 解除信號屏蔽 --- 等待被信號打斷 --- 屏蔽信號 的這三個步驟不原子導致的。

sigsuspend(2) 函數我們在這篇博文的最後面還會講解。

當使用 sigsuspend(2) 函數使這三個步驟原子化時我們再來分析一下程序的執行過程:

開始打印星號之前將 mask 位設置為 0,開始打印星號,此時如果接收到了信號 padding 被設置為 1,但是由於 mask 為 0 所以程序不會響應信號。當程序打印完星號時將 mask 位設置為 1,此時響應信號打印出感歎號,並原子化的解除信號屏蔽 + 被信號打斷 + 重新屏蔽信號,然後繼續開始打印下一行星號。

我們再來看另一種情況:開始打印星號之前將 mask 位設置為 0 並開始打印星號,當一行星號打印完成時沒有收到信號,那麼原子化的解除信號屏蔽並等待被信號打斷。當信號到來時重新屏蔽信號並繼續開始打印下一行星號。

根據上面的分析,只要 解除信號屏蔽 --- 等待被信號打斷 --- 屏蔽信號 的這三個步驟原子化後就沒問題了。當某件事情需要信號驅動時,在該事件未處理完成時又不希望再次被信號打斷的時候,就可以采用類似的這種方式。

當然,這個這個程序是用標准信號實現的,所以標准信號的特點也被它集成了下來:當連續接收到多個信號時只能驅動打印一行星號,而不能收到多少個信號就打印多少行星號,因為標准信號會丟失。

如果想要讓程序收到多少個信號就打印多少行星號,其實代碼別的地方都不用修改,直接把信號集中的標准信號替換成實時信號就可以了,因為實時信號的特點是不丟失。代碼很簡單 LZ 就不貼出來了,感興趣的小伙伴可以自己實驗一下。

 

13. sigpending(3P)

1 sigpending - examine pending signals
2 
3 #include <signal.h>
4 
5 int sigpending(sigset_t *set);

用於獲取當前收到但是沒有響應的信號。

它是一個系統調用,所以當它從內核中返回的時候需要對信號位圖做 & 操作,相應的信號已經被處理了,所以當它返回用戶態的時候,它帶回來的結果可能已經不准確了。

除非調用它之前先把所有的信號都 block 住,然後再調用它,返回的結果才是准確的。

LZ 目前還未發現這個函數在實際開發當中有什麼作用,主要有兩個理由:

  1)該函數沒有後續操作;

  2)沒有上面說的手段,取出來的信號集是不准確的。

如果小伙伴們發現了它的用途,請在評論中告知 LZ 哈。

 

14. sigaction(2) 

1 sigaction - examine and change a signal action
2 
3 #include <signal.h>
4 
5 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

這個函數也是信號這章比較重要的一個函數。sigaction(2) 是用來替換 signal(2) 函數的。因為 signal(2) 有一些設計上的缺陷,所以小伙伴們學過了這個函數之後以後就盡量不要再使用 signal(2) 函數了。

參數列表:

  signum:要設定信號處理函數的信號;

  act:對信號處理函數的設定;

  oldact:由函數回填之前的信號處理函數設定,備份用,如果不需要可以填 NULL。

下面看看 struct sigaction 這個結構體的成員表示什麼意思:

1 struct sigaction {
2     // 前兩個是信號處理函數,二選一,在某些平台上是一個共用體。
3     void     (*sa_handler)(int); // 為了兼容 signal(2) 函數
4     void     (*sa_sigaction)(int, siginfo_t *, void *); // 第二個參數可以獲得信號的來源和屬性。第三個參數最原始時是 ucontext_t* 而不是 void*,與 setcontext(3) 有關,目前該參數已經禁止使用。
5     sigset_t   sa_mask; // 信號集位圖,指定要處理的信號集,並且信號集中的任何一個信號被觸發時,信號集中的其它成員同時會被 block,避免像 signal(2) 的信號處理函數一樣當多個信號同時到來時發生重入。
6     int        sa_flags; // 特殊要求。如果使用三參的信號處理函數,需要指定為 SA_SIGINFO
7     void     (*sa_restorer)(void); // 基本被廢棄了,不用管
8 };

實際上一個參數的信號處理函數和三個參數的信號處理函數使用哪個都行,一般一個參數的就夠用了。假設你的程序需要區分信號的來源或屬性信息,那麼就需要使用三參的信號處理函數了。 

 

我們再來說說 signal(2) 函數哪裡不靠譜。

還記得使用 signal(2) 函數注冊的信號處理函數的原型嗎?它的參數 s 的作用被設計出來的目的是為了讓信號處理函數區別出來是哪個信號觸發了它,也就是允許多個不同的信號共用同一個信號處理函數,並且動作可以不一樣,可以根據 s 的不同做不同的事。

下面舉一個簡單的小栗子給大家演示一下如何使用 sigaction(2) 代替 signal(2),以及為什麼說 signal(2) 函數是不靠譜的。

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <fcntl.h>
  5 #include <syslog.h>
  6 #include <errno.h>
  7 #include <signal.h>
  8 #include <string.h>
  9 
 10 #include <sys/types.h>
 11 #include <sys/stat.h>
 12 
 13 #define FNAME        "/tmp/out"
 14 
 15 static FILE *fp;
 16 
 17 static int daemonize(void)
 18 {
 19     pid_t pid;    
 20     int fd;
 21 
 22     pid = fork();
 23     if(pid < 0)
 24 //      syslog(LOG_ERR,"fork():%s",strerror(errno));
 25         return -1;    
 26     
 27     if(pid > 0)
 28         exit(0);
 29 
 30     fd = open("/dev/null",O_RDWR);
 31     if(fd < 0)
 32         return -2;
 33             
 34     dup2(fd,0);    
 35     dup2(fd,1);    
 36     dup2(fd,2);    
 37 
 38     if(fd > 2)
 39         close(fd);
 40 
 41     setsid();
 42     
 43     chdir("/");
 44     umask(0);
 45 
 46     return 0;
 47 
 48 }
 49 
 50 static void daemon_exit(int s)
 51 {
 52     fclose(fp);
 53     closelog();
 54     syslog(LOG_INFO,"daemonize exited.");
 55     exit(0);
 56 }
 57 
 58 int main()
 59 {
 60     int i;
 61     struct sigaction sa;
 62 
 63 //    如果使用 signal(2) 函數則是這樣注冊信號處理函數
 64 //    signal(SIGINT,daemon_exit);
 65 //    signal(SIGTERM,daemon_exit);
 66 //    signal(SIGQUIT,daemon_exit);    
 67 
 68 
 69 //  現在改用 sigaction(2) 來替代 signal(2) 函數
 70     sa.sa_handler = daemon_exit;
 71     sigemptyset(&sa.sa_mask);
 72     sigaddset(&sa.sa_mask,SIGQUIT);
 73     sigaddset(&sa.sa_mask,SIGTERM);
 74     sigaddset(&sa.sa_mask,SIGINT);
 75     sa.sa_flags = 0;
 76     sigaction(SIGINT,&sa,NULL);
 77     /*if error*/
 78     sigaction(SIGTERM,&sa,NULL);
 79     /*if error*/
 80     sigaction(SIGQUIT,&sa,NULL);
 81     /*if error*/
 82 
 83 
 84 
 85     openlog("mydaemon",LOG_PID,LOG_DAEMON);
 86 
 87 //  啟動守護進程
 88     if(daemonize())
 89     {
 90         syslog(LOG_ERR,"daemonize() failed.");
 91         exit(1);
 92     }    
 93     else
 94     {
 95         syslog(LOG_INFO,"daemonize() successed.");
 96     }
 97 
 98     fp = fopen(FNAME,"w");
 99     if(fp == NULL)
100     {
101         syslog(LOG_ERR,"fopen():%s",strerror(errno));
102         exit(1);
103     }
104     
105     for(i = 0 ; ; i++)
106     {
107         fprintf(fp,"%d\n",i);
108         fflush(fp);
109         syslog(LOG_DEBUG,"%d was printed.",i);
110         sleep(1);
111     }
112 
113 
114     exit(0);
115 }

這段代碼很簡單,就是啟動一個守護進程每秒鐘向 /tmp/out 文件輸出一個序列。

上面的代碼動機是好的,注冊了三個信號處理函數,企圖將異常結束行為改變為正常結束行為。但是信號處理函數中並不需要區分不同的信號,只要任何一個信號到來想要殺死進程的時候把資源釋放掉再結束即可。

所以有一個重要的缺陷:當多個信號同時到來的時候,一定會發生內存洩漏。因為 signal(2) 函數在一個信號到來的時候不會把其它注冊了同一個信號處理函數的信號屏蔽掉。

上面已經說過了,sigaction(2) 在收到信號集中的任何一個信號的時候,都會將信號集中的其它信號屏蔽掉,所以就會避免信號處理函數發生重入。上面的代碼改成使用 sigaction(2) 的方式實現就變得安全了。

 

 15. setjmp(3) 和 sigsetjmp(3) 函數

1 setjmp, sigsetjmp - save stack context for nonlocal goto
2 
3 #include <setjmp.h>
4 
5 int setjmp(jmp_buf env);
6 
7 int sigsetjmp(sigjmp_buf env, int savesigs);

我們前面說過,在信號處理函數中是不能使用跨函數的長跳轉的還記得嗎?是因為進入處理函數之前系統會幫我們屏蔽對應的信號掩碼,而當信號處理完成的時候系統會幫我們還原信號掩碼。如果我們在信號處理函數中跳走了,那麼信號掩碼就不會被還原了,可能會造成當前進程再也無法接收到該信號了。

setjmp(3) 在 FreeBSD 平台上和其他平台上的實現不一致。FreeBSD 在跳轉的時候還會保存信號掩碼,並且在跳轉的時候恢復信號掩碼,所以在 FreeBSD 上使用 setjmp(3) 從信號處理函數中跳轉是安全的。

由於其它平台的實現在跳轉時不支持恢復信號掩碼,大家一定猜到了為什麼又出現了一個 sigsetjmp(3) 函數了。

果然標准再一次跳出來和稀泥了,制定了 sigsetjmp(3) 函數。

sigsetjmp(3) 函數的參數:如果 savesigs 為真,表示與 FreeBSD 平台的 setjmp(3) 實現相同,否則跳轉時不保存信號掩碼。就這麼一點差別,僅此而已。

 1 #include <stdio.h>
 2 #include <setjmp.h>
 3 #include <signal.h>
 4 #include <unistd.h>
 5 
 6 static sigjmp_buf env;
 7 
 8 static void fun (void)
 9 {
10     long long i = 0;
11 
12     sigsetjmp(env, 1);
13 
14     printf("before %s\n", __FUNCTION__);
15     for (i = 0; i < 1000000000; i++);
16     printf("end %s\n", __FUNCTION__);
17 }
18 
19 static void handler (int s)
20 {
21     printf("before %s\n", __FUNCTION__);
22     siglongjmp(env, 1);
23     printf("end %s\n", __FUNCTION__);
24 }
25 
26 int main (void)
27 {
28     long long i = 0;
29 
30     signal(SIGINT, handler);
31 
32     fun();
33 
34     for (i = 0; ; i++)
35     {
36         printf("%lld\n", i);
37         pause();
38     }
39 
40     return 0;
41 }

 編譯運行:

>$ gcc -Wall siglongjmp.c -o siglongjmp
>$ ./siglongjmp 
before fun
^Cbefore handler
before fun
^Cbefore handler
before fun
^Cbefore handler
before fun
end fun
0
^Cbefore handler
before fun
end fun
Segmentation fault (core dumped)
>$

從上面的執行結果可以看出來,第一次執行 fun() 函數的時候設置了跳轉點,在 fun() 函數執行完成之前發送 SIGINT 信號使程序切換到 handler() 函數運行,並且在 handler() 函數中再次跳轉到 fun() 函數。在 fun() 函數運行結束之前再次發送信號依然可以被程序看到,說明 siglongjmp(3) 在跳轉的時候確實恢復信號掩碼了。

但是繼續往下看,當 fun() 函數執行完畢時再次發送 SIGINT 信號給程序,handler() 函數會再次被調用,但是當從 handler() 跳轉到 fun() 函數的時候出現段錯誤了!

為什麼呢?經過 LZ 實驗發現:siglongjmp(3) 函數只能從信號處理函數中跳轉到當前被打斷的函數,而不能隨意跳轉到其它函數中!(信號處理的過程可以見上面的圖1)

也就是說當 fun() 函數在運行時被打斷,從內核態回到用戶態時發現收到了信號,這時候跳轉到信號處理函數中運行,這個信號處理函數如果使用 siglongjmp(3) 函數進行跳轉,則只能跳轉到 fun() 函數中,否則會報段錯誤。

同理,上面的代碼當 fun() 函數運行結束時回到 main() 函數繼續運行,在 main() 被打斷後進入內核排隊等待被調度,當它獲得調度機會從內核態回到用戶態時發現收到了信號並且需要處理,這個時候信號處理函數 handler() 開始運行,如果信號處理函數需要使用 siglongjmp(3) 進行跳轉,那麼它只能選擇跳轉到 main() 函數中,而不能跳轉到其它函數中。因為前面 LZ 說了,當前被打斷的是 main() 函數,誰被打斷就只能跳轉到誰那去。這時候信號處理函數依然選擇跳轉到 fun() 函數中,所以引發了段錯誤。

為什麼會有這麼奇怪的現象 LZ 也不明白,估計跟執行現場有關系,各位如果知道是什麼原因的話請在留言中告訴 LZ 哈。

 

16 abort(3)

1 abort - cause abnormal process termination
2 
3 #include <stdlib.h>
4 
5 void abort(void);

給調用者發送一個 SIGABRT 信號,收到這個信號的默認動作是終止 + 產生 coredump 文件。

我們在上面的 表1 中提到過它,一般都是程序發現自己出現了明顯的異常,為了避免缺陷擴散,自殺的時候使用。

 

17. system(3)

1 system - execute a shell command
2 
3 #include <stdlib.h>
4 
5 int system(const char *command);

在前面介紹進程相關的博文中我們介紹過 system(3) 函數,所以對於它的功能我們這裡就不再贅述了,今天聊點關於它與信號的花邊新聞。

對於它的使用有一些需要注意的內容,想要正確的使用 system(3) 函數,必須阻塞 SIGCHLD 信號並忽略 SIGINT、SIGQUIT 信號。

為什麼使用 system(3) 函數之前要做這些動作呢?這與 shell 的內部命令處理有關系,如果想要了解更詳細的內容,請自行參閱 《APUE》 第三版第九章。

 

18. select(2)

 1 select,  pselect - synchronous I/O multiplexing
 2 
 3 /* According to POSIX.1-2001 */
 4 #include <sys/select.h>
 5 
 6 /* According to earlier standards */
 7 #include <sys/time.h>
 8 #include <sys/types.h>
 9 #include <unistd.h>
10 
11 int select(int nfds, fd_set *readfds, fd_set *writefds,
12            fd_set *exceptfds, struct timeval *timeout);

其實 sleep(3) 函數是不好用的,因為某些平台上是使用 alarm(2) + pause(2) 封裝它的,大家知道 alarm(2) 的計時是不太准確的。

在當前平台(Linux)sleep(3) 函數是使用 nanosleep 封裝的,所以如果不考慮移植的話在當前平台上可以安全的使用 sleep(3) 函數。

其實 usleep(3)、nanosleep(2)、select(2) 這些函數都比 sleep(3) 好用。

select 我們在第14章還會講,這裡說一下利用它的副作用來為我們實現一個安全的定時器。

這樣設定它的參數列表就可以了:-1, NULL, NULL, NULL, 定時結構體。

具體的代碼 LZ 就不貼出來了,大家可以自己動手試試。

 

19.sigsuspend(2)

1 sigsuspend - wait for a signal
2 
3 #include <signal.h>
4 
5 int sigsuspend(const sigset_t *mask);

這個函數我們在上面已經見過了, 它就是為了解決解除信號阻塞和 pause(2) 之間不原子的問題。

如果本來程序期望的是解除該信號的阻塞之後用 pause(2) 來等待被該信號打斷,結果這個信號在解除阻塞和 pause(2) 之間到來了,這就導致它無法打斷 pause(2) 了,因為它是在進行 pause(2) 之前到來的。如果後面不會再見到該信號,那麼 pause(2) 將永遠阻塞下去。

我們用下面的栗子來說明這個問題。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <signal.h>
 4 #include <unistd.h>
 5 
 6 static void int_handler(int s)
 7 {
 8     write(1,"!",1);
 9 }
10 
11 int main()
12 {
13     sigset_t set,oset,saveset;
14     int i,j;
15 
16     signal(SIGINT,int_handler);
17 
18     sigemptyset(&set);
19     sigaddset(&set,SIGINT);    
20 
21     sigprocmask(SIG_UNBLOCK,&set,&saveset);
22 
23     for(j = 0 ; j < 10000; j++)
24     {
25         sigprocmask(SIG_BLOCK,&set,NULL);
26 //        sigprocmask(SIG_BLOCK,&set,&oset);
27         for(i = 0 ; i < 5; i++)
28         {
29             write(1,"*",1);
30             sleep(1);
31         }
32         write(1,"\n",1);
33         sigprocmask(SIG_UNBLOCK,&set,NULL);
34 //        sigprocmask(SIG_SETMASK,&oset,NULL);
35 //        pause();
36     }
37 
38     sigprocmask(SIG_SETMASK,&saveset,NULL);
39 
40     exit(0);
41 }

 

這個程序跟上面的栗子類似,每秒鐘打印一個星號,每 5 個星號組成一行,只有當一行星號打印完畢時才響應 SIGINT 信號。

如果解除阻塞和等待信號打斷不采用原子操作,那麼在 pause(2) 之前收到了信號就無法驅動下一行星號的打印了。

 

20.有關信號的其它內容

除了 kill -l 可以查看所有的信號,還可以通過 /usr/include/bits/signum.h 文件查看。

 

實時信號會按照先到先響應的順序處理,並且信號會排隊,不會丟失。

信號是否排隊、是否丟失,不取決於使用哪個函數,而是取決於使用哪種信號。

實時信號具有這些特點是因為它不是采用位圖實現的,而是采用鏈式結構實現的。

其它方面與標准信號沒有區別。

 

信號處理函數中只能安全的使用可重入函數(所有系統調用都是可重入函數)和所有的科學計算(科學計算都是可重入的),編寫信號處理函數要時刻防止重入發生。

盡量不要大范圍的混用信號和多線程,如果在小范圍內信號 + 多線程可以方便的解決某個問題時才可以在小范圍內混用它們。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved