宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

原創 宋寶華 Linux閱碼場 4月8日


深度睡眠與淺度睡眠!

衆所周知,Linux的進程睡眠有兩種常規狀態:

  • TASK_INTERRUPTIBLE(淺度睡眠):可以被等待的資源喚醒,也能被signal喚醒;
  • TASK_UNINTERRUPTIBLE(深度睡眠):可以被等待的資源喚醒,但是不能被signal喚醒。
    簡單來說,深度睡眠的進程必須等待資源來了才能醒,在此之前,甚至你給它發任何的信號,它都不可能醒來。

淺度睡眠的進程,則可以被信號喚醒,對於常規的鍵盤、串口、觸摸屏等等這些I/O設備,顯然符合此類模型。所以Linux內核的代碼裏面經常看到這樣的代碼模板,筆者在《Linux設備驅動開發詳解》一書中也花了大篇幅解釋如下模板:

宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

調用_ _set_current_state(TASK_INTERRUPTIBLE)並schedule()出去的進程,醒來第一件事往往就是通過signal_pending(current)查看信號是否存在,如果存在,就跳出去處理信號,無需等待I/O的完成(大不了信號處理完了再重新read)。
TASK_INTERRUPTIBLE看起來很理想,不至於在I/O沒完成的時候,連CTRL+C都不響應(當然也不會響應其他SIGIO、SIGUSR1等信號)。
那麼,有的童鞋就會問,既然淺度睡眠這麼好,那麼還要TASK_UNINTERRUPTIBLE這種完全不響應信號的深度睡眠幹什麼?

深度睡眠不可避免

正在讀本文的你,可能都有過這樣的悲催經歷,在NFS文件系統上面運行程序,但是NFS服務器掛了,你怎麼都ctrl + c不掉那個進程,因爲它就是個深度睡眠的場景。你徘徊,你迷茫,你問能不能直接都改爲TASK_INTERRUPTIBLE,徹底刪除TASK_UNINTERRUPTIBLE呢?

對此,祖師爺Linus的答覆是:不可能。請看他2002年的郵件:

宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

對於磁盤讀等場景,如果讀還沒完成,就跳出去響應信號,application可能break,所以深度睡眠必須存在是一個客觀的殘酷的現實(cold fact)

祖師爺還有更猛的一錘定音:
宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

祖師爺沒有點明爲什麼磁盤讀的時候不應該跑用戶態去執行信號處理函數,爲什麼引發application break。我理解其中的一個場景如下:Linux對於代碼段、數據段、堆和棧都通常依賴demanding page在發生page fault的時候從磁盤swap進來的,從而導致磁盤讀的行爲。在這個過程中,如果我們執行淺度睡眠並響應信號而跳過去執行應用程序代碼段設置的信號處理函數,則此信號處理函數的執行可能再次因爲swap in的需求引發進一步的磁盤讀,造成double page fault的場景。磁盤的讀很大程度上不一定是read系統調用引發的,考慮到代碼段、數據段、堆和棧的往往是發生了page fault後纔去從磁盤swap進來。磁盤有其特殊性,在Linux這樣的操作系統裏面,磁盤某種意義上是"內存"。

但是,如果響應信號後,哪怕application break都已經無所謂了呢?如果我們的目的乾脆就是發一個致命的信號,譬如殺死應用的信號(SIGKILL),那麼application break這個就顯得無關緊要了,因爲我們本身就不打算繼續玩下去了!這樣就使得深度睡眠的進程,還可以被殺死,媽媽再也不用擔心NFS服務器掛了後,我痛苦,我孤獨,我精分了!

可殺的深度睡眠

Linux因此推出了一個特殊的深度睡眠狀態,叫做

  • TASK_KILLABLE(可殺的深度睡眠):可以被等到的資源喚醒,不能被常規信號喚醒,但是可以被致命信號喚醒,醒後即死
    TASK_KILLABLE狀態的定義是:

    #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

