1. 程式人生 > >linux核心中非同步通知機制--訊號處理機制

linux核心中非同步通知機制--訊號處理機制

       什麼是非同步通知:很簡單,一旦裝置準備好,就主動通知應用程式,這種情況下應用程式就不需要查詢裝置狀態, 特像硬體上常提的“中斷的概念”。 比較準確的說法其實應該叫做“訊號驅動的非同步I/O”,訊號是在軟體層次上對中斷機制的一種模擬。阻塞I/O意味著一直等待裝置可訪問再訪問,非阻塞I/O意味著使用poll()來查詢是否可訪問,而非同步通知則意味著裝置通知應用程式自身可訪問。(希望用這麼一句話能表達我的意思)

一、系統中存在的非同步機制

       我認為非同步機制是一種理念,並不是某一種具體實現,同步/非同步的核心理解應該是如何獲取訊息的問題,你自身(在計算機中當然是程序本身了)親自去獲取訊息,那麼就是同步機制,但是如果別人使用某種方式通知你某一個訊息,那麼你採用的就是非同步機制。核心中使用到非同步機制的大概有:訊號,這是一種程序間通訊的非同步機制;epoll,這是一種高效處理IO的非同步通訊機制。也就是從通訊和IO兩個方面通過不同的方式使用了非同步機制。(可能還有別的,暫時不知道)

      下面進入正題:

二、訊號的基本概念      

       1)訊號的本質

       軟中斷訊號(signal,又簡稱為訊號)用來通知程序發生了非同步事件。在軟體層次上是對中斷機制的一種模擬,在原理上,一個程序收到一個訊號與處理器收到一箇中斷請求可以說是一樣的。訊號是程序間通訊機制中唯一的非同步通訊機制一個程序不必通過任何操作來等待訊號的到達,事實上,程序也不知道訊號到底什麼時候到達。程序之間可以互相通過系統呼叫kill傳送軟中斷訊號。核心也可以因為內部事件而給程序傳送訊號,通知程序發生了某個事件。訊號機制除了基本通知功能外,還可以傳遞附加資訊。

收到訊號的程序對各種訊號有不同的處理方法。處理方法可以分為三類:

第一種是類似中斷的處理程式,對於需要處理的訊號,程序可以指定處理函數,由該函式來處理。

第二種方法是,忽略某個訊號,對該訊號不做任何處理,就象未發生過一樣。

第三種方法是,對該訊號的處理保留系統的預設值,這種預設操作,對大部分的訊號的預設操作是使得程序終止。程序通過系統呼叫signal來指定程序對某個訊號的處理行為。

        在程序表的表項中有一個軟中斷訊號域,該域中每一位對應一個訊號,當有訊號傳送給程序時,對應位置位。由此可以看出,程序對不同的訊號可以同時保留,但對於同一個訊號,程序並不知道在處理之前來過多少個。

       2)訊號的種類

可以從兩個不同的分類角度對訊號進行分類:

可靠性方面:可靠訊號與不可靠訊號;

與時間的關係上:實時訊號與非實時訊號。

         3)可靠訊號與不可靠訊號

Linux訊號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的訊號機制比較簡單和原始,訊號值小於SIGRTMIN的訊號都是不可靠訊號。這就是"不可靠訊號"的來源。它的主要問題是訊號可能丟失。

        隨著時間的發展,實踐證明了有必要對訊號的原始機制加以改進和擴充。由於原來定義的訊號已有許多應用,不好再做改動,最終只好又新增加了一些訊號,並在一開始就把它們定義為可靠訊號,這些訊號支援排隊,不會丟失

訊號值位於SIGRTMIN和SIGRTMAX之間的訊號都是可靠訊號,可靠訊號克服了訊號可能丟失的問題。Linux在支援新版本的訊號安裝函式sigation()以及訊號傳送函式sigqueue()的同時,仍然支援早期的signal()訊號安裝函式,支援訊號傳送函式kill()。

