键盘可以说是我们最常使用的输入硬件设备了但身为程序员的你你知道「键盘敲入A 字母时操作系统期间发生了什么吗」那要想知道这个发生的过程我们得先了解了解「操作系统是如何管理多种多样的输入输出设备」的等了解完这个后我们再来看看这个问题你就会发现问题已经被迎刃而解了。#设备控制器我们的电脑设备可以接非常多的输入输出设备比如键盘、鼠标、显示器、网卡、硬盘、打印机、音响等等每个设备的用法和功能都不同那操作系统是如何把这些输入输出设备统一管理的呢?为了屏蔽设备之间的差异每个设备都有一个叫设备控制器Device Control的组件比如硬盘有硬盘控制器、显示器有视频控制器等。因为这些控制器都很清楚的知道对应设备的用法和功能所以 CPU 是通过设备控制器来和设备打交道的。设备控制器里有芯片它可执行自己的逻辑也有自己的寄存器用来与 CPU 进行通信比如通过写入这些寄存器操作系统可以命令设备发送数据、接收数据、开启或关闭或者执行某些其他操作。通过读取这些寄存器操作系统可以了解设备的状态是否准备好接收一个新的命令等。实际上控制器是有三类寄存器它们分别是状态寄存器Status Register、命令寄存器Command Register以及数据寄存器Data Register如下图这三个寄存器的作用数据寄存器CPU 向 I/O 设备写入需要传输的数据比如要打印的内容是「Hello」CPU 就要先发送一个 H 字符给到对应的 I/O 设备。命令寄存器CPU 发送一个命令告诉 I/O 设备要进行输入/输出操作于是就会交给 I/O 设备去工作任务完成后会把状态寄存器里面的状态标记为完成。状态寄存器目的是告诉 CPU 现在已经在工作或工作已经完成如果已经在工作状态CPU 再发送数据或者命令过来都是没有用的直到前面的工作已经完成状态寄存标记成已完成CPU 才能发送下一个字符和命令。CPU 通过读写设备控制器中的寄存器控制设备这可比 CPU 直接控制输入输出设备要方便和标准很多。另外 输入输出设备可分为两大类 块设备Block Device和字符设备Character Device。块设备把数据存储在固定大小的块中每个块有自己的地址硬盘、USB 是常见的块设备。字符设备以字符为单位发送或接收一个字符流字符设备是不可寻址的也没有任何寻道操作鼠标是常见的字符设备。块设备通常传输的数据量会非常大于是控制器设立了一个可读写的数据缓冲区。CPU 写入数据到控制器的缓冲区时当缓冲区的数据囤够了一部分才会发给设备。CPU 从控制器的缓冲区读取数据时也需要缓冲区囤够了一部分才拷贝到内存。这样做是为了减少对设备的频繁操作。那 CPU 是如何与设备的控制寄存器和数据缓冲区进行通信的存在两个方法端口 I/O每个控制寄存器被分配一个 I/O 端口可以通过特殊的汇编指令操作这些寄存器比如in/out类似的指令。内存映射 I/O将所有控制寄存器映射到内存空间中这样就可以像读写内存一样读写数据缓冲区。#I/O 控制方式在前面我知道每种设备都有一个设备控制器控制器相当于一个小 CPU它可以自己处理一些事情但有个问题是当 CPU 给设备发送了一个指令让设备控制器去读设备的数据它读完的时候要怎么通知 CPU 呢控制器的寄存器一般会有状态标记位用来标识输入或输出操作是否完成。于是我们想到第一种轮询等待的方法让 CPU 一直查寄存器的状态直到状态标记为完成很明显这种方式非常的傻瓜它会占用 CPU 的全部时间。那我们就想到第二种方法 ——中断通知操作系统数据已经准备好了。我们一般会有一个硬件的中断控制器当设备完成任务后触发中断到中断控制器中断控制器就通知 CPU一个中断产生了CPU 需要停下当前手里的事情来处理中断。另外中断有两种一种软中断例如代码调用INT指令触发一种是硬件中断就是硬件通过中断控制器触发的。但中断的方式对于频繁读写数据的磁盘并不友好这样 CPU 容易经常被打断会占用 CPU 大量的时间。对于这一类设备的问题的解决方法是使用DMADirect Memory Access功能它可以使得设备在 CPU 不参与的情况下能够自行完成把设备 I/O 数据放入到内存。那要实现 DMA 功能要有 「DMA 控制器」硬件的支持。DMA 的工作方式如下CPU 需对 DMA 控制器下发指令告诉它想读取多少数据读完的数据放在内存的某个地方就可以了接下来DMA 控制器会向磁盘控制器发出指令通知它从磁盘读数据到其内部的缓冲区中接着磁盘控制器将缓冲区的数据传输到内存当磁盘控制器把数据传输到内存的操作完成后磁盘控制器在总线上发出一个确认成功的信号到 DMA 控制器DMA 控制器收到信号后DMA 控制器发中断通知 CPU 指令完成CPU 就可以直接用内存里面现成的数据了可以看到 CPU 当要读取磁盘数据的时候只需给 DMA 控制器发送指令然后返回去做其他事情当磁盘数据拷贝到内存后DMA 控制机器通过中断的方式告诉 CPU 数据已经准备好了可以从内存读数据了。仅仅在传送开始和结束时需要 CPU 干预。#设备驱动程序虽然设备控制器屏蔽了设备的众多细节但每种设备的控制器的寄存器、缓冲区等使用模式都是不同的所以为了屏蔽「设备控制器」的差异引入了设备驱动程序。设备控制器不属于操作系统范畴它是属于硬件而设备驱动程序属于操作系统的一部分操作系统的内核代码可以像本地调用代码一样使用设备驱动程序的接口而设备驱动程序是面向设备控制器的代码它发出操控设备控制器的指令后才可以操作设备控制器。不同的设备控制器虽然功能不同但是设备驱动程序会提供统一的接口给操作系统这样不同的设备驱动程序就可以以相同的方式接入操作系统。如下图前面提到了不少关于中断的事情设备完成了事情则会发送中断来通知操作系统。那操作系统就需要有一个地方来处理这个中断这个地方也就是在设备驱动程序里它会及时响应控制器发来的中断请求并根据这个中断的类型调用响应的中断处理程序进行处理。通常设备驱动程序初始化的时候要先注册一个该设备的中断处理函数。我们来看看中断处理程序的处理流程在 I/O 时设备控制器如果已经准备好数据则会通过中断控制器向 CPU 发送中断请求保护被中断进程的 CPU 上下文转入相应的设备中断处理函数进行中断处理恢复被中断进程的上下文#通用块层对于块设备为了减少不同块设备的差异带来的影响Linux 通过一个统一的通用块层来管理不同的块设备。通用块层是处于文件系统和磁盘驱动中间的一个块设备抽象层它主要有两个功能第一个功能向上为文件系统和应用程序提供访问块设备的标准接口向下把各种不同的磁盘设备抽象为统一的块设备并在内核层面提供一个框架来管理这些设备的驱动程序第二功能通用层还会给文件系统和应用程序发来的 I/O 请求排队接着会对队列重新排序、请求合并等方式也就是 I/O 调度主要目的是为了提高磁盘读写的效率。Linux 内存支持 5 种 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 软件分层前面说到了不少东西设备、设备控制器、驱动程序、通用块层现在再结合文件系统原理我们来看看 Linux 存储系统的 I/O 软件分层。可以把 Linux 存储系统的 I/O 由上到下可以分为三个层次分别是文件系统层、通用块层、设备层。他们整个的层次关系如下图这三个层次的作用是文件系统层包括虚拟文件系统和其他文件系统的具体实现它向上为应用程序统一提供了标准的文件访问接口向下会通过通用块层来存储和管理磁盘数据。通用块层包括块设备的 I/O 队列和 I/O 调度器它会对文件系统的 I/O 请求进行排队再通过 I/O 调度器选择一个 I/O 发给下一层的设备层。设备层包括硬件设备、设备控制器和驱动程序负责最终物理设备的 I/O 操作。有了文件系统接口之后不但可以通过文件系统的命令行操作设备也可以通过应用程序调用read、write函数就像读写文件一样操作设备所以说设备在 Linux 下也只是一个特殊的文件。但是除了读写操作还需要有检查特定于设备的功能和属性。于是需要ioctl接口它表示输入输出控制接口是用于配置和修改特定设备属性的通用接口。另外存储系统的 I/O 是整个系统最慢的一个环节所以 Linux 提供了不少缓存机制来提高 I/O 的效率。为了提高文件访问的效率会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制目的是为了减少对块设备的直接调用。为了提高块设备的访问效率 会使用缓冲区来缓存块设备的数据。#键盘敲入字母时期间发生了什么看完前面的内容相信你对输入输出设备的管理有了一定的认识那接下来就从操作系统的角度回答开头的问题「键盘敲入字母时操作系统期间发生了什么」我们先来看看 CPU 的硬件架构图CPU 里面的内存接口直接和系统总线通信然后系统总线再接入一个 I/O 桥接器这个 I/O 桥接器另一边接入了内存总线使得 CPU 和内存通信。再另一边又接入了一个 I/O 总线用来连接 I/O 设备比如键盘、显示器等。那当用户输入了键盘字符键盘控制器就会产生扫描码数据并将其缓冲在键盘控制器的寄存器中紧接着键盘控制器通过总线给 CPU 发送中断请求。CPU 收到中断请求后操作系统会保存被中断进程的 CPU 上下文然后调用键盘的中断处理程序。键盘的中断处理程序是在键盘驱动程序初始化时注册的那键盘中断处理函数的功能就是从键盘控制器的寄存器的缓冲区读取扫描码再根据扫描码找到用户在键盘输入的字符如果输入的字符是显示字符那就会把扫描码翻译成对应显示字符的 ASCII 码比如用户在键盘输入的是字母 A是显示字符于是就会把扫描码翻译成 A 字符的 ASCII 码。得到了显示字符的 ASCII 码后就会把 ASCII 码放到「读缓冲区队列」接下来就是要把显示字符显示屏幕了显示设备的驱动程序会定时从「读缓冲区队列」读取数据放到「写缓冲区队列」最后把「写缓冲区队列」的数据一个一个写入到显示设备的控制器的寄存器中的数据缓冲区最后将这些数据显示在屏幕里。显示出结果后恢复被中断进程的上下文。