所以它顯然是屬於TASK_UNINTERRUPTIBLE的,只是可以被TASK_WAKEKILL。
什麼叫致命信號呢?talk is cheap,show me the code。

宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

所以,足夠致命的信號就是SIGKILL。SIGKILL何許人也,就是傳說中的信號9,無法阻擋無法被應用覆蓋的終極殺器:

宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

僅僅從這個代碼可以看出來,只有信號9才屬於fatal signals。那麼是不是隻有信號9,纔可以殺死TASK_KILLABLE的進程,信號2(CTRL+C)是否無能爲力呢?
猜想再多,不如玩一個真實的代碼,我們下面來改造下,把globalfifo.c的read改造爲TASK_KILLABLE。

宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

加載這個driver後,我們來讀取它:

# insmod globalfifo.ko 
# insmod globalfifo-dev.ko 
# cat /dev/globalfifo 

這個時候,我們ps命令看一下,可以清楚到看到cat進程處於D狀態:

root      7658  0.0  0.0  16800   752 pts/1    D+   19:21   0:00 cat /dev/globalfifo

從前面的代碼可以看出,CTRL+C是不應該可以殺死這個cat進程的,因爲它不是SIGKILL。但是我們來實際測試一下:

# cat /dev/globalfifo 
^C
#

實際卻是可以殺死!!!
我們查看一下我們加的那個內核打印代碼,看一下signal pending的情況:

# dmesg
[ 4670.082548] wake-up by fatal signal 100

明明我們發的是信號2,但是被置上的就是信號9(0x100的1對應SIGKILL的位)。這裏發生了神奇的化學反應!!!
這踏馬到底是怎麼回事?不是一定致命的信號2,爲什麼轉化爲了最最致命的信號9呢?

信號2是如何轉化爲信號9的?

這個時候我們重點關注kernel/signal.c內核代碼中的complete_signal()函數:

宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

實際上,當Linux內核發現進程(線程組)收到了一個sig_fatal()的信號的時候,會給這個進程中的每個線程人爲地插入一個SIGKILL信號,這個從while_each_thread循環可以看出。
sig_fatal()和fatal_signal_pending()不是一個概念。我們看看sig_fatal()的代碼:
宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)
基本上,一個信號的行爲如果是缺省的(SIG_DFL),它又沒有被忽略,那麼它就是滿足sig_fatal()條件的。
如下圖,流程大概是:
當我們給進程P1(假設內部有線程T1和T2,那麼每個線程會有個tast_struct)發送信號2,這個2會填入T1和T2共享的進程級signal pending,由於我們對信號2沒有綁定和忽略而是採用了默認行爲,於是導致sig_fatal()條件滿足。內核就會在T1和T2的各自獨佔的一份signal pending裏面填入9,從而刺激fatal_signal_pending()條件的滿足。
宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)
有的童鞋說,如果我的進程只有一個線程呢?那去掉上圖中的T2以及T2獨佔的signal pending框即可:
宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)
爲了進行驗證,我們不再使用cat。而是自己寫個app去訪問globalfifo,而在此app裏面修改信號2的行爲:
宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)
我們通過signal(2, sigint)給信號2綁定了信號處理函數sigint(),這個時候read(fd, buf, 10)引發TASK_KILLABLE睡眠,我們無論怎麼kill -2,都殺不死上面這個app。2到9的轉化過程不再發生。










下面的修改也可達到類似效果:
宋寶華:可以殺死的深度睡眠TASK_KILLABLE狀態(最透徹一篇)

上面我們是把信號2進行了SIG_IGN的忽略處理。
不僅信號2是這樣的,其他的很多信號也類似,比如SIGHUP、SIGIO、SIGTERM、SIGPIPE等都可以在沒有綁定和忽略的情況下,轉化爲信號9。但是SIGCHLD顯然不一樣,因爲SIGCHLD默認就是忽略的。

(END)

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