深入理解Linux核心--訊號(閱讀筆記)
第十一章訊號
訊號用於在使用者態處理序間通訊。核心也用訊號通知進程系統所發生的事情。
1、訊號的作用
訊號(signal)是很短的訊息,可以被發送到一個進程或一組進程。發送給進程的唯一資訊通常是一個數,以此來標識訊號。
使用訊號的兩個主要目的 :
讓進程知道已經發生了一個特定的事件。
強迫進程執行它自己代碼中的訊號處理常式。
當然,這兩個目的不是互斥的,因為進程經常通過執行一個特定的常式來對某一事件作出反應。
常規訊號: 前31個
即時訊號: 32-64
即時訊號與常規訊號有很大的不同,因為它們必須排隊以便發送的多個訊號能被接收 到。另一方面,同種類型的常規訊號並不排隊 ;如果一個常規訊號被連續發送多次,那麼,只有其中的一個發送到接收進程。儘管Linux核心並不使用即時訊號,它還是通過幾個特定的系統調用完全實現了POSIX標準。
許多系統調用允許程式員發送訊號,並決定他們的進程如何響應所接收的訊號。
訊號的一個重要特點 是它們可以隨時被發送給狀態經常不可預知的進程 。發送給非運行進程的訊號必須由核心儲存,直到進程恢複執行。
阻塞一個訊號要求訊號的傳遞拖延,直到隨後解除阻塞,這使得訊號產生一段時間之後才能對其傳遞這一問題變得更加嚴重。
核心區分訊號傳遞的兩個不同階段 :
訊號產生 :
核心更新目標進程的資料結構以表示一個新訊號已被發送。
訊號傳遞 :
核心強迫目標進程通過以下方式對訊號做出反應:或改變目標進程的執行狀態,或開始執行一個特定的訊號處理常式 ,或兩者都是。
每個所產生的訊號至多被傳遞一次。訊號是可消費資源:一旦它們已傳遞出去,進程描述符中有關這個訊號的所有資訊都被取消。
已經產生但還沒有傳遞的訊號稱為掛起訊號(pendingsignal)。任何時候,一個進程僅存在給定類型的一個掛起訊號,同一進程同種類型的其他訊號不被排隊,只被簡單地丟棄。但是,即時訊號是不同的:同種類型的掛起訊號可以有好幾個。
訊號可以保留不可預知的掛起時間,必須考慮的因素:
訊號通常只被當前正啟動並執行進程傳遞
給定類型的訊號可以由進程選擇性地阻塞
當進程執行一個訊號處理常式的函數時,通常“屏蔽”相應的訊號,即自動阻塞這個訊號到處理常式結束。因此,所處理的訊號的另一次出現不能中斷訊號處理常式,所以,訊號處理函數不必是可重新進入的 。
核心實現:
記住每個進程阻塞哪些訊號
當從核心態切換到使用者態時,對任何一個進程都要檢查是否有一個訊號已到達。這幾乎在每個定時中斷時都發生
確定是否可以忽略訊號。這個發生在下列所有的條件都滿足時:
目標進程沒有被另一個進程跟蹤
訊號沒有被目標進程阻塞
訊號被目標進程忽略
處理這樣的訊號,即訊號可能在進程運行期間的任一時刻請求把進程切換到一個訊號處理函數,並在這個函數返回以後恢複原來執行的上下文。
[1]傳遞訊號之前所執行的操作
進程以三種方式對一個訊號做出應答:
(1)顯示地忽略訊號
(2)執行與訊號相關的預設操作 。由核心預定義的預設操作取決於訊號的類型,下列類型:
Terminate:進程被終止(殺死)
Dump:進程被終止(殺死)
Ignore:訊號被忽略
Stop:進程被停止,即把進程置為TASK_STOPPED狀態
Continue:如果進程被停止,就把它置為TASK_RUNNING狀態
(3)通過調用相應的訊號處理函數捕獲訊號
注意,被對一個訊號的阻塞和忽略是不同的:只要訊號被阻塞,它就不被傳遞;只有在訊號解除阻塞後才傳遞它。而一個被忽略的訊號總是被傳遞,只是沒有進一步的操作。
SIGKILL和SIGSTOP訊號不可以被顯示地忽略、捕獲或阻塞,因此,通常必須執行它們的預設操作。因此,SIGKILL和SIGSTOP允許具有適當特權的使用者分別終止和停止任何進程,不管進程執行時採取怎樣的防禦措施。
如果訊號的傳遞會引起核心殺死一個進程,難麼這個訊號對該進程就是致命的。SIGKILL訊號總是致命的;而且,預設操作為Terminate的每個訊號,以及不被進程捕獲的訊號對該進程也是致命的。注意,如果一個被進程所捕獲的訊號,其對應的訊號處理函數終止了這個進程,那麼這個訊號就不是致命的,因為進程自己選擇了終止,而不是被核心殺死。
[2]POSIX訊號和多線程應用
POSIX1003.1標準對多線程應用的訊號處理有一些嚴格的要求 :
訊號處理常式必須在多線程應用的所有線程之間共用;不過,每個線程必須有自己的掛起訊號掩碼和阻塞訊號掩碼。
POSIX庫函數kill()和sigqueue()必須向所有的多線程應用而不是某個特殊的線程發送訊號。所有由核心產生的訊號同樣如此。
每個發送給多線程應用的訊號僅傳送給一個線程,這個線程是由核心在從不會阻塞該訊號的線程中隨意選擇出來的
如果向多線程應用發送了一個致命的訊號,那麼核心將殺死該應用的所有線程,而不僅僅是殺死接收訊號的那個線程。
Linux核心2.6把多線程應用實現為一組屬於同一個線程組的輕量級進程。
如果一個掛起訊號被發送給了某個特定進程,那麼這個訊號是私人的;如果被發送給了整個線程組,它就是共用的
[3]與訊號相關的資料結構
對系統中的每個進程來說,核心必須跟蹤什麼訊號當前正在掛起或被屏蔽,以及每個線程組是如何處理所有訊號的。為了完成這些操作,核心使用幾個處理器描述符可存取的資料結構:參考圖11-1***
(1)訊號描述符和訊號處理常式描述符
進程描述符signal欄位指向訊號描述符(signaldescriptor)--一個signal_struct 類型的結構,用來跟蹤共用掛起訊號。
除了訊號描述符以外,每個進程還引用一個訊號處理常式描述符(signal handler deseriplor),它是一個sighand_struct 類型的結構,用來描述每個訊號必須怎樣被線程組處理
(2)sigaction資料結構
一些體繫結構把特性賦給僅對核心可見的訊號。因此,訊號的特性存放在k_sigaction結構中,k_sigaciton結構既包含對使用者態進程所隱藏的特性,也包含大家熟悉的sigaction結構,該結構儲存了使用者態進程能看見的所有特性。實際上,在80x86平台上,訊號的所有特性對使用者態的進程都是可見的。因此,k_sigaction結構只不過簡化為類型為sigaction的單個sa結構。欄位:
sa_handler:指定要執行操作的類型。它的值可以是指向訊號處理常式的一個指標,SIG_DFL(即值0,指定執行預設操作),或者SIG_IGN(即值1,指定忽略訊號)
sa_flags:是一個標誌集,指定必須怎樣處理訊號。
sa_mask:類型為sigset_t的變數,指定當運行訊號處理常式時要屏蔽的訊號
(3)掛起訊號隊列
有幾個系統調用能產生髮送給整個線程組的訊號,如kill()和rt_sigqueueinfo() ,而其他的一些則產生髮送給特定進程的訊號,如tkill()和tgkill()
為了跟蹤當前的掛起訊號是什麼,核心把兩個掛起訊號隊列與每個進程相關聯:
共用掛起訊號隊列,它位於訊號描述符的shared_pending欄位,存放整個線程組的掛起訊號
私人掛起訊號隊列,它位於進程描述符的pending欄位,存放特定進程的掛起訊號
[4]在訊號資料結構上的操作 :參考p429-430的函數列表
2、產生訊號
很多核心功能都會產生訊號:它們完成訊號處理第一步的工作,即根據需要更新一個或多個進程的描述符。 它們不直接執行第二步的訊號傳遞操作,而是可能根據訊號的類型和目標進程的狀態喚醒一些進程,並促使這些進程接收訊號。
當發送給進程一個訊號時,這個訊號可能來自核心,也可能來自另一個進程。核心通過對如表11-9所示的某個函數進行調用而產生訊號
當一個訊號被發往整個線程組時,這個訊號可能來自核心,也可能來自另一個進程。核心通過對如表11-10所示的某個函數進行調用而產生訊號
[1]specific_send_sig_info()函數:向指定進程發送訊號,步驟:參考p433
[2]send_signal()函數:在掛起訊號隊列中插入一個新元素,步驟:參考p434
[3]group_send_sig_info()函數:向整個線程組發送訊號,步驟:參考p435-437
3、傳遞訊號
為確保進程的掛起訊號得到處理核心所執行的操作。
核心在允許進程恢複使用者態下的執行之前,檢查進程TIF_SIGPENDING標誌的值。每當核心處理完一個中斷或異常時,就檢查是否存在掛起訊號
為了處理非阻塞的掛起訊號,核心調用do_signal()函數
通常只是在CPU要返回到使用者態時才調用do_signal()函數
do_signal()函數的核心由重複調用dequeue_signal()函數的迴圈組成,直到在私人掛起訊號隊列和共用掛起訊號隊列中都沒有非阻塞的掛起訊號時,迴圈才結束。
dequeue_singal()函數首先考慮私人掛起訊號隊列中的所有訊號,並從最低編號的掛起訊號開始。然後考慮共用隊列中的訊號。它更新資料結構以表示訊號不再是掛起的,並返回它的編號。
do_signal()函數如何處理每一個掛起的訊號,其編號由dequeue_signal()返回。首先,它檢查current接收進程是否正受到其他一些進程的監控;在肯定的情況下,do_signal()調用do_notify_parent_cldstop()和schedule()讓監控進程知道進程的訊號處理。
然後,do_signal()把要處理訊號的k_sigaction資料結構的地址賦給局部變數ka;根據ka的內容可以執行三種操作:忽略訊號、執行預設操作或執行訊號處理常式。如果顯式忽略被傳遞的訊號,那麼do_signal()函數僅僅繼續執行迴圈,並由此考慮另一個掛起訊號
[1]執行訊號的預設操作
如果ka->sa.sa_handler等於SIG_DFL,do_signal()就必須執行訊號的預設操作。唯一的例外是當接收進程是init時,這個訊號被丟棄。
SIGSTOP與其他訊號的差異比較微妙:SIGSTOP總是停止線程組,而其他訊號只停止不在“孤兒進程組”中的線程組。POSIX標準規定,只要進程組中一個進程有父進程,儘管進程處於不同的進程組中但在同一個會話中,那麼這個進程組就不是孤兒。因此,如果父進程死亡,但啟動該進程的使用者並登入線上,那麼該進程組就不是一個孤兒。
預設操作為Dump的訊號可以在進程的工作目錄中建立一個“轉儲”檔案,這個檔案列出進程地址空間和CPU寄存器的全部內容
[2]捕獲訊號
如果訊號有一個專門的處理常式,do_signal()就函數必須強迫該處理常式執行。這是通過調用handle_signal()進行的
注意do_signal()的處理了一個單獨的訊號後怎樣返回。直到下一次調用do_signal()時才考慮其他掛起的訊號。這種方式確保了即時訊號將以適當的順序得到處理
執行一個訊號處理常式是件相當複雜的任務,因此在使用者態和核心態之間切換時需要謹慎地處理棧中的內容。我們將正確地解釋這裡所承擔的任務
訊號處理常式是使用者態進程所定義的函數,並包含在使用者態的程式碼片段中。handle_signal()函數運行在核心態,而訊號處理常式運行在使用者態,這就意味著在當前進程恢複“正常”執行之前,它必須首先執行使用者態的訊號處理常式。此外,當核心打算恢複進程的正常執行時,核心態堆棧不再包含被中斷程式的硬體上下文,因此每當從核心態向使用者態轉換時,核心態堆棧都被清空。而另外一個複雜性是因為訊號處理常式可以調用系統調用,在這種情況下,執行了系統調用的服務常式以後,控制權必須返回到訊號處理常式而不是到被中斷程式的正常代碼流。
linux所採用的解決方案是把儲存在核心態堆棧中的硬體上下文拷貝到當前進程的使用者態堆棧中。使用者態堆棧也以這樣的方式被修改,即當訊號處理常式終止時,自動調用sigreturn()系統調用把這個硬體上下文拷貝回到核心態堆棧中,並恢複使用者態堆棧中原來的內容。
圖11-2說明了有關捕捉一個訊號的函數的執行流:
一個非阻塞的訊號發送給一個進程。當中斷或異常發生時,進程切換到核心態。
正要返回到使用者態前,核心執行do_signal()函數,
這個函數又依次處理訊號(通過調用handle_signal())和建立使用者態堆棧(通過調用setup_frame()或setup_rt_frame())
當進程又切換到使用者態時,因為訊號處理常式的起始地址被強制放進程式計數器中,因此開始執行訊號處理常式。
當處理常式終止時,setup_frame()或setup_rt_frame()函數放在使用者態堆棧中的傳回碼就被執行。這個代碼調用sigreturn()或rt_sigrenturn()系統調用,相應的服務常式把正常程式的使用者態堆棧硬體上下文拷貝到核心堆棧,並把使用者態堆棧恢複到它原來的狀態(通過調用restore_sigcongtext()).當這個系統調用結束時,普通進程就因此能恢複自己的執行
圖:11-2***
(1)建立幀
為了適當地建立進程的使用者態堆棧,handle_signal()函數或者調用setup_frame()或者調用setup_rt_frame()
setup_frame()函數把一個叫做幀(frame)的資料結構推進使用者態堆棧中,這個幀含有處理訊號所需要的資訊,並確保正確返回到handle_signal()函數
setup_frame()函數把儲存在核心態堆棧的段寄存器內容重新設定成它們的預設值以後才結束。現在,訊號處理常式所有需的資訊就在使用者態堆棧的頂部。
(2)檢查訊號標誌
建立了使用者態堆棧以後,handle_signal()函數檢查與訊號相關的標誌值。如果訊號沒有設定SA_NODEFER標誌,在sigaction表中sa_make欄位對應的訊號就必須在訊號處理常式執行期間被阻塞,然後,handle_signal()返回到do_signal(),do_signal()也立即返回
(3)開始執行訊號處理常式
do_signal()返回時,當前進程恢複它在使用者態的執行。由於如前所述setup_frame()的準備,eip寄存器指向訊號處理常式的第一條指令,而esp指向已推進使用者態堆棧頂的幀的第一個記憶體單元。因此,訊號處理常式被執行。
(4)終止訊號處理常式
訊號處理常式結束時,返回棧頂地址,該地址指向幀的pretcode欄位所引用的vsyscall頁中的代碼。因此,訊號編號(即幀的sig欄位)被從棧中丟棄,然後調用sigreturn()系統調用
sys_rt_sigreturn()服務常式把來自擴充幀的進程硬體上下文拷貝到核心態堆棧,並通過從使用者態堆棧刪除擴充幀以恢複使用者態堆棧原來的內容。
(5)系統調用的重新執行
核心並不總是能立即滿足系統調用發出的請求,在這種情況發生時,把發出系統調用的進程置為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE狀態
如果進程處於TASK_INTERRUPTIBLE狀態,並且某個進程向它發送了一個訊號,那麼,核心不完成系統調用就把進程置成TASK_RUNNING狀態。當切換回使用者態時訊號被傳遞給進程。當這種情況發生時,系統調用服務常式沒有完成它的工作,但返回EINTR,ERESTARTNOHAND,ERESTART_RESTARTBLOCK,ERESTARTSYS或ERESTARTNOINTR錯誤碼。實際上,這種情況下使用者態進程獲得的唯一錯誤碼是EINTR,這個錯誤碼錶示系統調用還沒有執行完。核心內部使用剩餘的錯誤碼來指定訊號處理常式結束後是否自動重新執行系統調用。
與未完成的系統調用相關的出錯碼及這些出錯碼對訊號三種可能的操作產生的影響。
Terminate:不會自動重新執行系統調用
Reexecut:核心強迫使用者態進程把系統調用號重新裝入eax寄存器,並重新執行int$0x80指令或sysenter指令。進程意識不到這種重新執行,因此出錯碼也不傳遞給進程。
Depends:只有被傳遞訊號的SA_RESTART標誌被設定,才重新執行系統調用;否則系統調用-EINTER出錯碼結束
當傳遞訊號時,核心在試圖重新執行一個系統調用前必須確定進程確實發出過這個系統調用。這就是regs硬體內容相關的orig_eaz欄位起重要作用之處
a、重新執行被未捕獲訊號中斷的系統調用
如果訊號被顯式地忽略,或者如果它的預設操作已被強制執行,do_signal()就分析系統調用的出錯碼,並如表11-11中所說明的那樣決定是否重新自動執行未完成的系統調用。如果必須重新開始執行系統調用,那麼do_signal()就修改regs硬體上下文,以便在進程返回到使用者態時,eip指向int$0x80指令或sysenter指令,且eax包含系統調用號
b、為所捕獲的訊號重新執行系統調用
如果訊號被捕獲,那麼handle_signal()分析出錯碼,也可能分析sigaction表的SA_RESTART標誌來決定是否必須重新執行未完成的系統調用
如果系統調用必須被重新開始執行,handle_signal()就與do_signal()完全一樣地繼續執行;否則,它向使用者態進程返回一個出錯碼-ENTR
4、與訊號處理相關的系統調用
在使用者態啟動並執行進程可以發送和接收訊號。這意味著必須定義一組系統調用用來完成這些操作。遺憾的是,由於曆史的原因,已經存在幾個具有相同功能的系統調用,因此,其中一些系統調用從未被調用。例如:系統調用sys_sigaction()和sys_rt_sigaciton()幾乎是相同的,因此C庫中封裝函數sigaction()調用sys_rt_sigaction()而不是sys_sigaction()。
[1]kill()系統調用
一般用kill(pid,sig) 系統調用向普通進程或多線程應用發送訊號,其相應的服務常式是sys_kill()函數
kill()系統調用能發送任何訊號,即使編號在32-64之間的即時訊號。kill()系統調用不能確保把一個新的元素加入到目標進程的掛起訊號隊列,因此,掛起訊號的多個執行個體可能被丟失。即時訊號應該當通過rt_siggueueinfo()系統調用進行發送
[2]tkill和gkill()系統調用
tkill()和tgkill() 系統調用向線程組中的指定進程發送訊號
[3]改變訊號的操作
sigaction(sig,act,oact) 系統調用允許使用者為訊號指定一個操作。當然,如果沒有自訂的訊號操作,那麼核心執行與傳遞的訊號相關的預設操作
[4]檢查掛起的阻塞訊號
sigpending( )系統調用允許進程檢查掛起的阻塞訊號的集合,也就是說,檢查訊號被阻塞時已產生的那些訊號
[5]修改阻塞訊號的集合
sigprocmask() 系統調用允許進程修改阻塞訊號的集合。這個系統調用只應用於常規訊號
[6]掛起進程
sigsuspend() 系統調用把進程置為TASK_INTERRUPTIBLE狀態,當然這是把mask參數指向的位元遮罩數組所指定的標準訊號阻塞以後設定的。只有當一個非忽略、非阻塞的訊號發送到進程以後,進程才被喚醒
[7]即時訊號的系統調用
系統調用只應用到標準訊號,因此,必須引入另外的系統調用來允許使用者態進程處理即時訊號
即時訊號的幾個系統調用:rt_sigaction()rt_sigpending()rt_sigprocmask()rt_sigsuspend()