訊號的可靠與不可靠只與訊號值有關,與訊號的傳送及安裝函式無關。目前linux中的signal()是通過sigation()函式實現的,因此,即使通過signal()安裝的訊號,在訊號處理函式的結尾也不必再呼叫一次訊號安裝函式。同時,由signal()安裝的實時訊號支援排隊,同樣不會丟失。

對於目前linux的兩個訊號安裝函式:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的訊號變成可靠訊號(都不支援排隊,仍有可能丟失,仍然是不可靠訊號),而且對SIGRTMIN以後的訊號都支援排隊。這兩個函式的最大區別在於,經過sigaction安裝的訊號都能傳遞資訊給訊號處理函式,而經過signal安裝的訊號不能向訊號處理函式傳遞資訊。對於訊號傳送函式來說也是一樣的。

        4)實時訊號與非實時訊號

早期Unix系統只定義了32種訊號,前32種訊號已經有了預定義值,每個訊號有了確定的用途及含義,並且每種訊號都有各自的預設動作。如按鍵盤的CTRL ^C時,會產生SIGINT訊號,對該訊號的預設反應就是程序終止。後32個訊號表示實時訊號,等同於前面闡述的可靠訊號。這保證了傳送的多個實時訊號都被接收。

非實時訊號都不支援排隊,都是不可靠訊號;實時訊號都支援排隊,都是可靠訊號。

      5)linux 下訊號的生命週期如下:
        在目的程序中安裝該訊號。即是設定捕獲該訊號時程序程序該執行的操作碼。採用signal();sigaction()系統呼叫來實現。
            訊號被某個程序產生,同時設定該訊號的目的程序(使用pid),之後交給作業系統進行管理。採用kill()、arise()、alarm()等系統呼叫來實現。
            訊號在目的程序被註冊。訊號被新增進程序的PCB(task_struct)中相關的資料結構裡——未決訊號的資料成員。訊號在程序中註冊就是把訊號值加入到程序的未決訊號集裡。
                並且,訊號攜帶的其他資訊被保留到未決信的佇列的某個sigqueue結構中。
            訊號在程序中登出。在執行訊號處理函式前,要把訊號在程序中登出。對於非實時訊號(不可靠訊號),其在訊號未決訊號資訊鏈中最多隻有一個sigqueue結構,因此該結構被釋放後,相應的訊號要在未決訊號集刪除。而實時訊號(可靠訊號),如果有多個sigqueue,則不會把訊號從程序的未決訊號集中刪除。
            訊號生命的終結。程序終止當前的工作,保護上下文,執行訊號處理函式,之後回覆。如果核心是可搶佔的,那麼還需要排程。

三、信 號 機 制

       上 一節中介紹了訊號的基本概念,在這一節中,我們將介紹核心如何實現訊號機制。即核心如何向一個程序傳送訊號、程序如何接收一個訊號、程序怎樣控制自己對信 號的反應、核心在什麼時機處理和怎樣處理程序收到的訊號。還要介紹一下setjmp和longjmp在訊號中起到的作用。
1、核心對訊號的基本處理方法

內 核給一個程序傳送軟中斷訊號的方法,是在程序所在的程序表項的訊號域設定對應於該訊號的位。這裡要補充的是,如果訊號傳送給一個正在睡眠的程序,那麼要看 該程序進入睡眠的優先順序,如果程序睡眠在可被中斷的優先順序上,則喚醒程序;否則僅設定程序表中訊號域相應的位,而不喚醒程序。這一點比較重要,因為程序檢 查是否收到訊號的時機是:一個程序在即將從核心態返回到使用者態時;或者,在一個程序要進入或離開一個適當的低排程優先順序睡眠狀態時。

程序的task_struct結構中有關於本程序中未決訊號的資料成員:struct sigpending pending:

struct sigpending{

        struct sigqueue *head, *tail;

        sigset_t signal;

};

