介紹
本文是基於windows下異步IO一的後續,上一篇我們講了關於windows異步io設備訪問,包括初始化設備,執行IO設備請求,IO請求完成的通知三個部分,其中完成通知我們說有四種方式,上一篇我們講了其他的三種,如果有興趣請先移步上一篇文章。這篇文章我們主要介紹IO完成通知的最後一次方式,IOCP(完成端口)。
創建完成端口
首先完成端口是一個內核對象,有專有的api來創建CreateIoCompletionPort,我們來看下
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
- FileHandle是想要對哪個設備進行IO請求
- ExistingCompletionPort傳入已存在的完成端口
- CompletionKey完成是通知的變量,我們自己可以隨意填充
- NumberOfConcurrentThreads 允許同一時間運行的最大線程數。傳入0,等於電腦的cpu核數
接下來詳細講講這些相關的參數。
首先我們看ExistingCompletionPort這個參數,大家可能會覺得很奇怪,爲什麼我創建完成端口還會傳入一個已經存在的呢。這要從我們這個api的功能性說起,其實這個api擔任了兩個功能,第一個創建完成端口,第二綁定相應的設備到完成端口上,根據需要我們把這個api拆成兩個部分。
// 創建iocp
m_hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, nMaxConcurrency);
// 關聯設備
BOOL fOk = (CreateIoCompletionPort(hDevice, m_hIOCP, CompKey, 0) == m_hIOCP);
首先創建iocp,我們ExistingCompletionPort傳入NULL那肯定就是說要創建一個新的iocp了,因爲我們目前不綁定設備,那個FileHandle傳入INVALID_HANDLE_VALUE,CompletionKey傳入0,只傳入了一個同時運行線程數,這樣我們就創建了一個iocp的內核對象
然後是關聯設備,傳入設備,完成鍵,和已有的iocp。這樣我們就可以將多個設備關聯到同一個iocp上了。
相關數據結構
與iocp相關聯的有五個數據結構。
簡單說下這個圖,基本上都是列表和隊列,右邊示意是列表或隊列的每個元素。
- 設備列表,當我們調用關聯設備時CreateIoCompletionPort,就會被添加到這個列表中,在這個設備被關閉時從這個列表中刪除,這也是沒什麼好說的。
- IO完成隊列,當我們完成IO設備請求後,系統會檢查這個設備是不是與完成端口關聯,如果是關聯的,會將這個請求的相關數據放到IO完成隊列隊尾。
- 當我們做IO請求後且IO請求完成到完成隊列中,我們怎麼能獲取到呢?我們需要調用一個api來獲取,GetQueuedCompletionStatus。我們看下原型:
WINBASEAPI
BOOL
WINAPI
GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED* lpOverlapped,
_In_ DWORD dwMilliseconds
);
- CompletionPort這個是剛剛完成端口
- 後邊三個參數是完成隊列中回傳回來的數據
- 如果相應的完成端口的完成隊列中沒有數據,調用線程就會進入阻塞狀態,而dwMilliseconds表示等待的超時時間,如果完成隊列中有數據,或者超時時間過了這個函數就會返回。
如果調用線程進入等待狀態,那麼就會添加到等待線程隊列。我們也看到完成隊列是先入先出,等待線程隊列是後進先出。完成隊列先入先出很正常,但是爲什麼等待線程隊列要後進先出呢,即比如有多個線程,出現一個IO完成項,最後那個線程對該項處理,完成後進入等待隊列,如果有IO完成項,繼續喚醒這個線程處理。如果IO項完成的很慢,會不會只有這個線程喚醒執行,那麼其他未被調度的線程內存資源就可以換出到磁盤,節省了資源,同時也會減少上下文切換的開銷。
- 那大家可能回想,後邊那兩個數據結構是幹嘛的呢,這也是CreateIoCompletionPort這個函數最後一個參數的作用,同時也是IOCP比較智能的地方。大家有沒有想如果我們開了多個線程去調用GetQueuedCompletionStatus這個函數,讓等待線程隊列的數量就會比創建完成端口時的參數NumberOfConcurrentThreads多。這樣的話,IOCP也只會讓同時喚醒NumberOfConcurrentThreads個線程去處理任務,即使完成隊列有任務沒有完成,即使等待線程隊列還有線程在等待。而正在處理任務的線程就會從等待線程隊列移除被放到第四個數據結構(已釋放線程列表)中,當已釋放線程列表中的一個線程處理任務時自己進入到阻塞狀態(比如調用sleep)。那麼這個線程就會從已釋放線程列表中移除被放入到第五個數據結構中(已暫停線程列表),這時IOCP就會到等待線程隊列中喚醒一個線程去執行任務,維持NumberOfConcurrentThreads個線程在運行。當已暫定線程列表中的線程進入運行狀態時,從已暫定線程列表移除然後進入到已釋放線程列表中,這時會短時間超過NumberOfConcurrentThreads,再當線程調用GetQueuedCompletionStatus進入到等待線程隊列,這樣就能保證最大運行線程數是NumberOfConcurrentThreads。
PostQueuedCompletionStatus
大家可能看這個和GetQueuedCompletionStatus很像,沒錯這個調用時就會向完成隊列添加一項。一般都用來結束整個過程。比如CompletionKey傳一個標識,調用GetQueuedCompletionStatus線程返回發現CompletionKey就會優雅的退出這個線程,回收資源。
WINBASEAPI
BOOL
WINAPI
PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
應該比較簡單,也沒什麼要說的。
實例
因爲IOCP的高性能適用於網絡IO,我寫了一個tcp的服務器,所以這一章的實例我們就不用ReadFile和WriteFile來執行IO設備請求。,有專門針對網絡IO的api。我們主要講述IOCP,所以涉及到的網絡知識大家自行查找吧,或留言交流。
《windows核心編程》中對IOCP的api進行了封裝,我們直接拿來看:
class CIOCP {
public:
CIOCP(int nMaxConcurrency = -1) {
m_hIOCP = NULL;
if (nMaxConcurrency != -1)
(void) Create(nMaxConcurrency);
}
~CIOCP() {
if (m_hIOCP != NULL)
chVERIFY(CloseHandle(m_hIOCP));
}
BOOL Close() {
BOOL bResult = CloseHandle(m_hIOCP);
m_hIOCP = NULL;
return(bResult);
}
BOOL Create(int nMaxConcurrency = 0) {
m_hIOCP = CreateIoCompletionPort(
INVALID_HANDLE_VALUE, NULL, 0, nMaxConcurrency);
chASSERT(m_hIOCP != NULL);
return(m_hIOCP != NULL);
}
BOOL AssociateDevice(HANDLE hDevice, ULONG_PTR CompKey) {
BOOL fOk = (CreateIoCompletionPort(hDevice, m_hIOCP, CompKey, 0)
== m_hIOCP);
chASSERT(fOk);
return(fOk);
}
BOOL AssociateSocket(SOCKET hSocket, ULONG_PTR CompKey) {
return(AssociateDevice((HANDLE) hSocket, CompKey));
}
BOOL PostStatus(ULONG_PTR CompKey, DWORD dwNumBytes = 0,
OVERLAPPED* po = NULL) {
BOOL fOk = PostQueuedCompletionStatus(m_hIOCP, dwNumBytes, CompKey, po);
chASSERT(fOk);
return(fOk);
}
BOOL GetStatus(ULONG_PTR* pCompKey, PDWORD pdwNumBytes,
OVERLAPPED** ppo, DWORD dwMilliseconds = INFINITE) {
return(GetQueuedCompletionStatus(m_hIOCP, pdwNumBytes,
pCompKey, ppo, dwMilliseconds));
}
private:
HANDLE m_hIOCP;
};
這裏的功能我們都有講過,Create創建IOCP,AssociateSocket綁定一個SOCKET,GetStatus封裝了GetQueuedCompletionStatus,PostStatus封裝了PostQueuedCompletionStatus。
繼續看書中也對OVERLAPPED進行了封裝,我做了一些修改:
class IOReq : public OVERLAPPED
{
public:
IOReq() {
ResetOverlapped();
}
~IOReq() {}
enum ReqType {
ReqType_Send,
ReqType_Recv,
};
bool Recv(SOCKET socket) {
ResetOverlapped();
ZeroMemory(&(m_Data), sizeof(m_Data));
m_Type = ReqType_Recv;
m_Socket = socket;
m_WSABuffser.buf = m_Data;
m_WSABuffser.len = BUFFER_SIZE;
DWORD recvByte = 0;;
DWORD flag = 0;
int iRet = WSARecv(m_Socket, &m_WSABuffser, 1, &recvByte, &flag, this, NULL);
if (iRet == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
return false;
}
return true;
}
bool Send(SOCKET socket, const char* pData, int len) {
ResetOverlapped();
ZeroMemory(&(m_Data), sizeof(m_Data));
memcpy(m_Data, pData, len);
m_Type = ReqType_Send;
m_Socket = socket;
m_WSABuffser.buf = m_Data;
m_WSABuffser.len = len;
DWORD sendByte = 0;
DWORD flag = 0;
int iRet = WSASend(m_Socket, &m_WSABuffser, 1, &sendByte, flag, this, NULL);
if (iRet == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
return false;
}
return true;
}
SOCKET Socket() const {
return m_Socket;
}
void CloseSocket() {
Utils::CleanupSocket(m_Socket);
}
ReqType Type() const {
return m_Type;
}
const char* data() const {
return m_Data;
}
private:
void ResetOverlapped() {
Internal = InternalHigh = 0;
Offset = OffsetHigh = 0;
hEvent = NULL;
}
private:
char m_Data[BUFFER_SIZE];
WSABUF m_WSABuffser;
SOCKET m_Socket;
ReqType m_Type;
};
IOReq 繼承了OVERLAPPED,同時添加了一些相應的數據,包含要接收或者發送的緩存,同時也封裝了向socket的發送和接收數據。
接下來我們講解下關於IOCP的主程序:
void IocpServer::Run()
{
if (Init() < 0) {
return;
}
if (CreateSomeWorkThread() < 0) {
return;
}
AcceptReqAndRecv();
std::size_t size = m_Threads.size();
for (std::size_t i = 0; i < size; ++i) {
m_Threads[i]->join();
}
CleanupAllPendingSocket();
}
void IocpServer::Stop()
{
int size = m_Threads.size();
for (int i = 0; i < size; ++i) {
m_IocpHandle.PostStatus(END_SERVER, -1, NULL);
}
Utils::CleanupSocket(m_ListenSocket);
}
m_IocpHandle是CIOCP的對象,作爲IocpServer成員變量。
Init()是初始化網絡狀態,CreateSomeWorkThread()創建多個線程去調用m_IocpHandle的GetStatus(),然後就是AcceptReqAndRecv接收網絡請求和發起recv的IO請求。
Stop函數我們使用了PostStatus函數拋給完成隊列完成項,完成項的數量和線程數一致。
繼續看下AcceptReqAndRecv(),accept成功後我們需要綁定socket到IOCP上:
// recv
m_IocpHandle.AssociateSocket(acceptSocket, NULL);
IOReq *req = new IOReq;
m_PendingRecvMutex.lock();
m_PendingRecvReqs.emplace(std::make_pair(acceptSocket, req));
m_PendingRecvMutex.unlock();
bool rc = req->Recv(acceptSocket);
if (!rc) {
delete req;
std::cout << "recv one error:" << WSAGetLastError() << std::endl;
}
然後看下我們接收完成項的地方,每個線程都調用DoWork():
void IocpServer::DoWork()
{
ULONG_PTR compleKey;
DWORD numBytes;
IOReq *req = nullptr;
while (1) {
m_IocpHandle.GetStatus(&compleKey, &numBytes, (OVERLAPPED **)&req, INFINITE);
// server end
if (compleKey == END_SERVER) {
return;
}
// the socket disconnect
if (numBytes == 0) {
req->CloseSocket();
ClearPendingSocket(req->Socket());
continue;
}
// complete recv
if (req->Type() == IOReq::ReqType_Recv) {
DoResponse(req->Socket(), req->data(), numBytes);
if (!req->Recv(req->Socket())) {
delete req;
std::cout << "recv one error:" << WSAGetLastError() << std::endl;
}
continue;
}
// complete send
if (req->Type() == IOReq::ReqType_Send) {
ClearPendingSendSocket(req->Socket());
}
}
}
接收到數據後去DoResponse做一些事情。而發送完了就什麼也不做。
完
到此我就講完了IOCP的相關知識,歡迎指正,也歡迎交流,我會把示例代碼放到csdn上和github。
github上後期也會繼續完善的。
csdn地址: https://download.csdn.net/download/leapmotion/12234038
github地址: https://github.com/zhangdexin/WinIPC