[轉帖]內核情景分析:強制殺死一個進程的執行過程

前言

我們常常需要強制殺死一個進程,這種情況不同於正常退出的方式,一些退出流程將不會被執行。

按照正常的邏輯,這樣的行爲應該會導致一些資源沒有得到釋放,可是實際操作中多次強制殺死進程也沒有出現啥異常現象。

那麼問題來了: kill -9 殺死進程在內核中到底有怎樣的處理過程呢?本文中,我將逐步的揭開這個問題的謎團。

信號的基礎工作原理

信號模擬了硬件中斷的處理流程。cpu 在每條指令執行完成後檢測中斷引腳,判斷是否有中斷到來,檢測到有中斷髮生後打斷當前執行的任務並保存現場然後跳轉到中斷服務程序開始運行。

信號與硬件中斷的處理過程有類似之處,卻也有顯著的區別,它的主要步驟如下:

  1. 向某個進程發送信號事件,信號事件對應的結構被掛入到目標進程的 sigpending 鏈表中,並置位信號狀態掩碼中對應的位
  2. 目標進程在從內核態返回用戶態的過程中檢測是否有掛起的信號,發現有掛起的信號則從鏈表中每次拿出一個信號事件進行處理直到鏈表爲空
  3. 獲取到一個信號事件後,根據信號類型分發到不同的邏輯中,主要有一下三種大類
    • 對於設定爲 SIG_IGN 狀態的信號直接忽略
    • 對於有通過 signal、sigaction 註冊信號處理函數的信號,設定堆棧後跳轉到用戶態的信號處理函數開始執行
    • 對於設定爲 SIG_DFL、其它類型的信號執行殺死進程的操作
  4. 對於有註冊信號處理函數的信號,內核在設定好堆棧後返回到用戶態後直接從用戶態信號處理函數開始執行,此函數返回後觸發一個 sigreturn 系統調用後再次回到內核,然後恢復舊的堆棧繼續運行

對於 SIGKILL、SIGSTOP 這兩種不可被用戶程序捕獲的信號以及設定了 SIG_IGN、SIG_DEF 行爲的信號而言,這些信號的處理過程均在內核態完成。

由於信號處理函數是在用戶態程序的代碼段中,當用戶註冊了一個非默認值的可捕獲信號信號處理函數時,纔會進入用戶態執行,這裏的過程實際上涉及一些相對複雜的架構依賴性操作,與這裏要探討的問題關係不大,不展開描述了。

信號在何時被處理

信號不同於硬件中斷,它是軟件上的行爲,不能做到在每條指令執行完成後都進行檢測並響應。一般來說,它只在內核態返回用戶態的過程中被檢測並處理,主要有如下兩種情況:

  1. 當前進程由於系統調用、中斷、異常而進入系統空間後,從系統空間返回用戶空間的前夕
  2. 當前進程在內核中進入睡眠以後剛被喚醒的時候,由於信號的存在而提前返回到用戶空間

kill -9 信號的處理過程

kill -9 表示發送 SIGKILL 信號,這個信號是不能被用戶程序捕獲的,它的處理過程完全在內核態完成,核心過程在於調用 do_group_exit 來執行所謂的“組退出”過程殺死整個線程組。

do_group_exit 函數會殺死 current 線程組中的其他進程(如果存在的話),它會向所有不同於 current 的同一個 tgid 中的其它進程發送 SIGKILL 信號,這些進程最終都將調用 do_exit 函數,從而終止運行。

do_exit 是一個相當複雜的函數,它的主要目的是回收進程使用的資源,這也是我們調用了 kill -9 沒有出問題的根本原因——內核替我們完成了這些必要的回收工作。

在進一步描述前,先回憶回憶之前研究過的實時操作系統中任務退出函數的執行過程與原理。

實時操作系統中的任務退出函數

我在 rt-thread 與 ucos 中任務退出時如何調用退出函數 這篇博客中描述了實時操作系統中任務退出函數調用的過程,它其實是在每個任務的棧中預先設定了一個調用棧,將此棧的返回地址設置爲進程退出的函數,這樣當進程主函數執行完成後,彈棧過程會將預設的返回地址賦值給 pc 從而執行退出函數

