1. 程式人生 > >linux非同步訊號handle淺析

linux非同步訊號handle淺析

      在初學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函式的呼叫棧上稍做手腳就行了。
1、返回到原始返回地址並不是回到那個地址就行了,需要把整個現場都恢復(主要是暫存器什麼的)。當然,核心也可以在使用者棧上面壓一些程式碼,來完成這些事情;
2、現在可能不止一個訊號要處理,最好讓使用者程序返回核心,繼續處理其他訊號;

     為了返回核心,首先,核心在返回到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系統呼叫重新進入核心後,壓在使用者棧上的原始返回地址及其呼叫棧被獲取。最終核心又會修改棧,讓程序返回使用者空間時返回到這個原始返回地址上。