论文部分内容阅读
摘 要:完成端口是一种复杂的Win32内核对象。应用程序可以用完成端口管理线程池为大量的异步I/O请求提供服务,而不必为每个I/O请求分别创建服务线程。完成端口模型用于开发服务器应用程序,以提供优良的伸缩性和获得最好的系统性能。本文构建了一个基本的网络服务器程序框架,对完成端口模型的用法进行阐述与分析。
关键词:完成端口;线程池;套接字
中图分类号:TP311 文献标识码:a DoI: 10.3969/j.issn.1003-6970.2012.02.010
The Usage and Analysis of Completion Port Model ZHoU Peng,HUaNG Can,JIaNG Nan(Unit 91550 of PLA, Dalian 116023, Liaoning)
【Abstract】Completion port is a complicated win32 kernel object. applications can provide service for a large number of
asynchronous I/O requests by using completion port to manage thread pool, and needn’t create service threads for each I/O request. Completion port model is used to develop server application to provide excellent scalability and get the best system performance. This paper constructed a basic network server application framework, expatiated and analyzed the usage of Completion port model.
【Key words】completion port; thread pool; socket
0 引言
在Win32中,套接字是一个指向传输提供者的句柄[1]。Winsock提供了锁定和非锁定两种套接字模式,以决定Winsock函数随套接字调用时的行为。锁定模式虽然容易使用,但对于建立连接的多个套接字,或数据收发量不均,时间不规律的情况,却难以管理;而非锁定模式则难以处理Winsock函数调用时可能会收到的WSAEWOULDBLOCK错误。
为了解决套接字模式存在的限制,Winsock提供了一些套接字模型,帮助应用程序通过异步方式,一次对一个或多个套接字上进行的I/O操作加以管理。这些模型包括:select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、Overlapped I/O(重叠I/O)和Completion port(完成端口)。
相对于其它套接字模型,完成端口模型是最复杂的、也是伸缩性最好的一种模型。当应用程序作为服务器为大量套接字的I/O请求提供服务时,使用这种模型会获得最好的系统性能。许多高性能服务器,如Apache、IIS等,都使用了完成端口模型的技术[2]。因此,掌握完成端口模型的用法和工作机制是非常必要的。
1 完成端口模型的使用和分析
下面将构建一个基本的网络服务器程序框架,对使用完成端口模型管理大量套接字的方法加以论述。程序框架的构建步骤如下:
(1)创建一个完成端口;
(2)创建线程池;
(3)创建套接字监听客户连接请求;
(4)为完成端口关联连接返回的套接字;
(5)在建立的连接上进行I/O操作。
1.1 创建完成端口
要创建完成端口,应调用CreateIoCompletionPort函数:HANDLE CreateIoCompletionPort(HANDLE hFile,
HANDLE hExistingCompletionport,
DWORD dwCompletionKey,
DWORD dwNumberOfConcurrentThreads);该函数完成两种不同的工作:(1)创建一个完成端口;(2)把一个设备同完成端口相关联。如果只创建一个完成端口,可以这样调用函数:
HANDLE hIocp = CreateIoCompletionPort(INVALID_ HANDLE_VALUE, NULL, 0, 0);
参数dwNumberOfConcurrentThreads定义了完成端口能够同时运行的最多线程数。如果该参数为0,完成端口缺省允许的并发线程数目是计算机上的CPU数目。这样,每个
CPU可以运行一个线程,以避免多余的线程上下文切换,节省CPU的周期。如果处理一个客户请求需要一段比较长的时间,为了提高应用程序的伸缩性,可能需要增大该值。
1.2 创建线程池
成功创建完成端口对象之后,就需要创建一个线程池来处理客户请求。虽然创建线程时系统资源的开销比较小,但远非可以忽略不计。在程序初始化时创建一个线程池,可以避免运行时创建线程。如果这些线程在应用程序执行期间是空闲的,程序的性能就能进一步提高。
线程池中的线程数量应该有一个上限,因为创建太多的线程会浪费系统资源。对于池中应该有多少线程,微软公司给出的参考答案是将计算机上的CPU数目乘以2。值得注意的是,应该使线程池中至少有一个线程能接受客户请求。如果池中所有的线程都在忙,就可能拒绝客户请求,所以最好是用更多的上下文切换接受和处理客户请求。
如果计算机有两个CPU,那么应该创建4个线程等待完成的I/O请求:
HANDLE hThread = NULL; for (int n=0; n<4; ++n)
hThread = CreateThread(NULL, 0, WorkerThread, hIocp, 0, NULL);
其中,WorkerThread是处理客户请求的服务线程。
1.3 创建套接字监听客户连接请求
创建一个监听套接字:
SOCKET sListen = WSASocket(AF_INET, SOCK_ STREAM, 0, NULL, 0,
WSA_FLAG_OVERLAPPED);绑定监听套接字到本机地址,端口号为5150:struct sockaddr_in saServer;
saServer.sin_family = AF_INET;
saServer.sin_addr.s_addr = inet_addr(INADDR_ ANY);
saServer.sin_port = htons(5150);
bind(sListen, (SOCKADDR *)&saServer, sizeof(saServer));
套接字监听客户连接请求,允许的最大连接数为200:listen(sListen, 200);
1.4 为完成端口关联连接返回的套接字
监听套接字在接受客户连接请求后,返回一个建立真正连接的新套接字。调用CreateIoCompletionPort函数为完成端口对象关联这个新套接字。函数的前两个参数分别是套接字和完成端口的对象句柄,第三个参数dwCompletionKey称为“完成键”,是一个32位值的参数,通常用作指针指向一个描述套接字的相关信息的结构。应用程序把套接字关联到完成端口后,就可以通过该端口管理套接字上的异步I/O请求。
定义描述套接字相关信息的结构:
接受一个连接请求,返回套接字关联到完成端口上,然后投递一个I/O请求: while (true) { struct sockaddr_in saRemote; int nSaLen = sizeof(saRemote);
SOCKET sNew = accept(sListen, (sockaddr *)&saRemote,&nSaLen);
LPIOREQ pIoReq = new IOREQ; pIoReq->s = sNew;
pIoReq->op = OP_READ;
CreateIoCompletionPort((HANDLE)ioreq->s, hIocp, (DWORD)pIoReq, 0);
PostRecvReq(pIoReq);
}
为了避免定义的IOREQ结构变量作为临时变量被系统回收而导致应用程序出错,应该使用new操作符为该变量分配堆内存,其地址将作为完成端口的完成键传递套接字的相关信息。
1.5 在建立的连接上进行I/O操作
主线程创建完线程池后,池中的线程调用GetQueued CompletionStatus函数进入空闲状态,等待完成端口上的完成的I/O请求。
当一个套接字上的异步I/O请求完成时,系统检查该套接字是否关联了一个完成端口。如果是,该端口对象内部的I/O完成队列加入一个完成的I/O请求项。当线程池中运行的线程数目小于完成端口允许并发线程的最大数目,该端口就唤醒一个空闲线程处理完成的I/O请求,然后按照先进先出(FIFO)的顺序从I/O完成队列中移除被处理的I/O请求项。
值得注意的是,对线程池中空闲线程的调用是按照后进先出(LIFO)顺序的。如果I/O请求完成的足够慢,完成端口就总是唤醒同一个线程,而没有被调度的线程的内存资源就可以被交换到磁盘上和从CPU缓存中清除,这样会有效地提高应用程序的性能。
如果线程处理了一个失败的I/O请求,应该使用delete操作符从堆内存中释放相应的IOREQ结构变量,以防止因内存泄露而导致系统资源耗尽。
2 总结
完成端口模型的设计非常合理,它组合了重叠I/O和独立线程的特性[3]。使用线程池处理异步I/O请求,就是用少量线程管理大量的套接字,而不需要为每个客户端创建服务线程处理异步I/O请求,这样就大大节省了创建线程和切换线程上下文的系统开销。另外,完成端口使用后进先出的方式调用服务线程,也会有效地减少对系统资源的使用。因此,对于处理大量I/O请求的服务器程序,应该优先考虑使用完成端口模型,以获得良好的伸缩性和最好的系统性能。
参考文献
[1] 陈坚,陈伟,等. Visual C++网络高级编程[M]. 北京:人民邮电出版社,2001.
[2] 王艳平,张越. Windows网络与通信程序设计[M]. 北京:人民邮电出版社,2006.
[3] HART J M. Windows系统编程(原书第三版)[M]. 安娜,吴明军. 北京:机械工业出版社,2006.
关键词:完成端口;线程池;套接字
中图分类号:TP311 文献标识码:a DoI: 10.3969/j.issn.1003-6970.2012.02.010
The Usage and Analysis of Completion Port Model ZHoU Peng,HUaNG Can,JIaNG Nan(Unit 91550 of PLA, Dalian 116023, Liaoning)
【Abstract】Completion port is a complicated win32 kernel object. applications can provide service for a large number of
asynchronous I/O requests by using completion port to manage thread pool, and needn’t create service threads for each I/O request. Completion port model is used to develop server application to provide excellent scalability and get the best system performance. This paper constructed a basic network server application framework, expatiated and analyzed the usage of Completion port model.
【Key words】completion port; thread pool; socket
0 引言
在Win32中,套接字是一个指向传输提供者的句柄[1]。Winsock提供了锁定和非锁定两种套接字模式,以决定Winsock函数随套接字调用时的行为。锁定模式虽然容易使用,但对于建立连接的多个套接字,或数据收发量不均,时间不规律的情况,却难以管理;而非锁定模式则难以处理Winsock函数调用时可能会收到的WSAEWOULDBLOCK错误。
为了解决套接字模式存在的限制,Winsock提供了一些套接字模型,帮助应用程序通过异步方式,一次对一个或多个套接字上进行的I/O操作加以管理。这些模型包括:select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、Overlapped I/O(重叠I/O)和Completion port(完成端口)。
相对于其它套接字模型,完成端口模型是最复杂的、也是伸缩性最好的一种模型。当应用程序作为服务器为大量套接字的I/O请求提供服务时,使用这种模型会获得最好的系统性能。许多高性能服务器,如Apache、IIS等,都使用了完成端口模型的技术[2]。因此,掌握完成端口模型的用法和工作机制是非常必要的。
1 完成端口模型的使用和分析
下面将构建一个基本的网络服务器程序框架,对使用完成端口模型管理大量套接字的方法加以论述。程序框架的构建步骤如下:
(1)创建一个完成端口;
(2)创建线程池;
(3)创建套接字监听客户连接请求;
(4)为完成端口关联连接返回的套接字;
(5)在建立的连接上进行I/O操作。
1.1 创建完成端口
要创建完成端口,应调用CreateIoCompletionPort函数:HANDLE CreateIoCompletionPort(HANDLE hFile,
HANDLE hExistingCompletionport,
DWORD dwCompletionKey,
DWORD dwNumberOfConcurrentThreads);该函数完成两种不同的工作:(1)创建一个完成端口;(2)把一个设备同完成端口相关联。如果只创建一个完成端口,可以这样调用函数:
HANDLE hIocp = CreateIoCompletionPort(INVALID_ HANDLE_VALUE, NULL, 0, 0);
参数dwNumberOfConcurrentThreads定义了完成端口能够同时运行的最多线程数。如果该参数为0,完成端口缺省允许的并发线程数目是计算机上的CPU数目。这样,每个
CPU可以运行一个线程,以避免多余的线程上下文切换,节省CPU的周期。如果处理一个客户请求需要一段比较长的时间,为了提高应用程序的伸缩性,可能需要增大该值。
1.2 创建线程池
成功创建完成端口对象之后,就需要创建一个线程池来处理客户请求。虽然创建线程时系统资源的开销比较小,但远非可以忽略不计。在程序初始化时创建一个线程池,可以避免运行时创建线程。如果这些线程在应用程序执行期间是空闲的,程序的性能就能进一步提高。
线程池中的线程数量应该有一个上限,因为创建太多的线程会浪费系统资源。对于池中应该有多少线程,微软公司给出的参考答案是将计算机上的CPU数目乘以2。值得注意的是,应该使线程池中至少有一个线程能接受客户请求。如果池中所有的线程都在忙,就可能拒绝客户请求,所以最好是用更多的上下文切换接受和处理客户请求。
如果计算机有两个CPU,那么应该创建4个线程等待完成的I/O请求:
HANDLE hThread = NULL; for (int n=0; n<4; ++n)
hThread = CreateThread(NULL, 0, WorkerThread, hIocp, 0, NULL);
其中,WorkerThread是处理客户请求的服务线程。
1.3 创建套接字监听客户连接请求
创建一个监听套接字:
SOCKET sListen = WSASocket(AF_INET, SOCK_ STREAM, 0, NULL, 0,
WSA_FLAG_OVERLAPPED);绑定监听套接字到本机地址,端口号为5150:struct sockaddr_in saServer;
saServer.sin_family = AF_INET;
saServer.sin_addr.s_addr = inet_addr(INADDR_ ANY);
saServer.sin_port = htons(5150);
bind(sListen, (SOCKADDR *)&saServer, sizeof(saServer));
套接字监听客户连接请求,允许的最大连接数为200:listen(sListen, 200);
1.4 为完成端口关联连接返回的套接字
监听套接字在接受客户连接请求后,返回一个建立真正连接的新套接字。调用CreateIoCompletionPort函数为完成端口对象关联这个新套接字。函数的前两个参数分别是套接字和完成端口的对象句柄,第三个参数dwCompletionKey称为“完成键”,是一个32位值的参数,通常用作指针指向一个描述套接字的相关信息的结构。应用程序把套接字关联到完成端口后,就可以通过该端口管理套接字上的异步I/O请求。
定义描述套接字相关信息的结构:

