基于完成端口模型的应用程序实现

2014-04-04 13:58胡东红杜光海贺伟毛守备
物联网技术 2014年3期
关键词:应用程序

胡东红+杜光海+贺伟+毛守备

摘 要:介绍了Windows最复杂的内核对象IOCP(I/O Completion Port 输入/输出完成端口)的基本原理。利用完成端口机制,应用程序能够为数百上千的用户服务。文章通过完成端口对象指定一定数量的线程,对重叠I/O请求进行管理,从而为已完成的重叠I/O请求提供服务。通过该模型编写的网络服务应用程序可以达到较好的性能。

关键词:完成端口;异步I/O;线程池;应用程序

中图分类号:TP391 文献标识码:A 文章编号:2095-1302(2014)03-0060-03

0 引 言

与计算机执行的大多数其他操作相比,设备I/O是其中最慢、最不可预测的操作之一[1]。比如CPU从硬盘文件中读写、网络读取数据等,每一线程要等待I/O操作完成再执行后续的代码。让太多或者太少的服务器线程来处理线程,都可能会导致性能问题[2]。使用异步设备I/O可以将请求交给设备的驱动程序去处理,应用程序的线程可以执行其他有用的任务。这样可以更好地使用资源并创建出更高效的应用程序。

但是,随着客户端请求、退出的增加,会有许多的并发线程并发执行。由于这些线程都是可运行状态,Windows内核会浪费太多时间来进行活动线程的上下文切换,如不断新建和销毁线程。Windows提供了I/O完成端口机制可以很好地解决上述问题。

1 IOCP模型

当我们创建一个I/O完成端口的时候,系统内核实际上会创建5个不同数据结构。完成端口会将客户请求加入到一个公共的消息队列中,然后应用程序会创建一个线程池来处理客户的请求。当设备与I/O完成端口相关联后,系统会检查是否有与设备相关的一个I/O端口,若有则会将已完成的I/O请求追加到消息队列,并调用相关的工作线程来处理这个请求。当这个请求被处理完后,系统会通过一种机制通知客户,客户只需要取处理好的数据即可。图1所示是一个完成端口模型的结构示意图。

2 IOCP模型的使用

在实现异步通信机制的时候,一般要用到一个核心数据结构重叠(OVERLAPPED)结构。OVERLAPPED结构定义如下:

typedef struct _OVERLAPPED

{

DWORD Internal;//[out] 保存已处理的I/O请求的错误码

DWORD InternalHigh;//[out] 异步I/O完成保存已传输的字节数

DWORD Offset;//[int] 文件传送的字节偏移量的低位字

DWORD OffsetHigh;//[int] 文件传送的字节偏移量的高位字

HANDLE hEvent;//[in] 指定一个I/O操作完成后触发的事件

}OVERLAPPED,*LPOVERLAPPED;

OVERLAPPED结构执行两个重要的功能:第一,它像一把钥匙,用以识别每一个目前正在进行的overlapped操作,比如在网络发送和接收数据时,都会用到WSASend()和WSARecv()函数,参数里面都会附带一个重叠结构,这个重叠结构我们可以理解为一个网络操作的ID号,通过这个ID号就可以区分是对哪个网络进行操作了;第二,它在你和系统之间提供了一个共享区域,参数可以在该区域中双向传递。

图1 完成端口模型

2.1 创建I/O完成端口

在使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任意数量的设备句柄,管理多个I/O请求,需要调用CreateIoCompletionPort()函数,该函数定义如下:

HANDLE WINAPI CreateIoCompletionPort(

_in HANDLE FileHandle,

_in_opt HANDLE ExistingCompletionPort,

_in ULONG_PTR CompletionKey,

_in DWORD NumberOfConcurrentThreads

);

该函数用于创建一个完成端口对象和将一个句柄同完成端口关联在一起。在创建一个完成端口时,前三个参数都会忽略,NumberOfConcurrentThreads参数指定允许有多少线程处于可运行状态。通常给NumberOfConcurrentThreads参数设为0,那么I/O完成端口会使用默认值,也就是允许并发执行的线程数量等于主机的CPU数量,避免额外的上下文切换。代码如下:

hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);

2.2 工作者线程和完成端口

