縱橫捭闔C++之從異步談起

一般來說,簡單的異步(Asynchronous)調用是這樣一種調用方式:發起者請求一個異步調用,通知執行 者,然後處理其他工作,在某一個同步點等待執行者的完成;執行者執行調用的實際操作,完成後通知發起者。可以看出,在異步調用中有兩種角色:發起者和執行 者,它們都是能主動運行的對象,我們稱爲主動對象,同時還有一個同步點,主動對象在同步點協調同步。在本文中,我們討論主要是通用計算機、多進程多線程的 分時操作系統上的異步調用。在操作系統的角度上來看,主動對象包括了進程、線程和硬件上的IC等,至於中斷,可以看作總是在某個進程或者線程的上下文借用 一下CPU。而同步操作可以通過操作系統得各種同步機制:互斥鎖,信號燈等等來完成。

我們可以先看看異步調用在Windows(本文中一般不加指出的話,都是特指NT/2000)讀寫文件中的應用。Windows中的ReadFile和 WriteFile都提供了異步的接口。以ReadFile爲例,

BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);

 如果最後一個 參數lpOverlapped不爲NULL,並且文件以FILE_FLAG_OVERLAPPED標誌打開,那麼這個調用就是異步的:ReadFile會 立刻返回,如果操作沒有立刻完成(返回FALSE並且GetLastError()返回ERROR_IO_PENDING),那麼調用者可以在某個時刻通 過WaitForSingleObject等函數來等待中的hEvent來等待操作完成(可能已經完成)進行同步,當操作完成以後,可以調用 GetOverlappedResult者獲得操作的結果,比如是否成功,讀取了多少字節等等。這裏的發起者就是應用程序,而執行者就是操作系統本身,至 於執行者是怎麼執行的,我們會在後面的篇幅討論。而兩者的同步就是通過一個Windows Event來完成。

把這個異步調用的過程 再抽象和擴展一些,我們可以把異步調用需要解決的問題歸結爲兩個:一個是執行的動力,另一個是主動對象的調度。簡單來說,前者是各個主動對象(線程、進程 或者一些代碼)是如何獲得CPU,後者是各個主動對象如何協同工作,保證操作的流程是協調正確的。一般來說,進程和線程都可以由操作系統直接調度而獲得 CPU,而更細粒度的,比如一些代碼的調度,往往就需要一個更復雜的模型(比如在操作系統內部的實現,這時候線程的粒度太粗了)。而主動對象的調度,當參 與者較少的時候,可以通過基本的同步機制來完成,在更復雜的情況下,可能通過一個schedule機制來做會更實際一些。

動力和調度

如前所述,異步調用主要需要解決兩個問題:執行的動力和執行的調度。最普遍的情況就是,一個主導流程的調用者進程(線程),一個或多個工作者進程(線 程),通過操作系統提供的同步機制來完成異步調用。這個同步機制在擴展化的情形下,是一個或多個柵欄Barrier,對應於每個同步的執行點。所有需要在 這個執行點同步的主動對象會等待相應的Barrier,直到所有對象都完成。在一些簡化的情形,比如說工作者並不關心調用者的同步,那麼這個 Barrier可以簡化成信號燈,在只有一個工作者的情況下,可以簡化成一個Windows事件Event或者條件變量 Condition Variable。

現在來考慮複雜的情形。假設我們用一些線程來協作完成一項工作,各個線程的執行之間有先後順序上的限制,而操作系 統就是這項工作的調度者,負責在適當的時候調度適當的線程來獲得CPU。顯然,併發執行中的一個線程對於另外一個線程來說,本質上就是異步的,假如它們之 間有調用關係,那也就是一個異步調用。而操作系統可以通過基本的同步機制使得合適的線程才被調度,其他未完成的線程則處於等待狀態。舉例說,我們有4個線 程A,B,C,D來完成一項工作,其中的順序限制是A>B;C>D,“>”表示左邊的線程完成必須先於右邊的線程執行,而“;”表示兩 個線程可以同時進行。同時假設B的一個操作需要調用C來完成,顯而易見,這時候這個操作就是一個異步調用。我們可以在每個“>”的位置設定一個同步 點,然後通過一個信號燈來完成同步。線程B,C等待第一個信號燈,而D會等待第二個信號燈。這個例子的動力和調度都是通過操作系統的基本機制(線程調度和 同步機制)來完成。

把這個過程抽象一下,可以描述爲:若干個主動對象(包括代碼)協調來完成一項工作,通過一個調度器來調度,實際 上,這個調度器可能只是一些調度規則。顯然,進程或者線程只要被調度就能獲得CPU,所以我們主要考慮代碼(比如一個函數)怎麼樣才能獲得執行。用工作者 線程來調用這個函數顯然是直觀和通用的一個方案。事實上,在用戶空間(user space)或者用戶態(user mode),這個方法是很常用的。而在內核態(kernel mode),則可以通過中斷來獲得CPU,這個通過註冊IDT入口和觸發軟中斷就可以完成。硬件設備上的IC是另一個動力之源。而主動對象的調度,最基本 的也是前面說的各種同步機制。另一個常用的機制就是回調函數,需要注意的是,回調函數一般會發生在跟調用者不一樣的上下文,比如說同一個進程的不同線程, 這個差別會帶來一些限制。如果需要回調發生在調用者的進程(線程)上下文,則需要一些類似Unix下的signal或者Windows下的APC機制,這 一點我們在後面會有所闡述。那麼在回調函數裏面一般作些什麼事情呢?最常用的,跟同步機制結合在一起,當然就是釋放一個互斥鎖,信號燈或者Windows Event(Unix的條件變量)等等,從而使得等待同步的其他對象可以得到調度而重新執行,實際上,也可以看作是通知調度器(操作系統)某些主動對象 (等待同步的)可以重新被調度了,從而調度器重新調度。但是對於另外一些調度器,在這個過程中可能不需要同步對象的參與。在一些極端一些的例子裏,調度甚 至不要求嚴格有序的。

