写点什么

【计算机内功修炼】二:读取文件时,程序经历了什么

发布于: 2021 年 01 月 02 日
【计算机内功修炼】二:读取文件时,程序经历了什么

你有没有想过当我们执行 I/O 操作时计算机底层都发生了些什么?

在回答这个问题之前,我们先来看下为什么对于计算机来说 I/O 是极其重要的。

不能执行 I/O 的计算机是什么?

相信对于程序员来说 I/O 操作是最为熟悉不过的了:

当我们使用 C 语言中的 printf、C++中的"<<",Python 中的 print,Java 中的 System.out.println 等时,这是 I/O;当我们使用各种语言读写文件时,这也是 I/O;当我们通过 TCP/IP 进行网络通信时,这同样是 I/O;当我们使用鼠标龙飞凤舞时,当我们扛起键盘在评论区里指点江山亦或是埋头苦干努力制造 bug 时、当我们能看到屏幕上的漂亮的图形界面时等等,这一切都是 I/O。

想一想,如果没有 I/O 计算机该是一种多么枯燥的设备,不能看电影、不能玩游戏,也不能上网,这样的计算机最多就是一个大号的计算器。

既然 I/O 这么重要,那么到底什么才是 I/O 呢?


什么是 I/O

I/O 就是简单的数据 Copy,仅此而已。

这一点很重要,为了加深大家的印象,来,Everybody,Follow me,那边树上的朋友,还有那边墙上的朋友们,举起你们的双手,跟我唱,苍茫的天涯是。。。Sorry,I/O 仅仅就是数据 copy、I/O 仅仅就是数据 copy。

让我们先把演唱会的事情放在一边,既然是 copy 数据,又是从哪里 copy 到哪里呢?

如果数据是从外部设备 copy 到内存中,这就是 Input。

如果数据是从内存 copy 到外部设备,这就是 Output。

内存与外部设备之间不嫌麻烦的来回 copy 数据就是 Input and Output,简称 I/O(Input/Output),仅此而已。

img


I/O 与 CPU

现在我们知道了什么是 I/O,接下来就是重点部分了,大家注意,坐稳了。

我们知道现在的 CPU 其主频都是数 GHz 起步,这是什么意思呢?简单说就是 CPU 执行机器指令的速度是纳秒级别的,而通常的 I/O 比如磁盘操作,一次磁盘 seek 大概在毫秒级别,因此如果我们把 CPU 的速度比作战斗机的话,那么 I/O 操作的速度就是肯德鸡

img


也就是说当我们的程序跑起来时(CPU 执行机器指令),其速度是要远远快于 I/O 速度的,那么接下来的问题就是二者速度相差这么大,那么我们该如何设计、该如何更加合理的高效利用系统资源呢?

既然有速度差异,而且进程在执行完 I/O 操作前不能继续向前推进,那么显然只有一个办法,那就是等待,wait

同样是等待,有聪明的等待,也有傻傻的等待,简称傻等,那么是选择聪明的等待呢还是选择傻等呢?

假设你是一个急性子(CPU),需要等待一个重要的文件,不巧的是这个文件只能快递过来(I/O),那么这时你是选择什么事情都不干了,深情的注视着门口就像盼望着你的哈尼一样专心等待这个快递呢?还是暂时先不要管快递了,玩个游戏看个电影刷会儿短视频等快递来了再说呢?

很显然,更好的方法就是先去干其它事情,快递来了再说。

因此这里的关键点就是快递没到前手头上的事情可以先暂停,切换到其它任务,等快递过来了再切换回来

理解了这一点你就能明白执行 I/O 操作时底层都发生了什么。

接下来让我们以读取磁盘文件为例来讲解这一过程。

执行 I/O 时底层都发生了什么

在上一篇《一文彻底理解高并发高性能中的线程与线程池》中,我们引入了进程和线程的概念,在支持线程的操作系统中,实际上被调度的是线程而不是进程,为了更加清晰的理解 I/O 过程,我们暂时假设操作系统只有进程这样的概念,先不去考虑线程,这并不会影响我们的讨论。

现在内存中有两个进程,进程 A 和进程 B,当前进程 A 正在运行,如图所示:

img


