在初學linux編程的時候,一直以爲異步信號handle是個很神奇的東西,用戶程序可使用singal之類的系統調用爲某某信號註冊一個信號處理函數(handle函數)。
程序的二進制代碼在內存中都有着肯定的執行流程,爲何收到異步信號之後,程序會被「中斷」,而後跳轉到這個handle函數裏面去運行呢?內核怎麼有能力讓程序作這樣的跳轉呢,總不可能臨時修改程序的可執行代碼吧?
後來學習了一些內核知識,才知道原來進程收到信號之後,並非當即就被「中斷」的,而是先在進程的控制結構(task_struct)中記錄下收到了某某信號,而後等到進程即將從內核態返回用戶態的時候,流程才被「中斷」,handle函數才被調用。
用戶進程何時會從內核態返回用戶態呢?通常主要是三種狀況:系統調用(用戶進程主動進入內核)、中斷(用戶進程被動進入內核)、被調度執行(用戶進程從等待執行變爲正在執行)。
進程從收到信號到它從內核態返回用戶態的過程,是須要必定時間的。可是這個時間通常會很短,至少時鐘中斷會以較大的頻率(好比1毫秒一次)將用戶進程帶入內核(固然,只針對正在執行的進程)。
在進程即將從內核態返回用戶態時,若是有信號須要處理,對應的handle函數將被調用(固然,可能沒有註冊handle,這時內核對信號進行默認的處理)。注意,如今進程還在內核態,內核是怎麼調用用戶態的handle函數的呢?
直接調用能夠嗎?固然不行。內核代碼運行在高CPU特權級別下,若是直接調用handle函數,則handle函數也將在相同的CPU特權下被執行。那麼用戶將能夠在handle函數裏面隨心所欲。
因此,調用handle必須先返回用戶態。可是返回用戶態後,程序流程又不受內核控制了,難不成內核還真的把用戶進程的可執行代碼臨時改掉?
內核實際的作法仍是比較巧妙。用戶進程進入內核之後,都會在其對應的內核棧上留下返回地址,以便流程返回。內核調用handle函數的辦法就是臨時改掉棧上的返回地址,而後按原有的返回用戶態的流程去返回。結果這一返回,就到了handle函數去了。(固然,須要修改的並不止是返回地址,而是一整個調用棧。)
雖然如今臨時把返回地址改了,可是用戶進程最終仍是要返回到原先那個返回地址去的。那麼,原先的返回地址及其調用棧應該保存在哪裏呢?進程的內核棧空間有限,而且還須要應付handle函數中可能發生的系統調用,因此內核把這些信息放在內核棧上是不現實的,只能壓到了用戶棧上去。
當handle函數執行完畢,執行流程要返回到內核去。一樣,因爲CPU特權級別不一樣,從handle函數返回內核時不能單純地利用RET指令去返回的。須要執行一次系統調用。
在handle執行完後,爲何要回到內核,再從內核返回到原始返回地址呢?若是直接返回到原始的返回地址那天然是很便捷。而且要這麼作也不難,原始返回地址及其調用棧已經被壓到了用戶棧上,內核只須要在handle函數的調用棧上稍作手腳就好了。
一、返回到原始返回地址並非回到那個地址就好了,須要把整個現場都恢復(主要是寄存器什麼的)。固然,內核也能夠在用戶棧上面壓一些代碼,來完成這些事情;
二、如今可能不止一個信號要處理,最好讓用戶進程返回內核,繼續處理其餘信號;
爲了返回內核,首先,內核在返回到handle函數以前,先將某個返回地址壓到用戶棧上,以便從handle返回時可以返回到指定的地址上。這個指定的地址其實也在進程的用戶棧上,內核又在這個地址上放了幾條指令(在棧上放置可執行代碼),讓進程去調用一個名叫sigreturn的系統調用。
返回到handle函數前的用戶棧大體以下:
原有數據 -> 調用sigreturn的指令(設其地址爲a) -> 原始返回地址及其調用棧 -> 返回地址(值爲a) -> handle的棧變量
內核在handle函數的調用棧上放置sigreturn指令,這是在linux 2.4時的作法。每次調用用戶的handle函數都須要向用戶棧拷貝這麼幾條指令,這並不太好。
linux 2.6有一個叫vsyscall page的頁面,上面包含了內核爲用戶程序準備的一些指令,其中就包括調用sigreturn指令。這個vsyscall頁被映射到每一個進程的虛擬地址空間靠近末尾的部分,被全部用戶進程共享,對於用戶進程是隻讀的。這樣,handle函數的調用棧上就不須要再塞入sigreturn指令了,直接將handle函數的返回地址設爲vsyscall頁中對應的代碼便可。
爲了讓handle執行完之後自動調用sigreturn返回內核,內核作了不少事情。那麼可不能夠約定好,讓用戶本身去調用sigreturn呢?
固然,這是能夠的。只是爲了讓信號處理機制成爲一套完整的機制,內核並無這麼作。不然用戶在handle函數裏面忘記調用sigreturn的話,可能莫名其妙地進程就崩潰了。而編譯器也很難找出這樣的錯誤。
進程調用sigreturn系統調用從新進入內核後,壓在用戶棧上的原始返回地址及其調用棧被獲取。最終內核又會修改棧,讓進程返回用戶空間時返回到這個原始返回地址上。