什么是IOCPQ?br>先让我们看看对IOCP的评?br>I/O完成端口可能是Win32提供的最复杂的内核对象?br>[Advanced Windows 3rd] Jeffrey Richter
q是[IOCP]实现高容量网l服务器的最x法?br>[Windows Sockets2.0:Write Scalable Winsock Apps Using Completion Ports]
Microsoft Corporation
完成端口模型提供了最好的伸羃性。这个模型非帔R用来处理数百乃至上千个套接字?br>[Windows|络~程2nd] Anthony Jones & Jim Ohlund
I/O completion ports特别昑־重要Q因为它们是唯一适用于高负蝲服务器[必须同时l护许多q接U\]的一个技术。Completion ports利用一些线E,帮助q由I/Oh所引v的负载。这L架构特别适合用在SMPpȝ中生的”scalable”服务器?br>[Win32多线E程序设计] Jim Beveridge & Robert Wiener
看来我们完全有理q信IOCP是大型网l架构的首选?br>那IOCP到底是什么呢Q?br> 微Y在Winsock2中引入了IOCPq一概念 。IOCP全称I/O Completion PortQ中文译为I/O完成端口。IOCP是一个异步I/O的APIQ它可以高效地将I/O事g通知l应用程序。与使用select()或是其它异步Ҏ不同的是Q一个套接字[socket]与一个完成端口关联了hQ然后就可l进行正常的Winsock操作了。然而,当一个事件发生的时候,此完成端口就被操作pȝ加入一个队列中。然后应用程序可以对核心层进行查询以得到此完成端口?br> q里我要对上面的一些概늕作补充,在解释[完成]两字之前Q我惛_单的提一下同步和异步q两个概念,逻辑上来讲做完一件事后再d另一件事是同步Q而同时一起做两g或两件以上事的话是异步了。你也可以拿单线E和多线E来作比喅R但是我们一定要同步和堵塞Q异步和非堵塞区分开来,所谓的堵塞函数诸如accept(…)Q当调用此函数后Q此时线E将挂vQ直到操作系l来通知它,”HEY兄弟Q有q来?#8221;Q那个挂LU程l进行工作,也就W合”生?消费?#8221;模型。堵塞和同步看上L两分怼Q但却是完全不同的概c大安知道I/O讑֤是个相对慢速的讑֤Q不论打印机Q调制解调器Q甚至硬盘,与CPU相比都是奇慢无比的,坐下来等I/O的完成是一件不甚明智的事情Q有时候数据的动率非常惊人,把数据从你的文g服务器中以Ethernet速度搬走Q其速度可能高达每秒一百万字节Q如果你试从文件服务器中读?00KBQ在用户的眼光来看几乎是瞬间完成Q但是,要知道,你的U程执行q个命oQ已l浪费了10个一百万ơCPU周期。所以说Q我们一般用另一个线E来q行I/O。重叠IO[overlapped I/O]是Win32的一Ҏ术,你可以要求操作系lؓ你传送数据,q且在传送完毕时通知你。这也就是[完成]的含义。这Ҏ术你的E序在I/Oq行q程中仍然能够l处理事务。事实上Q操作系l内部正是以U程来完成overlapped I/O。你可以获得U程所有利益,而不需要付Z么痛苦的代h?br> 完成端口中所谓的[端口]q不是我们在TCP/IP中所提到的端口,可以说是完全没有关系。我到现在也没想通一个I/O讑֤[I/O Device]和端口[IOCP中的Port]有什么关pR估计这个端口也qh了不h。IOCP只不q是用来q行d操作Q和文gI/O倒是有些cM。既然是一个读写设备,我们所能要求它的只是在处理M写上的高效。在文章的第三部分你会轻而易丄发现IOCP设计的真正用意?/p>
IOCP和网l又有什么关p?
int main()
{
WSAStartup(MAKEWORD(2, 2), &wsaData);
ListeningSocket = socket(AF_INET, SOCK_STREAM, 0);
bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
listen(ListeningSocket, 5);
int nlistenAddrLen = sizeof(ClientAddr);
while(TRUE)
{
NewConnection = accept(ListeningSocket, (SOCKADDR*)&ClientAddr, &nlistenAddrLen);
HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, (void*) NewConnection, 0, &dwTreadId);
CloseHandle(hThread);
}
return 0;
}
怿只要写过|络的朋友,应该对这Ll构在熟悉不q了。accept后线E被挂vQ等待一个客户发求,而后创徏新线E来处理h。当新线E处理客戯求时Qv初的U程循环回去{待另一个客戯求。处理客戯求的U程处理完毕后终l?br> 在上q的q发模型中,Ҏ个客戯求都创徏了一个线E。其优点在于{待h的线E只需做很的工作。大多数旉中,该线E在休眠[因ؓrecv处于堵塞状态]?br> 但是当ƈ发模型应用在服务器端[ZWindows NT]QWindows NT组注意到这些应用程序的性能没有预料的那么高。特别的Q处理很多同时的客户h意味着很多U程q发地运行在pȝ中。因为所有这些线E都是可q行的[没有被挂起和{待发生什么事]QMicrosoft意识到NT内核p了太多的旉来{换运行线E的上下文[Context]Q线E就没有得到很多CPU旉来做它们的工作?br> 大家可能也都感觉到ƈ行模型的瓉在于它ؓ每一个客戯求都创徏了一个新U程。创建线E比起创E开销要小Q但也远不是没有开销的?br> 我们不妨设想一下:如果事先开好N个线E,让它们在那hold[堵塞]Q然后可以将所有用Lh都投递到一个消息队列中厅R然后那N个线E逐一从消息队列中d出消息ƈ加以处理。就可以避免针对每一个用戯求都开U程。不仅减了U程的资源,也提高了U程的利用率。理Z很不错,你想我等泛泛之辈都能惛_来的问题QMicrosoft又怎会没有考虑到呢 !
q个问题的解x法就是一个称为I/O完成端口的内核对象,他首ơ在Windows NT3.5中被引入?br> 其实我们上面的构惛_该就差不多是IOCP的设计机理。其实说I了IOCP不就是一个消息队列嘛Q你说这和[端口]q两字有何联pR我的理解就是IOCP最多是应用E序和操作系l沟通的一个接口Ş了?br> 至于IOCP的具体设计那我也很难说得上来Q毕竟我没看q实现的代码Q但你完全可以进行模拟,只不q性能可能…Q如果想深入理解IOCPQ?Jeffrey Ritchter的Advanced Windows 3rd其中W?3章和W?4张有很多宝贵的内容,你可以拿来窥视一下系l是如何完成q一切的?br>
实现Ҏ
Microsoft为IOCP提供了相应的API函数Q主要的׃个,我们逐一的来看一下:
HANDLE CreateIoCompletionPort (
HANDLE FileHandle, // handle to file
HANDLE ExistingCompletionPort, // handle to I/O completion port
ULONG_PTR CompletionKey, // completion key
DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
);
在讨论各参数之前Q首先要注意该函数实际用于两个截然不同的目的Q?br>1Q用于创Z个完成端口对?br>2Q将一个句柄[HANDLE]和完成端口关联到一?br> 在创Z个完成一个端口的时候,我们只需要填写一下NumberOfConcurrentThreadsq个参数可以了。它告诉pȝ一个完成端口上同时允许q行的线E最大数。在默认情况下,所开U程数和CPU数量相同Q但l验l我们一个公式:
U程?= CPU?* 2 + 2
要完成端口有用Q你必须把它同一个或多个讑֤相关联。这也是调用CreateIoCompletionPort完成的。你要向该函C递一个已有的完成端口的句柄,我们既然要处理网l事Ӟ那也是客Lsocket作ؓHANDLE传进厅R和一个完成键[对你有意义的一?2位|也就是一个指针,操作pȝq不兛_你传什么]。每当你向端口关联一个设备时Q系l向该完成端口的讑֤列表中加入一条信息纪录?br>另一个API是
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // handle to completion port
LPDWORD lpNumberOfBytes, // bytes transferred
PULONG_PTR lpCompletionKey, // file completion key
LPOVERLAPPED *lpOverlapped, // buffer
DWORD dwMilliseconds // optional timeout value
);
W一个参数指ZU程要监视哪一个完成端口。很多服务应用程序只是用一个I/O完成端口Q所有的I/Oh完成以后的通知都将发给该端口。简单的_GetQueuedCompletionStatus使调用线E挂P直到指定的端口的I/O完成队列中出C一Ҏ直到时。同I/O完成端口相关联的W?个数据结构是使线E得到完成I/O中的信息:传输的字节数Q完成键和OVERLAPPEDl构的地址。该信息是通过传递给GetQueuedCompletionSatatus的lpdwNumberOfBytesTransferredQlpdwCompletionKey和lpOverlapped参数q回l线E的?br>Ҏ到目前ؓ止已l讲到的东西Q首先来构徏一个frame。下面ؓ您说明了如何使用完成端口来开发一个echo服务器。大致如下:
1.初始化Winsock
2.创徏一个完成端?br> 3.Ҏ服务器线E数创徏一定量的线E数
4.准备好一个socketq行bind然后listen
5.q入循环accept{待客户h
6.创徏一个数据结构容Usocket和其他相关信?br> 7.连q来的socket同完成端口相兌
8.投递一个准备接受的h
以后׃断的重复5?的过E?br>那好Q我们用具体的代码来展示一下细节的操作?br> x文章也该告一D落?我带着您做了一旋风般的旅?游览了所谓的完成端口?br>
“怎么?zero Q胃口不好么Q?#8221;Q基本填p子之后,Solmyr 觉得g应该兛_一下他的学徒了?/p>
“呃,没什么,只是 …… Solmyr QC++ Z么不支持垃圾攉呢?Q注Q垃圾收集是一U机Ӟ保证动态分配了的内存块会自动释放,Java {语a支持q一机制。)”
Solmyr 叹了口气Q用一U^静的眼神盯着 zero Q?#8220;是不是在 BBS 上和人吵 C++ ?Java 哪个更好Q而且吵输了?我早告诉q你Q这U争论再无聊不过了?#8221;
“?…… ?#8221;Qzero 不得不承?——?Solmyr 的眼虽然一点也不锐利,但是却莫名其妙的?zero 产生了微微的恐惧感?/p>
“而且Q谁告诉?C++ 不支持垃圾收集的Q?#8221;
“啊!Solmyr 你不是开玩笑吧?Q?#8221;
“zero 你得转变一下观c我问你QC++ 支不支持可以动态改变大的数组Q?#8221;
“q?…… 好象也没有吧Q?#8221;
“?vector 是什么东西?”
“?……”
“支持一U特性,q不是说非得把这个特性加到语法里去,我们也可以选择用现有的语言机制实现一个库来支持这个特征。以垃圾攉ZQ这里我们的d是要保证每一个被动态分配的内存块都能够被释放,也就是说 ……”QSolmyr 不知从哪里找Z一张纸、一支笔Q写刎ͼ
int* p = new int; // 1
delete p; // 2
“也就是说Q对于每一?1 Q我们要保证有一?2 被调用,1 ?2 必须成对出现。我来问你,C++ 中有什么东西是pa本n保证一定成对出现的Q?#8221;
“……”Qzero 露出了努力搜索记忆的表情Q不q很明显一无所莗?/p>
“提示一下,和类的创建有兟?#8221;
“哦!构造函C析构函数Q?#8221;
“正确。可惜普通指针没有构造函C析构函数Q所以我们必要写一个类来加一层包装,最单的pq样Q?#8221;
class my_intptr
{
public:
int* m_p;
my_intptr(int* p){ m_p = p; }
~my_intptr(){ delete m_p; }
};
…………
my_intptr pi(new int);
*(pi.m_p) = 10;
…………
“q里我们可以攑ֿ的?my_intptr Q不用担心内存泄漏的问题Q一?pi q个变量被销毁,我们知道 pi.p 指向的内存块一定会被释放。不q如果每ơ?my_intptr 都得去访问它的成员未免太ȝ了。ؓ此,可以l这个类加上重蝲?* q算W:”
class my_intptr
{
private:
int* m_p;
public:
my_intptr(int* p){ m_p = p; }
~my_intptr(){ delete m_p; }
int& operator*(){ return *m_p; }
};
…………
my_intptr pi;
*pi = 10;
int a = *pi;
…………
“现在是不是看h my_intptr 像是一个真正的指针了?正因为如此,q种技术被UCؓ指针。现在我问你Q这个类q缺哪些东西?”
zero q眉头Q眼睛一眨一眨,看上d像一台慢速电脑正在辛苦的往它的盘上拷贝文件。良久,zero 抬v头来Q不太确定的_“是不是还~少一个拷贝构造函数和一个赋D符Q?#8221;
“说说Z么?#8221;QSolmyr 昄不打就q样放过 zero?/p>
“因ؓ …… 我记得没错的?…… ?0 ?》(注:指《Effective C++ 2/e》一书)中提到过Q如果你的类里面有指针指向动态分配的内存Q那么一定要为它写一个拷贝构造函数和一个赋D符 …… 因ؓ …… 否则的话Q一旦你做了赋|会导致两个对象的指针指向同一块内存。对了!如果是上面的c,q样一来会D同一个指针被 delete 两次Q?#8221;
“正确。那么我们应该怎样来实现呢Q?#8221;
“q简单,我们?memcpy 把目标指针指向的内存中的内容拯q来?#8221;
“如果我们的智能指针指向一个类的对象怎么办?注意Q类的对象中可能有指针,不能?memcpy?#8221;
“?…… 我们用拷贝构造的办法?#8221;
“如果我们的智能指针指向的对象不能拯构造怎么办?它可能有一个私有的拯构造函数?#8221;
“?……”Qzero 了一,军_老实承认Q?#8220;我不知道?#8221;
“问题在哪你知道么Q在于你没有把智能指针看作指针。想象一下,如果我们对一个指针做赋|它的含义是什么?”
“呃,我明白了Q在q种情况下,应该惛_法让两个指针指向同一个对?…… 可是 Solmyr Q这样以来岂不是仍然要对同一个对象删除两遍?”
“是的Q我们得惛_法解册个问题,办法不只一U。比较好的一U是为每个指针维护一个引用计数|每次赋值或者拷贝构造,p计数值加一Q这意味着指向q个内存块的指针又多了一个;而每有一个智能指针被销毁,p计数值减一Q这意味着指向q个内存块的指针了一个;一旦计数gؓ 0 Q就释放内存块。象q样Q?#8221;
class my_intptr
{
private:
int* m_p;
int* m_count;
public:
my_intptr(int* p)
{
m_p = p;
m_count = new int;
// 初始化计数gؓ 1
*m_count = 1;
}
my_intptr(const my_intptr& rhs) // 拯构造函?
{
m_p = rhs.m_p; // 指向同一块内?
m_count = rhs.m_count; // 使用同一个计数?
(*m_count)++; // 计数值加 1
}
~my_intptr()
{
(*m_count)--; // 计数值减 1
if( *m_count == 0 ) // 已经没有别的指针指向该内存块?
{
delete m_p;
delete m_count;
}
}
my_intptr& operator=(const my_intptr& rhs)
{
if( m_p == rhs.m_p ) // 首先判断是否本来指向同一内存?
return *this; // 是则直接q回
(*m_count)--; // 计数值减 1 Q因指针不再指向原来内存块了
if( *m_count == 0 ) // 已经没有别的指针指向原来内存块了
{
delete m_p;
delete m_count;
}
m_p = rhs.m_p; // 指向同一块内?
m_count = rhs.m_count; // 使用同一个计数?
(*m_count)++; // 计数值加 1
}
…………
};
“其他部分没有什么太大变化,我不费事了。现在想象一下我们怎样使用q种指针Q?#8221;QSolmyr 放下了笔Q再ơ拿起了{子Q有些惋惜的发现他爱吃的肉丸子已l冷了?/p>
zero 惌着Q有些迟疑?#8220;我们 …… 可以?new int 表达式作为构造函数的参数来构造一个智能指针,然后 …… 然后我们可以L的赋|”Q他开始抓住了思\Q越说越快,“L的用已经存在的智能指针来构造新的智能指针,指针的赋D符、拷贝构造函数和析构会保证计数值始l等于指向该内存块的指针数?#8221;zero g明白了他看到了怎样的功能,开始激动v来:“然后一旦计数gؓ 0 被分配的内存块就会释放!也就是说 …… 有指针指向内存块Q它׃释放Q一旦没有,它就自动释放Q太了Q我们只要一开始正的初始化智能指针,可以象普通指针那样用它Q而且完全不用担心内存释放的问题!太棒了!”zero Ȁ动的大叫Q?#8220;q就是垃圾收集!Solmyr Q我们在饭桌上实C一个垃圾收集器Q?#8221;
Solmyr 很明显没有分?zero 的激动:“我在吃饭Q你能不能不要大?#8216;饭桌上实C一个垃圾收集器’q种倒胃口的话?”了一,Solmyr 带着他招牌式的坏W,以一U可恶的口吻说道Q?#8220;而且h意一下自q形象?#8221;
“嗯?”Qzero 回过来Q发现自׃知什么时候站了v来,而整个餐厅里的h都在看着他嘿嘿偷W,q让他感觉自己像个傻瓜?/p>
zero U着脸坐下,压低了声音问 Solmyr Q?#8220;不过 Solmyr Q这实是一个的垃圾攉机制啊,只要我们把这个类Ҏ …… ?…… Ҏ模板c,象这P”zero 抓过了纸W,写到Q?/p>
template <typename T>
class my_ptr
{
private:
T* m_p;
int* m_count;
…………
};
“它不p支持Lcd的指针了吗?我们可以把它用在Q何地斏V?#8221;
Solmyr 摇了摇头Q?#8220;不,你把问题想的太简单了。对于简单的cdQ这个类实可以处理的很好,但实际情冉|很复杂的。考虑一个典型情况:c?Derived 是类 Base 的派生类Q我们希望这栯|”
Base* pb;
Derived pd;
…………
pb = pd;
“你倒说说看Q这U情况,怎样改用上面q个指针来处理?”
“……”Qzero 沉默了?/p>
“要实C个完整的垃圾攉机制q不ҎQ因为有许多l节要考虑?#8221;QSolmyr 开始ȝ了,“不过Q基本思\是上面说的q些。值得庆幸的是Q目前已l有了一个相当成熟的‘引用计数’指针Qboost::shared_ptr。大多数情况下,我们都可以用它。另外,除了指针之外Q还有一些技术也能够帮助我们避开释放内存的问题,比如内存池。但是,关键在于 ——?”
Solmyr 再度用那U^静的眼神盯着 zero Q?/p>
“wؓ C/C++ E序员,必须有创造力。那Uh在语a机制上不思进取的人,那种必须要靠语法强制才知道怎样~程的hQ那U没有别人告诉他该干什么就无所适从的hQ不适合q门语言?#8221;
Ƣ迎讉K梦断酒醒的博客:http://www.yanzhijun.com
本文来自CSDN博客Q{载请标明出处Q?a >http://blog.csdn.net/ishallwin/archive/2009/09/08/4533145.aspx