rt-thread 實時操作系統中也有類似 linux延後釋放 tcb 的過程,它實際是在 idle 任務中來回收進程的 tcb 的

對於實時操作系統來說,它佔用的資源並不像 linux 系統那樣多,其中最重要的應當是 ipc 資源了,對這些資源的回收也是其中的主要邏輯。

任務退出函數的複雜性

任務退出函數在某種意義上要比任務創建函數更爲複雜。例如對於 ipc 來說,如果有其它進程在等待當前進程佔用的 ipc 資源而睡眠,當前任務退出的時候必須考慮到這種情況,必須喚醒相關的進程。

試想如果它不做任何操作就悄無聲息的死亡了,佔用的 ipc 資源沒有被回收,那麼這些等待這些 ipc 資源的進程將一直睡眠,這是我們不願意看到的結果。

再次回到 do_exit

進程在退出系統之前要釋放所有的資源,在任務創建過程中從父進程繼承的資源有存儲空間、打開文件、工作目錄、信號處理表等等,相應的在 do_exit 中就有 __exit_mm()、__exit_files()、__exit_sighand。

對於其它非繼承的資源如信號量等也需要進行釋放。這裏有這樣一個準則:在 task_struct 結構體中,只要是一個指針,在進程創建時以及運行過程中要爲其在內核中分配一個數據結構或緩衝區,而且這個指針又是通向這個數據結構或緩衝區的唯一途徑,那就一定要把它釋放掉,不然就會造成內核的存儲空間泄露。(摘自 《Linux 內核源代碼情景分析》)

正是因爲內核在 do_exit 中針對用戶態程序使用的不同資源進行了回收,這才讓 kill -9 這樣的方式不至於導致存儲空間泄露。

malloc 與 free 對應堆空間的回收

我們可以想想在使用 c 語言編寫用戶態程序時中一般要求 malloc 與 free 成對存在,如果只調用了 malloc 而不調用 free,則會產生存儲空間的泄露,這裏的泄露實際上針對的是持續運行過程的說法

malloc 申請的動態內存空間會被映射到程序虛擬內存的堆中,堆也只是程序虛擬內存中的頁面,與其它存儲區域一樣都是通過底層的 mmap 映射到虛擬內存中的,通過執行 pmap、查看 /proc/pid/maps 可以看到。這些頁面在 __exit_mm 函數中最終調用到的 exit_mmap 中被釋放。

文件描述符的釋放

與此類似的還有打開與關閉文件的過程,這個過程也非常常見,一般來說仍舊要成對存在。對於一個持續運行的程序,不斷打開新的文件卻不關閉會造成文件句柄泄露。在程序退出時,內核調用 __exit_files 來關閉已經打開的文件描述符。

task_struct 與進程內核棧的釋放

do_exit 中沒有回收 task_struct tcb 結構體與進程內核棧的過程,實際上這個過程是由程序的父進程完成的。在 do_exit 中會調用 exit_notify 來向父進程發送 SIGCHLD 信號以通知父進程,讓父進程料理後事。

父進程通過執行 wait 系統調用來等待子進程死亡,子進程在死亡時負責喚醒父進程,讓父進程來爲自己”收屍“。

完成了 exit_notify 後,子進程最終調用 schedule 讓出 cpu,此時由於它已經從就緒表中被移除,因此從 schedule 切出就是它最後一次執行的代碼。

總結

強制殺死一個進程的過程與信號處理與進程退出流程的知識強相關,這裏的處理過程完全是在內核態完成的。

kill -9 這種強制殺死進程的方式會導致程序正常的退出過程不能得到執行,這應該會造成一些問題。但是我們可以看到操作系統在背後做的大量的資源回收工作,正是這些隱含的動作纔不致於讓 kill -9 這樣的行爲出現異常。

不過,我們必須意識到的是,kill -9 強制殺死一個進程的方式與進程主動死亡的方式其背後的行爲是不同的,在一些情況下可能需要深入的分析這一過程以定位某些疑難雜症。

</article>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章