进程 A 中有一段读取文件的代码,不管在什么语言中通常我们定义一个用来装数据的 buff,然后调用 read 之类的函数,像这样:

read(buff);

这就是一种典型的 I/O 操作,当 CPU 执行到这段代码的时候会向磁盘发送读取请求,注意与 CPU 执行指令的速度相比,I/O 操作操作是非常慢的,因此操作系统是不可能把宝贵的 CPU 计算资源浪费在无谓的等待上的,这时重点来了,注意接下来是重点哦。

由于外部设备执行 I/O 操作是相当慢的,因此在 I/O 操作完成之前进程是无法继续向前推进的,这就是所谓的阻塞,即通常所说的 block。操作系统检测到进程向 I/O 设备发起请求后就暂停进程的运行,怎么暂停运行呢?很简单,只需要记录下当前进程的运行状态并把 CPU 的 PC 寄存器指向其它进程的指令就可以了。

进程有暂停就会有继续执行,因此操作系统必须保存被暂停的进程以备后续继续执行,显然我们可以用队列来保存被暂停执行的进程,如图所示,进程 A 被暂停执行并被放到阻塞队列中(注意,不同的操作系统会有不同的实现,可能每个 I/O 设备都有一个对应的阻塞队列,但这种实现细节上的差异不影响我们的讨论)。

img


这时操作系统已经向磁盘发送了 I/O 请求,因此磁盘 driver 开始将磁盘中的数据 copy 到进程 A 的 buff 中,虽然这时进程 A 已经被暂停执行了,但这并不妨碍磁盘向内存中 copy 数据。注意,现代磁盘向内存 copy 数据时无需借助 CPU 的帮助,这就是所谓的 DMA(Direct Memory Access),这个过程如图所示:

img


让磁盘先 copy 着数据,我们接着聊。

实际上操作系统中除了有阻塞队列之外也有就绪队列,所谓就绪队列是指队列里的进程准备就绪可以被 CPU 执行了,你可能会问为什么不直接执行非要有个就绪队列呢?答案很简单,那就是僧多粥少,在即使只有 1 个核的机器上也可以创建出成千上万个进程,CPU 不可能同时执行这么多的进程,因此必然存在这样的进程,即使其一切准备就绪也不能被分配到计算资源,这样的进程就被放到了就绪队列。

现在进程 B 就位于就绪队列,万事俱备只欠 CPU,如图所示:

img


当进程 A 被暂停执行后 CPU 是不可以闲下来的,因为就绪队列中还有嗷嗷待哺的进程 B,这时操作系统开始在就绪队列中找下一个可以执行的进程,也就是这里的进程 B。

此时操作系统将进程 B 从就绪队列中取出,找出进程 B 被暂停时执行到的机器指令的位置,然后将 CPU 的 PC 寄存器指向该位置,这样进程 B 就开始运行啦,如图所示:

img


注意,注意,接下来的这段是重点中的重点。

注意观察上图,你能看出这种设计的精妙之处吗,这对于理解操作系统至关重要,关注公众号“码农的荒岛求生”回复“过程”二字你就能得到答案以及该过程的最后两个步骤啦。

零拷贝,Zero-copy

最后需要注意的一点就是上面的讲解中我们直接把磁盘数据 copy 到了进程空间中,但实际上一般情况下 I/O 数据是要首先 copy 到操作系统内部,然后操作系统再 copy 到进程空间中。因此我们可以看到这里其实还有一层经过操作系统的 copy,对于性能要求很高的场景其实也是可以绕过操作系统直接进行数据 copy 的,这也是本文描述的场景,这种绕过操作系统直接进行数据 copy 的技术被称为 Zero-copy,也就零拷贝,高并发、高性能场景下常用的一种技术,原理上很简单吧。

总结

本文讲解的是程序员常用的 I/O,一般来说作为程序员我们无需关心,但是理解 I/O 背后的底层原理对于设计高性能、高并发系统是极为有益的,希望这篇能对大家加深对 I/O 的认识有所帮助。


发布于: 2021 年 01 月 02 日阅读数: 25
用户头像

还未添加个人签名 2019.01.07 加入

公众号:码农的荒岛求生

评论

发布
暂无评论
【计算机内功修炼】二:读取文件时,程序经历了什么