完成端口(Completion Port)实现思路浅析

完成端口(Completion Port)实现思路浅析 

http://www.blogcn.com/user8/flier_lu/index.html?id=2401390&run=.0D9CAA6

完成端口是 NT 架构下一种高效的异步 IO 辅助机制,其使用方法已经被广为讨论,MSDN里面也有很详细的说明和示例。 《Windows网络编程》一书中有关于通过完成端口实现高效网络服务器设计的详细说明; 《Windows核心编程》一书中有关于异步IO以及线程池等相关知识的使用介绍;而 《Windows 2000 内部揭密 》一书中则有对完成端口实现原理的简要介绍;此外一些关于 Win32 下多线程编程方面的书籍也对完成端口有所提及。
    因此,对于完成端口的使用我这里就不再罗嗦,下面将直接从实现角度对其进行分析。

    首先,从设计角度来看:完成端口是一个 NT 执行子系统的核心对象。通过将完成端口与任意个 I/O 句柄(文件或Socket等)关联,使得用户可以通过完成端口这一统一途径,异步的获取并处理 I/O 操作的结果,同时能够最大限度利用多 CPU 的优势。而且因为完成端口实际上是 NT 系统很多底层机制如 APC 的实现手段,故而在效率上是最高的,因为绝大多数其他类似解决方法最终还是使用完成端口实现。与 WaitForMultipleObjects 函数不同,完成端口是由系统直接提供并行优化支持的,通过建立完成端口时指定的并行线程值,系统可以保证工作在同一完成端口上的线程数量受控(一般等于系统CPU数量),这样就可以避免无意义的线程上下文切换,获取更高的性能。

    其次,从使用角度来看:完成端口使用 CreateIoCompletionPort 函数和 CloseHandle 函数创建和释放,使用 GetQueuedCompletionStatus 函数和 PostQueuedCompletionStatus 函数实现完成端口的读写操作。因此我们的分析从这三个函数开始。

    CreateIoCompletionPort 函数在 ExistingCompletionPort 参数为空的时候调用 NtCreateIoCompletion 函数(iocomplete.c:51)创建一个新的完成端口对象;然后处理 FileHandle 参数为 INVALID_HANDLE_VALUE 时的情况;最后调用 NtSetInformationFile 函数(dll estrfil.c:740)试图将 I/O 句柄绑定到完成端口上。伪代码如下:
以下为引用:

#define IO_COMPLETION_ALL_ACCESS 0x001F0003

typedef enum _FILE_INFORMATION_CLASS
{
  //...
  FileCompletionInformation = 30,
  //...
} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;

typedef struct _IO_STATUS_BLOCK
{
  union {
    NTSTATUS Status;
    PVOID Pointer;
  };

  ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

HANDLE CreateIoCompletionPort(
  HANDLE FileHandle,
  HANDLE ExistingCompletionPort,
  ULONG_PTR CompletionKey,
  DWORD NumberOfConcurrentThreads)
{
  HANDLE CompletionPort = ExistingCompletionPort;

  // 创建一个新的完成端口对象
  if(ExistingCompletionPort == NULL)
  {
    NTSTATUS st = NtCreateIoCompletion(&CompletionPort, IO_COMPLETION_ALL_ACCESS, NULL, NumberOfConcurrentThreads);

    if(!NT_SUCCESS(st))
    {
      SetLastNTError(RtlNtStatusToDosError(st));
      return NULL;
    }
  }

  if(FileHandle == INVALID_HANDLE_VALUE)
  {
    // FileHandle 参数为 INVALID_HANDLE_VALUE 时 ExistingCompletionPort 参数必须为空
    if(ExistingCompletionPort != NULL)
    {
      CompletionPort = NULL;
      BaseSetLastNTError(STATUS_INVALID_PARAMETER);
    }

    // 只创建完成端口对象,但并不绑定 I/O 句柄到完成端口上
    return CompletionPort;
  }

  IO_STATUS_BLOCK blk;

  // 将 I/O 句柄绑定到完成端口上
  NTSTATUS st = NtSetInformationFile(FileHandle, &blk, &CompletionKey, 8, FileCompletionInformation);

  if(NT_SUCCESS(st))
  {
    return CompletionPort;
  }
  else
  {
    SetLastNTError(RtlNtStatusToDosError(st));

    if(CompletionPort)
    {
      CloseHandle(CompletionPort);
    }

    return NULL;
  }
}


    CloseHandle 在处理完成端口时则直接调用 IopDeleteIoCompletion 函数(iocomplete.c:675)。而 GetQueuedCompletionStatus 函数和 PostQueuedCompletionStatus 函数,则分别是 NtRemoveIoCompletion 函数(iocomplete.c:485)和 NtSetIoCompletion 函数(iocomplete.c:417)的简单包装。    
以下为引用:

BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes, 
  PULONG_PTR lpCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds)
{
  LARGE_INTEGER Timeout;
  
  BaseFormatTimeOut(&Timeout, dwMilliseconds);
  
  IO_STATUS_BLOCK blk;
  
  NTSTATUS st = NtRemoveIoCompletion(CompletionPort, lpCompletionKey, &dwMilliseconds, &blk, &Timeout);
  
  if(FAILED(st) || st == STATUS_TIMEOUT)
  {
    // 设置错误代码
  }
  else
  {
    if(SUCCEEDED(blk.Status))
    {
      // 设置 lpOverlapped 和 lpNumberOfBytes
    }
  }
}  


    从实现角度来看,完成端口实际上是一个 IoCompletionObjectType 类型的内核对象,其对象体就是一个 KQUEUE 结构维护的内核队列。创建此内核对象类型信息的代码如下(io/ioinit.c:1527):
