1.現(xiàn)在睡覺的話會做夢而現(xiàn)在學(xué)習(xí)的話會讓夢實現(xiàn)
This moment will nap, you will have a dream; But this moment study,you will interpret a dream.
2.我無所事事地度過的今天是昨天死去的人們所奢望的明天
I leave uncultivated today, was precisely yesterday
perishes tomorrow which person of the body implored.
3.感到晚了的時候其實是最早的時候
Thought is already is late, exactly is the earliest time.
4.不要把今天的事拖到明天
Not matter of the today will drag tomorrow.
5.學(xué)習(xí)的痛苦是一時的而沒有學(xué)習(xí)的痛苦是一輩子的
Time the study pain is temporary, has not learned the pain is life-long.
6.學(xué)習(xí)不是人生的全部但連學(xué)習(xí)都征服不了你還能做什么?
The study certainly
is not the life complete. But, sincecontinually life part of - studies
also is unable to conquer, what butalso can make?
7.學(xué)習(xí)不是因為缺少時間而是缺少努力
Studies this matter, lacks the time, but is lacks diligently.
8.所有人的成功都不是偶然的
Nobody can casually succeed, it comes from the thoroughself-control and the will.
9.無法避免的痛苦就去享受吧!
Please enjoy the pain which is unable to avoid.
10.早起的鳥兒有蟲吃
Only has compared to the others early, diligently diligently, canfeel the successful taste.
11.成功并不屬于每個人
Nobody can casually succeed
12.時間在流逝
HOW time flies!
13.今天流下的口水將變成明天流下的淚水
Now drips the saliva, will become tomorrow the tear
WINDOWS完成端口編程
1、基本概念
2、WINDOWS完成端口的特點
3、完成端口(Completion Ports )相關(guān)數(shù)據(jù)結(jié)構(gòu)和創(chuàng)建
4、完成端口線程的工作原理
5、Windows完成端口的實例代碼
Linux的EPoll模型
1、為什么select落后
2、內(nèi)核中提高I/O性能的新方法epoll
3、epoll的優(yōu)點
4、epoll的工作模式
5、epoll的使用方法
6、Linux下EPOll編程實例
總結(jié)
WINDOWS完成端口編程
摘要:開發(fā)網(wǎng)絡(luò)程序從來都不是一件容易的事情,盡管只需要遵守很少的一些規(guī)則;創(chuàng)建socket,發(fā)起連接,接受連接,發(fā)送和接受數(shù)據(jù)。真正的困難在于:
讓你的程序可以適應(yīng)從單單一個連接到幾千個連接乃至于上萬個連接。利用Windows平臺完成端口進(jìn)行重疊I/O的技術(shù)和Linux在2.6版本的內(nèi)核中
引入的EPOll技術(shù),可以很方便在在在Windows和Linux平臺上開發(fā)出支持大量連接的網(wǎng)絡(luò)服務(wù)程序。本文介紹在Windows和Linux平臺
上使用的完成端口和EPoll模型開發(fā)的基本原理,同時給出實際的例子。本文主要關(guān)注C/S結(jié)構(gòu)的服務(wù)器端程序,因為一般來說,開發(fā)一個大容量,具可擴展
性的winsock程序一般就是指服務(wù)程序。
1、基本概念
設(shè)備---windows操作系統(tǒng)上允許通信的任何東西,比如文件、目錄、串行口、并行口、郵件槽、命名管道、無名管道、套接字、控制臺、邏輯磁盤、物理
磁盤等。絕大多數(shù)與設(shè)備打交道的函數(shù)都是CreateFile/ReadFile/WriteFile等。所以我們不能看到**File函數(shù)就只想到文件
設(shè)備。與設(shè)備通信有兩種方式,同步方式和異步方式。同步方式下,當(dāng)調(diào)用ReadFile函數(shù)時,函數(shù)會等待系統(tǒng)執(zhí)行完所要求的工作,然后才返回;異步方式
下,ReadFile這類函數(shù)會直接返回,系統(tǒng)自己去完成對設(shè)備的操作,然后以某種方式通知完成操作。
重疊I/O----顧名思義,當(dāng)你調(diào)用了某
個函數(shù)(比如ReadFile)就立刻返回做自己的其他動作的時候,同時系統(tǒng)也在對I/0設(shè)備進(jìn)行你要求的操作,在這段時間內(nèi)你的程序和系統(tǒng)的內(nèi)部動作是
重疊的,因此有更好的性能。所以,重疊I/O是用于異步方式下使用I/O設(shè)備的。 重疊I/O需要使用的一個非常重要的數(shù)據(jù)結(jié)構(gòu)OVERLAPPED。
2、WINDOWS完成端口的特點
Win32重疊I/O(Overlapped
I/O)機制允許發(fā)起一個操作,然后在操作完成之后接受到信息。對于那種需要很長時間才能完成的操作來說,重疊IO機制尤其有用,因為發(fā)起重疊操作的線程
在重疊請求發(fā)出后就可以自由的做別的事情了。在WinNT和Win2000上,提供的真正的可擴展的I/O模型就是使用完成端口(Completion
Port)的重疊I/O.完成端口---是一種WINDOWS內(nèi)核對象。完成端口用于異步方式的重疊I/0情況下,當(dāng)然重疊I/O不一定非使用完成端口不
可,還有設(shè)備內(nèi)核對象、事件對象、告警I/0等。但是完成端口內(nèi)部提供了線程池的管理,可以避免反復(fù)創(chuàng)建線程的開銷,同時可以根據(jù)CPU的個數(shù)靈活的決定
線程個數(shù),而且可以讓減少線程調(diào)度的次數(shù)從而提高性能其實類似于WSAAsyncSelect和select函數(shù)的機制更容易兼容Unix,但是難以實現(xiàn)
我們想要的“擴展性”。而且windows的完成端口機制在操作系統(tǒng)內(nèi)部已經(jīng)作了優(yōu)化,提供了更高的效率。所以,我們選擇完成端口開始我們的服務(wù)器程序的
開發(fā)。
1、發(fā)起操作不一定完成,系統(tǒng)會在完成的時候通知你,通過用戶在完成端口上的等待,處理操作的結(jié)果。所以要有檢查完成端口,取操作結(jié)果的線
程。在完成端口上守候的線程系統(tǒng)有優(yōu)化,除非在執(zhí)行的線程阻塞,不會有新的線程被激活,以此來減少線程切換造成的性能代價。所以如果程序中沒有太多的阻塞
操作,沒有必要啟動太多的線程,CPU數(shù)量的兩倍,一般這樣來啟動線程。
2、操作與相關(guān)數(shù)據(jù)的綁定方式:在提交數(shù)據(jù)的時候用戶對數(shù)據(jù)打相應(yīng)的標(biāo)記,記錄操作的類型,在用戶處理操作結(jié)果的時候,通過檢查自己打的標(biāo)記和系統(tǒng)的操作結(jié)果進(jìn)行相應(yīng)的處理。
3、
操作返回的方式:一般操作完成后要通知程序進(jìn)行后續(xù)處理。但寫操作可以不通知用戶,此時如果用戶寫操作不能馬上完成,寫操作的相關(guān)數(shù)據(jù)會被暫存到到非交換
緩沖區(qū)中,在操作完成的時候,系統(tǒng)會自動釋放緩沖區(qū)。此時發(fā)起完寫操作,使用的內(nèi)存就可以釋放了。此時如果占用非交換緩沖太多會使系統(tǒng)停止響應(yīng)。
3、完成端口(Completion Ports )相關(guān)數(shù)據(jù)結(jié)構(gòu)和創(chuàng)建
其實可以把完成端口看成系統(tǒng)維護(hù)的一個隊列,操作系統(tǒng)把重疊IO操作完成的事件通知放到該隊列里,由于是暴露
“操作完成”的事件通知,所以命名為“完成端口”(COmpletion
Ports)。一個socket被創(chuàng)建后,可以在任何時刻和一個完成端口聯(lián)系起來。
完成端口相關(guān)最重要的是OVERLAPPED數(shù)據(jù)結(jié)構(gòu)
typedef struct _OVERLAPPED {
ULONG_PTR Internal;//被系統(tǒng)內(nèi)部賦值,用來表示系統(tǒng)狀態(tài)
ULONG_PTR InternalHigh;// 被系統(tǒng)內(nèi)部賦值,傳輸?shù)淖止?jié)數(shù)
union {
struct {
DWORD Offset;//和OffsetHigh合成一個64位的整數(shù),用來表示從文件頭部的多少字節(jié)開始
DWORD OffsetHigh;//操作,如果不是對文件I/O來操作,則必須設(shè)定為0
};
PVOID Pointer;
};
HANDLE hEvent;//如果不使用,就務(wù)必設(shè)為0,否則請賦一個有效的Event句柄
} OVERLAPPED, *LPOVERLAPPED;
下面是異步方式使用ReadFile的一個例子
OVERLAPPED Overlapped;
Overlapped.Offset=345;
Overlapped.OffsetHigh=0;
Overlapped.hEvent=0;
//假定其他參數(shù)都已經(jīng)被初始化
ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped);
這樣就完成了異步方式讀文件的操作,然后ReadFile函數(shù)返回,由操作系統(tǒng)做自己的事情,下面介紹幾個與OVERLAPPED結(jié)構(gòu)相關(guān)的函數(shù)
等待重疊I/0操作完成的函數(shù)
BOOL GetOverlappedResult (
HANDLE hFile,
LPOVERLAPPED lpOverlapped,//接受返回的重疊I/0結(jié)構(gòu)
LPDWORD lpcbTransfer,//成功傳輸了多少字節(jié)數(shù)
BOOL fWait //TRUE只有當(dāng)操作完成才返回,F(xiàn)ALSE直接返回,如果操作沒有完成,通過調(diào)//用GetLastError ( )函數(shù)會返回ERROR_IO_INCOMPLETE
);
宏HasOverlappedIoCompleted可以幫助我們測試重疊I/0操作是否完成,該宏對OVERLAPPED結(jié)構(gòu)的Internal成員進(jìn)行了測試,查看是否等于STATUS_PENDING值。
一般來說,一個應(yīng)用程序可以創(chuàng)建多個工作線程來處理完成端口上的通知事件。工作線程的數(shù)量依賴于程序的具體需要。但是在理想的情況下,應(yīng)該對應(yīng)一個CPU
創(chuàng)建一個線程。因為在完成端口理想模型中,每個線程都可以從系統(tǒng)獲得一個“原子”性的時間片,輪番運行并檢查完成端口,線程的切換是額外的開銷。在實際開
發(fā)的時候,還要考慮這些線程是否牽涉到其他堵塞操作的情況。如果某線程進(jìn)行堵塞操作,系統(tǒng)則將其掛起,讓別的線程獲得運行時間。因此,如果有這樣的情況,
可以多創(chuàng)建幾個線程來盡量利用時間。
應(yīng)用完成端口:
創(chuàng)建完成端口:完成端口是一個內(nèi)核對象,使用時他總是要和至少一個有效的設(shè)備句柄進(jìn)行關(guān)聯(lián),完成端口是一個復(fù)雜的內(nèi)核對象,創(chuàng)建它的函數(shù)是:
HANDLE CreateIoCompletionPort(
IN HANDLE FileHandle,
IN HANDLE ExistingCompletionPort,
IN ULONG_PTR CompletionKey,
IN DWORD NumberOfConcurrentThreads
);
通常創(chuàng)建工作分兩步:
第一步,創(chuàng)建一個新的完成端口內(nèi)核對象,可以使用下面的函數(shù):
HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads)
{
return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
};
第二步,將剛創(chuàng)建的完成端口和一個有效的設(shè)備句柄關(guān)聯(lián)起來,可以使用下面的函數(shù):
bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey)
{
HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0);
return h==hCompPort;
};
說明
1) CreateIoCompletionPort函數(shù)也可以一次性的既創(chuàng)建完成端口對象,又關(guān)聯(lián)到一個有效的設(shè)備句柄
2) CompletionKey是一個可以自己定義的參數(shù),我們可以把一個結(jié)構(gòu)的地址賦給它,然后在合適的時候取出來使用,最好要保證結(jié)構(gòu)里面的內(nèi)存不是分配在棧上,除非你有十分的把握內(nèi)存會保留到你要使用的那一刻。
3)
NumberOfConcurrentThreads通常用來指定要允許同時運行的的線程的最大個數(shù)。通常我們指定為0,這樣系統(tǒng)會根據(jù)CPU的個數(shù)來自
動確定。創(chuàng)建和關(guān)聯(lián)的動作完成后,系統(tǒng)會將完成端口關(guān)聯(lián)的設(shè)備句柄、完成鍵作為一條紀(jì)錄加入到這個完成端口的設(shè)備列表中。如果你有多個完成端口,就會有多
個對應(yīng)的設(shè)備列表。如果設(shè)備句柄被關(guān)閉,則表中自動刪除該紀(jì)錄。
4、完成端口線程的工作原理
完成端口可以幫助我們管理線程池,但是線程池中的線程需要我們使用_beginthreadex來創(chuàng)建,憑什么通知完成端口管理我們的新線程呢?答案在函數(shù)GetQueuedCompletionStatus。該函數(shù)原型:
BOOL GetQueuedCompletionStatus(
IN HANDLE CompletionPort,
OUT LPDWORD lpNumberOfBytesTransferred,
OUT PULONG_PTR lpCompletionKey,
OUT LPOVERLAPPED *lpOverlapped,
IN DWORD dwMilliseconds
);
這
個函數(shù)試圖從指定的完成端口的I/0完成隊列中抽取紀(jì)錄。只有當(dāng)重疊I/O動作完成的時候,完成隊列中才有紀(jì)錄。凡是調(diào)用這個函數(shù)的線程將被放入到完成端
口的等待線程隊列中,因此完成端口就可以在自己的線程池中幫助我們維護(hù)這個線程。完成端口的I/0完成隊列中存放了當(dāng)重疊I/0完成的結(jié)果----
一條紀(jì)錄,該紀(jì)錄擁有四個字段,前三項就對應(yīng)GetQueuedCompletionStatus函數(shù)的2、3、4參數(shù),最后一個字段是錯誤信息
dwError。我們也可以通過調(diào)用PostQueudCompletionStatus模擬完成了一個重疊I/0操作。
當(dāng)I/0完成隊列中
出現(xiàn)了紀(jì)錄,完成端口將會檢查等待線程隊列,該隊列中的線程都是通過調(diào)用GetQueuedCompletionStatus函數(shù)使自己加入隊列的。等待
線程隊列很簡單,只是保存了這些線程的ID。完成端口會按照后進(jìn)先出的原則將一個線程隊列的ID放入到釋放線程列表中,同時該線程將從等待
GetQueuedCompletionStatus函數(shù)返回的睡眠狀態(tài)中變?yōu)榭烧{(diào)度狀態(tài)等待CPU的調(diào)度。所以我們的線程要想成為完成端口管理的線程,
就必須要調(diào)用GetQueuedCompletionStatus函數(shù)。出于性能的優(yōu)化,實際上完成端口還維護(hù)了一個暫停線程列表,具體細(xì)節(jié)可以參考
《Windows高級編程指南》,我們現(xiàn)在知道的知識,已經(jīng)足夠了。
完成端口線程間數(shù)據(jù)傳遞線程間傳遞數(shù)據(jù)最常用的辦法是在_beginthreadex函數(shù)中將參數(shù)傳遞給線程函數(shù),或者使用全局變量。但是完成端口還有自
己的傳遞數(shù)據(jù)的方法,答案就在于CompletionKey和OVERLAPPED參數(shù)。
CompletionKey被保存在完成端口的設(shè)備表
中,是和設(shè)備句柄一一對應(yīng)的,我們可以將與設(shè)備句柄相關(guān)的數(shù)據(jù)保存到CompletionKey中,或者將CompletionKey表示為結(jié)構(gòu)指針,這
樣就可以傳遞更加豐富的內(nèi)容。這些內(nèi)容只能在一開始關(guān)聯(lián)完成端口和設(shè)備句柄的時候做,因此不能在以后動態(tài)改變。
OVERLAPPED參數(shù)是在每次
調(diào)用ReadFile這樣的支持重疊I/0的函數(shù)時傳遞給完成端口的。我們可以看到,如果我們不是對文件設(shè)備做操作,該結(jié)構(gòu)的成員變量就對我們幾乎毫無作
用。我們需要附加信息,可以創(chuàng)建自己的結(jié)構(gòu),然后將OVERLAPPED結(jié)構(gòu)變量作為我們結(jié)構(gòu)變量的第一個成員,然后傳遞第一個成員變量的地址給
ReadFile函數(shù)。因為類型匹配,當(dāng)然可以通過編譯。當(dāng)GetQueuedCompletionStatus函數(shù)返回時,我們可以獲取到第一個成員變
量的地址,然后一個簡單的強制轉(zhuǎn)換,我們就可以把它當(dāng)作完整的自定義結(jié)構(gòu)的指針使用,這樣就可以傳遞很多附加的數(shù)據(jù)了。太好了!只有一點要注意,如果跨線
程傳遞,請注意將數(shù)據(jù)分配到堆上,并且接收端應(yīng)該將數(shù)據(jù)用完后釋放。我們通常需要將ReadFile這樣的異步函數(shù)的所需要的緩沖區(qū)放到我們自定義的結(jié)構(gòu)
中,這樣當(dāng)GetQueuedCompletionStatus被返回時,我們的自定義結(jié)構(gòu)的緩沖區(qū)變量中就存放了I/0操作的數(shù)據(jù)。
CompletionKey和OVERLAPPED參數(shù),都可以通過GetQueuedCompletionStatus函數(shù)獲得。
線程的安全退出
很多線程為了不止一次的執(zhí)行異步數(shù)據(jù)處理,需要使用如下語句
while (true)
{
......
GetQueuedCompletionStatus(...);
......
}
那么如何退出呢,答案就在于上面曾提到的PostQueudCompletionStatus函數(shù),我們可以用它發(fā)送一個自定義的包含了OVERLAPPED成員變量的結(jié)構(gòu)地址,里面包含一個狀態(tài)變量,當(dāng)狀態(tài)變量為退出標(biāo)志時,線程就執(zhí)行清除動作然后退出。
5、Windows完成端口的實例代碼:
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
ULONG_PTR *PerHandleKey;
OVERLAPPED *Overlap;
OVERLAPPEDPLUS *OverlapPlus,
*newolp;
DWORD dwBytesXfered;
while (1)
{
ret = GetQueuedCompletionStatus(
hIocp,
&dwBytesXfered,
(PULONG_PTR)&PerHandleKey,
&Overlap,
INFINITE);
if (ret == 0)
{
// Operation failed
continue;
}
OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
switch (OverlapPlus->OpCode)
{
case OP_ACCEPT:
// Client socket is contained in OverlapPlus.sclient
// Add client to completion port
CreateIoCompletionPort(
(HANDLE)OverlapPlus->sclient,
hIocp,
(ULONG_PTR)0,
0);
// Need a new OVERLAPPEDPLUS structure
// for the newly accepted socket. Perhaps
// keep a look aside list of free structures.
newolp = AllocateOverlappedPlus();
if (!newolp)
{
// Error
}
newolp->s = OverlapPlus->sclient;
newolp->OpCode = OP_READ;
// This function divpares the data to be sent
PrepareSendBuffer(&newolp->wbuf);
ret = WSASend(
newolp->s,
&newolp->wbuf,
1,
&newolp->dwBytes,
0,
&newolp.ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
// Put structure in look aside list for later use
FreeOverlappedPlus(OverlapPlus);
// Signal accept thread to issue another AcceptEx
SetEvent(hAcceptThread);
break;
case OP_READ:
// Process the data read
// Repost the read if necessary, reusing the same
// receive buffer as before
memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
ret = WSARecv(
OverlapPlus->s,
&OverlapPlus->wbuf,
1,
&OverlapPlus->dwBytes,
&OverlapPlus->dwFlags,
&OverlapPlus->ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
break;
case OP_WRITE:
// Process the data sent, etc.
break;
} // switch
} // while
} // WorkerThread
查看以上代碼,注意如果Overlapped操作立刻失敗(比如,返回SOCKET_ERROR或其他非WSA_IO_PENDING的錯誤),則
沒有任何完成通知時間會被放到完成端口隊列里。反之,則一定有相應(yīng)的通知時間被放到完成端口隊列。更完善的關(guān)于Winsock的完成端口機制,可以參考
MSDN的Microsoft PlatFormSDK,那里有完成端口的例子。訪問http://msdn.microsoft.com/library/techart/msdn_servrapp.htm可以獲得更多信息。
Linux的EPoll模型
Linux 2.6內(nèi)核中提高網(wǎng)絡(luò)I/O性能的新方法-epoll I/O多路復(fù)用技術(shù)在比較多的TCP網(wǎng)絡(luò)服務(wù)器中有使用,即比較多的用到select函數(shù)。
1、為什么select落后
首先,在Linux內(nèi)核中,select所用到的FD_SET是有限的,即內(nèi)核中有個參數(shù)__FD_SETSIZE定義了每個FD_SET的句柄個數(shù),在我用的2.6.15-25-386內(nèi)核中,該值是1024,搜索內(nèi)核源代碼得到:
include/linux/posix_types.h:#define __FD_SETSIZE 1024
也
就是說,如果想要同時檢測1025個句柄的可讀狀態(tài)是不可能用select實現(xiàn)的。或者同時檢測1025個句柄的可寫狀態(tài)也是不可能的。其次,內(nèi)核中實現(xiàn)
select是用輪詢方法,即每次檢測都會遍歷所有FD_SET中的句柄,顯然,select函數(shù)執(zhí)行時間與FD_SET中的句柄個數(shù)有一個比例關(guān)系,即
select要檢測的句柄數(shù)越多就會越費時。當(dāng)然,在前文中我并沒有提及poll方法,事實上用select的朋友一定也試過poll,我個人覺得
select和poll大同小異,個人偏好于用select而已。
2、內(nèi)核中提高I/O性能的新方法epoll
epoll是什么?按照man手冊的說法:是為處理大批量句柄而作了改進(jìn)的poll。要使用epoll只需要這三個系統(tǒng)調(diào)用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。
當(dāng)然,這不是2.6內(nèi)核才有的,它是在2.5.44內(nèi)核中被引進(jìn)的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
Linux2.6內(nèi)核epoll介紹
先介紹2本書《The Linux Networking
Architecture--Design and Implementation of Network Protocols in the
Linux Kernel》,以2.4內(nèi)核講解Linux
TCP/IP實現(xiàn),相當(dāng)不錯.作為一個現(xiàn)實世界中的實現(xiàn),很多時候你必須作很多權(quán)衡,這時候參考一個久經(jīng)考驗的系統(tǒng)更有實際意義。舉個例子,linux內(nèi)
核中sk_buff結(jié)構(gòu)為了追求速度和安全,犧牲了部分內(nèi)存,所以在發(fā)送TCP包的時候,無論應(yīng)用層數(shù)據(jù)多大,sk_buff最小也有272的字節(jié).其實
對于socket應(yīng)用層程序來說,另外一本書《UNIX Network Programming Volume
1》意義更大一點.2003年的時候,這本書出了最新的第3版本,不過主要還是修訂第2版本。其中第6章《I/O
Multiplexing》是最重要的。Stevens給出了網(wǎng)絡(luò)IO的基本模型。在這里最重要的莫過于select模型和Asynchronous
I/O模型.從理論上說,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告訴你IO操作完成。但是一直以來,如何實現(xiàn)就沒有一個完美的方
案。最著名的windows完成端口實現(xiàn)的AIO,實際上也是內(nèi)部用線程池實現(xiàn)的罷了,最后的結(jié)果是IO有個線程池,你應(yīng)用也需要一個線程池......
很多文檔其實已經(jīng)指出了這帶來的線程context-switch帶來的代價。在linux
平臺上,關(guān)于網(wǎng)絡(luò)AIO一直是改動最多的地方,2.4的年代就有很多AIO內(nèi)核patch,最著名的應(yīng)該算是SGI那個。但是一直到2.6內(nèi)核發(fā)布,網(wǎng)絡(luò)
模塊的AIO一直沒有進(jìn)入穩(wěn)定內(nèi)核版本(大部分都是使用用戶線程模擬方法,在使用了NPTL的linux上面其實和windows的完成端口基本上差不多
了)。2.6內(nèi)核所支持的AIO特指磁盤的AIO---支持io_submit(),io_getevents()以及對Direct
IO的支持(就是繞過VFS系統(tǒng)buffer直接寫硬盤,對于流服務(wù)器在內(nèi)存平穩(wěn)性上有相當(dāng)幫助)。
所以,剩下的select模型基本上就是我們
在linux上面的唯一選擇,其實,如果加上no-block
socket的配置,可以完成一個"偽"AIO的實現(xiàn),只不過推動力在于你而不是os而已。不過傳統(tǒng)的select/poll函數(shù)有著一些無法忍受的缺
點,所以改進(jìn)一直是2.4-2.5開發(fā)版本內(nèi)核的任務(wù),包括/dev/poll,realtime signal等等。最終,Davide
Libenzi開發(fā)的epoll進(jìn)入2.6內(nèi)核成為正式的解決方案
3、epoll的優(yōu)點
<1>支持一個進(jìn)程打開大數(shù)目的socket描述符(FD)
select
最不能忍受的是一個進(jìn)程所打開的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是2048。對于那些需要支持的上萬連接數(shù)目的IM服務(wù)器來說顯
然太少了。這時候你一是可以選擇修改這個宏然后重新編譯內(nèi)核,不過資料也同時指出這樣會帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的
Apache方案),不過雖然linux上面創(chuàng)建進(jìn)程的代價比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完
美的方案。不過
epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于2048,舉個例子,在1GB內(nèi)存的機器上大約是10萬左
右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
<2>IO效率不隨FD數(shù)目增加而線性下降
傳
統(tǒng)的select/poll另一個致命弱點就是當(dāng)你擁有一個很大的socket集合,不過由于網(wǎng)絡(luò)延時,任一時間只有部分的socket是"活躍"的,但
是select/poll每次調(diào)用都會線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個問題,它只會對"活躍"的socket進(jìn)行操
作---這是因為在內(nèi)核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。那么,只有"活躍"的socket才會主動的去調(diào)用
callback函數(shù),其他idle狀態(tài)socket則不會,在這點上,epoll實現(xiàn)了一個"偽"AIO,因為這時候推動力在os內(nèi)核。在一些
benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環(huán)境,epoll并不比select/poll有什么效率,相
反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle
connections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了。
<3>使用mmap加速內(nèi)核與用戶空間的消息傳遞。
這
點實際上涉及到epoll的具體實現(xiàn)了。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很
重要,在這點上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實現(xiàn)的。而如果你想我一樣從2.5內(nèi)核就關(guān)注epoll的話,一定不會忘記手工
mmap這一步的。
<4>內(nèi)核微調(diào)
這一點其實不算epoll的優(yōu)點了,而是整個linux平臺的優(yōu)點。也許你可以懷疑
linux平臺,但是你無法回避linux平臺賦予你微調(diào)內(nèi)核的能力。比如,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu),那么可以在運行時
期動態(tài)調(diào)整這個內(nèi)存pool(skb_head_pool)的大小--- 通過echo
XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個參數(shù)(TCP完成3次握手
的數(shù)據(jù)包隊列長度),也可以根據(jù)你平臺內(nèi)存大小動態(tài)調(diào)整。更甚至在一個數(shù)據(jù)包面數(shù)目巨大但同時每個數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng)
卡驅(qū)動架構(gòu)。
4、epoll的工作模式
令人高興的是,2.6內(nèi)核的epoll比其2.5開發(fā)版本的/dev/epoll簡潔了許多,所以,大部分情況下,強大的東西往往是簡單的。唯一有點麻煩是epoll有2種工作方式:LT和ET。
LT(level
triggered)是缺省的工作方式,并且同時支持block和no-block
socket.在這種做法中,內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進(jìn)行IO操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你
的,所以,這種模式編程出錯誤可能性要小一點。傳統(tǒng)的select/poll都是這種模型的代表.
ET
(edge-triggered)是高速工作方式,只支持no-block
socket。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時,內(nèi)核通過epoll告訴你。然后它會假設(shè)你知道文件描述符已經(jīng)就緒,并且不會再為那個文件描述
符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請求,或者發(fā)送接收的數(shù)據(jù)少于一定量時導(dǎo)致
了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導(dǎo)致它再次變成未就緒),內(nèi)核不會發(fā)送更多的通知(only
once),不過在TCP協(xié)議中,ET模式的加速效用仍需要更多的benchmark確認(rèn)。
epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統(tǒng)調(diào)用,具體用法請參考http://www.xmailserver.org/linux-patches/nio-improve.html ,在http://www.kegel.com/rn/也有一個完整的例子,大家一看就知道如何使用了
Leader/follower模式線程pool實現(xiàn),以及和epoll的配合。
5、 epoll的使用方法
首先通過create_epoll(int
maxfds)來創(chuàng)建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個函數(shù)會返回一個新的epoll句柄,之后的所有操作
將通過這個句柄來進(jìn)行操作。在用完之后,記得用close()來關(guān)閉這個創(chuàng)建出來的epoll句柄。
之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max
events, int timeout)來查詢所有的網(wǎng)絡(luò)接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其
中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個epoll_event*的指針,當(dāng)epoll_wait這個函數(shù)操作成功
之后,epoll_events里面將儲存所有的讀寫事件。max_events是當(dāng)前需要監(jiān)聽的所有socket句柄數(shù)。最后一個timeout是
epoll_wait的超時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件范圍,為任意正整數(shù)的時候表示等這么長的時間,如果一直沒
有事件,則范圍。一般如果網(wǎng)絡(luò)主循環(huán)是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環(huán)
的效率。
epoll_wait范圍之后應(yīng)該是一個循環(huán),遍利所有的事件:
for(n = 0; n < nfds; ++n) {
if(events[n].data.fd == listener) { //如果是主socket的事件的話,則表示有新連接進(jìn)入了,進(jìn)行新連接的處理。
client = accept(listener, (struct sockaddr *) &local,
&addrlen);
if(client < 0){
perror("accept");
continue;
}
setnonblocking(client); // 將新連接置于非阻塞模式
ev.events = EPOLLIN | EPOLLET; // 并且將新連接也加入EPOLL的監(jiān)聽隊列。
注意,這里的參數(shù)EPOLLIN | EPOLLET并沒有設(shè)置對寫socket的監(jiān)聽,如果有寫操作的話,這個時候epoll是不會返回事件的,如果要對寫操作也監(jiān)聽的話,應(yīng)該是EPOLLIN | EPOLLOUT | EPOLLET
ev.data.fd = client;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
//
設(shè)置好event之后,將這個新的event通過epoll_ctl加入到epoll的監(jiān)聽隊列里面,這里用EPOLL_CTL_ADD來加一個新的
epoll事件,通過EPOLL_CTL_DEL來減少一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監(jiān)聽方式。
fprintf(stderr, "epoll set insertion error: fd=%d0,
client);
return -1;
}
}
else // 如果不是主socket的事件的話,則代表是一個用戶socket的事件,則來處理這個用戶socket的事情,比如說read(fd,xxx)之類的,或者一些其他的處理。
do_use_fd(events[n].data.fd);
}
對,epoll的操作就這么簡單,總共不過4個API:epoll_create, epoll_ctl, epoll_wait和close。
如果您對epoll的效率還不太了解,請參考我之前關(guān)于網(wǎng)絡(luò)游戲的網(wǎng)絡(luò)編程等相關(guān)的文章。
以前公司的服務(wù)器都是使用HTTP連接,但是這樣的話,在手機目前的網(wǎng)絡(luò)情況下不但顯得速度較慢,而且不穩(wěn)定。因此大家一致同意用
SOCKET來進(jìn)行連接。雖然使用SOCKET之后,對于用戶的費用可能會增加(由于是用了CMNET而非CMWAP),但是,秉著用戶體驗至上的原則,
相信大家還是能夠接受的(希望那些玩家月末收到帳單不后能夠保持克制...)。
這次的服務(wù)器設(shè)計中,最重要的一個突破,是使用了EPOLL模型,雖然對之也是一知半解,但是既然在各大PC網(wǎng)游中已經(jīng)經(jīng)過了如此嚴(yán)酷的考驗,相信他不會讓我們失望,使用后的結(jié)果,確實也是表現(xiàn)相當(dāng)不錯。在這里,我還是主要大致介紹一下這個模型的結(jié)構(gòu)。
6、Linux下EPOll編程實例
EPOLL模型似乎只有一種格式,所以大家只要參考我下面的代碼,就能夠?qū)POLL有所了解了,代碼的解釋都已經(jīng)在注釋中:
while (TRUE)
{
int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);//等待EPOLL時間的發(fā)生,相當(dāng)于監(jiān)聽,至于相關(guān)的端口,需要在初始化EPOLL的時候綁定。
if (nfds <= 0)
continue;
m_bOnTimeChecking = FALSE;
G_CurTime = time(NULL);
for (int i=0; i
{
try
{
if (m_events[i].data.fd == m_listen_http_fd)//如果新監(jiān)測到一個HTTP用戶連接到綁定的HTTP端口,建立新的連接。由于我們新采用了SOCKET連接,所以基本沒用。
{
OnAcceptHttpEpoll ();
}
else if (m_events[i].data.fd == m_listen_sock_fd)//如果新監(jiān)測到一個SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。
{
OnAcceptSockEpoll ();
}
else if (m_events[i].events & EPOLLIN)//如果是已經(jīng)連接的用戶,并且收到數(shù)據(jù),那么進(jìn)行讀入。
{
OnReadEpoll (i);
}
OnWriteEpoll (i);//查看當(dāng)前的活動連接是否有需要寫出的數(shù)據(jù)。
}
catch (int)
{
PRINTF ("CATCH捕獲錯誤\n");
continue;
}
}
m_bOnTimeChecking = TRUE;
OnTimer ();//進(jìn)行一些定時的操作,主要就是刪除一些短線用戶等。
}
其實EPOLL的精華,也就是上述的幾段短短的代碼,看來時代真的不同了,以前如何接受大量用戶連接的問題,現(xiàn)在卻被如此輕松的搞定,真是讓人不得不感嘆,對哪。
總結(jié)
Windows完成端口與Linux epoll技術(shù)方案是這2個平臺上實現(xiàn)異步IO和設(shè)計開發(fā)一個大容量,具可擴展性的winsock程序指服務(wù)程序的很好的選擇,本文對這2中技術(shù)的實現(xiàn)原理和實際的使用方法做了一個詳細(xì)的介紹。
GlobalAlloc是標(biāo)準(zhǔn)內(nèi)存管理函數(shù),標(biāo)準(zhǔn)內(nèi)存管理函數(shù)都是操作進(jìn)程的默認(rèn)堆,所以這個函數(shù)是從進(jìn)程的默認(rèn)堆中分配內(nèi)存空間,分配的空間可以是可移動的也可以是不可移動的。可移動的內(nèi)存是指Windows在需要的時候可以將這個內(nèi)存移動到另外一個地址.
關(guān)于GlobalAlloc and LocalAlloc,from
MSDN
The global and local functions supported for
porting from 16-bit code, or maintaining
source code compatibility with 16-bit Windows. The
global and local functions are slower than other
memory management functions and do not provide as many features.
Therefore, new applications
should use the heap functions.However, the global functions are
still used with DDE and the clipboard functions.
Windows memory management does not provide a
separate local heap and global heap, as 16-bit Windows does. As a
result, there is no difference
between the memory objects allocated by the GlobalAlloc and
LocalAlloc functions. In addition, the change from
a 16-bit segmented memory model to a 32-bit virtual memory model
has made some of the related global and local functions and their
options unnecessary or meaningless. For example, there are no longer near and far
pointers, because both local and global allocations
return 32-bit virtual addresses.
Memory objects allocated by GlobalAlloc and
LocalAlloc are in private, committed pages with read/write access
that cannot be accessed by other processes. Memory allocated by
using GlobalAlloc with GMEM_DDESHARE is not actually shared
globally as it is in 16-bit Windows. This value has no effect and
is available only for compatibility. Applications requiring shared memory for other purposes
must use file-mapping objects. Multiple processes can map a
view of the same file-mapping object to provide named shared
memory. For more information, see File Mapping.
HeapAllock是堆管理函數(shù),堆管理函數(shù)可以操作非默認(rèn)堆(當(dāng)然也可以操作默認(rèn)堆),創(chuàng)建一個堆是用HeapCreate,這個函數(shù)返回一個堆句
柄,然后可以用在HeapAllock函數(shù)中,即從返回的這個堆中申請內(nèi)存空間,HeapAllock申請的內(nèi)存只能是不可以移動的.
而new則是c++的標(biāo)準(zhǔn)函數(shù),在Windows的VC++編譯器中,new在申請內(nèi)存時最終調(diào)用的是GlabalAlloc,不過new還可以調(diào)用類的構(gòu)造函數(shù).
Windows的內(nèi)存管理除了標(biāo)準(zhǔn)內(nèi)存管理函數(shù)和堆管理函數(shù)之外,還有更加底層的虛擬內(nèi)存管理函數(shù),VirtualAlloc就是一個虛擬內(nèi)存管理函數(shù).
Personal Comprehension
GlobalAlloc分配的內(nèi)存,還可以調(diào)用GlobalLock鎖定該內(nèi)存塊(該函數(shù)可以被多次調(diào)用),在我們沒有調(diào)用GlobalUnlock之
前,該內(nèi)存塊會一直保持有效(即使調(diào)用了GlobalFree函數(shù),但如果該內(nèi)存的鎖計數(shù)不為0,該內(nèi)存塊也不會被釋放掉,依然保持有效)!而如果只調(diào)用
一次delete,則使用new所分配的內(nèi)存就會被釋放掉.
virtual BOOL PreTranslateMessage(MSG* pMsg)
{
return CWindow::IsDialogMessage(pMsg);
}
改成:
virtual BOOL PreTranslateMessage(MSG* pMsg)
{
if(pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_ESCAPE) return TRUE;
if(pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) return TRUE;
else
return CWindow::IsDialogMessage(pMsg);
}
---------------------------------------------
MFC:
BOOL CxxxDlg::PreTranslateMessage(MSG* pMsg)
{
if(pMsg->message==WM_KEYDOWN && pMsg->wParam==VK_ESCAPE) return TRUE;
if(pMsg->message==WM_KEYDOWN && pMsg->wParam==VK_RETURN) return TRUE;
else
return CDialog::PreTranslateMessage(pMsg);
}
--------------------
在
一般情況下編寫的對話框程序,用戶在運行的時候,如果不注意按下了ENTER或者ESC鍵,程序就會立刻退出,之所以會這樣,是因為按下Enter鍵
時,Windows就會自動去找輸入焦點落在了哪一個按鈕上,當(dāng)獲得焦點的按鈕的四周將被點線矩形包圍。如果所有按鈕都沒有獲得輸入焦點,Windows
就會自動去尋找程序或資源所指定的默認(rèn)按鈕(默認(rèn)按鈕邊框較粗)。如果對話框沒有默認(rèn)按鈕,那么即使對話框中沒有OK按鈕,OnOK函數(shù)也會自動被調(diào)用,
對于一個普通的對話框程序來說,OnOK函數(shù)的調(diào)用,以為著程序會立刻退出。為了使Enter鍵無效,最簡單的辦法就是將CExDlg的OnOK函數(shù)寫成
空函數(shù),然后針對OK按鈕寫一個新的函數(shù)來響應(yīng)。ESC鍵的原理也是如此,它是默認(rèn)和OnCancel函數(shù)映射在一起的。對于ESC鍵,需要自己重載
CDialog類的PreTranslateMessage函數(shù),當(dāng)發(fā)現(xiàn)是ESC鍵的時候,過濾掉這個消息或者是替換掉這個消息。
函數(shù)原型:BOOL BitBlt(HDC hdcDest,int nXDest,int nYDest,int nWidth,int nHeight,HDC hdcSrc,int nXSrc,int nYSrc,DWORD dwRop);
參數(shù):
hdcDest:指向目標(biāo)設(shè)備環(huán)境的句柄。
nXDest:指定目標(biāo)矩形區(qū)域左上角的X軸邏輯坐標(biāo)。
nYDest:指定目標(biāo)矩形區(qū)域左上角的Y軸邏輯坐標(biāo)。
nWidth:指定源和目標(biāo)矩形區(qū)域的邏輯寬度。
nHeight:指定源和目標(biāo)矩形區(qū)域的邏輯高度。
hdcSrc:指向源設(shè)備環(huán)境的句柄。
nXSrc:指定源矩形區(qū)域左上角的X軸邏輯坐標(biāo)。
nYSrc:指定源矩形區(qū)域左上角的Y軸邏輯坐標(biāo)。
dwRop:指定光柵操作代碼。這些代碼將定義源矩形區(qū)域的顏色數(shù)據(jù),如何與目標(biāo)矩形區(qū)域的顏色數(shù)據(jù)組合以完成最后的顏色。
下面列出了一些常見的光柵操作代碼:
BLACKNESS:表示使用與物理調(diào)色板的索引0相關(guān)的色彩來填充目標(biāo)矩形區(qū)域,(對缺省的物理調(diào)色板而言,該顏色為黑色)。
DSTINVERT:表示使目標(biāo)矩形區(qū)域顏色取反。
MERGECOPY:表示使用布爾型的AND(與)操作符將源矩形區(qū)域的顏色與特定模式組合一起。
MERGEPAINT:通過使用布爾型的OR(或)操作符將反向的源矩形區(qū)域的顏色與目標(biāo)矩形區(qū)域的顏色合并。
NOTSRCCOPY:將源矩形區(qū)域顏色取反,于拷貝到目標(biāo)矩形區(qū)域。
NOTSRCERASE:使用布爾類型的OR(或)操作符組合源和目標(biāo)矩形區(qū)域的顏色值,然后將合成的顏色取反。
PATCOPY:將特定的模式拷貝到目標(biāo)位圖上。
PATPAINT:通過使用布爾OR(或)操作符將源矩形區(qū)域取反后的顏色值與特定模式的顏色合并。然后使用OR(或)操作符將該操作的結(jié)果與目標(biāo)矩形區(qū)域內(nèi)的顏色合并。
PATINVERT:通過使用XOR(異或)操作符將源和目標(biāo)矩形區(qū)域內(nèi)的顏色合并。
SRCAND:通過使用AND(與)操作符來將源和目標(biāo)矩形區(qū)域內(nèi)的顏色合并。
SRCCOPY:將源矩形區(qū)域直接拷貝到目標(biāo)矩形區(qū)域。
SRCERASE:通過使用AND(與)操作符將目標(biāo)矩形區(qū)域顏色取反后與源矩形區(qū)域的顏色值合并。
SRCINVERT:通過使用布爾型的XOR(異或)操作符將源和目標(biāo)矩形區(qū)域的顏色合并。
SRCPAINT:通過使用布爾型的OR(或)操作符將源和目標(biāo)矩形區(qū)域的顏色合并。
WHITENESS:使用與物理調(diào)色板中索引1有關(guān)的顏色填充目標(biāo)矩形區(qū)域。(對于缺省物理調(diào)色板來說,這個顏色就是白色)。
返回值:如果函數(shù)成功,那么返回值非零;如果函數(shù)失敗,則返回值為零。
Windows NT:若想獲取更多錯誤信息,請調(diào)用GetLastError函數(shù)。
備注:如果在源設(shè)備環(huán)境中可以實行旋轉(zhuǎn)或剪切變換,那么函數(shù)BitBlt返回一個錯誤。如果存在其他變換(并且目標(biāo)設(shè)備環(huán)境中匹配變換無效),那么目標(biāo)設(shè)備環(huán)境中的矩形區(qū)域?qū)⒃谛枰獣r進(jìn)行拉伸、壓縮或旋轉(zhuǎn)。
如果源和目標(biāo)設(shè)備環(huán)境的顏色格式不匹配,那么BitBlt函數(shù)將源場景的顏色格式轉(zhuǎn)換成能與目標(biāo)格式匹配的格式。當(dāng)正在記錄一個增強型圖元文件時,如果源設(shè)備環(huán)境標(biāo)識為一個增強型圖元文件設(shè)備環(huán)境,那么會出現(xiàn)錯誤。如果源和目標(biāo)設(shè)備環(huán)境代表不同的設(shè)備,那么BitBlt函數(shù)返回錯誤。
Windows CE:在Windows CE 1.0版中,參數(shù)dwRop只可以指定為下列值:SRCCOPY、SRCAND、SRCPAINT、SRCINVERT。在Windows CE 2.0版中,參數(shù)dwRop可以是任何光柵操作代碼值。
速查:Windows NT:3.1及以上版本;Windows:95及以上版本;Windows CE:1.0及以上版本;頭文件:wingdi.h;庫文件:gdi32.lib。
很多東西你可以百度下先撒,把內(nèi)存里兩個圖片合在一起可以用這個