完成連接埠是windows為了處理不同裝置之間並行執行階段運行速度差異而設計的一種非同步資料處理的機制。完成連接埠是《windows核心編程》中的一個重點內容,最近我花了三天時間讀完核心編程裡面關於完成連接埠的章節,並瞭解微軟相關的技術資料文檔,感覺完成連接埠的設計和實現可以在一定程度上加快windows的服務速度,用不用完成連接埠對程式邏輯沒什麼影響,但對於需要速度的服務來說,這就至關重要了。由於源碼不公開的緣故,一些內部原理只能夠猜,而且很多參數的使用彼此關聯,有一定的迷惑性,個人覺得微軟的這些底層介面一開始就設計不好,加上後來為了相容早前版本開發的程式,錯而不改,越來越亂。
即使如此,只要細心也足以學習好怎樣使用完成連接埠。可以說,對於windows程式員,對完成連接埠的掌握程度顯示程式員的平台底層技術水平。
1. 三個基本函數,看似不多,把參數弄清楚還是需要不少時間的。
CreateIoCompletionPort()
PostQueuedCompletionStatus()
GetQueuedCompletionStatus()
2. 建立完成連接埠
使用HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,dwThreadCount);建立完成連接埠,這裡要注意參數的設定,與原本windows設計的初衷有區別了。最後一個參數是指定完成連接埠核心對象工作時最多開闢的線程數,線程的管理由完成連接埠核心對象管理,對使用者不透明。原型如下:
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
請參考:http://msdn.microsoft.com/en-us/library/windows/desktop/aa363862(v=vs.85).aspx
3. 裝置控制代碼與完成連接埠關聯(相當一個註冊動作)
CreateIoCompletionPort(hFileRead,hIOCP,CK_READ,0);這裡hFileRead是對應裝置的控制代碼(這裡的裝置可以是windows檔案系統泛用的裝置的意思),hIOCP是已經存在的完成連接埠控制代碼;CK_READ是設定為完成連接埠事件完成時發出的通知的參數,可以分辨這個完成的事件是哪個裝置的;後面我們用到的例子就使用了CK_READ與CK_WRITE去分辨已經完成的事件是哪個裝置發出的。
4. 手動通知核心,事件已完成,即把完成事件放到完成隊列中
使用PostQueuedCompletionStatus(hIOCP,BUFFER_SIZE,CK_WRITE,&ior[nIOReq]);這裡hIOCP是完成連接埠的控制代碼,BUFFER_SIZE參數是傳給GetQueuedCompletionStatus擷取的時候得到的資料,CK_WRITE標誌這個完成的事件是哪一個事件(與裝置控制代碼關聯的時候設定的那個一致)。原型如下:
BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
請參考:http://msdn.microsoft.com/en-us/library/windows/desktop/aa365458(v=vs.85).aspx
5. 從完成事件隊列取出一個完成事件
使用GetQueuedCompletionStatus(hIOCP,&dwNumBytes,&CompletionKey,(OVERLAPPED**)&pior,INFINITE);來取得當前完成事件隊列的隊頭資料。在接下來的動作去處理這個完成事件。這個動作需要不停的迴圈重複做,而且要在設計的任務完成的最後一個事件讓處理的程式知道任務已經完成。原型如下:
BOOL WINAPI GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytes,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED *lpOverlapped,
_In_ DWORD dwMilliseconds
);
請參考:http://msdn.microsoft.com/en-us/library/windows/desktop/aa364986(v=vs.85).aspx
6. 非同步IO事件
上面說了處理完成的IO,但是IO動作怎麼產生和運行呢?其實可以有很多種IO動作,第3步註冊的是什麼裝置,就使用什麼動作。比如我需要開啟檔案讀寫,我需要使用CreateFile函數,需要注意的就是在開啟的時候,設定重疊模式(OVERLAPPED)。返回的hFileRead控制代碼就是需要關聯上IO完成連接埠的裝置控制代碼了。
HANDLE hFileRead = CreateFile(readfilename,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING|FILE_FLAG_OVERLAPPED,
NULL);
其他邏輯的設計也是比較煩的,可以看以下一個例子:使用IO完成連接埠,設定4個線程,實現檔案快速複製功能。代碼內容注釋應該足夠協助理解,並可直接複製到VC2008工程下調試運行理解。
// 輔助讀寫檔案的類,class CIOReq:public OVERLAPPED{public:CIOReq(){Internal = InternalHigh = 0;Offset = OffsetHigh = 0;hEvent = NULL;m_nBuffSize = 0;m_pvData = 0;};~CIOReq(){if (m_pvData != NULL){VirtualFree(m_pvData,0,MEM_RELEASE);}};BOOL AllocBuffer(SIZE_T nBuffSize){m_nBuffSize = nBuffSize;m_pvData = VirtualAlloc(NULL,m_nBuffSize,MEM_COMMIT,PAGE_READWRITE);return (m_pvData !=NULL);};BOOL Read(HANDLE hDevice,PLARGE_INTEGER pliOffset = NULL){if (pliOffset!= NULL){Offset = pliOffset->LowPart;OffsetHigh = pliOffset->HighPart;}return (::ReadFile(hDevice,m_pvData,m_nBuffSize,NULL,this));};BOOL Write(HANDLE hDevice,PLARGE_INTEGER pliOffset = NULL){if (pliOffset!=NULL){Offset = pliOffset->LowPart;OffsetHigh = pliOffset->HighPart;}return (::WriteFile(hDevice,m_pvData,m_nBuffSize,NULL,this));};private:SIZE_T m_nBuffSize;PVOID m_pvData;};long long chROUNDUP(long long src_size, long long align){long long t = src_size%align;if (t){return (src_size/align)*align+align;}return src_size;}bool FileCopy(const WCHAR *des, const WCHAR *src){#define BUFFER_SIZE (64*1024)// 為配合FILE_FLAG_NO_BUFFERING模式,使用檔案大小向上對齊到此大小,並以這個大小作為IO資料基本單位#define CK_WRITE 1// 寫檔案資料事件完成的標記#define CK_READ 2// 讀檔案資料事件完成的標記#define MAX_PENDING_IO_REQS 4//設定同時運行IO的最高線程數為4。因為如果太小,體現不出多線程優勢,太大的話,用於切換的時間太多,效率低。bool fOk = FALSE;const WCHAR *readfilename = src;const WCHAR *writefilename = des;LARGE_INTEGER liFileSizeSrc = {0};LARGE_INTEGER liFileSizeDes = {0};try{// 建立完成連接埠HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,MAX_PENDING_IO_REQS);// 以OVERLAPPED方式開啟源檔案,以備讀出HANDLE hFileRead = CreateFile(readfilename,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_FLAG_NO_BUFFERING|FILE_FLAG_OVERLAPPED,NULL);// 把源檔案裝置關聯到完成連接埠,使用的事件標誌是CK_READif (hIOCP != CreateIoCompletionPort(hFileRead,hIOCP,CK_READ,0)){return 0;}// 以OVERLAPPED方式打目標檔案,以備寫入HANDLE hFileWrite = CreateFile(writefilename,GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,CREATE_ALWAYS,FILE_FLAG_OVERLAPPED|FILE_FLAG_NO_BUFFERING,NULL);// 把目標檔案裝置關聯到完成連接埠,使用的事件標記是CK_WRITEif (hIOCP != CreateIoCompletionPort(hFileWrite,hIOCP,CK_WRITE,0)){return 0;}// 取得源檔案邏輯大小(不是物理大小)GetFileSizeEx(hFileRead,&liFileSizeSrc);// 因為FILE_FLAG_NO_BUFFERING開啟參數,把目標檔案向上對齊到BUFFER_SIZE大小liFileSizeDes.QuadPart = chROUNDUP(liFileSizeSrc.QuadPart,BUFFER_SIZE);SetFilePointerEx(hFileWrite,liFileSizeDes,NULL,FILE_BEGIN);SetEndOfFile(hFileWrite);// 這兩個變數分別用來代表目前進行的讀和寫的IO運算元int nReadsInProgress = 0;int nWritesInProgress = 0;// 建立4個線程需要的操作對象,主要封裝了讀寫操作CIOReq ior[MAX_PENDING_IO_REQS];LARGE_INTEGER liNextReadOffset = {0};for (int nIOReq = 0; nIOReq<_countof(ior);nIOReq++){ior[nIOReq].AllocBuffer(BUFFER_SIZE);// 每個對象手動觸發寫完成事件,讓接收端開始工作。PostQueuedCompletionStatus(hIOCP,BUFFER_SIZE,CK_WRITE,&ior[nIOReq]);nWritesInProgress++;}// 只要還有IO操作沒完成,就繼續監聽完成事件列表while ( nWritesInProgress>0 || nReadsInProgress>0 ){ULONG_PTR CompletionKey;DWORD dwNumBytes;CIOReq * pior;// INFINITE表示無限期的監聽,直到有完成事件GetQueuedCompletionStatus(hIOCP,&dwNumBytes,&CompletionKey,(OVERLAPPED**)&pior,INFINITE);switch (CompletionKey){case CK_WRITE:// 收到的是寫完成事件nWritesInProgress--;//正在寫的數量減少1if (liNextReadOffset.QuadPart < liFileSizeDes.QuadPart)//如果源檔案還沒讀完{//讀源檔案pior->Read(hFileRead,&liNextReadOffset);nReadsInProgress++;//正在讀的數量增加1liNextReadOffset.QuadPart += BUFFER_SIZE;}break;case CK_READ: // 收到的時讀完成事件nReadsInProgress--;//正在讀的數量減少1pior->Write(hFileWrite);nWritesInProgress++;//正在寫的數量增加1break;}}CloseHandle(hFileWrite);CloseHandle(hFileRead); CloseHandle(hIOCP); fOk = true;}catch (...){}if (fOk == true){// 修複目標檔案的大小,改回原來的邏輯大小HANDLE hFile = CreateFile(writefilename,GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);SetFilePointerEx(hFile,liFileSizeSrc,NULL,FILE_BEGIN);SetEndOfFile(hFile);CloseHandle(hFile);}return fOk;}
增加兩個講述完成連接埠的串連:
1. 詳細講述了原理和使用文章,強烈推薦
http://blog.csdn.net/piggyxp/article/details/6922277
2. 簡單程式,展示了網路中完成連接埠的應用情景
http://www.cnblogs.com/pen-ink/articles/1834088.html