【轉】深入理解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()

發佈了55 篇原創文章 · 獲贊 11 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章