以下为引用:

//
// Create the object type for I/O completion objects.
//

RtlInitUnicodeString( &nameString, L"IoCompletion" );                   // 对象名称
objectTypeInitializer.DefaultNonPagedPoolCharge = sizeof( KQUEUE );     // 对象体
objectTypeInitializer.InvalidAttributes = OBJ_PERMANENT | OBJ_OPENLINK;
objectTypeInitializer.GenericMapping = IopCompletionMapping;
objectTypeInitializer.ValidAccessMask = IO_COMPLETION_ALL_ACCESS;
objectTypeInitializer.DeleteProcedure = IopDeleteIoCompletion;          // CloseHandle 调用函数
if (!NT_SUCCESS( ObCreateObjectType( &nameString, &objectTypeInitializer,
  (PSECURITY_DESCRIPTOR) NULL, &IoCompletionObjectType )))
{
  return FALSE;
}



    NtCreateIoCompletion 函数(iocomplete.c:51)只是简单地构造并初始化这个内核对象。
    首先对来自用户态的调用验证 IoCompletionHandle 参数指向内存可写;然后调用 ObCreateObject 函数构造内核对象;如果成功则调用 KeInitializeQueue 函数初始化内核队列;并且调用 ObInsertObject 函数将内核对象加入到当前进程的句柄表中;最后将构造的内核对象的句柄返回给调用者。伪代码如下:
以下为引用:

NTSTATUS
NtCreateIoCompletion (
  IN PHANDLE IoCompletionHandle,
  IN ACCESS_MASK DesiredAccess,
  IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
  IN ULONG Count OPTIONAL)
{
  NTSTATUS st;
  try
  {
    // 对来自用户态的调用验证 IoCompletionHandle 参数指向内存可写
    if(KeGetPreviousMode() != KernelMode)
    {
      ProbeForWriteHandle(IoCompletionHandle);
    }

    PVOID IoCompletion; // 完成端口内核对象

    // 构造内核对象
    st = ObCreateObject(..., IoCompletionObjectType, ..., sizeof(KQUEUE), ..., &IoCompletion);

    if(NT_SUCCESS(st))
    {
      // 初始化内核队列
      KeInitializeQueue((PKQUEUE)IoCompletion, Count);

      HANDLE Handle;

      // 将内核对象加入到当前进程的句柄表中
      st = ObInsertObject(IoCompletion, ..., &Handle);

      if(NT_SUCCESS(st))
      {
        try
        {
          // 将构造的内核对象的句柄返回给调用者
          *IoCompletionHandle = Handle;
        }
        catch(...)
        {
          // 完成端口内核对象已创建并插入当前进程句柄表,因此需要忽略错误
        }
      }
    }
  }
  catch(...)
  {
    st = GetExceptionCode();
  }
  return st;
}


    系统还提供了一个相应的 NtOpenIoCompletion 函数(iocomplete.c:178)支持打开已经建立的有名字的完成端口。其调用 ObOpenObjectByName 函数完成名字到内核对象的映射,并最终将完成端口对象句柄返回给调用者。伪代码如下:
以下为引用:

NTSTATUS
NtOpenIoCompletion (
  OUT PHANDLE IoCompletionHandle,
  IN ACCESS_MASK DesiredAccess,
  IN POBJECT_ATTRIBUTES ObjectAttributes)
{
  NTSTATUS st;
  try
  {
    // 对来自用户态的调用验证 IoCompletionHandle 参数指向内存可写
    if(KeGetPreviousMode() != KernelMode)
    {
      ProbeForWriteHandle(IoCompletionHandle);
    }

    HANDLE Handle;

    st = ObOpenObjectByName(ObjectAttributes, IoCompletionObjectType, ..., DesiredAccess, ..., &Handle);

    if(NT_SUCCESS(st))
    {
      try
      {
        // 将构造的内核对象的句柄返回给调用者
        *IoCompletionHandle = Handle;
      }
      catch(...)
      {
      }
    }
  }
  catch(...)
  {
    st = GetExceptionCode();
  }
  return st;
}


    在建立了完成端口对象后,可以通过 NtQueryIoCompletion 函数(iocomplete.c:282)查询此完成端口队列中阻塞的 I/O 完成包的数量。此函数实际上是对读取内核队列状态 KeReadStateQueue 函数(kequeueobj.c:92)的封装,从 KQUEUE::Header::SignalState 字段读取完成端口内部使用的内核队列对象的阻塞深度。
    NtRemoveIoCompletion 函数(iocomplete.c:485)和 NtSetIoCompletion 函数(iocomplete.c:417) 实际上也只是直接调用内核队列维护函数 KeRemoveQueue 函数(kequeueobj.c:238) 和 KeRundownQueue 函数(kequeueobj.c:566) 完成对内核队列的操作。

    在构造和初始化完成端口内核对象后,需要开始对完成端口进行操作,以实现对完成端口获取或发送完成包的操作。而这些操作归根结底实际上都是对内核队列进行操作,因此得先对内核队列有所了解。我会单独写一篇 BLog 讨论内核对象的实现思路,这里暂且不深入讨论。


... to be continue ...

你可能感兴趣的:(多线程,编程,C++,c,C#)