玩透Linux信號機制
1.信號處理函數(shù)
如果我們想捕捉進(jìn)程的這兩個信號:SIGCHLD、SIGCONT,用到函數(shù)sigaction
圖片
Linux內(nèi)核提供了多少種信號呢?shell終端運行shell -l即可看到,64種。我們代碼中捕捉的兩個信號,分別是17號、18號信號,這兩個數(shù)字記一下,等下查看內(nèi)核數(shù)據(jù)能看到
圖片
先在內(nèi)核中找到我們注冊的信號處理函數(shù),再說底層實現(xiàn)原理。這個你得自己寫內(nèi)核驅(qū)動程序,市面上沒有任何工具可以讓你看
圖片
找到了。再確定一下我們通過函數(shù)sigaction注冊的信號處理函數(shù)地址是不是這兩個,一毛一樣
圖片
接下來說說Linux內(nèi)核是如何存儲我們注冊的信號處理函數(shù)
struct task_struct {
struct sighand_struct *sighand;
……
}
struct sighand_struct {
spinlock_t siglock;
refcount_t count;
wait_queue_head_t signalfd_wqh;
struct k_sigaction action[_NSIG];
};
struct k_sigaction {
struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
__sigrestore_t ka_restorer;
#endif
};
struct sigaction {
#ifndef __ARCH_HAS_IRIX_SIGACTION
__sighandler_t sa_handler;
unsigned long sa_flags;
#else
unsigned int sa_flags;
__sighandler_t sa_handler;
#endif
#ifdef __ARCH_HAS_SA_RESTORER
__sigrestore_t sa_restorer;
#endif
sigset_t sa_mask; /* mask last for extensibility */
};
Linux內(nèi)核中,每個進(jìn)程對應(yīng)一個task_struct實例,里面有個屬性sighand就是用來存儲你使用函數(shù)sigaction注冊的信號處理函數(shù),具體存儲在sighand_struct的action數(shù)組中,數(shù)組的索引就是信號的編號:1-64,數(shù)組的值是k_sigaction實例,真正存放信號處理函數(shù)的地方是sigaction.sa_handler
所以如果你想查看Linux內(nèi)核中,某個進(jìn)程注冊的所有信號處理函數(shù),代碼這樣寫即可
圖片
至此,我們寫代碼注冊的信號處理函數(shù),在內(nèi)核中如何存儲的,就徹底搞明白了。那內(nèi)核是何時、怎么調(diào)用這個函數(shù)的呢?接著走……
2.kill-18
比如我們通過kill -18向進(jìn)程18226發(fā)送信號,中間發(fā)生了什么?我就不貼源碼了,直接單步調(diào)試內(nèi)核,貼調(diào)用棧吧
圖片
這里看到的只是內(nèi)核態(tài)的調(diào)用棧,用戶態(tài)的,kill命令底層調(diào)用的就是glibc庫中的kill函數(shù),而kill函數(shù)則是通過syscall+kill的內(nèi)核調(diào)用號,進(jìn)入內(nèi)核,調(diào)用相關(guān)函數(shù)
圖片
關(guān)于用戶態(tài)切內(nèi)核態(tài),CPU提供了四個門、兩個快速調(diào)用,以前的實現(xiàn)方式是0x80中斷門,現(xiàn)在都是走syscall快速調(diào)用。如果你非科班,或者沒學(xué)過操作系統(tǒng),應(yīng)該沒聽過這個,或者對這個沒概念。建議非科班出身的小伙伴,一定要把操作系統(tǒng)補一下
那這個信號在內(nèi)核中是如何存儲的呢?核心邏輯在send_signal中,我就不貼代碼了,直接說它做了什么吧
struct task_struct {
struct signal_struct *signal;
sigset_t blocked;
……
}
struct signal_struct {
/* shared signal handling: */
struct sigpending shared_pending;
……
}
struct sigpending {
struct list_head list;
sigset_t signal;
};
struct sigqueue {
struct list_head list;
int flags;
kernel_siginfo_t info;
struct user_struct *user;
};
進(jìn)入內(nèi)核的時候,信號會被包裝成kernel_siginfo
圖片
真正與進(jìn)程關(guān)聯(lián)起來的步驟是,將kernel_siginfo再包裝成sigqueue,然后將sigqueue實例掛到sigpending中,進(jìn)程結(jié)構(gòu)體task_struct中有個屬性shared_pending就是鏈表頭,有點抽象,看圖
圖片
順便說一下,Linux內(nèi)核中的64種信號分成兩個陣營:可靠信號與不可靠信號,可靠信號又叫實時信號,不可靠信號又叫非實時信號
圖片
實時與非實時,表達(dá)的不是立刻去做的意思,是指信號不會丟失。這名字起的真讓人容易產(chǎn)生誤解。Linux內(nèi)核中的很多函數(shù)名也是,比如get_signal,它里面做了很多重要的事情,我看代碼的時候以為就是去信息相關(guān)信息
嗯,差不多就這些。至此,信號在內(nèi)核中是如何存儲的就清晰了。那進(jìn)程何時處理信號,Linux內(nèi)核是如何設(shè)計的呢?
3.信號處理
如果這塊由你來設(shè)計,你會怎么做?不知道?好吧……
Linux內(nèi)核是如何設(shè)計的呢?它的設(shè)計是在進(jìn)程由內(nèi)核態(tài)返回用戶態(tài)的路徑上實現(xiàn)的。為什么要這么做呢?
因為要兼容運行用戶態(tài)注冊的信號處理函數(shù),這個節(jié)點是最優(yōu)選擇。反正都要進(jìn)入用戶態(tài)執(zhí)行,在這之前,順便把信號處理函數(shù)執(zhí)行了。這里要怎么實現(xiàn)呢?改線程棧結(jié)構(gòu),你如果學(xué)了我講的匯編,你就知道要怎么改了。
同樣,不貼代碼了,直接單步調(diào)試Linux內(nèi)核看吧!
圖片
函數(shù)do_signal就是信號處理的核心函數(shù)
接下來詳細(xì)分析代碼層面實現(xiàn),如果你不會匯編,你可能就看不懂了
4.執(zhí)行信號處理函數(shù)
什么時候CPU會由用戶態(tài)進(jìn)入內(nèi)核態(tài)呢?發(fā)生中斷、異常,還有比較常見的:系統(tǒng)調(diào)用。比如write函數(shù)
圖片
當(dāng)CPU執(zhí)行syscall指令,CPU就進(jìn)入Linux內(nèi)核中了,這時候用戶態(tài)的棧比如是這樣(我就畫關(guān)鍵信息了哦)
圖片
如果有信號需要處理的時候,這時候用戶又設(shè)置了信號處理函數(shù),那內(nèi)核在回用戶態(tài)前會把棧改成這樣
圖片
會把rcx設(shè)置為信號處理函數(shù)的地址,syscall進(jìn)入內(nèi)核,配套的返回指令是sysret,會返回到rcx中內(nèi)存地址的位置執(zhí)行代碼。所有的函數(shù)的最后一條指令都是ret,會pop出棧頂元素,并跳轉(zhuǎn)到那個內(nèi)存地址開始執(zhí)行代碼。
syscall進(jìn)入內(nèi)核,把該干的事情干完,就執(zhí)行sysret返回用戶態(tài)。返回到哪里?rcx中內(nèi)存地址的位置,這個位置就是信號處理函數(shù)。執(zhí)行完信號處理函數(shù),pop出棧底元素,跳過去,這個位置就是CPU執(zhí)行syscall進(jìn)入內(nèi)核后面的哪一行代碼的內(nèi)存地址,從而實現(xiàn)接著執(zhí)行。這樣就完成了進(jìn)入用戶態(tài),順便執(zhí)行信號處理函數(shù)的動作。怎么樣,是不是特別有智慧!