完成端口内部机制
当传递NULL值给ExistingCompletionPort参数来调用CreateIoCompletionPort来创建完成端口时,将调用同名的NtCreateIoCompletion系统服务。实质上,IoCompletion对象是建立在一个称为队列的内核同步对象基础上。系统创建一个完成端口的同时,在完成端口所分配到的内存中初始化一个队列对象(指向完成端口的指针同时指向了此队列对象,因为队列对象位于完成端口对象内存的开始处)。当一个线程调用CreateIoCompletionPort来创建完成端口时,第四个参数NumberOfConcurrentThreads即为队列的并发值。NtCreateIoCompletion函数将调用KeInitializeQueue系统服务来初始化该端口的消息队列。
当应用程序再次调用CreateIoCompletionPort时,将调用NtSetInformationFile服务来使参数一(文件句柄)与参数二(一个已有的完成端口)关联起来。完成通知包FileCompletionInformation包含的信息:CreateIoCompletionPort的参数二ExistingCompletionPort(已有的完成端口句柄)和参数三CompletionKey(完成键)。NtSetInformationFile通过解引用操作从该文件句柄获得对应的文件对象,并且申请一个记录完成上下文的数据结构。这个数据结构在NTDDK.H定义如下:
typedef struct _IO_COMPLETION_CONTEXT {
PVOID Port;
ULONG Key;
} IO_COMPLETION_CONTEXT, *PIO_COMPLETION_CONTEXT;
最后,将调用NtSetInformationFile系统服务设置文件对象中CompletionContext域的值。当一个异步I/O在一个文件对象上完成时,系统内部执行具有I/O管理功能的IopCompleteRequest系统服务,检查文件对象中的CompletionContext域是否为非NULL。如果是,则I/O管理器生成一个完成通知包,通过调用KeInsertQueue系统服务将完成通知包投递到完成端口队列(注意,完成端口对象和队列对象是同义的)。
当一个服务器线程调用GetQueuedCompletionStatus时,它将调用NtRemoveIoCompletion系统服务。在验证参数后,并且将完成端口句柄转换成一个指向该端口的指针后,NtRemoveIoCompletion调用KeRemoveQueue。
正如你所看到的,KeRemoveQueue和KeInsertQueue是完成端口模型的两个引擎级函数,它们决定阻塞在完成端口上等待I/O完成通知包的线程什么时候被唤醒。在系统内部,队列对象维护了完成端口上当前活动线程的计数值,以及最大的并发活动线程的数量。当一个线程调用KeRemoveQueue并且当前活动线程数大于或等于并发数上限时,那么该线程将被投放到一个阻塞线程队列(按LIFO顺序)中,等待系统调度来获取并处理完成通知包。此线程列表挂在队列对象的外面,线程的控制块数据结构中有一个指针引用了一个与之相关的队列对象;如果这个指针为NULL,则该线程没有与队列关联。
Windows依赖与线程控制块中的队列指针来跟踪和记录那些“由于被阻塞在除了完成端口之外的其他事情上而变成不活动”的线程。那些有可能会导致一个线程阻塞的调度例程(例如KeWaitForSingleObject,KeDelayExecutionThread等等)要检查该线程的队列指针。如果该指针不为NULL,则这些函数调用KiActivateWaiterQueue—一个与队列相关的函数,它会递减与该队列相关联的活动线程的计数值。如果计数值递减到小于设置的并发值,并且此时至少有一个完成通知包在该队列中,那么处于该队列的线程列表最前面的那个线程被唤醒,并且把最老的(the oldest)完成通知包交给它处理。相反,无论何时,与一个队列相关联的线程在阻塞之后被唤醒时,调度程序执行KiUnwaitThread函数来增加该队列上活动线程的计数值。
最后,PostQueuedCompletionStatus这个Windows API将调用NtSetIoCompletion服务。该函数只是简单的调用KeInsertQueue将自定义的完成通知包插入到完成端口的队列中。
本文导航
- 第1页: 首页
- 第2页: 使用I/O完成端口
- 第3页: 完成端口内部机制
- 第4页: 没有公开的完成端口