电竞比分网-中国电竞赛事及体育赛事平台

分享

《Windows核心編程系列》十異步IO之IO完成端口

 求真我 2014-04-20

              http://blog.csdn.net/ithzhang/article/details/8508161轉(zhuǎn)載請注明出處?。?nbsp;                                                  

                                                                 IO完成端口

 

    為了將Windows打造成一個出色的服務(wù)器環(huán)境,Microsoft開發(fā)出了IO完成端口。完成端口需要與線程池配合使用。

 

    完成端口背后的理論是并發(fā)運行的線程數(shù)量必須有一個上限。由于太多的線程將會導(dǎo)致系統(tǒng)花費很大的代價在各個線程cpu上下文進行切換。

 

   使用并發(fā)模型與創(chuàng)建進程相比開銷要低很多,但是也需要為每個客戶請求創(chuàng)建一個新的線程。這開銷仍然很大。通過使用線程池可以是性能有很大的提高。IO完成端口需要配合線程池配合使用。

 

    IO完成端口也是一個內(nèi)核對象。調(diào)用以下函數(shù)創(chuàng)建IO完成端口內(nèi)核對象。

 

 

  1. HANDLE CreateIoCompletionPort(  
  2.   
  3.       HANDLE hFile,  
  4.   
  5.       HANDLE hExistingCompletionPort,  
  6.   
  7.       ULONG_PTR CompletionKey,  
  8.   
  9.       DWORD dwNumberOfConcurrentThreads);  


 

