IOCP Internals
Buffer Type
Buffer I/O
针对Buffer I/O的请求,系统会为其分配一个非换页内存作为缓存区,其大小等同于I/O请求的缓存区大小。对于写操作,I/O管理器在创建IRP时,将请求者的缓存区数据拷贝到申请得到的非换页缓存区中。对于读操作,I/O管理器将会在I/O完成时,将数据从非换页内存缓存区中拷贝到请求者的缓存区中。在I/O完成后,会释放掉该次申请到的非换页缓存区。
Direct I/O
I/O管理器在处理Direct I/O类型的请求时,会将请求者的缓存区锁定在内存中,使其变成不可换页的内存。当该I/O处理完成时,I/O管理器会解除请求者缓存区的锁定。I/O管理器用MDL机制锁定请求的缓存区,在需要操作缓存区的时候,可以使用相应的API映射请求者的缓存区到系统的地址空间。
Neither I/O
对于此种I/O请求,I/O管理器不会执行任何缓存区管理,对应的缓存管理交给相应的设备驱动程序自己处理。但是不管设备驱动程序如何处理,都不会超过上面两种方式的处理。
I/O Type
Synchronous I/O
应用程序发出的大部分I/O操作是同步的。在设备处理I/O请求时,应用程序需要一直等待,直到I/O完成时,设备返回结果,最终程序才可以继续执行。并且可以立即访问这些被传输的数据。
Asynchronous I/O
异步I/O是程序先发出一个I/O请求,交由设备处理,程序可以继续执行。该程序可以在处理其他事务后检查I/O是否完成,或者当I/O完成时,I/O管理器主动告诉程序I/O已完成。 为了在WINDOWS使用异步I/O,打开文件或建立SOCKET时,需要指定FILE_FLAG_OVERLAPPED标志。
在发起一个异步I/O请求后,必须十分小心:在I/O未完成时,不能访问该I/O操作中任何数据,要确保缓存区有效。发起异步I/O后,为了拿到最终有效的数据,需要做同步操作,WINDOWS下支持OVERLAPPED,也就是基于EVENT内核对象同步,APC(异步过程调用)还有IOCP(I/O Completion Port)。
I/O Completion Process
当一个设备处理完I/O请求时,会调用内核API IoCompleteRequest,通知I/O管理器自己完成了该I/O请求。为了安全操作用户的缓存区,I/O管理器会根据I/O请求类型会执行相应的动作:如果是同步I/O请求,那么可以直接操作请求的缓存区;如果是异步I/O请求,I/O管理器会延迟该I/O完成动作直到请求者对应的地址空间为止。完成该动作的方式是向该请求的线程投递一个APC。在切换到该请求的I/O请求所在的线程时,该APC会得到执行。会把已完成的I/O请求中得到的数据拷贝到对应的缓存区中。这个时候,对于异步I/O请求,像ReadFileEx/WriteFileEx可以投递一个APC例程,I/O管理器在执行I/O完成最后一步后,检测到有APC,就把该APC例程投递到请求线程内,当线程处于警告状态时(SleepEx, WaitForSingleObjectEx)会得到执行。如果该异步I/O请求对应的文件对象或者SOCKET绑定了IOCP,则会在处理完成时,把对应的OVERLAPPED结构投递到IOCP队列中。 这也就是当处理WSARecv时(假设该请求的SOCKET已经绑定了IOCP),如果这个时候系统中已经缓存了数据,这个时候不需要判断是否接收到数据,而只需要判断LastError是否是ERROR_IO_PENDING的原因。
IOCP
在实现服务端程序时,常用的一个简单处理网络请求的方式是针对连接分配一个线程单独处理。太少或太多都会带来性能问题。线程太少,对于客户端的请求无法及时处理;太多,会导致线程频繁轮换。理想的情形是,在服务器的每个处理器上都有一个线程在积极的处理一个用户的请求,如果有其他的请求在等待的话,会立刻有线程来处理这些请求。为了达到这一最优过程得以最终正确的进行,需要有一个办法,能够在处理客户请求的线程在I/O阻塞时,能激活另一个线程处理其他请求。
WINDOWS中引入了IOCP机制来实现这一最优过程。IOCP的内部是依靠一个叫队列的内核对象完成这一过程。当创建一个文件句柄或者SOCKET时,可以把句柄与IOCP句柄绑定在一块,任何以此句柄发起的异步I/O请求操作在完成时都会生成一个CompletionPackage,并把该CompletionPackage排队到该完成端口上,也就是排队到内部的队列内核对象上。应用程序可以通过API GetQueuedCompletionStatus获取已完成的CompletionPackage,处理已完成的I/O数据。
WINDOWS API中的WaitForMultipleObjects函数提供了类似的功能,但是,完成端口的好处是,在系统协助下并发是可控的。处理方式是应用程序主动处理客户端请求的线程的数量。前面提到,理想情况是每个处理器都由一个线程在处理客户端请求。在创建IOCP时,会制定一个值,这个值为关注IOCP的线程数。因为IOCP的内部机制依赖于队列内核对象,在WINDOWS中,内核对象都是可等待的,也就是用类似WaitForSingleObject类似的机制。而IOCP在创建时制定的值会传给内部的队列内核对象作为一个并发值。如果与IOCP关联的活动线程的数量超过了这个并发值,那么正在该IOCP上等待的线程将不允许运行。当有异步I/O完成时,会投递CompletionPackage到绑定的IOCP中的队列对象中,如果此时有线程正在等待,则会直接把这个CompletionPackage交给这个正在等待的线程处理。这样的过程中,没有线程环境切换,更好的把CPU的能力利用起来。
关于IOCP的并发值,Microsoft的指导原则是,将并发值设置成大约等于该系统中处理器的数目。
参考文档:
- 《深入解析Windows操作系统第四版》
- Windows Research Kernel