IOCP完成端口大致分析(windows核心编程)

341 阅读8分钟

1. IOCP的相关函数

CreateIoCompletionPort
PostQueuedCompletionStatus
GetQueuedCompletionStatus

2. IOCP相关的几个内核数据结构

设备列表

每条记录包含
[ hDevice, dwCompletionKey ]
当满足以下条件时,会在列表中添加项

  • CreateCompletionPort被调用
    当满足以下条件时,会将列中的项删除
  • 设备句柄被关闭

IO完成队列(先入先出)

每条记录包含 [ dwBytesTransferred, dwCompletionKey, pOverlapped, dwError ]
当满足以下条件时,会在列表中添加新项

  • IO请求完成
  • PostQueuedCompletionStatus被调用
    当满足以下条件时,会将列中的项删除
  • 完成端口从等待线程队列中删除一项

等待线程队列(后入先出)

每条记录包含 [ dwThreadId ]
当满足以下条件时,会在列表中添加新项

  • 线程调用GetQueuedCompletionStatus
    当满足以下条件时,会将列中的项删除
  • IO完成队列不为空,而且正在运行的线程数小于最大并发线程(GetQueuedCompletionStatus,会先从IO完成队列中删除对应的项,接着将dwThreadId转移到已释放线程列表,最后函数返回)

已释放线程列表

每条记录包含
[ dwThreadId ]
当满足以下条件时,会在列表中添加新项

  • 完成端口在等待线程队列中唤醒一个线程
  • 已暂停的线程被唤醒
    当满足以下条件时,会将中的项删除
  • 线程再次调用GetQueuedCompletionStatus(dwThreadId再次回到等待线程队列)
  • 线程调用一个函数将自己挂起(dwThreadId转移到已暂停线程列表)

已暂停线程列表

每条记录包含
[ dwThreadId ] 当满足以下条件时,会在列表中添加新项

  • 已释放的线程调用一个函数将自己挂起
    当满足以下条件时,会将列中的项删除
  • 已挂起的线程被唤醒(dwThreadId回到已释放线程队列)

3. 描述各个队列以及每个函数所做的事情

  1. 首先创建一个IO完成端口,调用CreateIoCompletionPort
  2. 然后将IO完成端口和一个设备句柄进行关联,此时,在设备列表中添加一项, [ hDevcie, dwCompletionKey ], 一个设备句柄和一个completionKey(这个只对我们有用信息,操作系统并不关心这个值)
  3. 调用PostQueuedCompletionStatus.向完成队列(先入先出)添加一项内容,当IO请求完成的时候,会匹配对应的一项然后从完成队列中删除
  4. 然后线程池的线程去调用GetQueuedCompletionStatus,这个函数会将当前线程阻塞,然后向等待线程队列(后入先出,是一个栈) 添加一条记录,这条记录是当前线程的id.当IO完成队列不为空,而且正在运行的线程数小于最大并发线程数(GetQueuedCompletionStatus会先从IO完成队列中删除对应的项,接着将dwThreadId转移到已释放列表,最后函数返回)则会将当前项从等待线程队列里删除.

移除IO完成队列的各项是以先入先出的方式进行的。但是,唤醒了那些调用了GetQueuedCompletionStatus的线程是以后入先出的方式添加到等待线程队列

所以,这就说有4个线程在等待线程队列中等待,如果出现一个已完成的IO项,那么最后一个调用GetQueuedCompletionStatus的线程会被唤醒,来处理一项。当这个线程完成处理后,再次调用GetQueuedCompletionStatus进入等待线程队列.现在又出现另一个完成的IO项,那么这个线程还是会被唤醒,来处理这个新的IO项.
如果IO请求完成得足够慢,那么系统会不断的唤醒同一个线程,而让其他线程继续睡眠.通过这种后入先出算法,系统可以将那些未被调度的线程的内存资源(比如栈空间)换出到磁盘,并将它们从高速缓存中清除.这意味着让许多线程等待一个完成端口并不是什么坏事.如果正在等待的线程数量大于已完成的IO请求的数量,那么系统会将多余线程的大多数资源换出内存。

  1. 还剩余两个队列列表,都是和等待线程队列相关。
  1. 已释放线程列表: 当一个IO请求完成时,唤醒等待线程则将等待线程队列的一项删除,然后这项转移到已释放线程列表中. 这个时候有两种情况转移:

第一种情况: 现在线程将自己挂起,则转移到已暂停线程列表
第二种情况: 再次调用GetQueuedCompletionStatus,则当前项转移回等待线程队列去等待IO完成

  1. 已暂停列表: 已释放列表已经介绍什么情况会转移到已暂停列表.那么已暂停列表的线程被唤醒的话,则从已暂停列表移除,回到已暂停线程列表.接下的情况请参考已释放线程列表的描述。

4. IO完成端口如何管理线程池

IOCP在windows设计之处就是要配合使用线程池一起使用的。创建IO端口的时候,通常将设置主要CPU数量个线程并发运行,而线程池的数量一般设置为CPU数量的两倍,为什么不也是CPU数量,而是让多余的线程在线程池等待呢?

假设在一个有两个CPU的机器上运行,创建一个IO完成端口,并设置只有两个线程来处理已完成的项(在CreateIoCompletionPort的最后一个参数设置),但是我们在线程池只创建了4个线程(CPU数量两倍).看起来创建了多余的线程,它们永远不会被唤醒来处理任何操作.
但IO完成端口非常智能。当唤醒一个线程的时候,将线程标识符从等待线程队列移动到已释放线程列表中。这使得IO完成端口知道哪些线程已经被唤醒,并监视它们的执行情况。如果一个已释放的线程调用的任何函数将该线程挂起,那么完成端口回检测到这一情况,将该线程标识符从已释放线程列表中移除,将其添加到已暂停列表中。
完成端口的目标是根据创建完成端口时指定的并发线程的数量,将尽可能多的线程保持在已释放线程列表中.如果一个已释放线程由于任何原因,那么已释放线程列表回缩减,完成端口就可以释放另一个正在等待线程.如果一个已暂停的线程被唤醒,那么它会离开已暂停线程列表并重新进入已释放线程列表。这意味着此时已释放线程列表中的线程数量将大于最大允许的并发线程数量。
一旦一个线程调用了GetQueuedCompletionStatus,该线程会被指派给指定的完成端口.当指派给给完成端口的正在运行的线程数量小于它最大允许的并发线程数量时,完成端口才会从线程池中唤醒线程. 我们可以从以下3中方式之一结束线程/完成端口指派

  • 线程退出
  • 线程调用GetQueuedCompletionStatus,并传入不同的IO完成端口的句柄
  • 销毁线程当前被指派的IO完成端口
    总结一下: 假设在一台有两个CPU的机器上运行.我们创建了一个同时最多只允许两个线程被唤醒的完成端口,还创建了4个线程来等待已完成的IO请求.如果3个已完成IO请求被添加到端口的队列中,只有两个线程会被唤醒处理请求,这降低了可运行线程数量,并节省了上下文切换时间.现在,如果一个可运行线程调用Sleep、WaitForSingleObject、WaitForMultipleObjects、SignalObjectAndWait,一个异步IO调用或任何能够导致线程变不可运行状态函数,IO完成端口会检测到这情况并立即唤醒第3个线程.完成端口的目标是使CPU保持满负荷状态下工作.
    最后,第一个线程再次变成可运行状态,当发生这种情况的时候,可运行线程的数量超过系统中CPU的数量.但是,完成端口仍然知道这一点,在线程数量降低到地狱CPU数量之前,它是不会唤醒任何线程.IO完成端口假定可运行线程数量只会在很短一段时间内高于最大允许的线程数量,一旦线程进入下一次循环并调用GetQueuedCompletionStatus,可运行线程的数量就会迅速下降.这就解释了为什么线程中线程数量应该大于在完成端口中设置的并发线程数量.