這個函數(shù)會完成兩個任務(wù):

     一是創(chuàng)建一個IO完成端口對象。

 

     二是將一個設(shè)備與一個IO完成端口關(guān)聯(lián)起來。

 

     hFile就是設(shè)備句柄。

 

     hExistingCompletionPort是與設(shè)備關(guān)聯(lián)的IO完成端口句柄。為NULL時,系統(tǒng)會創(chuàng)建新的完成端口。

 

     dwCompletionKey是一個對我們有意義的值,但是操作系統(tǒng)并不關(guān)心我們傳入的值。一般用它來區(qū)分各個設(shè)備。

 

     dwNumberOfConcurrentThreads告訴IO完成端口在同一時間最多能有多少進程處于可運行狀態(tài)。如果傳入0,那么將使用默認值(并發(fā)的線程數(shù)量等于cpu數(shù)量)。在第二章我們曾介紹說幾乎所有的內(nèi)核對象都需要安全屬性參數(shù)。那時的幾乎就是因為IO完成端口這個例外。它是唯一一個不需要安全屬性的內(nèi)核對象。 這是因為IO完成端口在設(shè)計時就是只在一個進程中使用。

 

      每次調(diào)用CreateIoCompletionPort時,函數(shù)會判斷hExistingCompletionKey是否為NULL,如果為NULL,會創(chuàng)建新的完成端口內(nèi)核對象。并為此完成端口創(chuàng)建設(shè)備列表然后將設(shè)備加入到此完成端口設(shè)備列表中(先入先出)。

 

    設(shè)備列表存儲與該完成端口相關(guān)聯(lián)的所有設(shè)備。

 

    設(shè)備列表只是調(diào)用CreateIoCompletionPort函數(shù)時的一個數(shù)據(jù)結(jié)構(gòu)。除此之外還有四個結(jié)構(gòu)。

 

    第二個結(jié)構(gòu)是IO完成隊列。當(dāng)設(shè)備的一個異步IO請求完成時,系統(tǒng)會檢查該設(shè)備是否與一個完成端口相關(guān)聯(lián),如果關(guān)聯(lián),系統(tǒng)會將這個已完成的IO請求添加到完成端口的IO完成隊列中。每一項包括已傳輸字節(jié)數(shù),完成鍵(dwCompletionKey)值,以及一個指向IO請求的OVERLAPPED結(jié)構(gòu)指針和錯誤碼。

 

     Windows為IO完成端口提供了一個函數(shù),可以將線程切換到睡眠狀態(tài),來等待設(shè)備IO請求完成并進入完成端口。

 

 

  1. BOOL GetQueuedCompletionStatus(  
  2.   
  3.      HANDLE hCompletionPort,  
  4.   
  5.      PDWORD pdwNumberOfBytesTransferred,  
  6.   
  7.      ULONG_PTR pCompletionKey,  
  8.   
  9.      OVERLAPPED** ppOverlapped,  
  10.   
  11.      DWORD dwMilliSeconds);  


 

     hCompletionPort表示線程希望對哪個完成端口進行監(jiān)視,GetQueuedCompletionStatus的任務(wù)就是將調(diào)用線程切換到睡眠狀態(tài),也就是阻塞在此函數(shù)上,直到指定的IO完成端口出現(xiàn)一項或者超時。

        pdwNumberOfBytesTransferred返回在異步IO完成時傳輸?shù)淖止?jié)數(shù)。

      pCompletionKey返回完成鍵。

      ppOverlapped返回異步IO開始時傳入的OVERLAPPED結(jié)構(gòu)地址。

      dwMillisecond指定等待時間。

           函數(shù)執(zhí)行成功則返回true,否則返回false。

     第三個結(jié)構(gòu)是等待線程隊列。當(dāng)線程池中的每個線程調(diào)用GetQueuedCompletionStatus時,調(diào)用線程的線程標(biāo)識符會被添加到這個等待線程隊列,這使得IO完成端口對象能知道,有哪些線程當(dāng)前正在等待對已完成的IO請求進行處理。當(dāng)IO完成端口的IO完成隊列中出現(xiàn)一項時,完成端口會喚醒等待線程隊列中的一個線程。這個線程會得到已完成IO項的所有信息:已傳輸字節(jié)數(shù),完成鍵以及OVERLAPPED結(jié)構(gòu)地址。這些信息是通過傳給GetQueuedCompletionStatus的參數(shù)來返回的。

 

     IO完成隊列中的各項是以先入先出方式來進行的。但是喚醒等待隊列中的線程是按照后入先出的方式進行。假設(shè)有四個線程正在等待隊列中等待,如果出現(xiàn)了一個已完成的IO項,那么最后一個由于調(diào)用GetQueuedCompletionStatus而被掛起的線程會被喚醒來處理這一項。當(dāng)處理完該項后,線程會由于再次調(diào)用GetQueuedCompletionStatus而進入等待線程隊列。使用這種算法,系統(tǒng)可以將哪些長時間睡眠的線程換出到磁盤。

 

     作者一直在推崇IO完成端口。接下來我們來討論下為什么IO完成端口這么有用?。。?/span>

 

      前面我們提到過IO完成端口只有配合線程池才能發(fā)揮更大的作用。當(dāng)我們創(chuàng)建并關(guān)聯(lián)設(shè)備時,需要指定有多少個線程并發(fā)運行。一般將這個值設(shè)置為cpu的數(shù)量。當(dāng)已完成的IO項被添加到完成隊列中時,IO完成端口會喚醒正在等待的線程,但是喚醒的線程數(shù)最多不會超過我們指定的數(shù)量。如果有四個IO請求已完成,且有四個線程等待GetQueuedCompletionStatus而被掛起,那么IO完成端口只喚醒兩個線程處理,另外兩個線程繼續(xù)睡眠。此時讀者可能會疑問:既然完成端口只允許喚醒指定數(shù)量的線程,那么為什么還指定更多的線程在線程池中呢?這就涉及到IO完成端口的第四個數(shù)據(jù)結(jié)構(gòu):已釋放線程列表。它存儲已被喚醒的線程句柄。這使得IO完成端口能夠直到哪些線程已經(jīng)被喚醒并監(jiān)視它們的執(zhí)行情況。如果此時已釋放線程由于調(diào)用某些函數(shù)將線程切換到了等待狀態(tài),完成端口會將其從已釋放隊列中移除,并將其添加到已暫停線程列表。

 

     已暫停隊列是IO完成端口第五個數(shù)據(jù)結(jié)構(gòu)。

 

     完成端口的目標(biāo)是根據(jù)創(chuàng)建完成端口時指定的并發(fā)線程數(shù)量,將盡可能多的線程保持在已釋放線程列表中。如果一個已暫停的線程被喚醒,它會離開已暫停線程列表并重新進入已釋放線程列表。這意味著已釋放列表中的線程數(shù)量將大于最大允許的并發(fā)線程數(shù)量。這句話什么意思呢?正在運行的線程數(shù)量加上從暫停線程列表中被釋放的線程數(shù)量使總數(shù)大于最大允許的數(shù)量。這可以使線程數(shù)量在短時間內(nèi)超過指定數(shù)量。

 

    IO完成端口并不一定要用于設(shè)備IO,它還可以進行線程間通信。在可提醒IO中我們介紹了QueueUserAPC。該函數(shù)允許線程將一個APC項添加到另一個線程的隊列中。IO完成端口也存在一個類似的函數(shù):