成功创建一个完成端口后,便可开始将套接字句柄和对象关联到一起。但是在关联套接字之前,必须创建一个或多个工作者线程,以便在I/O请求投递完成端口对象后,为完成端口提供服务。应该创建多少个线程?在此要记住一个重点,在调用CreateIoCompletionPort时指定的并发线程数量,与打算创建的线程池线程数量是有区别的。假如在完成端口上创建的工作者线程数量超过指定并发执行的线性数量(这里设为n个),那么系统最多只允许n个线程运行。因为我们随时都能执行更多的线程,比如调用了函数Sleep或WaitForSingleObject使其处于暂停状态,就要用另外线程代替。为了充分发挥系统性能,一般设置为CPU的数量乘以2。

2.3 完成端口与重叠I/O

创建好工作者线程后,调用GetQueuedCompletionStatus()函数让句柄和完成端口相关联起来,进行I/O请求处理。它将调用线程切换到睡眠状态,直到指定的完成端口的队列中出现该请求。如以套接字句柄为基础,投递数据发送和接收请求,会扫描完成端口的队列里是否有网络通信的请求存在(如读取数据、发送数据等),一旦发现消息队列中出现一项的时候,该完成端口会唤醒线性池中的一个线程。这个线程会得到已完成I/O项中的所有信息:

已传输的字节数、完成键以及OVERLAPPED结构的地址。GetQueuedCompletionStatus()函数定义如下:

BOOL GetQueuedCompletionStatus(

HANDLE CompletionPort, //指定的IOCP,该值由CreateIoCompletionPort函数创建

LPDWORD lpNumberOfBytes, //一次完成后的I/O操作所传送数据的字节数

PULONG_PTR lpCompletionKey,//当文件I/O操作完成后,用于存放与之关联的CK(套接字信息结构体指针)

LPOVERLAPPED *lpOverlapped,//为调用IOCP机制所引用的OVERLAPPED结构

DWORD dwMilliseconds,//用于指定调用者等待CP的时间

);

当一个工作者线程从GetQueuedCompletionStatus调用中接收到I/O完成通知后,在lpCompletion和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上进行I/O处理。通过这些参数,可获得两种重要的套接字数据类型:单句柄数据以及单I/O操作数据。单I/O操作数据是CompletionKey(完成键)参数标识的是某个特定的套接字句柄数据,相当于用一个标志来绑定每一个I/O操作,这样收到网络操作完成的通知后,可以通过这个标志来找出返回的数据对应的I/O操作。该标志可以定义如下:

typedef struct _PER_IO_CONTEXT{

OVERLAPPED m_Overlapped; // 每一个重叠I/O

网络操作都要有一个

SOCKET m_sockAccept; // 这个I/O操作所使用的

Socket,每个连接的都是一样的

WSABUF m_wsaBuf; //存储数据的缓冲区,用来给

重叠操作传递参数的

char m_szBuffer[MAX_BUFFER_LEN]; // 对应

WSABUF里的缓冲区

OPERATION_TYPE m_OpType; // 标志这个重叠I/

O操作是做什么的,例如Accept/Recv等

} PER_IO_CONTEXT, *PPER_IO_CONTEXT;

该结构关联了与I/O操作的某些重要数据元素,例如完成I/O操作发送或接受请求的类型m_OpType。每一个I/O操作对应了响应的PER_IO_CONTEXT,我们还要定义单句柄数据来管理句柄上的所有I/O请求,如在Socket上投递了多个AcceptEx请求,该结构定义如下:

typedef struct _PER_SOCKET_CONTEXT{

SOCKET m_Socket;// 每一个客户端连接的Socket

SOCKADDR_IN m_ClientAddr;// 这个客户端的地址

CArray<_PER_IO_CONTEXT*> m_arrayIoContext; // 数组,所有客户端IO操作的参数,也就是说对于每一个客户端Socket是可以在上面同时投递多个IO请求的

} PER_SOCKET_CONTEXT, *PPER_SOCKET_CONTEXT;

2.4 关闭IOCP

