程序员在开发一些功能时,经常要注意的便是“交互逻辑”,这儿的“交互”便是用户经过核算机界面(如键盘、鼠标)与核算机体系进行交流,以履行使命、获取信息或操控体系。或许说,交互也指核算设备的输入与输出(Input/Output, I/O)。在这篇文章中,咱们将会介绍I/O 的实现原理,了解I/O 与CPU,与程序的关联,还有一些高档的I/O 技能

1 CPU 是怎么处理I/O 操作的

就像CPU 内部有寄存器,键盘,鼠标内部也有自己的寄存器:设备寄存器

CPU 中的寄存器能够暂时存储从内存读取的数据或许存储CPU 核算的中心结果,而设备寄存器中寄存的则是一些和设备有关的信息,首要有两种寄存器:

  1. 寄存数据的寄存器:比方用户按下键盘,信息就会寄存在这类寄存器中。
  2. 寄存操控信息以及状况信息的寄存器,经过读写这类寄存器能够对设备进行操控或许检查设备状况

所以,其实设备在底层中,也便是一堆寄存器罢了,获取设备产生的数据或许对设备进行操控,都是经过读写寄存器来完结的

1.1 怎么读写设备寄存器

1.1.1 I/O 机器指令

咱们能够规划出特定的机器指令来专门读写设备寄存器,这类特别的指令便是I/O 指令,在这种计划下,设备会被赋予仅有的地址I/O 指令会指明设备的地址,这样CPU 宣布I/O 指令后,硬件电路就知道应该去读写哪个设备了。

这种计划很简略理解,可是咱们从CPU 的视点想想,在上一篇文章核算机底层5 缓存cache的最初,咱们介绍了冯诺伊曼结构,咱们发现,CPU 和内存是隔离开的,也便是说,内存和咱们这篇文章讲的设备寄存器,其实在CPU 看来,都是“外部设备”,咱们能不能像读写内存相同简略地读取设备寄存器

1.1.2 内存映射I/O

咱们能够这样,把内存地址空间的一部分分给内存,把别的一部分分给设备寄存器,由于CPU 只知道要从地址空间中的某个地址获取数据或许指令,至于这个地址来自谁,CPU 并不关怀。

这种把一部分地址空间分给设备,然后能够像读写内存相同操作设备的办法便是内存映射I/O。

计算机底层6 I/O

所以,一共有两种I/O 实现办法:

  1. 运用特定的I/O 机器指令
  2. 复用内存读写指令,把地址空间的一部分分配给设备

1.2 CPU 怎么获取当时设备的作业状况

1.2.1 轮询:一遍遍问

这是最简略想到的一种办法:不断地检测设备状况寄存器,看是否有状况改动,没有就持续检测

假如以键盘为例子,轮询就可能是这样的:

while(没人按键) {
    ...;
}
读取键盘寄存器信息

咱们学习了这么久的核算机底层常识,很简略就能看出这种办法的坏处:CPU 一直在循环中空跑,浪费CPU 资源。在本质上,轮询是一种同步的处理方法,把同步改为异步,是一种很常见的优化方法

1.2.2 中止处理

咱们在核算机底层4 CPU:6.3 中止与中止函数栈中就介绍过中止处理。

中止的本质是打断当时CPU 的履行流,跳转到具体的中止处理函数中,当中止处理函数履行完结后,再跳转回来。

有了中止机制,CPU 就能够不用一直在循环中空跑,浪费资源了,而是能够一直处理自己的事情,比及设备触发中止机制再去处理,处理完结后,再持续履行之前被中止的使命,而在之前被中止的程序看来,CPU 一直在履行自己的指令,就好像从来没中止过。

计算机底层6 I/O

那么,CPU 是怎么检测中止信号怎么保存并康复被中止程序的履行状况的呢?

  1. 中止恳求(IRQ): 外部设备(如键盘、鼠标等)或软件程序产生中止恳求信号。这一般是经过向 CPU 的中止操控器发送信号来完结的。
  2. 中止操控器: 中止操控器是一个硬件组件,一般负责办理多个中止源。它会将中止恳求信号传递给 CPU,以告知 CPU 有一个中止事情需求处理。
  3. 中止向量表: CPU 有一个中止向量表,其中包括与不同中止类型相关联的中止服务程序的地址。每个中止类型都有一个仅有的标识符,称为中止向量号
  4. 中止处理程序: 当 CPU 接收到中止恳求后,它会查找中止向量表,找到与中止恳求相关的中止向量号。然后,CPU会跳转到相应的中止处理程序的地址,开端履行与该中止相关的代码。
  5. 中止服务程序(ISR): ISR 是特定中止类型的代码段,它负责处理中止事情。ISR 能够保存当时进程的上下文,履行中止处理操作,然后在完结后复原上下文,以便持续履行原来的程序
  6. 中止结束: 一旦中止处理程序履行结束,CPU 会回到之前的程序履行点,持续履行原来的使命。

