<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # 第?33?章?信號 **目錄** + [1\. 信號的基本概念](ch33s01.html) + [2\. 產生信號](ch33s02.html) + [2.1\. 通過終端按鍵產生信號](ch33s02.html#id2884244) + [2.2\. 調用系統函數向進程發信號](ch33s02.html#id2884400) + [2.3\. 由軟件條件產生信號](ch33s02.html#id2884567) + [3\. 阻塞信號](ch33s03.html) + [3.1\. 信號在內核中的表示](ch33s03.html#id2884694) + [3.2\. 信號集操作函數](ch33s03.html#id2884876) + [3.3\. sigprocmask](ch33s03.html#id2885022) + [3.4\. sigpending](ch33s03.html#id2885205) + [4\. 捕捉信號](ch33s04.html) + [4.1\. 內核如何實現信號的捕捉](ch33s04.html#id2885289) + [4.2\. sigaction](ch33s04.html#id2885439) + [4.3\. pause](ch33s04.html#id2885627) + [4.4\. 可重入函數](ch33s04.html#id2885983) + [4.5\. sig_atomic_t類型與volatile限定符](ch33s04.html#id2886197) + [4.6\. 競態條件與sigsuspend函數](ch33s04.html#id2886686) + [4.7\. 關于SIGCHLD信號](ch33s04.html#id2887260) ## 1.?信號的基本概念 為了理解信號,先從我們最熟悉的場景說起: 1. 用戶輸入命令,在Shell下啟動一個前臺進程。 2. 用戶按下Ctrl-C,這個鍵盤輸入產生一個硬件中斷。 3. 如果CPU當前正在執行這個進程的代碼,則該進程的用戶空間代碼暫停執行,CPU從用戶態切換到內核態處理硬件中斷。 4. 終端驅動程序將Ctrl-C解釋成一個`SIGINT`信號,記在該進程的PCB中(也可以說發送了一個`SIGINT`信號給該進程)。 5. 當某個時刻要從內核返回到該進程的用戶空間代碼繼續執行之前,首先處理PCB中記錄的信號,發現有一個`SIGINT`信號待處理,而這個信號的默認處理動作是終止進程,所以直接終止進程而不再返回它的用戶空間代碼執行。 注意,Ctrl-C產生的信號只能發給前臺進程。在[第?3.3?節 “wait和waitpid函數”](ch30s03.html#process.wait)中我們看到一個命令后面加個`&`可以放到后臺運行,這樣Shell不必等待進程結束就可以接受新的命令,啟動新的進程。Shell可以同時運行一個前臺進程和任意多個后臺進程,只有前臺進程才能接到像Ctrl-C這種控制鍵產生的信號。前臺進程在運行過程中用戶隨時可能按下Ctrl-C而產生一個信號,也就是說該進程的用戶空間代碼執行到任何地方都有可能收到`SIGINT`信號而終止,所以信號相對于進程的控制流程來說是異步(Asynchronous)的。 用`kill -l`命令可以察看系統定義的信號列表: ``` $ 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 ... ``` 每個信號都有一個編號和一個宏定義名稱,這些宏定義可以在`signal.h`中找到,例如其中有定義`#define SIGINT 2`。編號34以上的是實時信號,本章只討論編號34以下的信號,不討論實時信號。這些信號各自在什么條件下產生,默認的處理動作是什么,在`signal(7)`中都有詳細說明: ``` Signal Value Action Comment ------------------------------------------------------------------------- SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process SIGINT 2 Term Interrupt from keyboard SIGQUIT 3 Core Quit from keyboard SIGILL 4 Core Illegal Instruction ... ``` 上表中第一列是各信號的宏定義名稱,第二列是各信號的編號,第三列是默認處理動作,`Term`表示終止當前進程,`Core`表示終止當前進程并且Core Dump(下一節詳細介紹什么是Core Dump),`Ign`表示忽略該信號,`Stop`表示停止當前進程,`Cont`表示繼續執行先前停止的進程,表中最后一列是簡要介紹,說明什么條件下產生該信號。 產生信號的條件主要有: * 用戶在終端按下某些鍵時,終端驅動程序會發送信號給前臺進程,例如Ctrl-C產生`SIGINT`信號,Ctrl-\產生`SIGQUIT`信號,Ctrl-Z產生`SIGTSTP`信號(可使前臺進程停止,這個信號將在[第?34?章 _終端、作業控制與守護進程_](ch34.html#jobs)詳細解釋)。 * 硬件異常產生信號,這些條件由硬件檢測到并通知內核,然后內核向當前進程發送適當的信號。例如當前進程執行了除以0的指令,CPU的運算單元會產生異常,內核將這個異常解釋為`SIGFPE`信號發送給進程。再比如當前進程訪問了非法內存地址,,MMU會產生異常,內核將這個異常解釋為`SIGSEGV`信號發送給進程。 * 一個進程調用`kill(2)`函數可以發送信號給另一個進程。 * 可以用`kill(1)`命令發送信號給某個進程,`kill(1)`命令也是調用`kill(2)`函數實現的,如果不明確指定信號則發送`SIGTERM`信號,該信號的默認處理動作是終止進程。 * 當內核檢測到某種軟件條件發生時也可以通過信號通知進程,例如鬧鐘超時產生`SIGALRM`信號,向讀端已關閉的管道寫數據時產生`SIGPIPE`信號。 如果不想按默認動作處理信號,用戶程序可以調用`sigaction(2)`函數告訴內核如何處理某種信號(`sigaction`函數稍后詳細介紹),可選的處理動作有以下三種: 1. 忽略此信號。 2. 執行該信號的默認處理動作。 3. 提供一個信號處理函數,要求內核在處理該信號時切換到用戶態執行這個處理函數,這種方式稱為捕捉(Catch)一個信號。 ## 2.?產生信號 ### 2.1.?通過終端按鍵產生信號 上一節講過,`SIGINT`的默認處理動作是終止進程,`SIGQUIT`的默認處理動作是終止進程并且Core Dump,現在我們來驗證一下。 首先解釋什么是Core Dump。當一個進程要異常終止時,可以選擇把進程的用戶空間內存數據全部保存到磁盤上,文件名通常是`core`,這叫做Core Dump。進程異常終止通常是因為有Bug,比如非法內存訪問導致段錯誤,事后可以用調試器檢查`core`文件以查清錯誤原因,這叫做Post-mortem Debug。一個進程允許產生多大的`core`文件取決于進程的Resource Limit(這個信息保存在PCB中)。默認是不允許產生`core`文件的,因為`core`文件中可能包含用戶密碼等敏感信息,不安全。在開發調試階段可以用`ulimit`命令改變這個限制,允許產生`core`文件。 首先用`ulimit`命令改變Shell進程的Resource Limit,允許`core`文件最大為1024K: ``` $ ulimit -c 1024 ``` 然后寫一個死循環程序: ``` #include <unistd.h> int main(void) { while(1); return 0; } ``` 前臺運行這個程序,然后在終端鍵入Ctrl-C或Ctrl-\: ``` $ ./a.out (按Ctrl-C) $ ./a.out (按Ctrl-\)Quit (core dumped) $ ls -l core* -rw------- 1 akaedu akaedu 147456 2008-11-05 23:40 core ``` `ulimit`命令改變了Shell進程的Resource Limit,`a.out`進程的PCB由Shell進程復制而來,所以也具有和Shell進程相同的Resource Limit值,這樣就可以產生Core Dump了。 ### 2.2.?調用系統函數向進程發信號 仍以上一節的死循環程序為例,首先在后臺執行這個程序,然后用`kill`命令給它發`SIGSEGV`信號。 ``` $ ./a.out & [1] 7940 $ kill -SIGSEGV 7940 $(再次回車) [1]+ Segmentation fault (core dumped) ./a.out ``` 7940是`a.out`進程的id。之所以要再次回車才顯示`Segmentation fault`,是因為在7940進程終止掉之前已經回到了Shell提示符等待用戶輸入下一條命令,Shell不希望`Segmentation fault`信息和用戶的輸入交錯在一起,所以等用戶輸入命令之后才顯示。指定某種信號的`kill`命令可以有多種寫法,上面的命令還可以寫成`kill -SEGV 7940`或`kill -11 7940`,11是信號`SIGSEGV`的編號。以往遇到的段錯誤都是由非法內存訪問產生的,而這個程序本身沒錯,給它發`SIGSEGV`也能產生段錯誤。 `kill`命令是調用`kill`函數實現的。`kill`函數可以給一個指定的進程發送指定的信號。`raise`函數可以給當前進程發送指定的信號(自己給自己發信號)。 ``` #include <signal.h> int kill(pid_t pid, int signo); int raise(int signo); ``` 這兩個函數都是成功返回0,錯誤返回-1。 `abort`函數使當前進程接收到`SIGABRT`信號而異常終止。 ``` #include <stdlib.h> void abort(void); ``` 就像`exit`函數一樣,`abort`函數總是會成功的,所以沒有返回值。 ### 2.3.?由軟件條件產生信號 `SIGPIPE`是一種由軟件條件產生的信號,在[例?30.7 “管道”](ch30s04.html#process.pipe)中已經介紹過了。本節主要介紹`alarm`函數和`SIGALRM`信號。 ``` #include <unistd.h> unsigned int alarm(unsigned int seconds); ``` 調用`alarm`函數可以設定一個鬧鐘,也就是告訴內核在`seconds`秒之后給當前進程發`SIGALRM`信號,該信號的默認處理動作是終止當前進程。這個函數的返回值是0或者是以前設定的鬧鐘時間還余下的秒數。打個比方,某人要小睡一覺,設定鬧鐘為30分鐘之后響,20分鐘后被人吵醒了,還想多睡一會兒,于是重新設定鬧鐘為15分鐘之后響,“以前設定的鬧鐘時間還余下的時間”就是10分鐘。如果`seconds`值為0,表示取消以前設定的鬧鐘,函數的返回值仍然是以前設定的鬧鐘時間還余下的秒數。 **例?33.1.?alarm** ``` #include <unistd.h> #include <stdio.h> int main(void) { int counter; alarm(1); for(counter=0; 1; counter++) printf("counter=%d ", counter); return 0; } ``` 這個程序的作用是1秒鐘之內不停地數數,1秒鐘到了就被`SIGALRM`信號終止。 ## 3.?阻塞信號 ### 3.1.?信號在內核中的表示 以上我們討論了信號_產生_(Generation)的各種原因,而實際執行信號的處理動作稱為信號_遞達_(Delivery),信號從產生到遞達之間的狀態,稱為信號_未決_(Pending)。進程可以選擇阻塞(Block)某個信號。被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。信號在內核中的表示可以看作是這樣的: **圖?33.1.?信號在內核中的表示示意圖** ![信號在內核中的表示示意圖](https://box.kancloud.cn/2016-04-02_56ff80d9ba527.png) 每個信號都有兩個標志位分別表示阻塞和未決,還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。在上圖的例子中, 1. `SIGHUP`信號未阻塞也未產生過,當它遞達時執行默認處理動作。 2. `SIGINT`信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。 3. `SIGQUIT`信號未產生過,一旦產生`SIGQUIT`信號將被阻塞,它的處理動作是用戶自定義函數`sighandler`。 如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理?POSIX.1允許系統遞送該信號一次或多次。Linux是這樣實現的:常規信號在遞達之前產生多次只計一次,而實時信號在遞達之前產生多次可以依次放在一個隊列里。本章不討論實時信號。從上圖來看,每個信號只有一個bit的未決標志,非0即1,不記錄該信號產生了多少次,阻塞標志也是這樣表示的。因此,未決和阻塞標志可以用相同的數據類型`sigset_t`來存儲,`sigset_t`稱為信號集,這個類型可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決狀態。下一節將詳細介紹信號集的各種操作。阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這里的“屏蔽”應該理解為阻塞而不是忽略。 ### 3.2.?信號集操作函數 `sigset_t`類型對于每種信號用一個bit表示“有效”或“無效”狀態,至于這個類型內部如何存儲這些bit則依賴于系統實現,從使用者的角度是不必關心的,使用者只能調用以下函數來操作`sigset_t`變量,而不應該對它的內部數據做任何解釋,比如用`printf`直接打印`sigset_t`變量是沒有意義的。 ``` #include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(const sigset_t *set, int signo); ``` 函數`sigemptyset`初始化`set`所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含任何有效信號。函數`sigfillset`初始化`set`所指向的信號集,使其中所有信號的對應bit置位,表示該信號集的有效信號包括系統支持的所有信號。注意,在使用`sigset_t`類型的變量之前,一定要調用`sigemptyset`或`sigfillset`做初始化,使信號集處于確定的狀態。初始化`sigset_t`變量之后就可以在調用`sigaddset`和`sigdelset`在該信號集中添加或刪除某種有效信號。這四個函數都是成功返回0,出錯返回-1。`sigismember`是一個布爾函數,用于判斷一個信號集的有效信號中是否包含某種信號,若包含則返回1,不包含則返回0,出錯返回-1。 ### 3.3.?sigprocmask 調用函數`sigprocmask`可以讀取或更改進程的信號屏蔽字。 ``` #include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset); ``` 返回值:若成功則為0,若出錯則為-1 如果`oset`是非空指針,則讀取進程的當前信號屏蔽字通過`oset`參數傳出。如果`set`是非空指針,則更改進程的信號屏蔽字,參數`how`指示如何更改。如果`oset`和`set`都是非空指針,則先將原來的信號屏蔽字備份到`oset`里,然后根據`set`和`how`參數更改信號屏蔽字。假設當前的信號屏蔽字為`mask`,下表說明了`how`參數的可選值。 **表?33.1.?how參數的含義** | | | | --- | --- | | `SIG_BLOCK` | `set`包含了我們希望添加到當前信號屏蔽字的信號,相當于mask=mask&#124;set | | `SIG_UNBLOCK` | `set`包含了我們希望從當前信號屏蔽字中解除阻塞的信號,相當于mask=mask&~set | | `SIG_SETMASK` | 設置當前信號屏蔽字為`set`所指向的值,相當于mask=set | 如果調用`sigprocmask`解除了對當前若干個未決信號的阻塞,則在`sigprocmask`返回前,至少將其中一個信號遞達。 ### 3.4.?sigpending ``` #include <signal.h> int sigpending(sigset_t *set); ``` `sigpending`讀取當前進程的未決信號集,通過`set`參數傳出。調用成功則返回0,出錯則返回-1。 下面用剛學的幾個函數做個實驗。程序如下: ``` #include <signal.h> #include <stdio.h> #include <unistd.h> void printsigset(const sigset_t *set) { int i; for (i = 1; i < 32; i++) if (sigismember(set, i) == 1) putchar('1'); else putchar('0'); puts(""); } int main(void) { sigset_t s, p; sigemptyset(&s); sigaddset(&s, SIGINT); sigprocmask(SIG_BLOCK, &s, NULL); while (1) { sigpending(&p); printsigset(&p); sleep(1); } return 0; } ``` 程序運行時,每秒鐘把各信號的未決狀態打印一遍,由于我們阻塞了`SIGINT`信號,按Ctrl-C將會使`SIGINT`信號處于未決狀態,按Ctrl-\仍然可以終止程序,因為`SIGQUIT`信號沒有阻塞。 ``` $ ./a.out 0000000000000000000000000000000 0000000000000000000000000000000(這時按Ctrl-C) 0100000000000000000000000000000 0100000000000000000000000000000(這時按Ctrl-\) Quit (core dumped) ``` ## 4.?捕捉信號 ### 4.1.?內核如何實現信號的捕捉 如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜,舉例如下: 1. 用戶程序注冊了`SIGQUIT`信號的處理函數`sighandler`。 2. 當前正在執行`main`函數,這時發生中斷或異常切換到內核態。 3. 在中斷處理完畢后要返回用戶態的`main`函數之前檢查到有信號`SIGQUIT`遞達。 4. 內核決定返回用戶態后不是恢復`main`函數的上下文繼續執行,而是執行`sighandler`函數,`sighandler`和`main`函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是兩個獨立的控制流程。 5. `sighandler`函數返回后自動執行特殊的系統調用`sigreturn`再次進入內核態。 6. 如果沒有新的信號要遞達,這次再返回用戶態就是恢復`main`函數的上下文繼續執行了。 **圖?33.2.?信號的捕捉** ![信號的捕捉](https://box.kancloud.cn/2016-04-02_56ff80d9c9f23.png) 上圖出自[[ULK]](bi01.html#bibli.ulk "Understanding the Linux Kernel")。 ### 4.2.?sigaction ``` #include <signal.h> int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); ``` `sigaction`函數可以讀取和修改與指定信號相關聯的處理動作。調用成功則返回0,出錯則返回-1。`signo`是指定信號的編號。若`act`指針非空,則根據`act`修改該信號的處理動作。若`oact`指針非空,則通過`oact`傳出該信號原來的處理動作。`act`和`oact`指向`sigaction`結構體: ``` struct sigaction { void (*sa_handler)(int); /* addr of signal handler, */ /* or SIG_IGN, or SIG_DFL */ sigset_t sa_mask; /* additional signals to block */ int sa_flags; /* signal options, Figure 10.16 */ /* alternate handler */ void (*sa_sigaction)(int, siginfo_t *, void *); }; ``` 將`sa_handler`賦值為常數`SIG_IGN`傳給`sigaction`表示忽略信號,賦值為常數`SIG_DFL`表示執行系統默認動作,賦值為一個函數指針表示用自定義函數捕捉信號,或者說向內核注冊了一個信號處理函數,該函數返回值為`void`,可以帶一個`int`參數,通過參數可以得知當前信號的編號,這樣就可以用同一個函數處理多種信號。顯然,這也是一個回調函數,不是被`main`函數調用,而是被系統所調用。 當某個信號的處理函數被調用時,內核自動將當前信號加入進程的信號屏蔽字,當信號處理函數返回時自動恢復原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產生,那么它會被阻塞到當前處理結束為止。如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用`sa_mask`字段說明這些需要額外屏蔽的信號,當信號處理函數返回時自動恢復原來的信號屏蔽字。 `sa_flags`字段包含一些選項,本章的代碼都把`sa_flags`設為0,`sa_sigaction`是實時信號的處理函數,本章不詳細解釋這兩個字段,有興趣的讀者參考[[APUE2e]](bi01.html#bibli.apue "Advanced Programming in the UNIX Environment")。 ### 4.3.?pause ``` #include <unistd.h> int pause(void); ``` `pause`函數使調用進程掛起直到有信號遞達。如果信號的處理動作是終止進程,則進程終止,`pause`函數沒有機會返回;如果信號的處理動作是忽略,則進程繼續處于掛起狀態,`pause`不返回;如果信號的處理動作是捕捉,則調用了信號處理函數之后`pause`返回-1,`errno`設置為`EINTR`,所以`pause`只有出錯的返回值(想想以前還學過什么函數只有出錯返回值?)。錯誤碼`EINTR`表示“被信號中斷”。 下面我們用`alarm`和`pause`實現`sleep(3)`函數,稱為`mysleep`。 **例?33.2.?mysleep** ``` #include <unistd.h> #include <signal.h> #include <stdio.h> void sig_alrm(int signo) { /* nothing to do */ } unsigned int mysleep(unsigned int nsecs) { struct sigaction newact, oldact; unsigned int unslept; newact.sa_handler = sig_alrm; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGALRM, &newact, &oldact); alarm(nsecs); pause(); unslept = alarm(0); sigaction(SIGALRM, &oldact, NULL); return unslept; } int main(void) { while(1){ mysleep(2); printf("Two seconds passed\n"); } return 0; } ``` 1. `main`函數調用`mysleep`函數,后者調用`sigaction`注冊了`SIGALRM`信號的處理函數`sig_alrm`。 2. 調用`alarm(nsecs)`設定鬧鐘。 3. 調用`pause`等待,內核切換到別的進程運行。 4. `nsecs`秒之后,鬧鐘超時,內核發`SIGALRM`給這個進程。 5. 從內核態返回這個進程的用戶態之前處理未決信號,發現有`SIGALRM`信號,其處理函數是`sig_alrm`。 6. 切換到用戶態執行`sig_alrm`函數,進入`sig_alrm`函數時`SIGALRM`信號被自動屏蔽,從`sig_alrm`函數返回時`SIGALRM`信號自動解除屏蔽。然后自動執行系統調用`sigreturn`再次進入內核,再返回用戶態繼續執行進程的主控制流程(`main`函數調用的`mysleep`函數)。 7. `pause`函數返回-1,然后調用`alarm(0)`取消鬧鐘,調用`sigaction`恢復`SIGALRM`信號以前的處理動作。 以下問題留給讀者思考: 1、信號處理函數`sig_alrm`什么都沒干,為什么還要注冊它作為`SIGALRM`的處理函數?不注冊信號處理函數可以嗎? 2、為什么在`mysleep`函數返回前要恢復`SIGALRM`信號原來的`sigaction`? 3、`mysleep`函數的返回值表示什么含義?什么情況下返回非0值?。 ### 4.4.?可重入函數 當捕捉到信號時,不論進程的主控制流程當前執行到哪兒,都會先跳到信號處理函數中執行,從信號處理函數返回后再繼續執行主控制流程。信號處理函數是一個單獨的控制流程,因為它和主控制流程是異步的,二者不存在調用和被調用的關系,并且使用不同的堆棧空間。引入了信號處理函數使得一個進程具有多個控制流程,如果這些控制流程訪問相同的全局資源(全局變量、硬件資源等),就有可能出現沖突,如下面的例子所示。 **圖?33.3.?不可重入函數** ![不可重入函數](https://box.kancloud.cn/2016-04-02_56ff80d9df8d7.png) `main`函數調用`insert`函數向一個鏈表`head`中插入節點`node1`,插入操作分為兩步,剛做完第一步的時候,因為硬件中斷使進程切換到內核,再次回用戶態之前檢查到有信號待處理,于是切換到`sighandler`函數,`sighandler`也調用`insert`函數向同一個鏈表`head`中插入節點`node2`,插入操作的兩步都做完之后從`sighandler`返回內核態,再次回到用戶態就從`main`函數調用的`insert`函數中繼續往下執行,先前做第一步之后被打斷,現在繼續做完第二步。結果是,`main`函數和`sighandler`先后向鏈表中插入兩個節點,而最后只有一個節點真正插入鏈表中了。 像上例這樣,`insert`函數被不同的控制流程調用,有可能在第一次調用還沒返回時就再次進入該函數,這稱為重入,`insert`函數訪問一個全局鏈表,有可能因為重入而造成錯亂,像這樣的函數稱為不可重入函數,反之,如果一個函數只訪問自己的局部變量或參數,則稱為可重入(`Reentrant`)函數。想一下,為什么兩個不同的控制流程調用同一個函數,訪問它的同一個局部變量或參數就不會造成錯亂? 如果一個函數符合以下條件之一則是不可重入的: * 調用了`malloc`或`free`,因為`malloc`也是用全局鏈表來管理堆的。 * 調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構。 SUS規定有些系統函數必須以線程安全的方式實現,這里就不列了,請參考[[APUE2e]](bi01.html#bibli.apue "Advanced Programming in the UNIX Environment")。 ### 4.5.?sig_atomic_t類型與volatile限定符 在上面的例子中,`main`和`sighandler`都調用`insert`函數則有可能出現鏈表的錯亂,其根本原因在于,對全局鏈表的插入操作要分兩步完成,不是一個原子操作,假如這兩步操作必定會一起做完,中間不可能被打斷,就不會出現錯亂了。下一節線程會講到如何保證一個代碼段以原子操作完成。 現在想一下,如果對全局數據的訪問只有一行代碼,是不是原子操作呢?比如,`main`和`sighandler`都對一個全局變量賦值,會不會出現錯亂呢?比如下面的程序: ``` long long a; int main(void) { a=5; return 0; } ``` 帶調試信息編譯,然后帶源代碼反匯編: ``` $ gcc main.c -g $ objdump -dS a.out ``` 其中main函數的指令中有: ``` a=5; 8048352: c7 05 50 95 04 08 05 movl $0x5,0x8049550 8048359: 00 00 00 804835c: c7 05 54 95 04 08 00 movl $0x0,0x8049554 8048363: 00 00 00 ``` 雖然C代碼只有一行,但是在32位機上對一個64位的`long long`變量賦值需要兩條指令完成,因此不是原子操作。同樣地,讀取這個變量到寄存器需要兩個32位寄存器才放得下,也需要兩條指令,不是原子操作。請讀者設想一種時序,`main`和`sighandler`都對這個變量`a`賦值,最后變量`a`的值發生錯亂。 如果上述程序在64位機上編譯執行,則有可能用一條指令完成賦值,因而是原子操作。如果`a`是32位的`int`變量,在32位機上賦值是原子操作,在16位機上就不是。如果在程序中需要使用一個變量,要保證對它的讀寫都是原子操作,應該采用什么類型呢?為了解決這些平臺相關的問題,C標準定義了一個類型`sig_atomic_t`,在不同平臺的C語言庫中取不同的類型,例如在32位機上定義`sig_atomic_t`為`int`類型。 在使用`sig_atomic_t`類型的變量時,還需要注意另一個問題。看如下的例子: ``` #include <signal.h> sig_atomic_t a=0; int main(void) { /* register a sighandler */ while(!a); /* wait until a changes in sighandler */ /* do something after signal arrives */ return 0; } ``` 為了簡潔,這里只寫了一個代碼框架來說明問題。在`main`函數中首先要注冊某個信號的處理函數`sighandler`,然后在一個`while`死循環中等待信號發生,如果有信號遞達則執行`sighandler`,在`sighandler`中將`a`改為1,這樣再次回到`main`函數時就可以退出`while`循環,執行后續處理。用上面的方法編譯和反匯編這個程序,在`main`函數的指令中有: ``` /* register a sighandler */ while(!a); /* wait until a changes in sighandler */ 8048352: a1 3c 95 04 08 mov 0x804953c,%eax 8048357: 85 c0 test %eax,%eax 8048359: 74 f7 je 8048352 <main+0xe> ``` 將全局變量`a`從內存讀到`eax`寄存器,對`eax`和`eax`做AND運算,若結果為0則跳回循環開頭,再次從內存讀變量`a`的值,可見這三條指令等價于C代碼的`while(!a);`循環。如果在編譯時加了優化選項,例如: ``` $ gcc main.c -O1 -g $ objdump -dS a.out ``` 則`main`函數的指令中有: ``` 8048352: 83 3d 3c 95 04 08 00 cmpl $0x0,0x804953c /* register a sighandler */ while(!a); /* wait until a changes in sighandler */ 8048359: 74 fe je 8048359 <main+0x15> ``` 第一條指令將全局變量`a`的內存單元直接和0比較,如果相等,則第二條指令成了一個死循環,注意,這是一個真正的死循環:即使`sighandler`將`a`改為1,只要沒有影響Zero標志位,回到`main`函數后仍然死在第二條指令上,因為不會再次從內存讀取變量`a`的值。 是編譯器優化得有錯誤嗎?不是的。設想一下,如果程序只有單一的執行流程,只要當前執行流程沒有改變`a`的值,`a`的值就沒有理由會變,不需要反復從內存讀取,因此上面的兩條指令和`while(!a);`循環是等價的,并且優化之后省去了每次循環讀內存的操作,效率非常高。所以不能說編譯器做錯了,只能說_編譯器無法識別程序中存在多個執行流程_。之所以程序中存在多個執行流程,是因為調用了特定平臺上的特定庫函數,比如`sigaction`、`pthread_create`,這些不是C語言本身的規范,不歸編譯器管,程序員應該自己處理這些問題。C語言提供了`volatile`限定符,如果將上述變量定義為`volatile sig_atomic_t a=0;`那么即使指定了優化選項,編譯器也不會優化掉對變量a內存單元的讀寫。 對于程序中存在多個執行流程訪問同一全局變量的情況,`volatile`限定符是必要的,此外,雖然程序只有單一的執行流程,但是變量屬于以下情況之一的,也需要`volatile`限定: * 變量的內存單元中的數據不需要寫操作就可以自己發生變化,每次讀上來的值都可能不一樣 * 即使多次向變量的內存單元中寫數據,只寫不讀,也并不是在做無用功,而是有特殊意義的 什么樣的內存單元會具有這樣的特性呢?肯定不是普通的內存,而是映射到內存地址空間的硬件寄存器,例如串口的接收寄存器屬于上述第一種情況,而發送寄存器屬于上述第二種情況。 _`sig_atomic_t`類型的變量應該總是加上`volatile`限定符_,因為要使用`sig_atomic_t`類型的理由也正是要加`volatile`限定符的理由。 ### 4.6.?競態條件與sigsuspend函數 現在重新審視[例?33.2 “mysleep”](ch33s04.html#signal.mysleep),設想這樣的時序: 1. 注冊`SIGALRM`信號的處理函數。 2. 調用`alarm(nsecs)`設定鬧鐘。 3. 內核調度優先級更高的進程取代當前進程執行,并且優先級更高的進程有很多個,每個都要執行很長時間 4. `nsecs`秒鐘之后鬧鐘超時了,內核發送`SIGALRM`信號給這個進程,處于未決狀態。 5. 優先級更高的進程執行完了,內核要調度回這個進程執行。`SIGALRM`信號遞達,執行處理函數`sig_alrm`之后再次進入內核。 6. 返回這個進程的主控制流程,`alarm(nsecs)`返回,調用`pause()`掛起等待。 7. 可是`SIGALRM`信號已經處理完了,還等待什么呢? 出現這個問題的根本原因是系統運行的時序(Timing)并不像我們寫程序時所設想的那樣。雖然`alarm(nsecs)`緊接著的下一行就是`pause()`,但是無法保證`pause()`一定會在調用`alarm(nsecs)`之后的`nsecs`秒之內被調用。由于異步事件在任何時候都有可能發生(這里的異步事件指出現更高優先級的進程),如果我們寫程序時考慮不周密,就可能由于時序問題而導致錯誤,這叫做競態條件(Race Condition)。 如何解決上述問題呢?讀者可能會想到,在調用`pause`之前屏蔽`SIGALRM`信號使它不能提前遞達就可以了。看看以下方法可行嗎? 1. 屏蔽`SIGALRM`信號; 2. `alarm(nsecs);` 3. 解除對`SIGALRM`信號的屏蔽; 4. `pause();` 從解除信號屏蔽到調用`pause`之間存在間隙,`SIGALRM`仍有可能在這個間隙遞達。要消除這個間隙,我們把解除屏蔽移到`pause`后面可以嗎? 1. 屏蔽`SIGALRM`信號; 2. `alarm(nsecs);` 3. `pause();` 4. 解除對`SIGALRM`信號的屏蔽; 這樣更不行了,還沒有解除屏蔽就調用`pause`,`pause`根本不可能等到`SIGALRM`信號。要是“解除信號屏蔽”和“掛起等待信號”這兩步能合并成一個原子操作就好了,這正是`sigsuspend`函數的功能。`sigsuspend`包含了`pause`的掛起等待功能,同時解決了競態條件的問題,在對時序要求嚴格的場合下都應該調用`sigsuspend`而不是`pause`。 ``` #include <signal.h> int sigsuspend(const sigset_t *sigmask); ``` 和`pause`一樣,`sigsuspend`沒有成功返回值,只有執行了一個信號處理函數之后`sigsuspend`才返回,返回值為-1,`errno`設置為`EINTR`。 調用`sigsuspend`時,進程的信號屏蔽字由`sigmask`參數指定,可以通過指定`sigmask`來臨時解除對某個信號的屏蔽,然后掛起等待,當`sigsuspend`返回時,進程的信號屏蔽字恢復為原來的值,如果原來對該信號是屏蔽的,從`sigsuspend`返回后仍然是屏蔽的。 以下用`sigsuspend`重新實現`mysleep`函數: ``` unsigned int mysleep(unsigned int nsecs) { struct sigaction newact, oldact; sigset_t newmask, oldmask, suspmask; unsigned int unslept; /* set our handler, save previous information */ newact.sa_handler = sig_alrm; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGALRM, &newact, &oldact); /* block SIGALRM and save current signal mask */ sigemptyset(&newmask); sigaddset(&newmask, SIGALRM); sigprocmask(SIG_BLOCK, &newmask, &oldmask); alarm(nsecs); suspmask = oldmask; sigdelset(&suspmask, SIGALRM); /* make sure SIGALRM isn't blocked */ sigsuspend(&suspmask); /* wait for any signal to be caught */ /* some signal has been caught, SIGALRM is now blocked */ unslept = alarm(0); sigaction(SIGALRM, &oldact, NULL); /* reset previous action */ /* reset signal mask, which unblocks SIGALRM */ sigprocmask(SIG_SETMASK, &oldmask, NULL); return(unslept); } ``` 如果在調用`mysleep`函數時`SIGALRM`信號沒有屏蔽: 1. 調用`sigprocmask(SIG_BLOCK, &newmask, &oldmask);`時屏蔽`SIGALRM`。 2. 調用`sigsuspend(&suspmask);`時解除對`SIGALRM`的屏蔽,然后掛起等待待。 3. `SIGALRM`遞達后`suspend`返回,自動恢復原來的屏蔽字,也就是再次屏蔽`SIGALRM`。 4. 調用`sigprocmask(SIG_SETMASK, &oldmask, NULL);`時再次解除對`SIGALRM`的屏蔽。 ### 4.7.?關于SIGCHLD信號 進程一章講過用`wait`和`waitpid`函數清理僵尸進程,父進程可以阻塞等待子進程結束,也可以非阻塞地查詢是否有子進程結束等待清理(也就是輪詢的方式)。采用第一種方式,父進程阻塞了就不能處理自己的工作了;采用第二種方式,父進程在處理自己的工作的同時還要記得時不時地輪詢一下,程序實現復雜。 其實,子進程在終止時會給父進程發`SIGCHLD`信號,該信號的默認處理動作是忽略,父進程可以自定義`SIGCHLD`信號的處理函數,這樣父進程只需專心處理自己的工作,不必關心子進程了,子進程終止時會通知父進程,父進程在信號處理函數中調用`wait`清理子進程即可。 請編寫一個程序完成以下功能:父進程`fork`出子進程,子進程調用`exit(2)`終止,父進程自定義`SIGCHLD`信號的處理函數,在其中調用`wait`獲得子進程的退出狀態并打印。 事實上,由于UNIX的歷史原因,要想不產生僵尸進程還有另外一種辦法:父進程調用`sigaction`將`SIGCHLD`的處理動作置為`SIG_IGN`,這樣`fork`出來的子進程在終止時會自動清理掉,不會產生僵尸進程,也不會通知父進程。系統默認的忽略動作和用戶用`sigaction`函數自定義的忽略通常是沒有區別的,但這是一個特例。此方法對于Linux可用,但不保證在其它UNIX系統上都可用。請編寫程序驗證這樣做不會產生僵尸進程。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看