接受一个连接请求,返回套接字关联到完成端口上,然后投递一个I/O请求: while (true) { struct sockaddr_in saRemote; int nSaLen = sizeof(saRemote);
SOCKET sNew = accept(sListen, (sockaddr *)&saRemote,&nSaLen);
LPIOREQ pIoReq = new IOREQ; pIoReq->s = sNew;
pIoReq->op = OP_READ;
CreateIoCompletionPort((HANDLE)ioreq->s, hIocp, (DWORD)pIoReq, 0);
PostRecvReq(pIoReq);
}
为了避免定义的IOREQ结构变量作为临时变量被系统回收而导致应用程序出错,应该使用new操作符为该变量分配堆内存,其地址将作为完成端口的完成键传递套接字的相关信息。
1.5 在建立的连接上进行I/O操作
主线程创建完线程池后,池中的线程调用GetQueued CompletionStatus函数进入空闲状态,等待完成端口上的完成的I/O请求。
当一个套接字上的异步I/O请求完成时,系统检查该套接字是否关联了一个完成端口。如果是,该端口对象内部的I/O完成队列加入一个完成的I/O请求项。当线程池中运行的线程数目小于完成端口允许并发线程的最大数目,该端口就唤醒一个空闲线程处理完成的I/O请求,然后按照先进先出(FIFO)的顺序从I/O完成队列中移除被处理的I/O请求项。
值得注意的是,对线程池中空闲线程的调用是按照后进先出(LIFO)顺序的。如果I/O请求完成的足够慢,完成端口就总是唤醒同一个线程,而没有被调度的线程的内存资源就可以被交换到磁盘上和从CPU缓存中清除,这样会有效地提高应用程序的性能。


如果线程处理了一个失败的I/O请求,应该使用delete操作符从堆内存中释放相应的IOREQ结构变量,以防止因内存泄露而导致系统资源耗尽。
2 总结
完成端口模型的设计非常合理,它组合了重叠I/O和独立线程的特性[3]。使用线程池处理异步I/O请求,就是用少量线程管理大量的套接字,而不需要为每个客户端创建服务线程处理异步I/O请求,这样就大大节省了创建线程和切换线程上下文的系统开销。另外,完成端口使用后进先出的方式调用服务线程,也会有效地减少对系统资源的使用。因此,对于处理大量I/O请求的服务器程序,应该优先考虑使用完成端口模型,以获得良好的伸缩性和最好的系统性能。
参考文献
[1] 陈坚,陈伟,等. Visual C++网络高级编程[M]. 北京:人民邮电出版社,2001.
[2] 王艳平,张越. Windows网络与通信程序设计[M]. 北京:人民邮电出版社,2006.
[3] HART J M. Windows系统编程(原书第三版)[M]. 安娜,吴明军. 北京:机械工业出版社,2006.