一起,中止处理的进程仍然是依靠这种数据结构完结的。

2 磁盘处理I/O时,CPU 在干什么

其实对于现代操作体系来说,其实磁盘处理I/O 是不需求CPU 参加的,在磁盘处理I/O恳求的这段时刻里,CPU 会被操作体系调度去履行其他有用的操作

假定CPU 开端履行线程A,履行一段时刻后,发起触及磁盘的I/O 恳求,磁盘比较于CPU 是十分慢的,因而线程A无法持续向前推动,此刻操作体系就把CPU 时刻分配给了线程B,这样线程B 开端运转,磁盘也在处理线程A 发起的I/O 恳求,也便是CPU 和磁盘都在做自己的事情,它们是独立,互不依靠的,当磁盘处理完I/O后,CPU 持续履行线程A

计算机底层6 I/O

那么为什么磁盘处理I/O 不需求CPU 参加呢?

2.1 设备操控器

设备操控器是核算机体系结构中的一种硬件,它负责办理和操控外部设备,以便与核算机体系进行通信和数据交换。每种类型的外部设备都需求一个相应的设备操控器来和谐其操作和与核算机体系的交互。

注意不要把设备操控器和设备驱动混杂,设备驱动是一段属于操作体系的代码,而设备操控器是一种硬件,意图是为了接收设备驱动的命令,来操控外部设备。

计算机底层6 I/O

能够说,设备操控器是一座桥梁,架设起了操作体系和外部设备,设备操控器越来越杂乱,意图便是为了解放CPU。

咱们说过,CPU 是核算机的核心,资源宝贵,那么CPU 应该亲自去把磁盘的数据仿制到内存中吗?清楚明了,数据的仿制,搬运,都是很简略的作业,不应该浪费宝贵的CPU 资源去做这些简略的事情,那么把设备操控器中的数据仿制到内存这些作业是由谁来完结呢?

答案是一种机制:直接存储器拜访 DMA

2.2 直接存储器拜访 DMA

如上面所说,DMA 的意图很简略:在不需求CPU 的状况下,直接在设备和内存之间传输数据。

现在咱们来简略看看DMA 的作业进程:CPU 虽然不需求直接去把数据从设备仿制到内存,但CPU 需求去下达指令告知DMA 该怎么去仿制数据,是把数据从内存写入设备还是把数据从设备读取给内存,读写多少数据,从哪里开端读取,这些数据都必须要CPU 告知DMA,尔后DMA 才干翻开作业。

DMA 明确了自己的目标后,开端进行总线仲裁,也便是恳求对总线的运用权,尔后开端操作设备。

计算机底层6 I/O

那么,CPU 怎么知道数据传输完了呢?答案还是中止机制:当DMA 完结数据传输时,再经过中止机制来告知CPU。

3 读取文件时,程序经历了什么

假定现在有一个单核CPU 体系,该体系中正在运转A 和B 两个进程,CPU 正在运转进程A,进程B 处在安排妥当行列中,这个时分,进程A 需求读取文件,这个时分,进程A 进入了I/O 堵塞行列,操作体系向磁盘发起了I/O 恳求,磁盘开端作业,DMA 把数据复制到某一块内存中,一起CPU 被调度去履行进程B,于是把进程B 从安排妥当行列中取出,CPU 开端运转进程B。

上面这个进程中,咱们发现,都是操作体系在调度,使得CPU 资源,磁盘都能得到充沛的运用。

尔后,磁盘完结I/O 读取,DMA 把数据都复制到了进程A 的内存里边,此刻,DMA 也向CPU 宣布中止信号,CPU 接收到中止信号后,去处理中止处理函数,一起进程B 又回到安排妥当行列中,发现数据复制结束,这个时分,操作体系就把进程A 从I/O 堵塞行列中取出,放入安排妥当行列中,这个时分,安排妥当行列中有两个使命A和B,操作体系需求决议到底是把CPU 分配给A 还是B。

在这儿就会出现两种状况,第一种是刚刚分配给进程B 的CPU 时刻片还没有用完,那么这个时分就应该让B 持续运转,A 持续在安排妥当行列中等候,当分配给B 的时刻片用完后,体系中的定时器宣布定时器中止信号,CPU 又跳转到中止处理函数,操作体系把进程B 又放到安排妥当行列,把进程A 取出,并分配CPU 给进程A,当然,这儿的进程B 被暂停运转仅仅由于操作体系分配给B 的时刻片用完了,而不是发起了堵塞式I/O 恳求而被暂停