>IO通知追加到IO完成端口?size:14pt"> 

  1. BOOL PostQueuedCompletionStatus(  
  2.   
  3.      HANDLE hCompletionPort,  
  4.   
  5.      DWORD dwNumBytes,  
  6.   
  7.      ULONG_PTR CompletionKey,  
  8.   
  9.      OVERLAPPED*pOverlapped);  


 

    這個函數(shù)用來將已完成的IO通知追加到IO完成端口的隊列中。

 

     hCompletionPort表示我們要將已完成的IO項添加到哪個完成端口的隊列中。

 

    剩下的三個參數(shù)表示應(yīng)該返回給主調(diào)線程的值。

 

    每個線程都調(diào)用一次GetQueuedCompletionStatus,將它們都喚?E7??程通信。例如:當(dāng)用戶終止服務(wù)程序時,我們想要所有線程退出。如果各個線程還在等待完成端口但有沒有已完成的IO請求,那么無法將它們喚醒。我們可以為線程池中的每個線程都調(diào)用一次GetQueuedCompletionStatus,將它們都喚醒。各線程對GetQueuedCompletionStatus函數(shù)返回值進行檢查,如果發(fā)現(xiàn)應(yīng)用程序正在終止,就會正常退出。(由于線程等待隊列是以棧方式喚醒各線程,為了保證線程池中每個線程都有機會得到模擬IO項,我們還必須在程序中采用其他線程同步機制)

 

    當(dāng)對完成端口調(diào)用CloseHandle時,系統(tǒng)會將所有正在等待GetQueuedCompletionStatus返回的線程喚醒,并返回false。GetLastError返回ERROR_INVALID_HANDLE,此時線程就可以知道應(yīng)該退出了。

 

            下面的例子使用異步IO 實現(xiàn)了文件復(fù)制工作。首先選擇要復(fù)制的文件,并取得源文件大小。點擊復(fù)制按鈕創(chuàng)建一個線程。新線程將完成創(chuàng)建IO完成端口和關(guān)聯(lián)設(shè)備及執(zhí)行文件復(fù)制工作。將源文件和目標(biāo)文件都關(guān)聯(lián)到同一個完成端口,根據(jù)GetQueuedCompletionStatus返回時的完成鍵來區(qū)分到底是屬于誰的異步IO返回。在啟動循環(huán)時采用了一個小伎倆:使用PostQueuedCompletionStatus向IO完成端口發(fā)送一個模擬的異步IO請求。完成鍵設(shè)置為WRITE_KEY。此時程序?qū)?zhí)行從源文件讀數(shù)據(jù)操作。這樣就開動了引擎。直到文件復(fù)制完成。注意源文件和目標(biāo)文件以及GetQueuedCompletionStatus使用的OVERLAPPED結(jié)構(gòu)不要使用同一個。
 
選擇文件函數(shù):
  1. void CIOCompletionPortDlg::OnBnClickedBtnChoosefile()  
  2. {  
  3.     // TODO: 在此添加控件通知處理程序代碼  
  4.     CFileDialog dlg(true);  
  5.     dlg.DoModal();  
  6.     m_fileName=dlg.GetPathName();  
  7.     SetDlgItemText(IDC_EDIT_FILENAME,m_fileName);  
  8.   
  9.     m_hSrcFile=CreateFile(m_fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_FLAG_OVERLAPPED,NULL);  
  10.     if(m_hSrcFile==INVALID_HANDLE_VALUE)  
  11.     {  
  12.         return ;  
  13.     }  
  14.     DWORD filesize;  
  15.     DWORD filesizeHigh;  
  16.     m_SrcFileSize=GetFileSize(m_hSrcFile,&filesizeHigh);  
  17.     DWORD t=m_SrcFileSize/1024.0;  
  18.     //filesize/=1024.0;  
  19.     CString temp;  
  20.     temp.Format(TEXT("%d KB"),t);  
  21.     SetDlgItemText(IDC_EDIT_FILESIZE,temp);  
  22. }  
//創(chuàng)建任務(wù)線程:
  1. void CIOCompletionPortDlg::OnBnClickedBtnCopy()  
  2. {  
  3.     // TODO: 在此添加控件通知處理程序代碼  
  4.   
  5.     m_hCopyThread=CreateThread(NULL,0,CopyThread,this,0,NULL);  
  6.     //CloseHandle(m_hCopyThread);  
  7.   
  8. }  

