前言
網(wǎng)絡(luò)上大部分的講解IOCP模型文章都比較斷章取義,要么是這里冒出一個術(shù)語,那邊出來一個不知名的名詞。
本文主要是給那些暫時還無太多的Windows編程基礎(chǔ)的人閱讀,里面解釋了一些相應(yīng)的前驅(qū)知識。比如管道、重疊I/O模型等等。
如果你已經(jīng)對這些了如指掌了,可以直接忽略本文——因為本文是給那些初學者看的。
不過即使是給初學者看的,很多概念只是提個大概,讓讀者心里有個印象而已。更進一步的詳細知識還是需要讀者自行翻閱相關(guān)資料。
前驅(qū)知識
管道
管道(PIPE)是用于進程間通信的一段共享內(nèi)存。創(chuàng)建管道的進程稱為管道服務(wù)器,連接到一個管道的進程稱為管道客戶機。一個進程在向管道寫入數(shù)據(jù)之后,另一個進程就可以從管道的另一端將其讀出來。
管道分兩種,匿名管道和命名管道。
匿名管道
匿名管道是在父進程和子進程間單向傳輸數(shù)據(jù)的一種未命名管道,只能在本地計算機中使用,而不能用于網(wǎng)絡(luò)間通信。
匿名管道由 CreatePipe() 函數(shù)創(chuàng)建。該函數(shù)在創(chuàng)建匿名管道的同時返回兩個句柄:讀句柄和寫句柄。其原型如下:
BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
其中
hReadPipe為指向讀句柄的指針,hWritePipe為指向?qū)懢浔闹羔槪?lpPipeAttributes為指向安全屬性的指針;最后的nSize為管道大小,若為0則由系統(tǒng)來決定。
匿名管道不支持異步讀寫操作。
命名管道
命名管道是在管道服務(wù)器和一臺或多臺管道客戶機之間進行單向或者雙向通信的一種命名的管道。一個命名管道的所有實例都共享同一個管道名,但是每一個實例都擁有獨立的緩存和句柄,并且為 客戶機 - 服務(wù)器 通信提供一個分離的管道。
命名管道可以在同一臺計算機的不同進程之間或者跨越一個網(wǎng)絡(luò)的不同計算機的不同進程間進行有連接的可靠數(shù)據(jù)通信。如果連接中斷,連接雙方都能立即受到連接斷開的信息。
每個命名管道都有一個唯一的名字,以區(qū)分存在于系統(tǒng)的命名對象列表中的其它命名管道。管道服務(wù)器在調(diào)用 CreateNamedPipe() 函數(shù)創(chuàng)建管道的一個或多個實例時為其指定了名稱。對于管道客戶機,則是在調(diào)用 CreateFile() 或 CallNamedPipe() 函數(shù)在連接一個命名管道實例時對管道名進行指定。
命名管道對其標識采用 UNC格式:
\\Server\Pipe\[Path]Name
其中第一部分 \\Server 指定了服務(wù)器的名字,命名管道服務(wù)就在此服務(wù)器創(chuàng)建。其字符串部分可以為一個小數(shù)點(表示本機)、星號(當前網(wǎng)絡(luò)字段)、域名或者是一個真正的服務(wù);第二部分是一個不可變化的硬編碼字符串;第三部分 \[Path]Name 則使應(yīng)用程序可以唯一定義及標識一個命名管道的名字,而且可以設(shè)置多級目錄。
管道服務(wù)器首次調(diào)用 CreateNamedPipe() 函數(shù)時,使用 nMaxInstance 參數(shù)指定了能同時存在的管道實例的最大數(shù)目。服務(wù)器可以重復調(diào)用 CreateNamedPipe() 函數(shù)去創(chuàng)建新的管道實例,直至達到設(shè)定的最大實例數(shù)。
下面給出 CreateNamedPipe() 的函數(shù)原型:
HANDLE CreateNamedPipe(
LPCTSTR lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstance,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTimeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
這里的
lpName就是所謂的管道名稱指針了,dwOpenMode為管道打開的模式(用來指示管道在創(chuàng)建好之后,它的傳輸方向、I/O控制以及安全模式),dwPipeMode為管道模式,nMaxInstance正如之前所說的是最大的管道實例數(shù),nOutBufferSize為輸出緩存的大小,nInBufferSize為輸入緩存的大小,nDefaultTimeOut為超時設(shè)置,最后的lpSecurityAttributes為安全屬性的指針。
CreateFile, ReadFile等API
CreateFile()
這個函數(shù)可以創(chuàng)建或者打開一個對象的句柄,憑借此句柄我們就可以控制這些對象:
- 控制臺對象
- 通信資源對象
- 目錄對象(只能打開)
- 磁盤設(shè)備對象
- 文件對象
- 郵槽對象
- 管道對象
函數(shù)原型:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
參數(shù)解析
lpFileName: 一個指向無終結(jié)符的字符串指針,用來指明要創(chuàng)建或者打開的對象的名字。
dwDesiredAccess: 指明對象的控制模式。一個應(yīng)用程序可以包含讀控制、寫控制、讀/寫控制、設(shè)備查詢控制。
dwShareMode: 指定對象的共享模式。如果
dwShareMode == 0則表示是互斥使用的。如果CreateFile打開成功,則別的程序只能等到當前程序關(guān)閉對象句柄CloseHandle后才能再打開或者使用。lpSecurityAttributes: 一個指向
SECURITY_ATTRIBUTES結(jié)構(gòu)對象的指針,決定返回的句柄是否被子進程所繼承。如果lpSecurityAttributes參數(shù)為NULL,句柄就不能被子進程繼承。dwCreationDisposition: 指明當打開的對象存在或不存在的時候各需要怎么樣去處理。
dwFlagsAndAttributes: 指定文件屬性和標志。
hTemplateFile: 把具有
GENERIC_READ權(quán)限的句柄指定為一個模板文件。這個模板文件提供了文件屬性和擴展屬性,用于創(chuàng)建文件。返回值
如果調(diào)用成功,返回值是一個打開文件的句柄。
如果調(diào)用之前文件已經(jīng)存在,且
dwCreationDisposition參數(shù)為CREATE_ALWAYS或者OPEN_AWAYS,用GetLastError返回ERROR_ALREADY_EXISTS(即使調(diào)用成功也會返回這個值)。如果調(diào)用之前不存在GetLastError返回0。如果調(diào)用失敗,返回值是
INVALID_HANDLE_VALUE。要進一步了解出錯原因,調(diào)用GetLastError。
CloseHandle()
用于關(guān)掉一個打開的對象句柄。
函數(shù)原型如下:
BOOL CloseHandle(
HANDLE hObject
);
ReadFile()
ReadFile() 函數(shù)從文件指針指定的位置讀取數(shù)據(jù)。讀操作完畢之后,文件指針將根據(jù)實際讀出的數(shù)據(jù)自動進行調(diào)整,除非文件句柄是以 OVERLAPPED 屬性值打開的。如果是以 OVERLAPPED 打開的I/O,應(yīng)用程序就需要自己手動調(diào)整文件指針。
這個函數(shù)被設(shè)計成兼有同步和異步操作。 ReadFileEx() 函數(shù)則設(shè)計成只支持異步操作,異步操作允許應(yīng)用程序在讀文件期間可以同時進行其它的操作。
函數(shù)原型:
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
參數(shù)解析
hFile: 文件句柄(必須具有
GENERIC_READ訪問權(quán)限)。lpBuffer: 用來接收從文件中讀出的數(shù)據(jù)的緩沖區(qū)。
nNumberOfBytesToRead: 指明要讀取的字節(jié)總數(shù)。
lpNumberOfBytesRead: 一個變量指針,用來存儲實際傳輸?shù)淖止?jié)總數(shù)。
ReadFile在做所有事情(包括錯誤檢查)之前,先將這個值賦為0。當ReadFile從一個命名管道上返回TRUE時這個參數(shù)為0,說明消息管道另一端調(diào)用WriteFile時設(shè)置的nNumberOfBytesToWrite參數(shù)為0。如果lpOverlapped不是NULL,lpNumberOfBytesRead可以設(shè)置為NULL。如果是一個Overlapped形式的讀操作,我們可以動用GetOverlappedResult函數(shù)來獲得傳輸?shù)膶嶋H字節(jié)數(shù)。如果hFile關(guān)聯(lián)的是一個完成端口(I/O Completion Port),那么可以調(diào)用GetQueuedCompletionStatus函數(shù)來獲得傳輸?shù)膶嶋H字節(jié)數(shù)。如果完成端口被占用,而你用的是一個用于釋放內(nèi)存的回調(diào)例程,對于lpOverlapped參數(shù)指向的OVERLAPPED結(jié)構(gòu)體來說,為這個參數(shù)指定NULL可以避免重新分配內(nèi)存時發(fā)生內(nèi)存泄露。內(nèi)存泄露會導致返回這個參數(shù)值時是一個非法值。lpOverlapped: 一個指向
OVERLAPPED結(jié)構(gòu)體的指針。如果hFile是以FILE_FLAG_OVERLAPPED方式獲得的句柄,這個結(jié)構(gòu)是必須的,不能為NULL(否則函數(shù)會在錯誤的時刻報告讀操作已經(jīng)完成了)。這時,讀操作在由OVERLAPPED中Offset成員指定的偏移地址開始讀,并且在實際完成讀操作之前就返回了。在這種情況下,ReadFile返回FALSE,GetLastError報告的錯誤類型是ERROR_IO_PENDING。這允許調(diào)用進程繼續(xù)其它工作直到讀操作完成。OVERLAPPED結(jié)構(gòu)中的事件將會在讀操作完成時被使用。返回值
有如下任一種情況發(fā)生都會導致函數(shù)返回:
- 在管道另一端的寫操作完成后。
- 請求的字節(jié)數(shù)傳輸完畢。
- 發(fā)生錯誤。
如果函數(shù)正確,返回非零。
如果返回值是非零但接受的字節(jié)數(shù)為
0,那么可能是文件指針在讀操作期間超出了文件的end位置。然而如果文件以FILE_FLAG_OVERLAPPED方式打開,lpOverlapped參數(shù)不為NULL,文件指針在讀操作期間超出了文件的end位置,那么返回值肯定是FALSE,GetLastError返回的錯誤是ERROR_HANDLE_EOF。
WriteFile
可以以同步或異步方式向一個對象句柄中寫數(shù)據(jù)。
函數(shù)原型:
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
其它信息與 ReadFile 極其相似,可以參考 ReadFile 。
Winsock重疊I/O模型
重疊I/O模型的概念
當調(diào)用 ReadFile() 和 WriteFile() 時,如果最后一個參數(shù) lpOverlapped 設(shè)置為 NULL ,那么線程就阻塞在這里,知道讀寫完指定的數(shù)據(jù)后,它們才會返回。這樣在讀寫大文件的時候,很多時間都浪費在等待 ReadFile() 和 WriteFile() 的返回上面。如果 ReadFile() 和 WriteFile() 是往管道里面讀寫數(shù)據(jù),那么有可能阻塞更久,導致程序性能下降。
為了解決這個問題,Windows引進了重疊I/O的概念,它能夠同時以多個線程處理多個I/O。其實你自己開多個線程也可以處理多個I/O,但是系統(tǒng)內(nèi)部對I/O的處理在性能上有很大的優(yōu)化。它是Windows下實現(xiàn)異步I/O的最常用的方式。
Windows為幾乎全部類型的文件提供這個工具:磁盤文件、通信端口、命名管道和套接字。通常,使用 ReadFile() 和 WriteFile() 就可以很好地執(zhí)行重疊I/O。
重疊模型的核心是一個重疊數(shù)據(jù)結(jié)構(gòu)。若想以重疊方式使用文件,必須用 FILE_FLAG_OVERLAPPED 標志打開它,例如:
HANDLE hFile = CreateFile(
lpFileName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
如果沒有規(guī)定該標志,則針對這個文件(句柄),重疊I/O是不可用的。如果設(shè)置了該標志,當調(diào)用 ReadFile() 和 WriteFile() 操作這個文件(句柄)時,必須為最后一個參數(shù)提供 OVERLAPPED 結(jié)構(gòu):
// WINBASE.H
typedef struct _OVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
頭兩個32位的結(jié)構(gòu)字 Internal 和 InternalHigh 由系統(tǒng)內(nèi)部使用。