而别的一种就很简略,由于操作体系分配给B 的CPU 时刻片用完了,那么B 直接进入安排妥当行列,CPU 履行进程A。

当然,不管是进程A 或许是进程B,它们都不会觉得自己被暂停过,进程对自己被暂停的事一窍不通,这便是操作体系的法力。

除此之外,在这篇文章中,咱们简略以为文件数据直接被复制到了进程的内存中,可是实际上,I/O数据首先要被复制到操作体系的内部,然后操作体系再将其复制到进程地址空间中,也便是还有一层操作体系的复制。

关于I/O 的理论常识咱们已经介绍的不少了,接下来咱们来认识一下两种比较高档的I/O 技能:I/O 多路复用mmap

4 I/O 多路复用

首先,“全部皆文件”,所有I/O 设备都能够被笼统成文件这个概念,磁盘,网络数据等等都能够被当作文件对待。

所有的I/O 操作也能够经过文件读写来实现,这一笼统能够让程序员运用一套接口就能操作所有外部设备,如用open来翻开文件,用read/write来读写文件,用seek来改动读写方位,用close来封闭文件,这便是文件这个概念的强壮之处。

那么咱们应该在哪里找到文件呢?

咱们需求凭借“文件描述符”:当咱们翻开文件时,内核会回来给咱们一个文件描述符,当进行文件操作时,咱们需求把该文件描述符告知内核,内核获取到文件描述符后,就能找到该描述符也便是一个数字所对应的文件信息而且完结文件操作

有了文件描述符,进程能够对文件一窍不通,比方文件是否储存在磁盘上,储存在磁盘的什么方位,当时读到了哪里,这些信息都由操作体系打理,程序员只需求针对文件描述符编程即可。

在介绍完文件描述符,咱们现在正式介绍I/O 多路复用

I/O 多路复用(I/O Multiplexing)是一种用于处理多个输入/输出操作的核算机编程技能。它答应一个程序在同一时刻内监听和处理多个输入或输出通道,而不需求为每个通道创立一个单独的线程或进程。 这能够提高程序的功能和功率。

具体来说,I/O 多路复用指的是这样一个进程:

  1. 咱们得到了一堆文件描述符,不管是与网络相关的,还是与文件相关的。
  2. 经过调用某个函数让内核去监督这一堆文件描述符当其中有能够进行的读写操作时,再回来
  3. 当该函数回来后,咱们就能够获取到具备读写条件的文件描述符,并对其进行相应的处理。

咱们也因而能够看到I/O 多路复用的一些特色和用途:

  1. 单线程处理多个通道: 经过 I/O 多路复用,一个单独的线程能够一起监听多个输入或输出通道,而不需求为每个通道创立一个线程。这能够削减线程创立和上下文切换的开支,然后提高程序功能。
  2. 非堵塞 I/O: I/O 多路复用一般与非堵塞 I/O 操作一起运用。非堵塞 I/O 答应程序在等候数据安排妥当时持续履行其他使命,而不会堵塞整个进程或线程。 这对于处理许多连接或客户端的服务器程序十分有用。
  3. 事情驱动编程: I/O 多路复用是事情驱动编程的一种基本技能。程序会等候特定事情的产生,然后根据事情类型履行相应的操作。 这种方法适用于网络编程、图形用户界面(GUI)应用程序等需求实时响应事情的场景。
  4. 高效网络编程: 在网络编程中,I/O 多路复用能够用于一起办理多个客户端连接服务器能够监听多个套接字,以确定哪个套接字已经准备好读取或写入数据。
  5. 节省资源: 相对于多线程或多进程模型,I/O 多路复用能够削减资源消耗,由于它运用一个线程来办理多个通道,而不是为每个通道分配一个线程或进程。

常见的 I/O 多路复用的体系调用包括 selectpollepoll(在Linux体系中)等,它们在不同的操作体系上有不同的实现。

5 mmap:像读写内存相同操作文件

对于程序员来说,读写内存是一件很自然的事情,可是读写文件就要杂乱得多。

咱们读写内存:

int arr[10];
arr[0] = 2;

如此简略,甚至没有意识到自己在读写内存。

可是当咱们读取文件时:

char buf[1024];
int fd = open("/filepath/abc.txt");
read(fd, buf, 1024);

这便是咱们上面讲的内容,经过read告知操作体系,要开端读取abc.txt 文件了,把信息准备好后,传给咱们文件描述符,有了文件描述符,咱们就能够知道这个文件的全部信息。