在實際應用中,根據環境的限制,異步調用的動力和調度的實現方式可以有很大差別。我們會在後面的例子里加以說明。 操作系統中的異步:Windows的異步I/O。

Windows NT/2000是一個搶佔式的分時操作系統。Windows的調度單位是線程,它的 I/O架構是完全異步的,也就是說同步的I/O實際上都基於異步I/O來完成。一個用戶態的線程請求一個I/O的時候會導致一個運行狀態從user mode到kernel mode的轉變(操作系統把內核映射到每個進程的2G-4G的地址上,對於每個進程都是一樣的)。這個過程是通過中斷調用內核輸出的一些System Service來完成,比如說ReadFile實際上會執行NtReadFile(ZwReadFile),需要注意的是,運行上下文仍然是當前線程。 NtReadFile的實現則基於Windows內核的異步I/O框架,在I/O Manager的協助下完成。需要指出的是,I/O Manager只是由若干API構成的一個抽象概念,並沒有一個真正的I/O Manager線程在運行。

Windows的I/O驅 動程序是層次堆積的。每個驅動程序會提供一致的接口以供初始化、清理和功能調用。驅動程序的調用基於I/O請求包(I/O Request Packet, IRP),而不是像普通的函數調用那樣使用棧來傳遞參數。操作系統和PnP管理器根據註冊表在適當的時機初始化和清理相應的驅動程序。在一般的功能調用的 時候,IRP裏面會指定功能調用號碼以及相應的上下文或者參數(I/O stack location)。一個驅動程序可能調用別的驅動程序,這個過程可能是同步的(線程上下文不改變),也可能是異步的。NtReadFile的實現,大致 是向最上層的驅動程序發出一個或多個IRP,然後等待相應事件的完成(同步的情況),或者直接返回(帶Overlapped的情況),這些都在發起請求的 線程執行。

當驅動程序處理IRP的時候,它可能立刻完成,也可能在中斷裏才能完成,比如說,往硬件設備發出一個請求(通常可以是寫 I/O port),當設備完成操作的時候會觸發一箇中斷,然後在中斷處理函數裏得到操作結果。Windows有兩類中斷,硬件設備的中斷和軟中斷,分成若干個不 同的優先級(IRQL)。軟中斷主要有兩種:DPC(Delayed Procedure Call)和APC(Asynchronous Procedure Call),都處於較低的優先級。驅動程序可以爲硬件中斷註冊ISR(Interrupt Service Routine),一般就是修改IDT某個條目的入口。同樣,操作系統也會爲DPC和APC註冊適當的中斷處理例程(也是在IDT中)。

值得指出的是,DPC是跟處理器相關的,每個處理器會有一個DPC隊列,而APC是跟線程相關的,每個線程會有它的APC隊列(實際上包括一個 Kernel APC隊列和User APC隊列,它們的調度策略有所區別),可以想象,APC並不算嚴格意義上的中斷,因爲中斷可能發生在任何一個線程的上下文中,它被稱爲中斷,主要是因爲 IRQL的提升(從PASSIVE到APC),APC的調度一般在線程切換等等情形下進行。當中斷髮生的時候,操作系統會調用中斷處理例程,對於硬件設備 的ISR,一般處理是關設備中斷,發出一個DPC請求,然後返回。不在設備的中斷處理中使用太多的CPU時間,主要考慮是否則可能丟失別的中斷。由於硬件 設備中斷的IRQL比DPC中斷的高,所以在ISR裏面DPC會阻塞,直到ISR返回IRQL回到較低的水平,纔會觸發DPC中斷,在DPC中斷裏執行從 硬件設備讀取數據以及重新請求、開中斷等操作。ISR或者DPC可能在任何被中斷的線程上下文(arbitrary thread context)執行,事實上線程的上下文是不可見的,可以認爲是系統借用一下時間片而已。

總的來說,Windows的異步I/O架 構中,主要有兩種動力,一是發起請求的線程,一部分內核代碼會在這個線程上下文執行,二是ISR和DPC,這部分內核代碼會在中斷裏完成,可能使用任何一 個線程的上下文。而調度常見使用回調和事件(KEVENT),比如說在往下一層的驅動程序發出請求的時候,可以指定一個完成例程Completion Routine,當下層的驅動完成這個請求的時候會調用這個例程,而往往在這個例程裏,就是簡單的觸發一下一個事件。

 

發佈了53 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章