调用PostQueuedCompletionStatus函数,向每个工作者线程都发送一个特殊的完成数据包。可以唤醒那些还在等待完成端口但又没有已完成的I/O请求,每个线程会对GetQueuedCompletionStatus的返回值进行检查,如果发现应用程序正在终止,那么它就可以进行清理工作并正常的退出。

3 IOCP程序流程

该程序调用高性能特性的AcceptEx函数用来完成端口异步,取消了阻塞方式的Accept调用。我们知道,AcceptEx是在客户端连入之前就把客户端的Socket建立好了,而不需要像Accept那样在客户端连入之后,再去时间去建立Socket。系统创建一个Socket的开销是相当高了,用Accept的话,系统可能来不及为更多的并发客户端现场准备Socket。另外,相比Accept只能阻塞方式建立一个连入接口,而AcceptEx可以同时在完成端口上投递多个请求。图2所示是其程序的整体流程图。

4 结 语

采用I/O完成端口编写的服务应用程序,经过Process Explorer测试发现当服务器收到3 000个并发线程的时候CPU占有率约为4%,而采用了多个并发线程的客户端程序CPU占有率约为12%。所以,如果预计到自己的服务器在任何给定的时间,都会为大量I/O请求提供服务,便应考虑使用I/O完成端口模型,从而获得更好的性能。但是在编写基于完成端口的服务应用程序时,还应注意重叠操作可确保按照应用程序安排好的顺序执行。然而,不能确保从完成端口返回的完成通知也按照上述顺序执行。在对数据包有要求的时候,比如传送大数据的时候,要注意这个顺序。

参 考 文 献

[1] RICHTER J,NASARRE C. Windows 核心编程[M].Fifth Edition,葛子敖,周靖,等,译.北京:清华大学出版社,2008.

[2] BEVERIDGE J,WIENER R.Wind32多线程程序设计[M].侯捷,译.武汉:华中科技大学出版社,2006.

[3] ANTHONY Jones, JIM Ohlund. Network programming for Microsoft Windows [M]. 北京: 清华大学出版社,2002.

[4] 周鹏,黄灿,江楠. 完成端口模型的使用与分析[J]. 软件, 2012(2): 37-38.

[5] 林延君. 局域网企业信息安全系统的设计与实现[D]. 大连:大连理工大学,2006.

已传输的字节数、完成键以及OVERLAPPED结构的地址。GetQueuedCompletionStatus()函数定义如下:

BOOL GetQueuedCompletionStatus(

HANDLE CompletionPort, //指定的IOCP,该值由CreateIoCompletionPort函数创建

LPDWORD lpNumberOfBytes, //一次完成后的I/O操作所传送数据的字节数

PULONG_PTR lpCompletionKey,//当文件I/O操作完成后,用于存放与之关联的CK(套接字信息结构体指针)

LPOVERLAPPED *lpOverlapped,//为调用IOCP机制所引用的OVERLAPPED结构

DWORD dwMilliseconds,//用于指定调用者等待CP的时间

);

当一个工作者线程从GetQueuedCompletionStatus调用中接收到I/O完成通知后,在lpCompletion和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上进行I/O处理。通过这些参数,可获得两种重要的套接字数据类型:单句柄数据以及单I/O操作数据。单I/O操作数据是CompletionKey(完成键)参数标识的是某个特定的套接字句柄数据,相当于用一个标志来绑定每一个I/O操作,这样收到网络操作完成的通知后,可以通过这个标志来找出返回的数据对应的I/O操作。该标志可以定义如下:

typedef struct _PER_IO_CONTEXT{

OVERLAPPED m_Overlapped; // 每一个重叠I/O

网络操作都要有一个

SOCKET m_sockAccept; // 这个I/O操作所使用的

Socket,每个连接的都是一样的

WSABUF m_wsaBuf; //存储数据的缓冲区,用来给

重叠操作传递参数的

char m_szBuffer[MAX_BUFFER_LEN]; // 对应

WSABUF里的缓冲区

OPERATION_TYPE m_OpType; // 标志这个重叠I/

O操作是做什么的,例如Accept/Recv等

} PER_IO_CONTEXT, *PPER_IO_CONTEXT;

