目的
本片文章主要讲解同步I/O与异步I/O相关知识,希望通过编写本篇文章为起点,对windows内核原理知识进行学习与梳理。发现并弥补遗漏的知识点并加以学习。同时通过理解windows内核原理,设计出更好、更合理的应用程序。
I/O
I/O即输入输出。在现在操作系统,输入输出是计算机完整功能必不可少的一部分。处理器负责各种计算任务,然后通过各种输入输出设备与外界进行交互。常见的输入输出设备包括键盘、鼠标、显示器、硬盘、网络适配器接口等。有了硬件设备,在软件层面上,使得操作系统通过以一致的方式与设备驱动交互从而的操控硬件设备。而应用程序通过统一的接口与系统内核进行交互。
Windows从一开始就设计了可扩展的I/O接口。在应用层通过统一的Win32 API
,将I/O请求分配给正确的设备驱动程序。设备驱动程序调用设备控制器来操控硬件。而内核通过硬件抽象层与硬件进行交互。硬件抽象层提供了供内核和驱动调用的例程。
例程就是系统提供的API或服务。
在Windows下分为内核模式和用户模式。应用程序运行在用户模式下,操作系统和驱动程序运行在内核模式下。应用程序通过调用Win32 API
与Windows内核交互。
Windows内核则通过设备驱动程序与设备控制器进行通讯,而设备控制器则直接操控硬件设备。
设备驱动程序分为即插即用驱动程序、内核扩展驱动程序和文件系统驱动程序。其中文件系统驱动程序用于接收I/O请求,然后将请求转换为真正的存储设备或网络设备的I/O请求。
设备控制器可以通过内存映射I/O的方式将设备的内存与主存映射,通过内存映射I/O后,处理器访问的就不是主存而是设备控制器的寄存器内存。但是这种方式的访问效率并不高,不适合大数据量I/O读写。通常硬盘和网络驱动器采用直接访问内存(DMA)的方式进行大量数据的I/O操作。DMA需要硬件支持,硬件会有DMA控制器,在硬件执行I/O操作的时候,不会占用CPU的指令周期,DMA控制器会和设备进行I/O操作。当数据传输完成后,DMA则会通知处理器I/O操作完成。
同步I/O
当我们要把文件从硬盘读取到内存时,硬盘的读取速度是远小于内存的写入速度的。因此当我们使用一个线程从硬盘读取文件到内存中时。通常需要等待硬盘将数据从硬盘读取到内存中,此时线程将被阻塞,但是不会消耗指令周期。当读取完毕时,线程继续执行后续操作。
虽然DMA执行的时候当前线程被阻塞,此时处理器可以获取另一个线程内核执行其他操作,由于线程是非常昂贵的资源,因此使用同步I/O的方式若需要并发执行时,需要大量的创建线程资源,这就产生了大量的线程上下文切换。
在大多数x86和x64的多处理器,线程上下文切换时间间隔大约为15ms。
CPU每过大约15ms将CPU寄存器当前的线程上下文存回到该线程的上下文,然后该线程不在运行。然后系统检查剩下的可调度线程内核对象,选择一个线程的内核对象,将其上下文载入导CPU寄存器中。
关于Windows线程相关内容可以查阅《Windows via C/C++ 第五版》的第七章
异步I/O
前面提到了当硬件进行I/O传输时,实际上通常使用DMA技术执行I/O操作,不会占用CPU的指令周期。因此只要操作系统支持异步I/O,则可以极大的提升系统性能,最大程度的降低线程数量,减少线程上下文切换产生的性能损失。
在Windows下的异步I/O我们也可以称之为重叠(overlapped)I/O。重叠的意思是执行I/O请求的时间与线程执行其他任务的时间是重叠的,即执行真正I/O请求的时候,我们的工作线程可以执行其他请求,而不会阻塞等待I/O请求执行完毕。
当使用一个线程向设备发出一个异步I/O请求时,该请求被传给设备驱动程序,设备驱动程序处理I/O请求时并不会等待I/O请求完成,而是将I/O请求加入到设备驱动程序的队列中,然后返回一个I/O处理中的信号。而实际的I/O操作则由设备驱动程序将I/O请求传给指定的硬件设备执行I/O操作。应用程序的线程并不需要挂起等待I/O请求的完成,从而可以继续执行其他任务。当某一时刻设备驱动程序完成了该I/O请求处理,设备控制器通过中断指令通知I/O请求完成,处理器则将通知I/O请求已完成。
I/O完成通知
在Windows中一共支持四种接收完成通知的方式。分别为触发设备内核对象、触发时间内核对象、可提醒I/O以及I/O完成端口。
触发设备内核
当设备驱动加载时会创建一个设备驱动对象,设备驱动程序还会为设备创建对应的设备对象。设备对象代表的是每一个物理设备或逻辑设备。设备对象描述了一个特定设备的状态信息,包括I/O请求的状态。在通过异步I/O将I/O请求添加到队列之前,会将设备内核对象设置为未触发,此时就可以使用该设备内核对象进行同步操作,当I/O请求完成后则会将设备内核对象设置为触发状态。使用设备内核对象进行线程同步时,无法区分当前完成通知的I/O是读操作还是写操作,因此无论是读还是写都会将其状态设置为触发状态。
事件内核对象
通过设备内核对象进行I/O通知由于无法区分读写操作,因此并没有什么用。通过事件内核对象我们可以将读写事件分离。在调用读写操作的时候会返回对应的读写事件内核对象。这样我们就可以等待对应的事件内核对象知道是什么I/O操作完成。我们可以通过等待多个事件内核对象,但是一次性最多只能等待64个事件内核对象,即一个线程最多只能创建64个事件内核对象进行等待。若需要监控上万个连接,则需要创建上百个线程进行监控。
可提醒I/O
在系统创建线程的时候会创建一个与线程相关的队列,该队列被称为异步调用(APC)队列,当发出一个I/O请求时,我们可以告诉设备驱动程序在调用线程的APC队列中添加一项完成函数,在I/O完成通知时调用完成函数进行回调。I/O完成通知最大的问题是,请求时哪个线程调用的,必须由哪个线程回调。它不支持负载均衡机制。
完成端口
I/O完成端口的设计理论依据是并发编程的线程数必须有一个上限,即最佳并发线程数为CPU的逻辑线程数。I/O完成端口充分的发挥了并发编程的优势的同时又避免了线程上下文切换带来的性能损失。
完成端口可能是最复杂的内核对现象,但是它又是Windows下性能最佳的I/O通知方式。
首先我们需要创建一个I/O完成端口,创建完成端口的时候可以指定线程数量。通过将设备与I/O完成端口进行关联。此使我们发出的I/O请求时,系统内核返回IO_PENDDING
状态,然后线程就可以继续处理其他事情。而DMA继续执行I/O操作,将数据从设备读取到设备控制器的缓冲区中,并对其进行必要的校验后,将数据通过系统总线传输到内存中。当数据传输完成后,DMA发出中断指令通知数据传输完毕,系统则会通过前面创建的I/O线程将I/O完成请求加入到I/O完成队列中。
然后我们通过调用Win32 API
就可以获取到对应的设备I/O完成请求通知,通知会将I/O完成请求从完成队列移除。
总结
- 同步I/O会阻塞线程,想要提高执行速度必须增加线程,但是会由于线程上下文切换造成性能损失。
- Windows下大约每15ms会进行一次线程调度。减少windows线程能降低内存占用(默认线程栈大小为1M),降低线程上下文切换造成的性能损失。
- Windows支持原生的异步I/O。异步I/O也可以称为重叠I/O。使用异步I/O时线程不会阻塞,系统底层将每个I/O请求生成I/O请求包(IRP)加入到设备驱动程序的请求队列中,然后直接返回
IO_PENDDING
状态表示请求受理成功,当底层设备完成了真实的I/O请求后会通过中断控制器通过中断操作通知CPU,CPU会调度一个线程通知上层设备驱动程序,将完成通知加入到完成队列中。此时上层应用即可获取到完成通知。 - 完成端口是windows下性能最佳的完成通知方式。它最大程度的减少线程上下文切换。
- 使用异步I/O和完成端口实现高性能I/O操作的主要原因有三点。一是减少I/O上下文切换;二是异步不阻塞线程,预先提供一个socket用于连接,而不是接受到时再创建socket(socket创建也是比较耗资源的);三是避免了内存复制。
- 如何减少线程,如何避免内存复制,如何提高线程利用率,避免线程阻塞。以上几点是所有高性能框架或高性能应用程序必备的条件。