什麼是異步通信?很簡單,一旦設備准備好,就主動通知應用程序,這種情況下應用程序就不需要查詢設備狀態,就像硬件上常提的“中斷的概念”。比較准確的說法其實應該叫做“信號驅動的異步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、內核對信號的基本處理方法
內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位。這裡要補充的是,如果信號發送給一個正在睡眠的進程,那麼要看 該進程進入睡眠的優先級,如果進程睡眠在可被中斷的優先級上,則喚醒進程;否則僅設置進程表中信號域相應的位,而不喚醒進程。這一點比較重要,因為進程檢 查是否收到信號的時機是:一個進程在即將從內核態返回到用戶態時;或者,在一個進程要進入或離開一個適當的低調度優先級睡眠狀態時。
內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。所以,當一個進程在內核態下運行時,軟中斷信號並不立即起作用,要等到將返回用戶態時才處理。進程只有處理完信號才會返回用戶態,進程在用戶態下不會有未處理完的信號。
內核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處於運行狀態。前面介紹概念的時候講過,處理信號有三種類型:進程接收到信號後退 出;進程忽略該信號;進程收到信號後執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似 的繼續運行。如果進程收到一個要捕捉的信號,那麼進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創 建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時,
才返回原先進入內核的地方。這樣做的原因是用戶定義的處理函數不能且不允許在內核態下執行(如果用戶定義的函數在內核態下運行的話,用戶就可以獲得任何權限)。
在信號的處理方法中有幾點特別要引起注意。第一,在一些系統中,當一個進程處理完中斷信號返回用戶態之前,內核清除用戶區中設 定的對該信號的處理例程的地址,即下一次進程對該信號的處理方法又改為默認值,除非在下一次信號到來之前再次使用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,使得進程知道該次系統調用失敗。這就是它們的作用。