原創 宋寶華 Linux閱碼場 4月8日服務器
衆所周知,Linux的進程睡眠有兩種常規狀態:app
淺度睡眠的進程,則能夠被信號喚醒,對於常規的鍵盤、串口、觸摸屏等等這些I/O設備,顯然符合此類模型。因此Linux內核的代碼裏面常常看到這樣的代碼模板,筆者在《Linux設備驅動開發詳解》一書中也花了大篇幅解釋以下模板:ide
調用_ _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年的郵件:操作系統
對於磁盤讀等場景,若是讀還沒完成,就跳出去響應信號,application可能break,因此深度睡眠必須存在是一個客觀的殘酷的現實(cold fact)。線程
祖師爺還有更猛的一槌定音:
3d
祖師爺沒有點明爲何磁盤讀的時候不該該跑用戶態去執行信號處理函數,爲何引起application break。我理解其中的一個場景以下:Linux對於代碼段、數據段、堆和棧都一般依賴demanding page在發生page fault的時候從磁盤swap進來的,從而致使磁盤讀的行爲。在這個過程當中,若是咱們執行淺度睡眠並響應信號而跳過去執行應用程序代碼段設置的信號處理函數,則此信號處理函數的執行可能再次由於swap in的需求引起進一步的磁盤讀,形成double page fault的場景。磁盤的讀很大程度上不必定是read系統調用引起的,考慮到代碼段、數據段、堆和棧的每每是發生了page fault後纔去從磁盤swap進來。磁盤有其特殊性,在Linux這樣的操做系統裏面,磁盤某種意義上是"內存"。
可是,若是響應信號後,哪怕application break都已經無所謂了呢?若是咱們的目的乾脆就是發一個致命的信號,譬如殺死應用的信號(SIGKILL),那麼application break這個就顯得可有可無了,由於咱們自己就不打算繼續玩下去了!這樣就使得深度睡眠的進程,還能夠被殺死,媽媽不再用擔憂NFS服務器掛了後,我痛苦,我孤獨,我精分了!code
Linux所以推出了一個特殊的深度睡眠狀態,叫作blog
TASK_KILLABLE(可殺的深度睡眠):能夠被等到的資源喚醒,不能被常規信號喚醒,可是能夠被致命信號喚醒,醒後即死。
TASK_KILLABLE狀態的定義是:
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
因此它顯然是屬於TASK_UNINTERRUPTIBLE的,只是能夠被TASK_WAKEKILL。
什麼叫致命信號呢?talk is cheap,show me the code。
因此,足夠致命的信號就是SIGKILL。SIGKILL何許人也,就是傳說中的信號9,沒法阻擋沒法被應用覆蓋的終極殺器:
僅僅從這個代碼能夠看出來,只有信號9才屬於fatal signals。那麼是否是隻有信號9,才能夠殺死TASK_KILLABLE的進程,信號2(CTRL+C)是否無能爲力呢?
猜測再多,不如玩一個真實的代碼,咱們下面來改造下,把globalfifo.c的read改造爲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呢?
這個時候咱們重點關注kernel/signal.c內核代碼中的complete_signal()函數:
實際上,當Linux內核發現進程(線程組)收到了一個sig_fatal()的信號的時候,會給這個進程中的每一個線程人爲地插入一個SIGKILL信號,這個從while_each_thread循環能夠看出。
sig_fatal()和fatal_signal_pending()不是一個概念。咱們看看sig_fatal()的代碼:
基本上,一個信號的行爲若是是缺省的(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()條件的知足。
有的童鞋說,若是個人進程只有一個線程呢?那去掉上圖中的T2以及T2獨佔的signal pending框便可:
爲了進行驗證,咱們再也不使用cat。而是本身寫個app去訪問globalfifo,而在此app裏面修改信號2的行爲:
咱們經過signal(2, sigint)給信號2綁定了信號處理函數sigint(),這個時候read(fd, buf, 10)引起TASK_KILLABLE睡眠,咱們不管怎麼kill -2,都殺不死上面這個app。2到9的轉化過程再也不發生。
下面的修改也可達到相似效果:
上面咱們是把信號2進行了SIG_IGN的忽略處理。
不只信號2是這樣的,其餘的不少信號也相似,好比SIGHUP、SIGIO、SIGTERM、SIGPIPE等均可以在沒有綁定和忽略的狀況下,轉化爲信號9。可是SIGCHLD顯然不同,由於SIGCHLD默認就是忽略的。
(END)