Skip to content

1. 设备管理

1.1 设备控制器

操作系统为了统一管理输入输出设备,为了屏蔽设备之间的差异,每个设备都有一个叫设备控制器(Device Control) 的组件,比如硬盘有硬盘控制器、显示器有视频控制器等。设备控制器位于每个外部设备的内部。

CPU 是通过设备控制器来和设备打交道的。

设备控制器里有芯片,它可执行自己的逻辑,也有自己的寄存器,用来与 CPU 进行通信,比如:

  • 通过写入这些寄存器,操作系统可以命令设备发送数据、接收数据、开启或关闭,或者执行某些其他操作。
  • 通过读取这些寄存器,操作系统可以了解设备的状态,是否准备好接收一个新的命令等。

控制器中的寄存器分为三类,分别是状态寄存器(Status Register)命令寄存器(Command Register)以及数据寄存器(Data Register)

  • 数据寄存器,CPU向 I/O 设备写入需要传输的数据。
  • 命令寄存器,CPU 向I/O设备发送命令,I/O设备工作完成之后,把状态寄存器里面的状态标记为完成。
  • 状态寄存器,指示CPU现在的工作是否已完成。如果已经处于工作状态,CPU再发送数据或命令过来都是没用的。

输入输出设备可分为两大类:

  • 块设备,把数据存储在固定大小的块中,每个块都有自己的地址。比如磁盘、USB
  • 字符设备,以字符为单位发送或接收一个字符流,字符设备不可寻址,也没有寻道操作。比如鼠标

块设备通常传输的数据量会非常大,于是控制器设立了一个可读写的数据缓冲区。是为了减少对设备的频繁操作:

  • CPU 写入数据到控制器的缓冲区时,当缓冲区的数据囤够了一部分,才会发给设备。
  • CPU 从控制器的缓冲区读取数据时,也需要缓冲区囤够了一部分,才拷贝到内存。

CPU 与设备的控制寄存器和数据缓冲区进行通信的两个方法:

  • 端口I/O,每个控制寄存器被分配一个I/O端口,可以通过特殊的汇编指令操作这些寄存器,比如in/out类似的指令。
  • 内存映射I/O,将所有控制器映射到内存空间中,这样就可以像读写内存一样读写数据缓冲区。

1.2 DMA

本小节是简略介绍,更加详细的说明参考《网络系统》-> 零拷贝 -> DMA 小节。

当 CPU 给设备发送了一个指令,让设备控制器去读设备的数据,它读完的时候,要怎么通知 CPU 呢?

第一种轮询等待的方法,让 CPU 一直查寄存器的状态,直到状态标记为完成,很明显,这种方式非常的傻瓜,它会占用 CPU 的全部时间。

第二种方法 —— 中断,通知操作系统数据已经准备好了。中断有两种,一种软中断,例如代码调用 INT 指令触发,一种是硬件中断,就是硬件通过中断控制器触发的。

但中断的方式对于频繁读写数据的磁盘,并不友好,这样 CPU 容易经常被打断,会占用 CPU 大量的时间。对于这一类设备的问题的解决方法是使用 DMA(Direct Memory Access) 功能,它可以使得设备在 CPU 不参与的情况下,能够自行完成把设备 I/O 数据放入到内存。那要实现 DMA 功能要有 DMA 控制器硬件的支持。

工作方式如下:

  1. CPU对DMA控制器下发指令,告诉它想要读取多少数据,读完的数据放在内存的某个地方
  2. DMA控制器会向磁盘控制器发送指令,通知它从磁盘读数据到其内部的数据缓冲区,接着磁盘控制器将数据缓冲区的数据传输到内存
  3. 传输到内存的操作完成后,磁盘控制器在总线上发出一个确认成功的信号到DMA控制器
  4. DMA控制器接收到信号后,发出中断通知CPU指令完成,CPU就可以用内存里面的数据了

可以看到, CPU 当要读取磁盘数据的时候,只需给 DMA 控制器发送指令,然后返回去做其他事情,当磁盘数据拷贝到内存后,DMA 控制机器通过中断的方式,告诉 CPU 数据已经准备好了,可以从内存读数据了。仅仅在传送开始和结束时需要 CPU 干预

【ChatGPT】 在传统的I/O操作中,数据必须通过CPU来完成输入或输出,这会占用CPU的处理时间和资源,限制了数据传输的效率。而DMA技术的出现解决了这一问题,它通过配置和控制专用的DMA控制器,让外部设备能够直接读取或写入内存中的数据。

DMA工作的基本流程如下:

  1. CPU与DMA控制器进行初始化和配置,设置需要进行数据传输的外设、内存地址等参数。
  2. 外设触发DMA请求,向DMA控制器发送请求信号。
  3. DMA控制器接收到外设的请求信号后,根据配置的参数自动启动数据传输操作。
  4. DMA控制器在数据传输过程中,直接将数据从外设读取或写入内存,无需CPU介入。
  5. 数据传输完成后,DMA控制器会向外设发送传输完成的信号。

