linux0.11信號機制
本文簡單描述linux0.11信號機制的實現
www.2cto.com
一:有關信號
當進程收到一個信號後,進程根據相關設定調用信號處理函數。
有三類信號處理方式:默認處理方式、忽略信號方式、執行用戶設定的信號處理函數。
發送信號的方式:按下相應的鍵(如CTRL+C)、使用kill命令或函數向指定進程發送信號。
typedef void sig_func(int);
sig_func *signal(int signr, sig_func *handler);
當在進程中調用signal(signo, handler)之後,
如果進程收到信號signo,則進程會執行handler指向的函數。
假設有進程A和進程B,當進程A給進程B發送了一個信號後,那進程B的信號處理函數什麼時候執行?
若要執行進程B的信號處理函數,則進程B必須處於執行狀態。也就是說只有當調度程序調度到了進程B後,
才可能執行進程B的信號處理函數。否則進程B不處於可執行狀態,收到了信號也沒用。
若進程B自己調用kill函數,給自己發送了一個信號,我們會假定這個信號處理函數會立即執行。
因此在執行系統函數kill之後會立即處理進程B的信號。
從這兩點可以意識到,內核需要在時鐘中斷和系統調用後對當前進程的信號進行處理。
需要在時鐘中斷時是因為時鐘中斷會調用schedule函數,因為這是分時系統,
如果進程A給B發了信號,而且現在調度到了B,那理所當然要執行B的信號處理函數。
二:linux0.11的信號機制
以 kill 函數為例來簡單說明大致流程, 下面再來詳細描述內核中的do_signal函數。
當以kill函數給當前進程發送一個信號之後。
因為這是個系統函數,因此會執行int 0x80進入system_call的入口點
_system_call:
cmpl $nr_system_calls-1, %eax # %eax保存kill函數的調用號
ja bad_sys_call # 無效的系統調用
push %ds
push %es
push %fs
push %edx
push %ecx
push %ebx # 相關數據入棧
.....................
call _sys_call_table(,%eax,4) # 執行系統調用, 這裡就是 sys_kill 函數了
pushl %eax # 系統調用的返回值入棧,也即是 sys_kill 的返回值
......................
ret_from_sys_call:
.....
pushl %ecx # %ecx中保存了信號的信號值。
call _do_signal # 對信號進行處理
popl %eax # 將信號值出棧
popl %eax # 將系統調用返回值出棧, 也就是sys_kill的返回值存入%eax寄存器
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
可見每次系統調用之後,可能會執行ret_from_sys_call,進而對信號進行處理。
除了在_system_call裡會這樣, 在一些中斷下也會調用ret_from_sys_call,時鐘中斷就是其中之一。
現在已經知道內核是“何時”來處理進程的信號了。
三:do_signal函數。
do_signal的功能主要是設置了內核的堆棧和應用的用戶堆棧,設置好堆棧後,
當執行ret_from_sys_call最下面的iret指令的時候,去自動執行進程的信號處理函數。當信號處理函數執行完成後,又會接著進程的下一條指令去執行。
下圖是《Linux0.11內核完全注釋》一書裡的,很好的顯示調用do_signal前後的堆棧變化。
左邊的為內核態堆棧,就是在執行call _do_signal之前的堆棧內容。
do_signal執行如下操作
1:將堆棧中的eip值,保存到old_eip中,old_eip就指向了用戶程序中即將執行代碼
2:將eip執行信號處理函數。這樣當執行ret_from_sys_call中的iret時,會執行cs:eip指向的代碼,也就是信號處理函數。
3:將用戶態堆棧的esp的值,向下移7或8個長字(32位)
4:然後將sa_resotrer, signr等值放入堆棧, 見圖右邊的用戶堆棧。
完成上述操作後,do_signal執行完畢,返回到ret_from_sys_call中,
ret_from_sys_call執行一些pop操作後執行iret指令, 這時會跳轉到信號處理函數去執行。
當信號處理函數執行完後,會執行ret操作(函數的返回使用ret,中斷的返回使用iret),這時會將sa_restorer存入eip,
因此接下來就會執行sa_restorer
sa_restorer會恢復用戶堆棧
__sig_restore:
addl $4, %esp
popl %eax # 將系統調用的返回值存入eax
popl %ecx
popl %edx
popfl
ret
當執行完popfl之後,明顯用戶堆棧裡面只剩下old_eip了, 因此執行ret,程序就會跳轉到cs:old_eip去執行,也就是系統調用的下一條用戶指令了。
至此信號處理函數已經執行,系統調用也已返回,用戶程序無憂無慮的繼續執行。