咱们也能够直观地看到,操作文件要比操作内存杂乱,根本原因在于磁盘寻址方法与内存不同(内存的寻址粒度是字节,而磁盘寻址则是“块”),以及CPU 和外部设备之间的速度差异

所以,咱们能不能像读写内存那样去读写磁盘文件呢?

还记得虚拟内存的概念吗?每个进程都以为自己独占一整份内存,那么文件也能够让运用者以为其保存在一段连续的磁盘空间中,这样的话,咱们就能够把这段空间映射到进程的地址空间中

这便是mmap

mmap(Memory-Mapped Files)是一种在Unix和类Unix操作体系中的内存映射文件的体系调用,它答应程序将文件映射到内存中的一段地址空间,然后使文件的内容能够直接在内存中拜访,而不需求经过规范的文件读写操作

计算机底层6 I/O

这样,咱们在读这段映射的内存地址空间的时分,实际上便是在操作文件,咱们就能够像操作内存相同操作文件了

这全部还是操作体系的劳绩。当咱们读取到这段映射的地址空间时,可能会由于与之对应的文件没有加载到内存中而出现缺页中止,这个时分,CPU 去处理中止函数,这个时分会发起实在的磁盘I/O 恳求,将文件读取到内存而且建立好虚拟内存到物理内存之间的关联,尔后,程序就能够像读写内存相同读写磁盘文件了

当咱们进行写操作的时分,咱们仍然能够直接修正这块内存,操作体系会在背面将修正的内容写回磁盘

由此咱们能够知道,即便有了mmap,咱们仍然需求实在地读写磁盘,只不过这个进程由操作体系完结,咱们看起来能够像读写普通内存那样直接读写磁盘文件。

那么,mmap 和传统读写操作(read/write)比较,哪个更好呢?

咱们常用的规范读写操作,比方read/write,其底层触及体系调用,运用时,需求先把数据从内核态复制到用户态,写数据的数据也需求从用户态复制到内核态,这些复制都是有开支的。

计算机底层6 I/O

mmap则没有这个问题,mmap在读写磁盘的时分,不会引起体系调用和数据复制,可是,内核中也需求有特定的数据结构来保护进程地址空间与文件的映射联系,这也是有功能开支的。一起还有缺页中止带来的开支。

因而,咱们不能必定mmap在功能上就比传统读写操作要好,或许说,谈到功能,单纯的理论分析有时分并不好用,需求基于实在的场景用分析东西进行测试才干做出判别。

可是在大文件处理的场景下,这儿的大文件指的是巨细超越物理内存的文件,假如咱们运用传统读写操作(read/write),那么咱们必须一块一块把文件搬到内存,处理完一部分再去处理别的一份,假如不慎恳求过多内存,那么还可能会引起OOM killer(因内存不足而杀死一部分进程)。

但假如运用mmap,咱们凭借虚拟内存只要咱们的进程地址空间足够大,就能够直接把整个大文件映射到进程地址空间中,即便该文件巨细超越物理内存也没有问题,操作体系并不关怀,对映射区域的修正将直接写入磁盘文件。

mmap 的妙用:动态链接库

在核算机底层1 怎么从编程语言一步步到可履行程序中咱们介绍过动态库,不管有多少程序依靠次动态库,可履行程序自身都不会包括该库的代码,不管多少进程都用到了这份动态库,在磁盘中也只要一份,这个时分,咱们也能够用mmap将其直接映射到各个依靠该库的进程地址空间中。

计算机底层6 I/O

这样虽然每个进程都以为自己的地址空间加载了这个库,可是实际上在物理内存中,这个库只要一份。

类似的,假如咱们有许多进程都以只读的方法依靠同一份数据,那么咱们能够运用mmap

6 总结

这是核算机底层体系的最后一篇文章,首要讲了I/O,咱们介绍了CPU 是怎么处理I/O 恳求的,一起,为了充沛运用核算机体系中速度悬殊的硬件资源,操作体系调度设备操控器,DMA,运用中止机制,最大极限地运用资源,提高功率,还有两种高档的I/O 技能:I/O 多路复用,mmap 它们能够提高程序功能像操作内存相同操作磁盘文件。它们的背面都离不开巨大的操作体系。

7 过往文章

核算机底层1 怎么从编程语言一步步到可履行程序

核算机底层2 程序在运转时产生了什么

核算机底层3 内存

核算机底层4 CPU

核算机底层5 缓存cache

8 参考资料

  • 陆小风.核算机底层的隐秘. 电子工业出版社, 2023.
  • 核算机底层的隐秘 gitbook