簡(jiǎn)介
一般來(lái)說(shuō),簡(jiǎn)單的異步(Asynchronous) 調(diào)用是這樣一種調(diào)用方式:發(fā)起者請(qǐng)求一個(gè)異步調(diào)用,通知執(zhí)行者,然后處理其他工作,在某一個(gè)同步點(diǎn)等待執(zhí)行者的完成;執(zhí)行者執(zhí)行調(diào)用的實(shí)際操作,完成后通 知發(fā)起者。可以看出,在異步調(diào)用中有兩種角色:發(fā)起者和執(zhí)行者,它們都是能主動(dòng)運(yùn)行的對(duì)象,我們稱為主動(dòng)對(duì)象,同時(shí)還有一個(gè)同步點(diǎn),主動(dòng)對(duì)象在同步點(diǎn)協(xié)調(diào) 同步。在本文中,我們討論主要是通用計(jì)算機(jī)、多進(jìn)程多線程的分時(shí)操作系統(tǒng)上的異步調(diào)用。在操作系統(tǒng)的角度上來(lái)看,主動(dòng)對(duì)象包括了進(jìn)程、線程和硬件上的IC等,至于中斷,可以看作總是在某個(gè)進(jìn)程或者線程的上下文借用一下CPU。而同步操作可以通過(guò)操作系統(tǒng)得各種同步機(jī)制:互斥鎖,信號(hào)燈等等來(lái)完成。
我們可以先看看異步調(diào)用在Windows(本文中一般不加指出的話,都是特指NT/2000)讀寫(xiě)文件中的應(yīng)用。Windows中的ReadFile和WriteFile都提供了異步的接口。以ReadFile為例,
BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);
如果最后一個(gè)參數(shù)lpOverlapped不為NULL,并且文件以FILE_FLAG_OVERLAPPED 標(biāo)志打開(kāi),那么這個(gè)調(diào)用就是異步的:ReadFile會(huì)立刻返回,如果操作沒(méi)有立刻完成(返回FALSE并且GetLastError()返回 ERROR_IO_PENDING),那么調(diào)用者可以在某個(gè)時(shí)刻通過(guò)WaitForSingleObject等函數(shù)來(lái)等待中的hEvent來(lái)等待操作完成 (可能已經(jīng)完成)進(jìn)行同步,當(dāng)操作完成以后,可以調(diào)用GetOverlappedResult者獲得操作的結(jié)果,比如是否成功,讀取了多少字節(jié)等等。這里 的發(fā)起者就是應(yīng)用程序,而執(zhí)行者就是操作系統(tǒng)本身,至于執(zhí)行者是怎么執(zhí)行的,我們會(huì)在后面的篇幅討論。而兩者的同步就是通過(guò)一個(gè)Windows Event來(lái)完成。
把這個(gè)異步調(diào)用的過(guò)程再抽象和擴(kuò)展一些,我們可以把異步調(diào)用需要解決的問(wèn)題歸結(jié)為兩個(gè):一個(gè)是執(zhí)行的動(dòng)力,另一個(gè)是主動(dòng)對(duì)象的調(diào)度。簡(jiǎn)單來(lái)說(shuō),前者是各個(gè)主動(dòng)對(duì)象(線程、進(jìn)程或者一些代碼)是如何獲得CPU,后者是各個(gè)主動(dòng)對(duì)象如何協(xié)同工作, 保證操作的流程是協(xié)調(diào)正確的。一般來(lái)說(shuō),進(jìn)程和線程都可以由操作系統(tǒng)直接調(diào)度而獲得CPU,而更細(xì)粒度的,比如一些代碼的調(diào)度,往往就需要一個(gè)更復(fù)雜的模 型(比如在操作系統(tǒng)內(nèi)部的實(shí)現(xiàn),這時(shí)候線程的粒度太粗了)。而主動(dòng)對(duì)象的調(diào)度,當(dāng)參與者較少的時(shí)候,可以通過(guò)基本的同步機(jī)制來(lái)完成,在更復(fù)雜的情況下,可 能通過(guò)一個(gè)schedule機(jī)制來(lái)做會(huì)更實(shí)際一些。
動(dòng)力和調(diào)度
如前所述,異步調(diào)用主要需要解決兩 個(gè)問(wèn)題:執(zhí)行的動(dòng)力和執(zhí)行的調(diào)度。最普遍的情況就是,一個(gè)主導(dǎo)流程的調(diào)用者進(jìn)程(線程),一個(gè)或多個(gè)工作者進(jìn)程(線程),通過(guò)操作系統(tǒng)提供的同步機(jī)制來(lái)完 成異步調(diào)用。這個(gè)同步機(jī)制在擴(kuò)展化的情形下,是一個(gè)或多個(gè)柵欄Barrier,對(duì)應(yīng)于每個(gè)同步的執(zhí)行點(diǎn)。所有需要在這個(gè)執(zhí)行點(diǎn)同步的主動(dòng)對(duì)象會(huì)等待相應(yīng)的 Barrier,直到所有對(duì)象都完成。在一些簡(jiǎn)化的情形,比如說(shuō)工作者并不關(guān)心調(diào)用者的同步,那么這個(gè)Barrier可以簡(jiǎn)化成信號(hào)燈,在只有一個(gè)工作者 的情況下,可以簡(jiǎn)化成一個(gè)Windows事件Event或者條件變量 Condition Variable。
現(xiàn)在來(lái)考慮復(fù)雜的情 形。假設(shè)我們用一些線程來(lái)協(xié)作完成一項(xiàng)工作,各個(gè)線程的執(zhí)行之間有先后順序上的限制,而操作系統(tǒng)就是這項(xiàng)工作的調(diào)度者,負(fù)責(zé)在適當(dāng)?shù)臅r(shí)候調(diào)度適當(dāng)?shù)木€程來(lái) 獲得CPU。顯然,并發(fā)執(zhí)行中的一個(gè)線程對(duì)于另外一個(gè)線程來(lái)說(shuō),本質(zhì)上就是異步的,假如它們之間有調(diào)用關(guān)系,那也就是一個(gè)異步調(diào)用。而操作系統(tǒng)可以通過(guò)基 本的同步機(jī)制使得合適的線程才被調(diào)度,其他未完成的線程則處于等待狀態(tài)。舉例說(shuō),我們有4個(gè)線程A,B,C,D來(lái)完成一項(xiàng)工作,其中的順序限制是 A>B;C>D,“>”表示左邊的線程完成必須先于右邊的線程執(zhí)行,而“;”表示兩個(gè)線程可以同時(shí)進(jìn)行。同時(shí)假設(shè)B的一個(gè)操作需要調(diào)用 C來(lái)完成,顯而易見(jiàn),這時(shí)候這個(gè)操作就是一個(gè)異步調(diào)用。我們可以在每個(gè)“>”的位置設(shè)定一個(gè)同步點(diǎn),然后通過(guò)一個(gè)信號(hào)燈來(lái)完成同步。線程B,C等待 第一個(gè)信號(hào)燈,而D會(huì)等待第二個(gè)信號(hào)燈。這個(gè)例子的動(dòng)力和調(diào)度都是通過(guò)操作系統(tǒng)的基本機(jī)制(線程調(diào)度和同步機(jī)制)來(lái)完成。
把這個(gè)過(guò)程抽象一下,可以描述為:若干個(gè)主動(dòng)對(duì)象(包括代碼)協(xié)調(diào)來(lái)完成一項(xiàng)工作,通過(guò)一個(gè)調(diào)度器來(lái)調(diào)度,實(shí)際上,這個(gè)調(diào)度器可能只是一些調(diào)度規(guī)則。顯 然,進(jìn)程或者線程只要被調(diào)度就能獲得CPU,所以我們主要考慮代碼(比如一個(gè)函數(shù))怎么樣才能獲得執(zhí)行。用工作者線程來(lái)調(diào)用這個(gè)函數(shù)顯然是直觀和通用的一 個(gè)方案。事實(shí)上,在用戶空間(user space)或者用戶態(tài)(user mode),這個(gè)方法是很常用的。而在內(nèi)核態(tài)(kernel mode),則可以通過(guò)中斷來(lái)獲得CPU,這個(gè)通過(guò)注冊(cè)IDT入 口和觸發(fā)軟中斷就可以完成。硬件設(shè)備上的IC是另一個(gè)動(dòng)力之源。而主動(dòng)對(duì)象的調(diào)度,最基本的也是前面說(shuō)的各種同步機(jī)制。另一個(gè)常用的機(jī)制就是回調(diào)函數(shù),需 要注意的是,回調(diào)函數(shù)一般會(huì)發(fā)生在跟調(diào)用者不一樣的上下文,比如說(shuō)同一個(gè)進(jìn)程的不同線程,這個(gè)差別會(huì)帶來(lái)一些限制。如果需要回調(diào)發(fā)生在調(diào)用者的進(jìn)程(線 程)上下文,則需要一些類似Unix下的signal或者Windows下的APC機(jī)制,這一點(diǎn)我們?cè)诤竺鏁?huì)有所闡述。那么在回調(diào)函數(shù)里面一般作些什么事情呢?最常用的, 跟同步機(jī)制結(jié)合在一起,當(dāng)然就是釋放一個(gè)互斥鎖,信號(hào)燈或者Windows Event(Unix的條件變量)等等,從而使得等待同步的其他對(duì)象可以得到調(diào)度而重新執(zhí)行,實(shí)際上,也可以看作是通知調(diào)度器(操作系統(tǒng))某些主動(dòng)對(duì)象 (等待同步的)可以重新被調(diào)度了,從而調(diào)度器重新調(diào)度。但是對(duì)于另外一些調(diào)度器,在這個(gè)過(guò)程中可能不需要同步對(duì)象的參與。在一些極端一些的例子里,調(diào)度甚 至不要求嚴(yán)格有序的。
在實(shí)際應(yīng)用中,根據(jù)環(huán)境的限制,異步調(diào)用的動(dòng)力和調(diào)度的實(shí)現(xiàn)方式可以有很大差別。我們會(huì)在后面的例子里加以說(shuō)明。 操作系統(tǒng)中的異步:Windows的異步I/O。
Windows NT/2000是一個(gè)搶占式的分時(shí)操作系統(tǒng)。Windows的調(diào)度單位是線程,它的 I/O架構(gòu)是完全異步的,也就是說(shuō)同步的I/O實(shí)際上都基于異步I/O來(lái)完成。一個(gè)用戶態(tài)的線程請(qǐng)求一個(gè)I/O的時(shí)候會(huì)導(dǎo)致一個(gè)運(yùn)行狀態(tài)從user mode到kernel mode的轉(zhuǎn)變(操作系統(tǒng)把內(nèi)核映射到每個(gè)進(jìn)程的2G-4G的地址上,對(duì)于每個(gè)進(jìn)程都是一樣的)。這個(gè)過(guò)程是通過(guò)中斷調(diào)用內(nèi)核輸出的一些System Service來(lái)完成,比如說(shuō)ReadFile實(shí)際上會(huì)執(zhí)行NtReadFile(ZwReadFile),需要注意的是,運(yùn)行上下文仍然是當(dāng)前線程。 NtReadFile的實(shí)現(xiàn)則基于Windows內(nèi)核的異步I/O框架,在I/O Manager的協(xié)助下完成。需要指出的是,I/O Manager只是由若干API構(gòu)成的一個(gè)抽象概念,并沒(méi)有一個(gè)真正的I/O Manager線程在運(yùn)行。
Windows的I/O驅(qū)動(dòng)程序是層次堆積的。每個(gè)驅(qū)動(dòng)程序會(huì)提供一致的接口以供初始化、清理和功能調(diào)用。驅(qū)動(dòng)程序的調(diào)用基于I/O請(qǐng)求包(I/O Request Packet, IRP),而不是像普通的函數(shù)調(diào)用那樣使用棧來(lái)傳遞參數(shù)。操作系統(tǒng)和PnP管理器根據(jù)注冊(cè)表在 適當(dāng)?shù)臅r(shí)機(jī)初始化和清理相應(yīng)的驅(qū)動(dòng)程序。在一般的功能調(diào)用的時(shí)候,IRP里面會(huì)指定功能調(diào)用號(hào)碼以及相應(yīng)的上下文或者參數(shù)(I/O stack location)。一個(gè)驅(qū)動(dòng)程序可能調(diào)用別的驅(qū)動(dòng)程序,這個(gè)過(guò)程可能是同步的(線程上下文不改變),也可能是異步的。NtReadFile的實(shí)現(xiàn),大致 是向最上層的驅(qū)動(dòng)程序發(fā)出一個(gè)或多個(gè)IRP,然后等待相應(yīng)事件的完成(同步的情況),或者直接返回(帶Overlapped的情況),這些都在發(fā)起請(qǐng)求的 線程執(zhí)行。
當(dāng)驅(qū)動(dòng)程序處理IRP的時(shí)候,它可能立刻完成,也可能在中斷里才能完成,比如說(shuō),往硬件設(shè)備發(fā)出一個(gè)請(qǐng)求(通常可以是寫(xiě) I/O port),當(dāng)設(shè)備完成操作的時(shí)候會(huì)觸發(fā)一個(gè)中斷,然后在中斷處理函數(shù)里得到操作結(jié)果。Windows有兩類中斷,硬件設(shè)備的中斷和軟中斷,分成若干個(gè)不 同的優(yōu)先級(jí)(IRQL)。軟中斷主要有兩種:DPC(Delayed Procedure Call)和APC(Asynchronous Procedure Call),都處于較低的優(yōu)先級(jí)。驅(qū)動(dòng)程序可以為硬件中斷注冊(cè)ISR(Interrupt Service Routine),一般就是修改IDT某個(gè)條目的入口。同樣,操作系統(tǒng)也會(huì)為DPC和APC注冊(cè)適當(dāng)?shù)闹袛嗵幚砝蹋ㄒ彩窃贗DT中)。
值得指出的是,DPC是跟處理器相關(guān)的,每個(gè)處理器會(huì)有一個(gè)DPC隊(duì)列,而APC是跟線程相關(guān)的,每個(gè)線程會(huì)有它的APC隊(duì)列(實(shí)際上包括一個(gè) Kernel APC隊(duì)列和User APC隊(duì)列,它們的調(diào)度策略有所區(qū)別),可以想象,APC并不算嚴(yán)格意義上的中斷,因?yàn)橹袛嗫赡馨l(fā)生在任何一個(gè)線程的上下文中,它被稱為中斷,主要是因?yàn)?IRQL的提升(從PASSIVE到APC),APC的調(diào)度一般在線程切換等等情形下進(jìn)行。當(dāng)中斷發(fā)生的時(shí)候,操作系統(tǒng)會(huì)調(diào)用中斷處理例程,對(duì)于硬件設(shè)備 的ISR,一般處理是關(guān)設(shè)備中斷,發(fā)出一個(gè)DPC請(qǐng)求,然后返回。不在設(shè)備的中斷處理中使用太多的CPU時(shí)間,主要考慮是否則可能丟失別 的中斷。由于硬件設(shè)備中斷的IRQL比DPC中斷的高,所以在ISR里面DPC會(huì)阻塞,直到ISR返回IRQL回到較低的水平,才會(huì)觸發(fā)DPC中斷,在 DPC中斷里執(zhí)行從硬件設(shè)備讀取數(shù)據(jù)以及重新請(qǐng)求、開(kāi)中斷等操作。ISR或者DPC可能在任何被中斷的線程上下文(arbitrary thread context)執(zhí)行,事實(shí)上線程的上下文是不可見(jiàn)的,可以認(rèn)為是系統(tǒng)借用一下時(shí)間片而已。
總的來(lái)說(shuō),Windows的異步I/O架 構(gòu)中,主要有兩種動(dòng)力,一是發(fā)起請(qǐng)求的線程,一部分內(nèi)核代碼會(huì)在這個(gè)線程上下文執(zhí)行,二是ISR和DPC,這部分內(nèi)核代碼會(huì)在中斷里完成,可能使用任何一 個(gè)線程的上下文。而調(diào)度常見(jiàn)使用回調(diào)和事件(KEVENT),比如說(shuō)在往下一層的驅(qū)動(dòng)程序發(fā)出請(qǐng)求的時(shí)候,可以指定一個(gè)完成例程Completion Routine,當(dāng)下層的驅(qū)動(dòng)完成這個(gè)請(qǐng)求的時(shí)候會(huì)調(diào)用這個(gè)例程,而往往在這個(gè)例程里,就是簡(jiǎn)單的觸發(fā)一下一個(gè)事件。 另外可以順便提一下Linux。Linux 2.6也有類似的中斷機(jī)制,它有更多的軟中斷優(yōu)先級(jí),即不同優(yōu)先級(jí)的softirq,而類似于DPC,Linux也提供了專門(mén)的軟中斷,對(duì)應(yīng)DPC的就是 tasklet。Linux沒(méi)有一個(gè)像windows這么一致的層次驅(qū)動(dòng)程序架構(gòu),所以它的異步I/O稍微粗糙一些,主要是通過(guò)以前的一些阻塞點(diǎn),現(xiàn)在直 接返回-EIOCBRETRY,而讓調(diào)用者在合適的時(shí)機(jī)繼續(xù)重試。在這個(gè)方法中,可以認(rèn)為整個(gè)操作由一個(gè)函數(shù)完成,每次操作有進(jìn)展時(shí),都把這個(gè)函數(shù)從頭執(zhí) 行一遍,當(dāng)然已經(jīng)完成的部分就不會(huì)再有實(shí)際的I/O。這樣的最大好處是原有的文件系統(tǒng)和驅(qū)動(dòng)程序不用完全重寫(xiě)。而對(duì)于同步調(diào)用,只要阻塞就可以了,這樣對(duì) 系統(tǒng)的修改較小。這時(shí)候,要提供POSIX aio的語(yǔ)義,就可能需要提供一些用戶線程來(lái)完成重試的過(guò)程了(回想Windows可以通過(guò)中斷和DPC完成的)。而對(duì)于Solaris,也是類似的處理,如果設(shè)備支持異步I/O,那就通過(guò)中斷可以完成,否則就使用內(nèi)部的LWP來(lái)模擬。
應(yīng)用程序:一個(gè)異步的HTTP服務(wù)器的設(shè)計(jì)
假設(shè)我們要設(shè)計(jì)一個(gè)HTTP服務(wù)器,它的設(shè)計(jì)目標(biāo)包括:高并發(fā)性、精簡(jiǎn) (部分支持HTTP/1.1)、支持plug-in結(jié)構(gòu)。在不少場(chǎng)合可能都有這個(gè)需求。總體上來(lái)說(shuō),HTTP服務(wù)器可以類比成一個(gè)基于多線程的操作系 統(tǒng):OS調(diào)度每個(gè)工作線程在適當(dāng)?shù)臅r(shí)候獲得執(zhí)行,而工作線程提供服務(wù)(也就是處理HTTP請(qǐng)求)。在這個(gè)基礎(chǔ)上,主要的考慮就是調(diào)度粒度的大小,粒度太大 的時(shí)候并發(fā)性會(huì)降低,而粒度太小又可能因?yàn)槿蝿?wù)切換(考慮OS的Context Switching)而導(dǎo)致效率降低,所以這又是一個(gè)折衷的結(jié)果。類似于Apache(以及其他的HTTP服務(wù)器),我們可以把一個(gè)HTTP處理過(guò)程分為 若干個(gè)狀態(tài),基于這些狀態(tài)可以構(gòu)造出一個(gè)HTTP處理的狀態(tài)機(jī)。這種情況下,我們就可以把每個(gè)狀態(tài)的處理作為調(diào)度的粒度。一個(gè)調(diào)度過(guò)程就是:一個(gè)工作線程 從全局的任務(wù)隊(duì)列里取出一個(gè)HTTP_Context結(jié)構(gòu);根據(jù)當(dāng)前的狀態(tài)完成相應(yīng)處理;然后根據(jù)狀態(tài)機(jī)設(shè)置下一個(gè)狀態(tài);再放回到全局的任務(wù)隊(duì)列里。這樣 子,若干個(gè)HTTP狀態(tài)就可以通過(guò)這個(gè)調(diào)度策略構(gòu)成一個(gè)完整HTTP處理過(guò)程。顯而易見(jiàn),一個(gè)狀態(tài)對(duì)于下一個(gè)狀態(tài)處理的調(diào)用都可以認(rèn)為是異步的。一個(gè) HTTP狀態(tài)機(jī)的設(shè)計(jì)如下圖所示。
圖1. HTTP狀態(tài)機(jī)
工作線程的函數(shù)其實(shí)就是兩個(gè)操作:從狀態(tài)隊(duì)列里取出一個(gè)HTTP_Context,調(diào)用HTTP_Context的service()函數(shù),周而復(fù)此。 在這個(gè)架構(gòu)上,就很容易引入異步I/O和Plug-in的機(jī)制了。事實(shí)上我們也可以使用基于事件(例如select/poll)的I/O策略來(lái)模擬異步I /O,實(shí)現(xiàn)中使用一個(gè)用戶線程就可以了。
對(duì)于異步I/O和Plug-in的調(diào)用,我們也是采用類似于Linux 2.6里面aio的重試方案,而異步完成的時(shí)候采用回調(diào)函數(shù)。在某個(gè)狀態(tài)上,如果系統(tǒng)需要I/O操作(recv或者send),則會(huì)請(qǐng)求一個(gè)異步I /O(操作系統(tǒng)提供的異步I/O或者由用戶線程模擬的異步I/O),這時(shí)候相應(yīng)的HTTP_Context不會(huì)重新回到狀態(tài)隊(duì)列里,而在I/O完成的回調(diào) 函數(shù)里面才會(huì)重新放回到狀態(tài)隊(duì)列,得到重新調(diào)度的機(jī)會(huì)。HTTP_Context得到重新調(diào)度的時(shí)候會(huì)檢查I/O狀態(tài)(這個(gè)可以通過(guò)一些標(biāo)志位來(lái)完成), 如果已經(jīng)完成,則處理然后設(shè)置下一狀態(tài),重新調(diào)度,否則可以重新請(qǐng)求一個(gè)新的I/O請(qǐng)求。Plug-in也可以使用類似的方案,比如說(shuō)一個(gè)Plug-in 要跟外部的一個(gè)服務(wù)器通信,這時(shí)候就可以在通信完成的時(shí)候才把HTTP_Context重新放回到狀態(tài)隊(duì)列。顯然,Plug-in跟HTTP狀態(tài)是多對(duì)多 的關(guān)系,一個(gè)Plug-in可以在若干個(gè)關(guān)心的狀態(tài)注冊(cè)自身,同時(shí)還可以設(shè)置一些short-path來(lái)提高處理的效率。
結(jié)論
總的來(lái)說(shuō),異步調(diào)用的設(shè)計(jì)和應(yīng)用歸根結(jié)底就是對(duì)多個(gè)主動(dòng)對(duì)象的管理問(wèn)題:如何提供執(zhí)行的動(dòng)力以及如何保證執(zhí)行的順序邏輯。主要考慮的問(wèn)題是主動(dòng)對(duì)象的粒 度以及執(zhí)行方式,同步或者回調(diào)來(lái)完成順序的調(diào)度,或者使用近似的調(diào)度而加一些魯棒的錯(cuò)誤處理機(jī)制來(lái)保證語(yǔ)義的正確。后者可以考慮在使用基于事件的 socket的時(shí)候,readable事件的通知可以是冗余的,或者說(shuō)可以比實(shí)際中發(fā)生的readable事件更多,這個(gè)時(shí)候使用非阻塞的socket, 有些read()(或者recv())會(huì)直接返回EWOULDBLOCK,系統(tǒng)只要考慮處理這種情況(使用non blocking socket而不是blocking socket),當(dāng)例外的情況不多的時(shí)候是可以接受的。這時(shí)候可以說(shuō)事件的報(bào)告就只是近似的。
from:
posted on 2010-09-06 17:33
chatler 閱讀(798)
評(píng)論(0) 編輯 收藏 引用 所屬分類:
OS 、
windows