《UNIX环境高级编程》笔记 第十章-信号
1. 信號概念
信號是軟件中斷,提供了一種處理異步事件的方法。
Unix早期版本提供的是不可靠信號,信號可能丟失。之后增加了可靠信號機制。
每個信號都有一個名字,以3個字符SIG開頭。如SIGABRT是終止信號,進程調用abort函數產生這種信號。
-
SIGABRT信號:
void abort(void);abort()首先解除了對SIGABRT(6)信號的阻止和忽略,然后為調用進程發送該信號(就像調用了 raise(SIGABRT)一樣)。 導致進程的非正常終止,除非SIGABRT 信號被捕獲,并且信號處理函數沒有返回(調用exit、_exit、 _Exit、longjmp或siglongjmp使信號處理函數沒有返回)。
如果abort()函數導致進程終止,則關閉和刷新所有打開的文件流。
Linux支持63種信號,不存在編號為0的信號(即為空信號),kill函數對信號編號0有特殊應用。1 ~ 31的信號為傳統UNIX支持的信號,是不可靠信號(非實時的),編號為32 ~ 63的信號是后來擴充的,稱做可靠信號(實時信號)。不可靠信號和可靠信號的區別在于前者不支持排隊,可能會造成信號丟失,而后者不會。
1.1 產生信號條件
- 當用戶按某些終端鍵時,引發終端產生的信號。如在終端上按delete或Ctrl+C鍵,通常產生中斷信號SIGINT。
- 硬件產生異常信號:除0、無效的內存引用等。因為這些條件通常由硬件檢測到,并且通知內核。然后內核為該進程產生適當信號。
- 進程調用kill(2)函數將任意信號發送給另一個進程或進程組:接收信號進程和發送信號進程的所有者必須相同,或發送方必須是超級用戶root
- 用kill(1)命令將信號(默認是SIGTERM)發送給進程。
- 當檢測到某種軟件條件發生時將產生信號通知相關進程。如SIGURG、SIGPIPE、SIGALRM(進程設置的定時器已經超時)等
1.2 對信號的處理方式
-
忽略此信號
除了兩種信號SIGKILL和SIGSTOP絕對不能忽略以外,大多數信號都可使用這種方式處理。
SIGKILL和SIGSTOP不能被忽略的原因是,它們向內核和超級用戶提供了使進程終止或停止的可靠方法(可以理解為終止、停止進程的終極方法)。
并且如果忽略某些由硬件異常(如除0、非法內存引用)產生的信號,會發生未定義行為。
-
捕捉該信號
通知內核在某種信號發生時,調用一個用戶函數。同樣SIGKILL和SIGSTOP不能被捕捉
-
執行系統默認動作
對大多數信號的系統默認動作是終止該進程
Linux對各種信號的默認動作:
注意“終止+core”表示在進程當前工作目錄的core文件中復制了該進程內存映像(默認文件名為core,這種行為即coredump),大多數Unix系統調試程序(如gdb)都使用core文件檢查進程終止時的狀態。
注意"硬件故障"對應于具體定義的硬件故障,需要通過對應操作系統手冊查看這些信號對應于哪些錯誤
Core Dump
當程序運行的過程中異常終止或崩潰,操作系統會將程序當時的內存狀態記錄下來,保存在一個文件中,這種行為就叫做Core Dump(中文有的翻譯成“核心轉儲”)。我們可以認為 core dump 是“內存快照”,但實際上,除了內存信息之外,還有些關鍵的程序運行狀態也會同時 dump 下來,例如寄存器信息(包括程序指針、棧指針等)、內存管理信息、其他處理器和操作系統狀態和信息。core dump 對于編程人員診斷和調試程序是非常有幫助的,因為對于有些程序錯誤是很難重現的,例如指針異常,而 core dump 文件可以再現程序出錯時的情景。
如果沒有進行core dump 的相關設置,默認是不開啟的。可以通過ulimit -c查看是否開啟。如果輸出為0,則沒有開啟,需要執行ulimit -c unlimited開啟core dump功能。
不產生core文件的條件:
- ulimit -c查看core文件有沒有限制大小。如果是0則說明禁止了core文件產生
- 程序設置了用戶id(即調用setuid),但當前用戶并非該程序文件的所有者
- 程序設置了組id(即調用setgid),但當前用戶并非該程序文件的組所有者
- 用戶沒有當前目錄或指定core文件產生目錄的寫權限
- 文件已存在,用戶對該文件沒有寫權限
- core文件太大,磁盤空間不足
2. 各種信號詳細信息
-
SIGABRT:
調用abort函數產生此信號,進程異常終止。
-
SIGALRM:
調用alarm、setitimer函數設置的定時器、間隔時間超時產生此信號。
-
SIGBUS:
表示發生了一個具體定義的硬件故障。當出現某種類型內存故障時,常產生此種信號
-
SIGCHLD:
子進程終止或停止時,SIGCHLD信號發送給父進程。系統默認會忽略此信號。如果父進程希望得知子進程的這種狀態改變,那么通常在信號捕捉函數中調用wait等函數獲取子進程pid及終止狀態。
-
SIGCONT:
用于作業控制。如果收到此信號的進程處于停止狀態,則系統默認動作是該進程繼續運行;否則忽略此信號
-
SIGEMT:
表示發生了一個具體定義的硬件故障。
-
SIGFPE:
算術運算異常,如除0、浮點溢出等
-
SIGHUP:掛斷信號。
-
終端接口檢測到一個連接斷開,則將此信號送給與該終端相關的控制進程(會話首進程)。
-
session首進程退出時,該信號被發送到該session中的前臺進程組和后臺進程組中的每一個進程
-
若進程的退出,導致一個進程組變成了孤兒進程組,新的孤兒進程組中處于停止(stopped)狀態的每一個進程都會收到掛斷(SIGHUP)信號,接著又收到繼續(SIGCONT)信號。
系統對SIGHUP信號的默認處理是終止收到該信號的進程。所以若程序中沒有捕捉該信號,當收到該信號時,進程就會退出。
-
-
SIGILL:
進程執行一條非法硬件指令
-
SIGINT:中斷信號
用戶按中斷鍵(delete或Ctrl+C),產生此信號發送至前臺進程組中的每一個進程。當一個進程失控或者在終端產生大量不需要輸出時,常通過此命令終止它。
-
SIGIO:
指示發生一個異步I/O事件。Linux中定義SIGIO與SIGPOLL有相同的值,默認行為是終止該進程。
#define SIGIO SIGPOLL -
SIGIOT:
表示發生了一個具體定義的硬件故障。Linux中定義SIGIOT與SIGABRT有相同值
#define SIGIOT SIGABRT -
SIGKILL:
這是兩個不能被阻塞、捕捉或忽略的信號中的一個,向內核和超級用戶提供了使進程終止的可靠方法
-
SIGPIPE:
管道讀進程已經終止后,寫進程寫入管道產生此信號。
當類型為SOCK_STREAM的socket已不再連接時,進程寫入該套接字也產生此信號。
-
SIGPOLL:
當在一個可輪詢設備上發生一個特定事件時產生此信號(如當系統發現有東西需要你讀的時候,就發個SIGPOLL信號來通知)。在未來可能將此信號移除。
-
SIGPROF:
setitimer函數設置的梗概統計間隔定時器超時產生此信號。在未來可能將此信號移除
-
SIGPWR:
電源故障信號。主要用于具有不間斷電源的系統。如果電源失效,系統依靠蓄電池繼續運行。但是如果蓄電池也將不能工作,此時發送SIGPWR信號。通常是接到蓄電池電壓過低信息的進程將SIGPWR發送給init進程,然后由init處理停機操作。
Linux對SIGPWR的默認動作是終止相關進程。
-
SIGQUIT:退出信號
用戶在終端上按退出鍵(Ctrl+\)產生此信號,并發送給前臺進程組的所有進程。磁信號不僅終止前臺進程組,還產生一個core文件。
-
SIGSEGV:
進程進行了一次無效的內存引用(比如訪問了一個未經初始化的指針),或發生段錯誤。
-
SIGSTOP:
作業控制信號,它停止一個進程。同SIGKILL,該信號不能被阻塞、忽略和捕獲
-
SIGSYS:
指示一個無效的系統調用。
-
SIGTERM:
是kill命令發出的默認信號(系統默認終止信號)。相比于SIGKILL,SIGTERM可以被捕獲或忽略,因此允許讓程序有機會在退出之前做好清理工作,從而優雅地終止。
-
SIGTRAP:
表示發生了一個具體定義的硬件故障。通常使用此信號將控制轉移至調試程序
-
SIGTSTP:
交互停止信號。當用戶在終端按掛起鍵(Ctrl+Z)時,將該信號發送到前臺進程組中的所有進程。 SIGTSTP與SIGSTOP都是使進程暫停(都使用SIGCONT讓進程重新激活)。唯一的區別是SIGSTOP不可以捕獲和忽略,而SIGTSTP可以。
-
SIGTTIN:
當一個后臺進程組中的進程試圖讀控制終端時收到此信號,并使該進程暫停。注意,如果讀進程屬于孤兒進程組,那么read控制終端操作返回出錯,不產生此信號,errno設置為EIO。
-
SIGTTOU:
如果禁止后臺作業向控制終端寫,此時當一個后臺進程組進程試圖寫控制終端時收到此信號,并使該進程暫停。注意,如果寫進程屬于孤兒進程組,則寫操作返回出錯,不產生此信號,errno設置為EIO。
除此之外,tcsetattr、tcsendbreak、tcdrain、tcflush、tcflow以及tcsetpgrp也能產生SIGTTOU信號。
-
SIGURG:
通知進程發生一個緊急情況(用于socket編程)。在網絡連接上接到帶外的數據時,可選擇地產生此信號。
帶外數據:
帶外數據用于迅速告知對方本端發生的重要的事件。它比普通的數據(帶內數據)擁有更高的優先級,不論發送緩沖區中是否有排隊等待發送的數據,它總是被立即發送。帶外數據的傳輸可以使用一條獨立的傳輸層連接,也可以映射到傳輸普通數據的連接中。 -
SIGUSR1:
用戶定義的信號
-
SIGUSR2:
另一個用戶定義的信號,與SIGUSR類似
-
SIGVTALRM:
當一個由setitimer函數設置的虛擬間隔時間超時產生此信號
-
SIGWINCH:
內核維護與每個終端或偽終端相關聯窗口的大小。進程可以用ioctl得到或設置窗口大小。如果用ioctl設置窗口大小命令更改了窗口大小,則內核將該信號發送至前臺進程組。
-
SIGXCPU:
如果進程超過其軟CPU時間限制,產生此信號。
-
SIGXFSZ:
如果進程超過其軟文件長度限制,產生此信號。
3. signal函數
設置調用進程收到指定信號時的動作(忽略、使用系統默認動作或捕獲)
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);-
signum參數:信號名,如SIGABRT
-
handler參數:
-
SIG_IGN:忽略此信號(不能用于SIGKILL和SIGSTOP)
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */ -
SIG_DFL:系統默認動作
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */ -
指定函數地址(不能用于SIGKILL和SIGSTOP):在信號發生時,調用該函數(該函數有一個int形參,即為該信號編號)。稱這個操作是捕捉該信號,稱此函數為信號處理程序或信號捕捉函數。
-
-
返回值:返回之前的信號處理程序的地址,當發生錯誤時返回 SIG_ERR
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
signal函數的一些特點:
-
注意,exec一個程序后,通常所有信號的處理都是忽略或者使用系統默認操作。如果調用exec前對某個信號忽略,則exec后仍為忽略;但是如果調用exec前對某個信號捕獲,則exec后對該信號更改為使用默認操作(因為信號捕捉函數的地址在exec的新程序中毫無意義)。
-
當fork時,子進程繼承父進程的信號處理方式,因為子進程復刻父進程內存映像,因此信號捕捉函數地址在子進程中有效
-
signal函數的一個缺陷:
不改變信號的處理方式就不能確定信號之前的處理方式(根據signal返回值知道之前對于指定信號的處理方式)。因此可以使用sigaction函數確定一個信號的處理方式,而無需改變它
4. 不可靠信號
https://blog.csdn.net/u013074465/article/details/45978755
在早期UNIX中,信號是不可靠的。意為信號可能會丟失,一個信號發生了但是進程可能一直不知道。同時不具備阻塞信號的能力(不要忽略該信號,在其發生時記住它,然后在進程做好準備時再通知它)。
不可靠信號的主要問題:
- 進程每次處理信號后,就將對信號的響應設置為默認動作。在某些情況下,將導致對信號的錯誤處理;因此,用戶如果不希望這樣的操作,那么就要在信號處理函數結尾再一次調用signal(),重新安裝該信號。
- 信號可能丟失,如果在進程對某個信號進行處理時,這個信號發生多次,對后到來的這類信號不排隊,那么僅傳送該信號一次,即發生了信號丟失。
因此,早期unix下的不可靠信號主要指的是進程可能對信號做出錯誤的反應以及信號可能丟失。
Linux支持不可靠信號,但是對不可靠信號機制做了改進:在調用完信號處理函數后,不必重新調用該信號的安裝函數(信號安裝函數是在可靠機制上的實現)。因此,Linux下的不可靠信號問題主要指的是信號可能丟失。
5. 中斷的系統調用
系統調用分類:
將系統調用分為兩類 -> 低速系統調用/其他系統調用。
低速系統調用是可能會使進程永遠阻塞的一類系統調用:
- 如果某些類型文件(如讀管道、終端設備、網絡設備)數據不存在,則讀操作可能會使調用者永遠阻塞
- 如果這些數據不能被相同類型文件立即接受,則寫操作可能會使調用者永遠阻塞。
- 在某種條件發生之前打開某些類型文件,可能發生阻塞
- pause函數(它使進程掛起直到收到一個信號)和wait函數
- 某些ioctl操作
- 某些進程間通信函數
在早期UNIX中,如果進程正在執行低速系統調用而阻塞期間捕捉到一個信號,則該系統調用就被中斷不再執行。該系統調用返回出錯,其errno設置為EINTR(系統調用被中斷)。可以理解為一個信號發生了,進程捕捉到它,這意味著已經發生了某種事情,所以是個好機會應當喚醒阻塞的系統調用。
對于處理已經read/write部分數據量的相應系統調用,此時被信號中斷,有兩種方式(例如read系統調用已經接收并傳送數據至應用程序緩沖區,但尚未接收到應用程序請求的全部數據):
- 認為系統調用失敗,errno設置為EINTR
- (現在使用這一種):允許該系統調用成功返回,返回值是已讀寫的數據量。
當被信號中斷時,下列系統調用支持自動重啟動:
ioctl、read、readv、write、writev、wait和waitpid
有些應用程序不希望這些函數被中斷后自動重啟動,因此進程可以設置對每個信號禁用此功能。(sigaction函數中只有中斷信號的SA_RESTART標志有效,才自動重啟動系統調用)在Linux中,當信號處理程序是用signal函數注冊時,被中斷的系統調用會自動重啟動。
注意,除了低速系統調用,信號也可以中斷類似sleep這樣的函數:調用sleep函數的線程休眠seconds秒。如果中間有一個未被忽略的信號到達則終止休眠。
6. 可重入函數
-
如果進程正在執行malloc,在其堆中分配另外的存儲空間,而此時由于捕捉到信號而插入執行該信號處理程序,其中又調用malloc會發生什么?
可能會對進程造成破壞。因為malloc通常為它所分配的存儲區維護一個鏈表,而插入執行信號處理程序時進程可能正在更改此鏈表。
-
**如果進程正在執行getpwnam這種將其結果存放在靜態存儲單元中的函數,其間插入執行信號處理程序,它又調用這樣的函數(如信號處理函數內部又調用getpwnam)**會發生什么?
返回給正常調用者的信息可能會被返回給信號處理程序的信息覆蓋。
可重入函數:
在信號處理程序中保證調用安全的函數,這些函數是可重入的并被稱為是異步信號安全的(即為在函數A執行期間中斷執行信號處理程序,在信號處理程序中可能再次調用函數A,但是不會造成問題)。這種函數除了可重入外(信號處理程序中再次調用被信號中斷的函數),在執行信號處理操作期間會阻塞任何會引起不一致的信號發送。
下列函數都是異步信號安全即可重入的:
沒有列入上表的函數大多是不可重入的,不可重入函數通常有以下特點:
- 它們使用靜態數據結構,重入可能導致結果被覆蓋
- 它們調用malloc或free
- 它們是標準I/O函數。標準I/O庫的很多實現都不以可重入方式而是使用全局數據結構
并且要注意,對于errno,因為信號處理程序可能會修改errno原先值,因此應當在調用前保存errno,在調用后恢復errno。
如果應用程序要做更新全局數據結構這樣的事情,而同時要捕捉某些信號,而這些信號的處理程序又會引起執行siglongjmp,則在更新這種數據結構時要阻塞此類信號。(因為可能導致這些全局數據結構是部分更新的)
綜上所述,在信號處理函數中調用一個非可重入函數,其結果不可預知。因此在信號處理函數中不能調用非可重入函數。
7. 多線程中的信號處理
使用了多線程后,便有些疑問:
對于以上問題,結論是:
- 如果是異常產生的信號(比如程序錯誤,像SIGPIPE、SIGEGV這些),則只有產生異常的線程收到并處理。
- 如果是用pthread_kill產生的內部信號,則只有pthread_kill參數中指定的目標線程收到并處理。
- 如果是外部使用kill命令產生的信號,通常是SIGINT、SIGHUP等job control信號,則會遍歷所有線程,直到找到一個不阻塞該信號的線程,然后調用它來處理。(一般從主線程找起),注意只有一個線程能收到。
- 其次,每個線程都有自己獨立的signal mask,但所有線程共享進程的signal action。這意味著,你可以在線程中調用pthread_sigmask(不是sigmask)來決定本線程阻塞哪些信號。但你不能調用sigaction來指定單個線程的信號處理方式。如果在某個線程中調用了sigaction處理某個信號,那么這個進程中的未阻塞這個信號的線程在收到這個信號都會按同一種方式處理這個信號。另外,注意子線程的mask是會從主線程繼承而來的。
8. 可靠信號
8.1 三種信號狀態
**信號產生:**generation
當造成信號的事件發生時,為進程產生一個信號(或向一個進程發送一個信號)。
這些造成信號的事件可以是硬件異常(如除0)、軟件條件(如alarm定時器超時)、終端產生的信號或調用kill函數。
當一個信號產生時,內核通常在進程中以某種形式設置一個標志。
**信號遞送:**delivery
指信號發送給進程之后,對該信號進行了處理(無論是忽略、捕獲還是使用系統默認操作)
**信號未決:**pending
在信號產生和遞送的時間間隔內稱為未決
8.2 阻塞信號遞送:
進程可以選擇對指定信號“阻塞信號遞送”。如果為進程產生了一個阻塞的信號,并且對該信號的動作是系統默認或捕捉,則此信號保持為未決的。直到該進程解除對此信號的阻塞,或者將對此信號的動作更改為忽略,內核才會遞送一個原來被阻塞的信號給進程(而不是解除阻塞后再產生的信號)。
如果在進程解除對某個信號的阻塞之前這種信號發生了多次,允許系統能夠遞送該信號一次或多次。如果遞送該信號多次,則稱這些信號進行了排隊。
8.3 信號屏蔽字:
每個進程都有信號屏蔽字(signal mask),它規定當前要阻塞遞送到該進程的信號集。對于每一種可能的信號,該屏蔽字中都有一位與之對應。如果該位已設置,則它對應的信號是被阻塞的。
注意,在信號處理函數被調用時,操作系統建立的新信號屏蔽字包含正被遞送的信號(即觸發本次捕獲的信號),信號處理函數返回時再恢復信號屏蔽字。因此保證在處理一個給定信號時,如果該信號再次發生,那么它將被阻塞到前一個信號的處理結束為止。
9. kill和raise函數
kill函數將信號發送給進程或進程組,raise函數向進程自身發送信號
int kill(pid_t pid, int sig); int raise(int sig);kill的pid參數:
- pid > 0 :將信號發送給指定進程
- pid == 0 : 將信號發送給與發送進程屬于同一進程組的所有進程,并且發送進程具有權限向這些進程發送信號
- pid < 0 : 將信號發送給進程組ID等于pid絕對值的所有進程,并且發送進程具有權限向這些進程發送信號
- pid == -1 : 將信號發送給有權限向它們發送的所有進程。除了進程1(init)
如果sig==0,則說明是空信號,kill仍然執行正常的錯誤檢查但是不發送信號。常被用來確定一個特定進程是否存在。如果向一個不存在的進程發送空信號,kill函數返回-1。
發送信號的權限問題:
- 超級用戶可以把信號發送給任一進程
- 非超級用戶,則要求發送的實際用戶ID或有效用戶ID等于接受者的實際用戶ID或有效用戶ID。
- 但是有一個特例:對于SIGCONT信號,則進程可以將它發送給屬于同一會話的任一進程
如果調用kill為調用進程產生信號,并且如果該信號是不被阻塞的,那么在kill函數返回之前,該信號或者其他某個未決、未阻塞信號就被遞送給了該進程。
10. alarm和pause函數
alarm函數設置一個定時器(秒數),將來某個時刻定時器超時產生SIGALRM信號。如果忽略或不捕捉該信號,默認動作是終止該進程
unsigned int alarm(unsigned int seconds);-
注意,每個進程只能有一個鬧鐘時間,因此調用alarm會覆蓋之前的alarm。即如果在調用alarm時上一次為該進程注冊的alarm還沒有超時,則該鬧鐘時間的余留值用作本次調用的返回值,并且以前注冊的鬧鐘時間被新值替代。
-
如果要捕獲SIGALRM,必須在alarm調用前安裝信號捕獲程序。
pause函數是一個慢速系統調用,使調用進程掛起直到捕捉到一個信號
int pause(void);只有執行了信號處理程序并從其返回時,pause才返回。此時pause返回-1,errno設置為EINTR
- 注意,在信號處理函數中使用longjmp函數一定要小心,因為如果該信號中斷了其他信號處理函數,那么longjmp將會提早終止這些信號處理函數。
11. 信號集
使用sigset_t以包含一個信號集,該數據類型能夠表示多個信號的集合,該數據類型會被sigprocmask等函數使用
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:
初始化set指向的信號集,清除其中所有信號
-
sigfillset:
初始化set指向的信號集,使其置位所有信號
-
注意,所有應用程序在使用信號集前,都要對該信號集數據結構調用sigemptyset或者sigfillset
-
sigaddset:
在set指向的信號集中添加指定信號signum
-
sigdelset:
在set指向的信號集中刪除指定信號signum
-
sigismember:
判斷set信號集中是否有信號signum
12. sigprocmask函數
信號屏蔽字:
阻塞而不能遞送給該進程的信號集。可以通過sigprocmask函數檢測、更改進程的信號屏蔽字
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);-
若oldset非空,那么進程之前的信號屏蔽字通過oldset返回。
-
若set非空,則how指示如何根據set修改當前信號屏蔽字
-
how參數:
- SIG_BLOCK:阻塞信號,即之前的信號集和set做按位或操作,即并集。set包含了希望被阻塞的信號
- SIG_UNBLOCK:解除信號阻塞,即和set的補集求交集。set包含了希望被解除阻塞的信號
- SIG_SETMASK:賦值信號屏蔽字
-
在調用該函數后如果有任何未決的、不再阻塞的信號,則在函數返回之前,至少將其中之一遞送給該進程。
13. sigpending函數(未決的信號)
sigpending函數返回一個信號集,以指示當前處于未決狀態的信號(即已經產生但是由于被阻塞而不能遞送的信號)
int sigpending(sigset_t *set);14. sigaction函數(信號action動作)
檢查、修改指定信號的處理動作。此函數用于取代UNIX早期版本使用的signal函數。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);若act指針非空,則表示要修改其動作。如果oldact非空,則通過該參數返回指定信號的上一個動作。
struct sigaction {void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void); };-
sa_handler:信號捕捉函數地址,可以是 SIG_DFL 表示默認動作,SIG_IGN 表示忽略此信號
-
sa_mask:
一個信號集,在調用該信號捕捉函數之前,這一信號集要加到進程的信號屏蔽字中。僅當從信號捕捉函數返回時再將進程的信號屏蔽字恢復為原來值。
注意,在信號處理函數被調用時,操作系統建立的新信號屏蔽字包含正被遞送的信號(即觸發本次捕獲的信號),信號處理函數返回時再恢復信號屏蔽字。因此保證在處理一個給定信號時,如果該信號再次發生,那么它將被阻塞到前一個信號的處理結束為止。
-
sa_flags:
對指定信號進行處理的各個選項
- SA_INTERRUPT:由此信號中斷的系統調用不自動重啟動
- SA_NOCLDSTOP:如果 signo 是 SIGCHLD,則在子進程停止時不產生此信號;在子進程終止時仍產生此信號
- SA_NOCLDWAIT:如果 signo 是 SIGCHLD,則調用進程的子進程終止時不會變成僵尸進程(不會發出SIGCHLD信號)
- SA_NODEFER:當捕捉到此信號時,在執行其信號捕捉函數時,系統不自動阻塞此信號(除非sa_mask中包含此信號)。
- SA_RESETHAND:在進入信號處理程序時,將該信號處理方式設置為SIG_DFL,并清除SA_SIGINFO標志
- SA_RESTART:由此信號中斷的系統調用自動重啟動
- SA_SIGINFO:此選項對信號處理程序提供了附加信息,即使用sa_sigaction信號處理程序而不是sa_handler
-
sa_sigaction:
一個替代的信號處理程序。當該信號使用了SA_SIGINFO標志時,使用該信號處理程序。由于sa_handler和sa_sigaction的實現可能共用同一存儲區,因此這兩個字段只能有一個。
之前信號處理函數是以下形式:
void handler(int signo);若設置了SA_SIGINFO標志,則信號處理函數是以下形式
void handler(int signo, siginfo_t *info, void *context);-
其中第二個參數siginfo_t結構體包含了信號產生原因的有關信息,這樣在信號處理函數中我們就可以通過第二個參數知道更多具體與該信號相關的信息。其應該至少包含以下字段
siginfo_t {int si_signo; /* Signal number */int si_errno; /* An errno value */int si_code; /* 發出信號具體原因 */pid_t si_pid; /* Sending process ID */uid_t si_uid; /* Real user ID of sending process */int si_status; /* Exit value or signal */sigval_t si_value; /* Signal value */void *si_addr; /* Memory location which caused fault */... }其中si_code即為信號發生具體原因:
-
-
第三個參數context可以被強制類型轉換為ucontext_t類型,該結構體標識信號傳遞時進程上下文,該ucontext_t結構至少包含以下字段
typedef struct ucontext_t{struct ucontext_t *uc_link;stack_t uc_stack;mcontext_t uc_mcontext;sigset_t uc_sigmask;...} ucontext_t;- uc_link:為當前context執行結束之后要執行的下一個context,若uc_link為空,執行完當前context之后退出程序。
- uc_sigmask : 執行當前上下文過程中需要阻塞的信號列表,即信號屏蔽字
- uc_stack : 為當前context運行的棧信息。
- uc_mcontext : 保存具體的程序執行上下文,如PC值,堆棧指針以及寄存器值等信息。它的實現依賴于底層,是平臺硬件相關的。此實現不透明
注意,對于支持實時信號擴展的系統,用SA_SIGINFO標志建立的信號處理程序將造成信號可靠排隊
- 注意,相較于signal(被指定信號中斷的系統調用默認自動重啟動),如果信號處理程序使用sigaction指定,那么被該信號中斷的系統調用默認不重新啟動(可以通過SA_RESTART標志重啟動被中斷的系統調用)
15. sigsetjmp和siglongjmp函數
系統在進入信號處理程序時,會將該信號自動加入到信號屏蔽字中,這阻止了后來產生的這種信號中斷該信號處理程序,然后再信號處理程序返回時恢復信號屏蔽字。
但是如果在信號處理函數中使用longjmp非局部轉移到setjmp處,會導致信號屏蔽字無法恢復。解決方案是調用sigsetjmp和siglongjmp而不是使用setjmp和longjmp。
int sigsetjmp(sigjmp_buf env, int savesigs); void siglongjmp(sigjmp_buf env, int val);這兩個函數和setjmp和longjmp的唯一區別是sigsetjmp增加了參數savesigs。如果該參數非0,則在env參數中保存進程的當前信號屏蔽字,此時在信號處理函數中調用siglongjmp進行非局部跳轉到sigsetjmp,會導致恢復保存的信號屏蔽字。
sig_atomic_t類型:
當把變量聲明為該類型會保證該變量在使用或賦值時, 無論是在32位還是64位的機器上都能保證操作是原子的, 它會根據機器的類型自動適應。
在處理信號(signal)的時候,有時對于一些變量的訪問希望不會被中斷,無論是硬件中斷還是軟件中斷,這就要求訪問或改變這些變量需要在計算機的一條指令內完成。通常情況下,int類型的變量通常是原子訪問的,也可以認為 sig_atomic_t就是int類型的數據,因為對這些變量要求一條指令完成,所以sig_atomic_t不可能是結構體,只會是數字類型。
sig_atomic_t類型總是用volatile修飾,因為該變量總是由兩個不同的控制線程-main函數和異步執行的信號處理程序訪問,因此必須保證每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據,如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,導致將出現不一致的現象。
如這樣使用:
volatile sig_atomic_t flag;16. sigsuspend函數
考慮sigprocmask函數中提出的一點:如果在調用該函數后如果有任何未決的、不再阻塞的信號,則在函數返回之前,至少將其中之一遞送給該進程。
那么如果針對下面代碼則會出現問題:
sigprocmask(SIG_SETMASK,&oldmask,NULL); pause();如果sigprocmask解除了某個信號阻塞,而在此期間的確該信號被阻塞了,由上所述,那么就好像該信號發生在sigprocmask和pause函數之間。(或者在sigprocmask和pause函數之間的確有某個未阻塞的信號被遞送了),那么將會導致pause函數一直阻塞下去,即sigprocmask和pause函數之間的這個時間窗口中的信號丟失了。
針對此問題,需要在一個原子操作中解除信號阻塞并使進程休眠。
因此可以使用sigsuspend函數,該函數在一個原子操作中先恢復信號屏蔽字,然后使進程休眠。
int sigsuspend(const sigset_t *mask);進程的信號屏蔽字設置為mask,在捕捉到一個信號或發生了一個會終止該進程的信號之前,該進程被掛起。如果捕捉到一個信號并且從該信號處理程序返回,則sigsuspend返回,并且恢復信號屏蔽字為sigsuspend之前的值(并且返回-1,errno設為EINTR)。
17. abort函數
使進程異常終止
void abort(void);此函數將SIGABRT信號發送給調用進程raise(SIGABRT),不應忽略此信號。
-
注意,若捕獲此信號并且由相應信號處理函數返回,abort仍不會返回到其調用者。如果捕捉到此信號,那么信號處理程序不能返回的唯一方法是調用exit _exit _Exit longjmp或siglongjmp。即abort導致進程的非正常終止,除非SIGABRT 信號被捕獲,并且信號處理函數沒有返回(使用了longjmp等函數使信號處理函數沒有返回)。
-
并且abort不理會進程對此信號的阻塞或忽略。
-
進程捕獲abort調用信號處理程序的意圖:
在進程終止之前執行所需的清理操作。如果進程并不在信號處理程序中終止自己,則當信號處理函數返回時,abort終止該進程。
-
如果abort要終止進程,則它對所有打開標準I/O流的效果應當與進程終止前對每個流調用fclose相同。
18. system函數
-
system函數阻塞SIGCHLD:
正在執行system函數時,應當阻塞對父進程遞送SIGCHLD信號。否則,當system創建的子進程結束時,system的調用者可能錯誤的認為它自己的一個子進程結束了,然后在SIGCHLD信號處理程序中通過wait函數獲取子進程終止狀態。由于該子進程終止狀態已被獲取過了,因此就阻止了system函數獲取子進程的終止狀態并將其作為返回值。
-
system函數忽略SIGINT和SIGQUIT:
由之前的知識可知,當在終端鍵入Ctrl+C會將SIGINT發送給前臺進程組、在終端鍵入Ctrl+\會將SIGQUIT發送給前臺進程組。但是在system期間這兩個信號應該只發送給正在運行的程序:即system函數中創建的子進程。因為由system執行的命令可能是交互式命令(如ed編輯器),以及system函數的調用者在system執行期間放棄了控制,等待該命令程序執行結束,所以system調用者就不應該接收這兩個終端產生的信號。這也是為何規定system的調用者在等待命令完成時應當忽略這兩個信號。
19. sleep、nanosleep、clock_nanosleep函數
19.1 sleep
sleep函數使調用進程被掛起直到滿足以下條件:
- 已經超過參數指定的秒數
- 進程捕捉到一個信號并從信號處理程序返回。
該函數返回未休眠剩余的秒數
unsigned int sleep(unsigned int seconds);注意,sleep可以由alarm函數實現,但是可能造成sleep和alarm函數互相影響。比如先alarm(10),然后再sleep(3),那么對SIGALRM信號的產生情況造成影響。
因此Linux使用nanosleep實現sleep。因為nanosleep不涉及產生任何信號,即與鬧鐘定時器相互獨立,所以該實現的sleep函數不會與其他時間相關函數如alarm產生交互影響。
19.2 nanosleep
int nanosleep(const struct timespec *req, struct timespec *rem);struct timespec {time_t tv_sec; /* seconds */long tv_nsec; /* nanoseconds [0 .. 999999999] */};該函數與sleep類似,但是提供納秒級別精度。
該函數掛起進程,直到要求的時間超時或者某個信號中斷該函數。req參數指定進程休眠時間,rem函數返回未休眠的剩余時間。
nanosleep函數并不涉及產生任何信號,所以不用擔心與其他函數的交互。
19.3 clock_nanosleep
該函數制定了基于特定時鐘的延遲時間來掛起線程
int clock_nanosleep(clockid_t clock_id, int flags,nconst struct timespec *request, struct timespec *remain);-
clock_id:延遲時間基于的時鐘
- CLOCK_REALTIME:系統實時時間,即從1970年開始的時間
- CLOCK_MONOTONIC:從系統啟動這一刻起開始計時,不受系統時間被用戶改變的影響
- CLOCK_PROCESS_CPUTIME_ID:本進程到當前代碼的CPU時間
- CLOCK_THREAD_CPUTIME_ID:本線程到當前代碼的CPU時間
-
flags:控制時間是絕對還是相對的
0:相對的,即希望休眠的時間長度
TIMER_ABSTIME:絕對的,即希望休眠到時鐘到達某個特定的時間
-
request和remain:和nanosleep一致。使用絕對時間時,remain參數無用
調用clock_nanosleep(CLOCK_REALTIME,0,req,rem)相當于nanosleep(req,rem)
20. sigqueue函數:信號排隊
如果支持信號實時擴展,那么就支持信號排隊。如果要使用排隊信號,則必須遵循以下條件:
- 使用sigaction函數安裝信號處理程序時指定SA_SIGINFO標志。如果沒有此標志,信號會阻塞延遲,但是是否進入隊列取決于具體實現
- 在sigaction結構的sa_sigaction成員中(不是sa_handler)提供信號處理程序。實現可能允許用戶使用sa_handler字段,但是不能獲取sigqueue函數發出的額外信息
- 使用sigqueue發出信號
該函數只把信號發給單個進程。可以通過value參數向信號處理程序傳遞整數和指針值。除此之外,sigqueue和kill類似
注意,信號不能被無限次排隊。到達相應限制后,sigqueue會失敗,errno設置為EAGAIN。
在支持實時信號擴展的Linux之中,sigqueue只能用于實時信號(SIGRTMIN~SIGRTMAN之間的信號,包括這兩個限制值),這些實時信號默認操作是終止進程。對于非實時信號(1-31信號),不支持信號排隊。
21. 作業控制信號
以下六個信號與作業控制有關:
- SIGCHLD:子進程停止或終止
- SIGCONT:如果進程已停止,則使其繼續運行
- SIGSTOP:停止信號(不能被捕捉或忽略)
- SIGTSTP:交互式停止信號
- SIGTTIN:后臺進程組成員讀控制終端
- SIGTTOU:后臺進程組成員寫控制終端
當一個進程產生4種停止信號(SIGTSTP、SIGSTOP、SIGTTIN、SIGTTOU),對進程的任一未決SIGCONT信號丟棄
當對一個進程產生SIGCONT信號時,對同一進程的任一未決停止信號被丟棄。
22. 信號名和編號
通過數組sys_siglist獲取信號編號和信號名間的映射
extern const char *const sys_siglist[_NSIG];//其中數組下標即為信號編號使用psignal函數打印與信號編號對應的字符串
void psignal(int sig, const char *msg);該函數類似于perror,通常是在stderr打印出msg參數,后面跟一個冒號一個空格,然后打印出該信號的說明。
如果在sigaction信號處理程序中有siginfo_t結構,也可以通過siginfo_t結構,使用psiginfo函數打印出信號編號更多的信息
void psiginfo(const siginfo_t *pinfo, const char *msg);使用strsignal獲取與信號相關字符串(而不是寫入到文件中),類似于strerror
char *strsignal(int sig);示例:
int main(int argc, char* argv[]) {cout << "sys_list數組 : " << sys_siglist[SIGCHLD] << endl;psignal(SIGCHLD,"psignal函數 ");cout << "strsignal函數 : " << strsignal(SIGCHLD) << endl; } /*打印: sys_list數組 : Child exited psignal函數 : Child exited strsignal函數 : Child exited */ist``獲取信號編號和信號名間的映射
extern const char *const sys_siglist[_NSIG];//其中數組下標即為信號編號使用psignal函數打印與信號編號對應的字符串
void psignal(int sig, const char *msg);該函數類似于perror,通常是在stderr打印出msg參數,后面跟一個冒號一個空格,然后打印出該信號的說明。
如果在sigaction信號處理程序中有siginfo_t結構,也可以通過siginfo_t結構,使用psiginfo函數打印出信號編號更多的信息
void psiginfo(const siginfo_t *pinfo, const char *msg);使用strsignal獲取與信號相關字符串(而不是寫入到文件中),類似于strerror
char *strsignal(int sig);示例:
int main(int argc, char* argv[]) {cout << "sys_list數組 : " << sys_siglist[SIGCHLD] << endl;psignal(SIGCHLD,"psignal函數 ");cout << "strsignal函數 : " << strsignal(SIGCHLD) << endl; } /*打印: sys_list數組 : Child exited psignal函數 : Child exited strsignal函數 : Child exited */總結
以上是生活随笔為你收集整理的《UNIX环境高级编程》笔记 第十章-信号的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 可视化优化:百度地图内网访问(通过ngi
- 下一篇: 使用squid内网代理百度地图