I/O完成端口

  I/O完成端口允许应用程序使用线程池来处理异步I/O请求的机制。线程池中的线程都负责处理I/O请求。相对于收到I/O请求时创建线程,通过 I/O完成端口应用程序可以更快更有效的处理异步I/O请求。

    CreateIoCompletionPort函数把一个或多个文件句柄关联到一个I/O完成端口。当对其中某个文件的异步I/O操作完成时,一个I/O完成包在相应的I/O完成端口排队。这可以把多个文件的同步点组合到一个对象中。

    线程调用GetQueuedCompletionStatus等待一个完成包到达完成端口,而不是直接等待异步I/O操作完成。线程池中所有线程都阻塞在这个函数,当一个完成包到达完成端口时,线程按后入先出(LIFO)的顺序释放。这意味着当一个完成包到达完成端口时,系统释放最后一个在该函数阻塞的线程来处理完成请求。

    一个线程调用GetQueuedCompletionStatus之后,它就被绑定到这个完成端口,直到它退出、或者绑定到不同的完成端口、或者调用CloseHandle撤销到该完成端口的绑定。一个线程最多只能绑定到一个完成端口。

完成端口最重要的特性就是并发量,其值是在完成端口创建时指定的。它限定了绑定到本完成端口的线程的可运行个数。当绑定到某完成端口的可运行的线程个数超过并发量,系统会阻塞后续线程的执行,直到可运行线程的个数低于并发量。当完成端口的队列中有完成包在排队,而可运行线程的个数也达到并发量,这时系统是最高效的,因为当一个运行着的线程调用GetQueuedCompletionStatus,它会立刻取得完成包,这样避免了操作系统内部的“线程上下文环境切换”。

    一般情况下,并发量设为CPU的个数,但如果对一个完成包的处理是比较耗时的操作,应该设个较大值,具体多少最好由试验值来定。

    PostQueuedCompletionStatus函数允许应用程序不通过发起一个异步I/O操作就可以发送一个自定义的完成包,这样就可以把外部事件通知线程池中的工作线程。

    完成端口也是通过引用计数来维持它的生命周期,当没有外部引用时,完成端口被释放。所有与完成端口绑定的文件句柄都引用了它,所以在释放完成端口之前,必须释放所有绑定到它的文件句柄。

使用I/O完成端口

    I/O完成端口是使用线程池的一种机制,除了负责处理异步I/O请求的工作者线程之外,还需要一个主线程创建、管理完成端口和线程池中的工作者线程。

    主线程的执行过程如下:

  1. 调用CreateIoCompletionPort创建完成端口;
  2. 根据CPU个数创建一定数量的线程;
  3. 调用CreateIoCompletionPort异步I/O操作涉及的文件句柄和第1步创建的完成端口关联。

    工作者线程的执行过程如下:

  1. 调用GetQueuedCompletionStatus等待异步I/O完成包到达;
  2. 根据完成包的的内容做响应处理。
  3. 循环执行1~2。

    整个机制中,最关键的是I/O完成包的流向。

    当调用一个异步操作接口(如调用WSARecv、WSASend或者调用ReadFileExWriteFileEx去读一个以FILE_FLAG_OVERLAPPED标志打开的文件)时,应用程序就向系统内核发起了一次异步I/O请求。这时需要传递一个LPOVERLAPPED结构和一个LPOVERLAPPED_COMPLETION_ROUTINE回调函数指针(指向异步I/O完成时回调的函数)给异步操作接口作参数。

    当系统内核处理完某异步I/O请求后,如果异步I/O请求发起时传入的LPOVERLAPPED_COMPLETION_ROUTINE不为空,该函数会被调用。对于I/O完成端口模型,该值都为空,这样系统内核构造一个I/O完成包把它放入I/O完成端口的队列。

    线程池中的工作者线程不断调用GetQueuedCompletionStatusI/O完成端口的队列中取出I/O完成包,根据完成包的的内容做响应处理。

    由此可见,I/O完成端口是个抽象的实体,它拥有(对应、管理)一个线程池(其中包括若干工作者线程)和一个I/O完成包的队列。

    同样,I/O完成包也是个抽象实体,它至少对应一个异步I/O操作的目标(可以是文件和Socket)句柄和一个描述一次异步I/O操作的结构,这样工作者线程就有了作出响应的依据。

    GetQueuedCompletionStatus的三个输出参数都可以看成是I/O完成包的内容,其中