//新線程入口函數(shù):
  1. <span style="font-size:12px;">DWORD WINAPI CIOCompletionPortDlg::CopyThread( PVOID ppram )  
  2. {  
  3.     CIOCompletionPortDlg *pdlg=(CIOCompletionPortDlg*)ppram;  
  4.     pdlg->m_hDesFile=CreateFile(TEXT("備份.exe"),GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_FLAG_OVERLAPPED,pdlg->m_hSrcFile);  
  5.     LARGE_INTEGER filesize;  
  6.     filesize.HighPart=0;  
  7.     filesize.LowPart=pdlg->m_SrcFileSize;  
  8.     SetFilePointerEx(pdlg->m_hDesFile,filesize,NULL,FILE_BEGIN);  
  9.     SetEndOfFile(pdlg->m_hDesFile);  
  10.   
  11.   
  12.     //創(chuàng)建IO完成端口。創(chuàng)建一個完成端口,將兩個設(shè)備將關(guān)聯(lián)到此完成端口上。  
  13.     HANDLE hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,4);//創(chuàng)建完成端口,但不關(guān)聯(lián)設(shè)備。  
  14.     if(hIOCP==NULL)  
  15.     {  
  16.         return 0;  
  17.     }  
  18.     CreateIoCompletionPort(pdlg->m_hSrcFile,hIOCP,READ_KEY,0);//與IO完成端口關(guān)聯(lián)。  
  19.     CreateIoCompletionPort(pdlg->m_hDesFile,hIOCP,WRITE_KEY,0);//在關(guān)聯(lián)時傳入了完成鍵??梢愿鶕?jù)完成鍵來區(qū)別從完成隊列中取出的請求屬于哪個設(shè)備。  
  20.   
  21.     OVERLAPPED ov={0};  
  22.     PostQueuedCompletionStatus(hIOCP,0,WRITE_KEY,&ov);//發(fā)送模擬完成異步IO消息。  
  23.     BYTE *pBuffer=new BYTE[BUFFERSIZE];  
  24.     OVERLAPPED ovDes={0};  
  25.     OVERLAPPED ovSrc={0};  
  26.     while(true)  
  27.     {  
  28.         //memset(pBuffer,0,sizeof(pBuffer));  
  29.         DWORD nTransfer;  
  30.         OVERLAPPED *overlapped;  
  31.         ULONG_PTR CompletionKey;  
  32.         GetQueuedCompletionStatus(hIOCP,&nTransfer,&CompletionKey,(OVERLAPPED**)&overlapped,INFINITE);//IO完成隊列沒有請求項則掛起。否則從IO完成隊列取出。  
  33.         switch(CompletionKey)  
  34.         {  
  35.         case READ_KEY://從IO完成端口取出讀完成。  
  36.             {                                                                                       BOOL r=WriteFile(pdlg->m_hDesFile,pBuffer,overlapped->InternalHigh,NULL,&ovDes);  
  37.   
  38.                 ovDes.Offset+=BUFFERSIZE;  
  39.             }  
  40.             break;  
  41.         case WRITE_KEY://從IO完成隊列中取出寫完成。  
  42.             {  
  43.                 memset(pBuffer,0,BUFFERSIZE);  
  44.                 if(ovSrc.Offset<pdlg->m_SrcFileSize)  
  45.                 {  
  46.                     DWORD nBytes;  
  47.                     if(ovSrc.Offset+BUFFERSIZE<pdlg->m_SrcFileSize)  
  48.                         nBytes=BUFFERSIZE;  
  49.                     else  
  50.                         nBytes=pdlg->m_SrcFileSize-ovSrc.Offset;  
  51.                     ReadFile(pdlg->m_hSrcFile,pBuffer,nBytes,NULL,&ovSrc);//異步IO忽略文件指針。所有對文件的定位操作由OVERLAPPED結(jié)構(gòu)指定。  
  52.                     //一定要注意為每次異步IO請求提供一個OVERLAPPED結(jié)構(gòu)。剛才由于在接收和發(fā)送使用了  
  53.                     //同一個OVERLAPPED結(jié)構(gòu),導(dǎo)致出現(xiàn)重疊 I/O 操作在進行中。錯誤代碼:997  
  54.                     ovSrc.Offset+=BUFFERSIZE;//OVERLAPPED的OffsetHigh結(jié)構(gòu)必須每次都得設(shè)置。  
  55.                 }  
  56.                 else  
  57.                 {  
  58.                     ::MessageBox(NULL,TEXT("文件復(fù)制完成"),TEXT(""),MB_OK);  
  59.                     return 0;  
  60.                 }  
  61.   
  62.             }  
  63.             break;  
  64.         default:  
  65.             break;  
  66.         }  
  67.     }  
  68.     return 0;  
  69. }</span>  

運行結(jié)果:

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多