该结构关联了与I/O操作的某些重要数据元素,例如完成I/O操作发送或接受请求的类型m_OpType。每一个I/O操作对应了响应的PER_IO_CONTEXT,我们还要定义单句柄数据来管理句柄上的所有I/O请求,如在Socket上投递了多个AcceptEx请求,该结构定义如下:

typedef struct _PER_SOCKET_CONTEXT{

SOCKET m_Socket;// 每一个客户端连接的Socket

SOCKADDR_IN m_ClientAddr;// 这个客户端的地址

CArray<_PER_IO_CONTEXT*> m_arrayIoContext; // 数组,所有客户端IO操作的参数,也就是说对于每一个客户端Socket是可以在上面同时投递多个IO请求的

} PER_SOCKET_CONTEXT, *PPER_SOCKET_CONTEXT;

2.4 关闭IOCP

调用PostQueuedCompletionStatus函数,向每个工作者线程都发送一个特殊的完成数据包。可以唤醒那些还在等待完成端口但又没有已完成的I/O请求,每个线程会对GetQueuedCompletionStatus的返回值进行检查,如果发现应用程序正在终止,那么它就可以进行清理工作并正常的退出。

3 IOCP程序流程

该程序调用高性能特性的AcceptEx函数用来完成端口异步,取消了阻塞方式的Accept调用。我们知道,AcceptEx是在客户端连入之前就把客户端的Socket建立好了,而不需要像Accept那样在客户端连入之后,再去时间去建立Socket。系统创建一个Socket的开销是相当高了,用Accept的话,系统可能来不及为更多的并发客户端现场准备Socket。另外,相比Accept只能阻塞方式建立一个连入接口,而AcceptEx可以同时在完成端口上投递多个请求。图2所示是其程序的整体流程图。

4 结 语

采用I/O完成端口编写的服务应用程序,经过Process Explorer测试发现当服务器收到3 000个并发线程的时候CPU占有率约为4%,而采用了多个并发线程的客户端程序CPU占有率约为12%。所以,如果预计到自己的服务器在任何给定的时间,都会为大量I/O请求提供服务,便应考虑使用I/O完成端口模型,从而获得更好的性能。但是在编写基于完成端口的服务应用程序时,还应注意重叠操作可确保按照应用程序安排好的顺序执行。然而,不能确保从完成端口返回的完成通知也按照上述顺序执行。在对数据包有要求的时候,比如传送大数据的时候,要注意这个顺序。

参 考 文 献

[1] RICHTER J,NASARRE C. Windows 核心编程[M].Fifth Edition,葛子敖,周靖,等,译.北京:清华大学出版社,2008.

[2] BEVERIDGE J,WIENER R.Wind32多线程程序设计[M].侯捷,译.武汉:华中科技大学出版社,2006.

[3] ANTHONY Jones, JIM Ohlund. Network programming for Microsoft Windows [M]. 北京: 清华大学出版社,2002.

[4] 周鹏,黄灿,江楠. 完成端口模型的使用与分析[J]. 软件, 2012(2): 37-38.

[5] 林延君. 局域网企业信息安全系统的设计与实现[D]. 大连:大连理工大学,2006.

已传输的字节数、完成键以及OVERLAPPED结构的地址。GetQueuedCompletionStatus()函数定义如下:

BOOL GetQueuedCompletionStatus(

HANDLE CompletionPort, //指定的IOCP,该值由CreateIoCompletionPort函数创建

LPDWORD lpNumberOfBytes, //一次完成后的I/O操作所传送数据的字节数

PULONG_PTR lpCompletionKey,//当文件I/O操作完成后,用于存放与之关联的CK(套接字信息结构体指针)

LPOVERLAPPED *lpOverlapped,//为调用IOCP机制所引用的OVERLAPPED结构

DWORD dwMilliseconds,//用于指定调用者等待CP的时间

);

当一个工作者线程从GetQueuedCompletionStatus调用中接收到I/O完成通知后,在lpCompletion和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上进行I/O处理。通过这些参数,可获得两种重要的套接字数据类型:单句柄数据以及单I/O操作数据。单I/O操作数据是CompletionKey(完成键)参数标识的是某个特定的套接字句柄数据,相当于用一个标志来绑定每一个I/O操作,这样收到网络操作完成的通知后,可以通过这个标志来找出返回的数据对应的I/O操作。该标志可以定义如下:

typedef struct _PER_IO_CONTEXT{

OVERLAPPED m_Overlapped; // 每一个重叠I/O

网络操作都要有一个

SOCKET m_sockAccept; // 这个I/O操作所使用的

Socket,每个连接的都是一样的

WSABUF m_wsaBuf; //存储数据的缓冲区,用来给

重叠操作传递参数的

char m_szBuffer[MAX_BUFFER_LEN]; // 对应

WSABUF里的缓冲区

OPERATION_TYPE m_OpType; // 标志这个重叠I/

O操作是做什么的,例如Accept/Recv等

} PER_IO_CONTEXT, *PPER_IO_CONTEXT;

该结构关联了与I/O操作的某些重要数据元素,例如完成I/O操作发送或接受请求的类型m_OpType。每一个I/O操作对应了响应的PER_IO_CONTEXT,我们还要定义单句柄数据来管理句柄上的所有I/O请求,如在Socket上投递了多个AcceptEx请求,该结构定义如下:

typedef struct _PER_SOCKET_CONTEXT{

SOCKET m_Socket;// 每一个客户端连接的Socket

SOCKADDR_IN m_ClientAddr;// 这个客户端的地址

CArray<_PER_IO_CONTEXT*> m_arrayIoContext; // 数组,所有客户端IO操作的参数,也就是说对于每一个客户端Socket是可以在上面同时投递多个IO请求的

} PER_SOCKET_CONTEXT, *PPER_SOCKET_CONTEXT;

2.4 关闭IOCP

调用PostQueuedCompletionStatus函数,向每个工作者线程都发送一个特殊的完成数据包。可以唤醒那些还在等待完成端口但又没有已完成的I/O请求,每个线程会对GetQueuedCompletionStatus的返回值进行检查,如果发现应用程序正在终止,那么它就可以进行清理工作并正常的退出。

3 IOCP程序流程

该程序调用高性能特性的AcceptEx函数用来完成端口异步,取消了阻塞方式的Accept调用。我们知道,AcceptEx是在客户端连入之前就把客户端的Socket建立好了,而不需要像Accept那样在客户端连入之后,再去时间去建立Socket。系统创建一个Socket的开销是相当高了,用Accept的话,系统可能来不及为更多的并发客户端现场准备Socket。另外,相比Accept只能阻塞方式建立一个连入接口,而AcceptEx可以同时在完成端口上投递多个请求。图2所示是其程序的整体流程图。

4 结 语

采用I/O完成端口编写的服务应用程序,经过Process Explorer测试发现当服务器收到3 000个并发线程的时候CPU占有率约为4%,而采用了多个并发线程的客户端程序CPU占有率约为12%。所以,如果预计到自己的服务器在任何给定的时间,都会为大量I/O请求提供服务,便应考虑使用I/O完成端口模型,从而获得更好的性能。但是在编写基于完成端口的服务应用程序时,还应注意重叠操作可确保按照应用程序安排好的顺序执行。然而,不能确保从完成端口返回的完成通知也按照上述顺序执行。在对数据包有要求的时候,比如传送大数据的时候,要注意这个顺序。

参 考 文 献

[1] RICHTER J,NASARRE C. Windows 核心编程[M].Fifth Edition,葛子敖,周靖,等,译.北京:清华大学出版社,2008.

[2] BEVERIDGE J,WIENER R.Wind32多线程程序设计[M].侯捷,译.武汉:华中科技大学出版社,2006.

[3] ANTHONY Jones, JIM Ohlund. Network programming for Microsoft Windows [M]. 北京: 清华大学出版社,2002.

[4] 周鹏,黄灿,江楠. 完成端口模型的使用与分析[J]. 软件, 2012(2): 37-38.

[5] 林延君. 局域网企业信息安全系统的设计与实现[D]. 大连:大连理工大学,2006.

猜你喜欢
应用程序
浅谈重大火灾隐患自动判定应用程序研发及成效
删除Win10中自带的应用程序
儿童应用程序4岁也能做设计
Windows环境下基于栈和堆的应用程序行为解析方法研究
保护移动设备的安全
三星电子将开设应用程序下载商店
微软软件商店开始接受应用程序