相较于直接使用CPU进行数据传输,使用DMA的主要优势在于减少了CPU的负担和时间开销。当CPU直接处理数据传输时,它需要执行以下操作:

  • 等待:CPU需要等待输入/输出设备的数据就绪,从设备读取数据或将数据写入设备。这种等待占用了CPU的时间,并且降低了CPU的处理能力。
  • 数据复制:CPU在数据传输过程中需要将数据从输入/输出设备的寄存器复制到内存中,或者将数据从内存复制到输出设备的寄存器中。这涉及到多次数据复制操作,占用了CPU的计算资源。

1.3 设备驱动程序

虽然设备控制器屏蔽了设备的众多细节,但每种设备的控制器的寄存器、缓冲区等使用模式都是不同的,所以为了屏蔽设备控制器(硬件)的差异,引入了设备驱动程序(软件)

设备驱动程序属于操作系统的一部分,操作系统的内核代码可以像本地调用代码一样使用设备驱动程序的接口,而设备驱动程序是面向设备控制器的代码,它发出操控设备控制器的指令后,才可以操作设备控制器。

不同的设备控制器虽然功能不同,但是设备驱动程序会提供统一的接口给操作系统,这样不同的设备驱动程序,就可以以相同的方式接入操作系统。

通常,设备驱动程序初始化的时候,要先注册一个该设备的中断处理函数。设备驱动程序会及时响应控制器发来的中断请求,并根据这个中断的类型调用响应的中断处理程序进行处理。

中断处理程序的处理流程:

  1. 在 I/O 时,设备控制器如果已经准备好数据,则会通过中断控制器向 CPU 发送中断请求;
  2. 保护被中断进程的 CPU 上下文;
  3. 转入相应的设备中断处理函数;
  4. 进行中断处理;
  5. 恢复被中断进程的上下文;

1.4 通用块层

通用块层是处于文件系统和磁盘驱动中间的一个块设备抽象层,它主要有两个功能:

  • 第一个功能,向上为文件系统和应用程序,提供访问块设备的标准接口,向下把各种不同的磁盘设备抽象为统一的块设备,并在内核层面,提供一个框架来管理这些设备的驱动程序
  • 第二个功能,通用块层还会给文件系统和应用程序发来的 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 压力比较大的场景,比如数据库等。

1.5 存储系统的 I/O 层次

可以把 Linux 存储系统的 I/O 由上到下可以分为三个层次,分别是文件系统层、通用块层、设备层。他们整个的层次关系如下图:

  • 文件系统层,包括虚拟文件系统和其他文件系统的具体实现,向上为应用提供了标准的文件访问接口,向下通过通用块层来存储和管理磁盘数据
  • 通用块层,包括设备的I/O队列和I/O调度器,它会对文件系统的I/O请求进行排队,再通过I/O调度器,选择一个I/O发给下一层的设备层
  • 设备层,包括硬件设备、设备控制器和设备驱动程序,负责最终物理设备的I/O操作

存储系统的 I/O 是整个系统最慢的一个环节,所以 Linux 提供了不少缓存机制来提高 I/O 的效率。

  • 为了提高文件访问的效率,会使用页缓存(Page cache)、索引节点缓存、目录项缓存(Dentry cache)等多种缓存机制,目的是为了减少对块设备的直接调用。
  • 为了提高块设备的访问效率,会使用缓冲区(Buffer Cache),来缓存块设备的数据。

1.6 键盘敲入字母时,期间发生了什么

CPU 的硬件架构图:

CPU 里面的内存接口,直接和系统总线通信,然后系统总线再接入一个 I/O 桥接器,这个 I/O 桥接器,另一边接入了内存总线,使得 CPU 和内存通信。再另一边,又接入了一个 I/O 总线,用来连接 I/O 设备,比如键盘、显示器等。

那当用户输入了键盘字符,键盘控制器就会产生扫描码数据,并将其缓冲在键盘控制器的寄存器中,紧接着键盘控制器通过总线给 CPU 发送中断请求。

CPU 收到中断请求后,操作系统会保存被中断进程的 CPU 上下文,然后调用键盘的中断处理程序

键盘的中断处理程序是在键盘驱动程序初始化时注册的,那键盘中断处理函数的功能就是从键盘控制器的寄存器的缓冲区读取扫描码,再根据扫描码找到用户在键盘输入的字符,如果输入的字符是显示字符,那就会把扫描码翻译成对应显示字符的 ASCII 码,比如用户在键盘输入的是字母 A,是显示字符,于是就会把扫描码翻译成 A 字符的 ASCII 码。

得到了显示字符的 ASCII 码后,就会把 ASCII 码放到「读缓冲区队列」,接下来就是要把显示字符显示屏幕了,显示设备的驱动程序会定时从「读缓冲区队列」读取数据放到「写缓冲区队列」,最后把「写缓冲区队列」的数据一个一个写入到显示设备的控制器的寄存器中的数据缓冲区,最后将这些数据显示在屏幕里。

显示出结果后,恢复被中断进程的上下文