第三個成員是程序中所有未決訊號集,第一、第二個成員分別指向一個sigqueue型別的結構鏈(稱之為"未決訊號資訊鏈")的首尾,資訊鏈中的每個sigqueue結構刻畫一個特定訊號所攜帶的資訊,並指向下一個sigqueue結構:

struct sigqueue{

        struct sigqueue *next;

        siginfo_t info;

}

訊號在程序中註冊指的就是訊號值加入到程序的未決訊號集sigset_t signal(每個訊號佔用一位)中,並且訊號所攜帶的資訊被保留到未決訊號資訊鏈的某個sigqueue結構中。只要訊號在程序的未決訊號集中,表明程序已經知道這些訊號的存在,但還沒來得及處理,或者該訊號被程序阻塞。

當一個實時訊號傳送給一個程序時,不管該訊號是否已經在程序中註冊,都會被再註冊一次,因此,訊號不會丟失,因此,實時訊號又叫做"可靠訊號"。這意味著同一個實時訊號可以在同一個程序的未決訊號資訊鏈中佔有多個sigqueue結構(程序每收到一個實時訊號,都會為它分配一個結構來登記該訊號資訊,並把該結構新增在未決訊號鏈尾,即所有誕生的實時訊號都會在目標程序中註冊)。

當一個非實時訊號傳送給一個程序時,如果該訊號已經在程序中註冊(通過sigset_t signal指示),則該訊號將被丟棄,造成訊號丟失。因此,非實時訊號又叫做"不可靠訊號"。這意味著同一個非實時訊號在程序的未決訊號資訊鏈中,至多佔有一個sigqueue結構

總之訊號註冊與否,與傳送訊號的函式(如kill()或sigqueue()等)以及訊號安裝函式(signal()及sigaction())無關,只與訊號值有關(訊號值小於SIGRTMIN的訊號最多隻註冊一次,訊號值在SIGRTMIN及SIGRTMAX之間的訊號,只要被程序接收到就被註冊)


核心處理一個程序收到的訊號的時機是在一個程序從核心態返回使用者態時。所以,當一個程序在核心態下執行時,軟中斷訊號並不立即起作用,要等到將返回使用者態時才處理。程序只有處理完訊號才會返回使用者態,程序在使用者態下不會有未處理完的訊號。 

內 核處理一個程序收到的軟中斷訊號是在該程序的上下文中,因此,程序必須處於執行狀態。前面介紹概念的時候講過,處理訊號有三種類型:程序接收到訊號後退 出;程序忽略該訊號;程序收到訊號後執行使用者設定用系統呼叫signal的函式。當程序接收到一個它忽略的訊號時,程序丟棄該訊號,就象沒有收到該訊號似 的繼續執行。如果程序收到一個要捕捉的訊號,那麼程序從核心態返回使用者態時執行使用者定義的函式。而且執行使用者定義的函式的方法很巧妙,核心是在使用者棧上創 建一個新的層,該層中將返回地址的值設定成使用者定義的處理函式的地址,這樣程序從核心返回彈出棧頂時就返回到使用者定義的函式處,從函式返回再彈出棧頂時, 才返回原先進入核心的地方。這樣做的原因是使用者定義的處理函式不能且不允許在核心態下執行(如果使用者定義的函式在核心態下執行的話,使用者就可以獲得任何權 限)。 

對於非實時訊號來說,由於在未決訊號資訊鏈中最多隻佔用一個sigqueue結構,因此該結構被釋放後,應該把訊號在程序未決訊號集中刪除(訊號登出完畢);而對於實時訊號來說,可能在未決訊號資訊鏈中佔用多個sigqueue結構,因此應該針對佔用sigqueue結構的數目區別對待:如果只佔用一個sigqueue結構(程序只收到該訊號一次),則執行完相應的處理函式後應該把訊號在程序的未決訊號集中刪除(訊號登出完畢)。否則待該訊號的所有sigqueue處理完畢後再在程序的未決訊號集中刪除該訊號。