BOOL GetQueuedCompletionStatus(
    [in]HANDLE CompletionPort,
    [out]LPDWORD lpNumberOfBytes,
    [out]PULONG_PTR lpCompletionKey,
    [out]LPOVERLAPPED* lpOverlapped,
    [in]DWORD dwMilliseconds
);

    lpNumberOfBytes    返回本次异步I/O操作完成传输的字节数,该值由系统内核填充;

    lpCompletionKey    返回本次异步I/O操作目标的相关信息,该值返回的仅仅是个内存地址,具体是些什么数据如何分布,由把异步I/O操作目标句柄和完成端口关联时(即主线程在第3步)传CreateIoCompletionPort的第三个参数指定,即CreateIoCompletionPort第三个参数指向的东西就是lpCompletionKey指向的东西,系统不作任何改动。要注意的是,CreateIoCompletionPort在把多个文件句柄管关联到同一个完成端口时,为每个句柄指定不同的CompletionKey值,内核在完成一次异步I/O操作时,取出相应的CompletionKey值放入I/O完成包。因而在Anthony Jones, Jim Ohlund写的《Network Programming for Microsoft Windows》中,这部分数据被称为Per-handle Data,也就是说这部分数据和完成端口关联的诸多文件句柄是一一对应的。

HANDLE CreateIoCompletionPort(
    [in]HANDLE FileHandle,
    [in]HANDLE ExistingCompletionPort,
    [in]ULONG_PTR CompletionKey,
    [in]DWORD NumberOfConcurrentThreads
);

    lpOverlapped        表面上它返回的就是一个LPOVERLAPPED,但在实际应用中通常通过它来传递描述一次异步I/O操作的相关信息,所以该值返回的也仅仅是个内存地址,具体是些什么数据如何分布,与发起异步I/O请求时传给异步API的LPOVERLAPPED/LPWSAOVERLAPPED 参数有关。在Anthony Jones, Jim Ohlund写的《Network Programming for Microsoft Windows》中,这部分数据被称为Per-I/O Operation Data,因为这部分数据和一次异步I/O操作一一对应。和lpCompletionKey不同的是,系统要求传入的lpOverlapped所指向的必须是个LPOVERLAPPED/LPWSAOVERLAPPED 结构,并且也会对这部分进行修改,但对lpOverlapped+sizeof(LPOVERLAPPED)之外的内存不作任何修改。

typedef struct _PER_IO_DATA
{
    OVERLAPPED Overlapped;
    WSABUF DataBuf;
    char buffer[1024];
    int BufferLen;
    int OperationType;
}PER_IO_DATA, *LPPER_IO_DATA;

typedef struct _PER_HANDLE_DATA
{
    SOCKET Socket;
    SOCKADDR_STORAGE ClientAddr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

LPPER_IO_DATA lpPerIoData = (LPPER_IO_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_DATA));
...
lpPerIoData->OperationType = 1001; // 任意值,只要自己明白其含义即可,这里用1001标识这是一次WSARecv操作,这样线程池中的工作者线程在获得此次异步I/O操作的完成包时,就可以知道刚刚完成的是一次WSARecv操作。
WSARecv(?,?,?,?,?,(LPOVERLAPPED)lpPerIoData,NULL);

//这里把Overlapped作为PER_IO_DATA的第一个成员,所以直接转换即可,否则

WSARecv(?,?,?,?,?,&(lpPerIoData->Overlapped),NULL);

GetQueuedCompletionStatus取得I/O完成包后,如果Overlapped是PER_IO_DATA的第一个成员,lpOverlapped所指的也就是lpPerIoData的地址,工作者线程可以获取OperationType成员的值;如果Overlapped不是PER_IO_DATA的第一个成员,可以使用CONTAINING_RECORD宏来获取lpPerIoData的地址:

lpPerIoData = (LPPER_IO_DATA)CONTAINING_RECORD(lpOverlapped, PER_IO_DATA, Overlapped);

 

from http://hzgmaxwell.bokee.com/4564081.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值