windows I/O完成端口

                                       I/O完成端口   

                   

                               

一.  基本概念:

         設備---windows操作系統上允許通信的任何東西,比如文件、目錄、串行口、並行口、郵件槽、命名管道、無名管道、套接字、控制檯、邏輯磁盤、物理磁盤等。絕大多數與設備打交道的函數都是CreateFile/ReadFile/WriteFile等。所以我們不能看到**File函數就只想到文件設備。

       與設備通信有兩種方式,同步方式和異步方式。同步方式下,當調用ReadFile函數時,函數會等待系統執行完所要求的工作,然後才返回;異步方式下,ReadFile這類函數會直接返回,系統自己去完成對設備的操作,然後以某種方式通知完成操作。

       重疊I/O----顧名思義,當你調用了某個函數(比如ReadFile)就立刻返回做自己的其他動作的時候,同時系統也在對I/0設備進行你要求的操作,在這段時間內你的程序和系統的內部動作是重疊的,因此有更好的性能。所以,重疊I/O是用於異步方式下使用I/O設備的。

重疊I/O需要使用的一個非常重要的數據結構OVERLAPPED

       完成端口---是一種WINDOWS內核對象。完成端口用於異步方式的重疊I/0情況下,當然重疊I/O不一定非使用完成端口不可,還有設備內核對象、事件對象、告警I/0等。但是完成端口內部提供了線程池的管理,可以避免反覆創建線程的開銷,同時可以根據CPU的個數靈活的決定線程個數,而且可以讓減少線程調度的次數從而提高性能。

 

 

二. OVERLAPPED數據結構

typedef struct _OVERLAPPED {

ULONG_PTR Internal;                 

ULONG_PTR InternalHigh;

 DWORD Offset;

DWORD OffsetHigh;

HANDLE hEvent;

} OVERLAPPED;

 

這個結構包含5個成員,其中的 Offset、OffsetHign和hEvent必須在調用ReadFile和WriteFile之前進行初始化,其他兩個成員由驅動程序來設置,當I/O操作完成時可以檢查他們的值。

 

hEvent成員: 用來接收I/O完成通知的4種方法(觸發設備內核對象、觸發時間內核對象、使用可提醒I/O、使用I/O完成端口)中,其中一種方法(使用I/O完成端口)會用到這個成員。

 

三. 創建I/O完成端口

 I/O完成端口的設計初衷就是與線程池配合使用,從而提高服務應用程序的性能。

服務應用程序的兩種架構模型:

-串行模型:一個線程等待一個客戶請求,當請求到達時,線程被喚醒並對客戶請求進行處理

-並行模型:一個線程等待一個客戶請求,當請求到達時,創建一個新的線程來處理請求

顯而易見,串行模型的缺點是不能很好地同時處理多個請求,並行模型中windows內核在各可運行的線程之間進程上下文切換花費太多時間。

I/O完成端口背後的理論是併發運行的線程數量必須有一個上限。

I/O完成端口是一個很複雜的內核對象,可以使用CreateIoCompletionPort來創建個I/O完成端口

 

HANDLE CreateIoCompletionPort (

HANDLE FileHandle, // handle to file

HANDLE ExistingCompletionPort,// handle to I/O completion port

ULONG_PTR CompletionKey, // completion key

DWORD NumberOfConcurrentThreads // number of threads to execute concurrently

);


 


事實上這個函數確執行了兩線不同的任務:1.創建一個I/O完成端口;2.將一個設備與一個I/O完成端口關聯起來

第一步:爲了只創建一個I/O完成端口,我們給CreateIoCompletionPort的前三個參數分貝傳入INVALID_HANDLE_VALUE,NULL,0。

第二步:將設備與I/O完成端口關聯起來,如果傳入的參數ExistingCompletionPort值不爲NULL,則將它和FileHandle關聯起來。

當創建一個I/O完成端口的時候,系統內核實際上會創建5個不同的數據結構,它們分別是:

1. 設備列表:表示與該端口相關聯的一個或多個設備

2. I/O完成隊列(先入先出):當設備的一個異步I/O請求完成時,系統會檢查設備是否與一個I/O完成端口想關聯,如果是,則將該I/O請求追加到I/O完成隊列的末尾。

3. 等待線程隊列(後入先出):當線程池中的每個線程調用GetQueuedCompletionStatue(切換線程到睡眠狀態)的時候,調用線程的線程標識符會被添加到這個等待線程隊列中,使得I/O完成端口內核對象始終都能夠知道,有哪些線程當前正在等待對已完成的I/O請求進行處理(也就是等待I/O完成隊列中有一項添加進來)

4. 已釋放線程列表:可以理解爲這些線程可以切換到運行狀態,等待線程隊列和已暫停線程列表中的線程被喚醒後添加到該列表。

5. 已暫停線程列表:已釋放的線程調用一個函數將自己掛起時將該線程的線程標識符添加到該列表, 當掛起的線程被喚醒時再添加到已釋放線程列表

 

弄明白這5個數據結構,就很容易搞清楚I/O完成端口是如何管理多線程的了。

 

I/O完成端口與線程池

I/O完成端口體系結構假定可運行線程的數量只會在很短一段時間內高於最大允許的線程數量,一旦線程進入下一次循環並調用GetQueuedCompletionStatus,可運行線程的數量就會迅速下降,這也就是爲什麼線程池中的線程數量應該大於在完成端口中設置的併發線程數量。

怎麼來確定線程池中應該有多線線程? 可以使用啓發式算法來對線程池進行管理。

 

線程間傳遞數據

線程間傳遞數據最常用的辦法是在_beginthreadex函數中將參數傳遞給線程函數,或者使用全局變量。但是完成端口還有自己的傳遞數據的方法,答案就在於CompletionKeyOVERLAPPED參數。

CompletionKey被保存在完成端口的設備表中,是和設備句柄一一對應的,我們可以將與設備句柄相關的數據保存到CompletionKey中,或者將CompletionKey表示爲結構指針,這樣就可以傳遞更加豐富的內容。這些內容只能在一開始關聯完成端口和設備句柄的時候做,因此不能在以後動態改變。

OVERLAPPED參數是在每次調用ReadFile這樣的支持重疊I/0的函數時傳遞給完成端口的。我們可以看到,如果我們不是對文件設備做操作,該結構的成員變量就對我們幾乎毫無作用。我們需要附加信息,可以創建自己的結構,然後將OVERLAPPED結構變量作爲我們結構變量的第一個成員,然後傳遞第一個成員變量的地址給ReadFile函數。因爲類型匹配,當然可以通過編譯。當GetQueuedCompletionStatus函數返回時,我們可以獲取到第一個成員變量的地址,然後一個簡單的強制轉換,我們就可以把它當作完整的自定義結構的指針使用,這樣就可以傳遞很多附加的數據了。太好了!只有一點要注意,如果跨線程傳遞,請注意將數據分配到堆上,並且接收端應該將數據用完後釋放。我們通常需要將ReadFile這樣的異步函數的所需要的緩衝區放到我們自定義的結構中,這樣當GetQueuedCompletionStatus被返回時,我們的自定義結構的緩衝區變量中就存放了I/0操作的數據。

 

CompletionKeyOVERLAPPED參數,都可以通過GetQueuedCompletionStatus函數獲得。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章