當所有未被遮蔽的訊號都處理完畢後,即可返回使用者空間。對於被遮蔽的訊號,當取消遮蔽後,在返回到使用者空間時會再次執行上述檢查處理的一套流程。

    

     在訊號的處理方法中有幾點特別要引起注意。第一,在一些系統中,當一個程序處理完中斷訊號返回使用者態之前,核心清除使用者區中設 定的對該訊號的處理例程的地址,即下一次程序對該訊號的處理方法又改為預設值,除非在下一次訊號到來之前再次使用signal系統呼叫。這可能會使得程序 在呼叫signal之前又得到該訊號而導致退出。在BSD中,核心不再清除該地址。但不清除該地址可能使得程序因為過多過快的得到某個訊號而導致堆疊溢 出。為了避免出現上述情況。在BSD系統中,核心模擬了對硬體中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。 

第二個要 引起注意的是,如果要捕捉的訊號發生於程序正在一個系統呼叫中時,並且該程序睡眠在可中斷的優先順序上,這時該訊號引起程序作一次longjmp,跳出睡眠 狀態,返回使用者態並執行訊號處理例程。當從訊號處理例程返回時,程序就象從系統呼叫返回一樣,但返回了一個錯誤程式碼,指出該次系統呼叫曾經被中斷。這要注 意的是,BSD系統中核心可以自動地重新開始系統呼叫。 

第三個要注意的地方:若程序睡眠在可中斷的優先順序上,則當它收到一個要忽略的訊號時,該程序被喚醒,但不做longjmp,一般是繼續睡眠。但使用者感覺不到程序曾經被喚醒,而是象沒有發生過該訊號一樣。 

第 四個要注意的地方:核心對子程序終止(SIGCLD)訊號的處理方法與其他訊號有所區別。當程序檢查出收到了一個子程序終止的訊號時,預設情況下,該程序 就象沒有收到該訊號似的,如果父程序執行了系統呼叫wait,程序將從系統呼叫wait中醒來並返回wait呼叫,執行一系列wait呼叫的後續操作(找 出僵死的子程序,釋放子程序的程序表項),然後從wait中返回。SIGCLD訊號的作用是喚醒一個睡眠在可被中斷優先順序上的程序。如果該程序捕捉了這個 訊號,就象普通訊號處理一樣轉到處理例程。如果程序忽略該訊號,那麼系統呼叫wait的動作就有所不同,因為SIGCLD的作用僅僅是喚醒一個睡眠在可被 中斷優先順序上的程序,那麼執行wait呼叫的父程序被喚醒繼續執行wait呼叫的後續操作,然後等待其他的子程序。 

如果一個程序呼叫signal系統呼叫,並設定了SIGCLD的處理方法,並且該程序有子程序處於僵死狀態,則核心將向該程序發一個SIGCLD訊號。 2、setjmp和longjmp的作用 

前面在介紹訊號處理機制時,多次提到了setjmp和longjmp,但沒有仔細說明它們的作用和實現方法。這裡就此作一個簡單的介紹。 

在 介紹訊號的時候,我們看到多個地方要求程序在檢查收到訊號後,從原來的系統呼叫中直接返回,而不是等到該呼叫完成。這種程序突然改變其上下文的情況,就是 使用setjmp和longjmp的結果。setjmp將儲存的上下文存入使用者區,並繼續在舊的上下文中執行。這就是說,程序執行一個系統呼叫,當因為資 源或其他原因要去睡眠時,核心為程序作了一次setjmp,如果在睡眠中被訊號喚醒,程序不能再進入睡眠時,核心為程序呼叫longjmp,該操作是核心 為程序將原先setjmp呼叫儲存在程序使用者區的上下文恢復成現在的上下文,這樣就使得程序可以恢復等待資源前的狀態,而且核心為setjmp返回1,使 得程序知道該次系統呼叫失敗。這就是它們的作用。