標籤:http 使用 資料 io for re
關於 windows IOCP
有人說 windows IOCP 是 windows 上最好的東西。 IOCP 是真正的非同步 IO,意味著每次發起一個 IO 請求,該調用本身則立即返回, 而包括 IO 操作和資料從核心緩衝區到使用者緩衝區之間的拷貝都由系統完成,直到這個過程結束系統才通知使用者進程。 linux 上沒有這樣的非同步 IO。
IOCP 的使用
- 建立一個新的完成連接埠。完成連接埠被設計成與一個線程池相互合作,線程池的線程並發的用來處理完成的 IO 通知。
CreateIoCompletionPort
這個 API 用於建立 IOCP, 最後一個參數則是指定線程池中線程個數,一般來說取 CPU * 2 ,這樣可以最充分使用多核 CPU ,又降低了線程間的切換。CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, dwNumberOfConcurrentThreads)
- 建立背景工作執行緒。
- 關聯一個 IO 裝置到完成連接埠。也是調用
CreateIoCompletionPort
(API 設計有些太隨意了吧,難道有什麼曆史原因?)。
HANDLE h = CreateIoCompletionPort(hDevice, hCompletionPort, dwCompletionKey, 0);
- 使用 overlapped IO ,例如 socket 的 WSARecv/WSASend,甚至 AcceptEx 和 ConnectEx。這些調用都只是發起一個 IO 請求然後立即返回。函數調用都需要初始化一個 OVERLAPPED 結構體,後面有提到其作用。
- 背景工作執行緒是一個 loop, 阻塞在
GetQueuedCompletionStatus
調用上。GetQueuedCompletionStatus
返回時從 IO 完成隊列中取出一個 completion packet。線程池線程阻塞時是由系統負責完成調度的。
IOCP 內部的一些資料結構
- Device List:包含所有與完成連接埠相關聯的裝置的一個列表。
- I/O Completion Queue(FIFO):當一個非同步 IO 請求完成了,系統會去檢查是否這個 IO 裝置與任何 IO 完成連接埠關聯了,如果是,系統會在 IO 完成連接埠隊列的末尾添加一個 completion packet(以 FIFO 的順序入隊),
GetQueuedCompletionStatus
就是在這個隊列上等待。
- IOCP 關聯線程等待隊列:線程池中的線程調用
GetQueuedCompletionStatus
時,就會被放進一個等待隊列,IO 完成連接埠核心對象根據此隊列知道有哪些線程在等待處理completion packet。線程等待隊列是按照 LIFO 的方式入隊的,也就是當有一個 completion packet 到來時,系統先喚醒最後調用GetQueuedCompletionStatus
進入等待隊列的線程。
IOCP 和線程池的相互作用
- 任何線程都可以調用
GetQueuedCompletionStatus
來與一個 IO 完成連接埠關聯起來,但是一個線程只能關聯一個 IOCP,當線程退出或者指定了其他的 IOCP或者關閉了 IOCP,線程才與這個 IOCP 解開綁定。
- 建立 IOCP 的時候會指定一個並發值,雖然任意個線程可以關聯到這個 IOCP,但是並發值限定了可以同時啟動並執行線程數。假設這樣的情境,有一個並發值為 1 的 IOCP,但是有多於一個的線程關聯到了這個 IOCP,如果完成隊列中總是有一個 completion packet 在等待,當正在啟動並執行線程調用
GetQueuedCompletionStatus
時就會立即返回,該線程處理完這個 completion packet 再次調用GetQueuedCompletionStatus
又會立即返回。在處理 completion packet過程中,雖然完成隊列中始終有 completion packet 待處理,但是因為並發值為 1 的原因,系統不會去調度其他線程來執行,儘管關聯 IOCP 的線程不止一個。同時也避免了線程切換的開銷,因為始終都是這一個線程在執行。
- 在上述情況中,看起來線程池中關聯的其他線程毫無用處,但是其實是沒有考慮到正在啟動並執行線程進入等待狀態或者因為某種情況與該 IOCP 解除綁定時的情況。如果正在啟動並執行線程調用
Sleep
, WaitFor*
,或者一個同步 IO 函數,或者任何可以引起當前線程從運行狀態變為等待狀態的函數時,IOCP 就會立即調度其他關聯的線程,維持始終有一個線程在運行。
IOCP 使用過程中遇到的問題
- 因為涉及到多線程會比 epoll + 單線程要編碼複雜。
- API 設計比較糟糕,這也加大了編碼難度。
- 文檔描述不清晰,甚至沒有一個官方的樣本程式,非官方文檔或者程式或多或少有些錯誤,讓人難以放心使用。以 WSARecv 為例子,MSDN上描述若 WSARecv 能立即返回,傳回值為 0。這是不是意味著程式要在兩處處理 IO 完成的情況,一處是 IO 立即返回時,一處是背景工作執行緒
GetQueuedCompletionStatus
等待 IOCP 完成隊列處。幾乎所有的非同步 IO 函數都是如此。但是所幸似乎即使立即返回 0 ,完成隊列中也會有一個 completion packet,所以只在背景工作執行緒中的完成隊列中等待 IO 完成也不會出錯。
- 一般的使用 TCP 進行通訊的網路程式,因為 TCP 流無界的特性,都會自訂成這樣的應用網路資料包:前面幾個位元組代表該包的長度,後面就是該包真正的內容。應用程式在解包的時候,對應的要先擷取包的長度,再截取對應長度的包資料。這樣的過程在多線程的 IOCP 會比較困難,多個線程取到了各個資料包的不同部分,而且因為 completion packet 的出隊順序並不能保證,各個線程擷取的資料包之間的順序已經丟失了。因此,必須想辦法解決包的順序問題,而且解包過程需要同步各個線程。這樣無疑使得代碼變得更複雜。
- IOCP 作為非同步 IO ,可以非常方便的發起 IO ,但是每次發起 IO 時候都必須提交一段使用者記憶體,在 IO 完成之前這段記憶體必須是被鎖住的,既你不能再使用。當然這不是 IOCP 的問題,這是非同步 IO特性決定的。
一個收發 TCP 應用協議包的程式樣本
- 協議包定義成頭兩個位元組儲存包長度 len,包頭後面 len 位元組是包的具體內容。為了簡化編碼,又能利用到 IOCP 一些特性,決定只啟動一個背景工作執行緒處理所有的 IO 完成操作,發包和收包都是非阻塞的非同步呼叫。
- 提交給非同步 IO 的 buffer,都是從一段預先分配的記憶體中取出來的,這樣使得IO 操作使用的記憶體是可控的,並且不會有記憶體片段,充分使用記憶體。
- IOCP 的幾個核心 API 都與參數 completionKey, overlapped 有關。在程式中 completionKey 可以對應是對哪個 socket 進行操作,overlapped 則對應成具體哪一個 IO 操作。
- 同一時間只允許一個同類的 IO 操作(讀或者寫)在提交。
代碼在此,服務端程式比較簡單,可以自己實現並驗證。