写点什么

从根上理解高性能、高并发 (二):深入操作系统,理解 I/O 与零拷贝技术

用户头像
JackJiang
关注
发布于: 2020 年 12 月 28 日
从根上理解高性能、高并发(二):深入操作系统,理解I/O与零拷贝技术

1、系列文章引言

1.1 文章目的

作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll 等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java 的Netty、Php 的workman、Go 的nget等熟练掌握。但真正到了面视或者技术实践过程中遇到无法释怀的疑惑时,方知自已所掌握的不过是皮毛。

返璞归真、回归本质,这些技术特征背后的底层原理到底是什么?如何能通俗易懂、毫不费力真正透彻理解这些技术背后的原理,正是《从根上理解高性能、高并发》系列文章所要分享的。

1.2 文章源起

我整理了相当多有关 IM、消息推送等即时通讯技术相关的资源和文章,从最开始的开源 IM 框架MobileIMSDK,到网络编程经典巨著《TCP/IP详解》的在线版本,再到 IM 开发纲领性文章《新手入门一篇就够:从零开发移动端IM》,以及网络编程由浅到深的《网络编程懒人入门》、《脑残式网络编程入门》、《高性能网络编程》、《不为人知的网络编程》系列文章。

越往知识的深处走,越觉得对即时通讯技术了解的太少。于是后来,为了让开发者门更好地从基础电信技术的角度理解网络(尤其移动网络)特性,我跨专业收集整理了《IM开发者的零基础通信技术入门》系列高阶文章。这系列文章已然是普通即时通讯开发者的网络通信技术知识边界,加上之前这些网络编程资料,解决网络通信方面的知识盲点基本够用了。

对于即时通讯 IM 这种系统的开发来说,网络通信知识确实非常重要,但回归到技术本质,实现网络通信本身的这些技术特征:包括上面提到的线程池、零拷贝、多路复用、事件驱动等等,它们的本质是什么?底层原理又是怎样?这就是整理本系列文章的目的,希望对你有用。

1.3 文章目录

1.4 本篇概述

接上篇《深入计算机底层,理解线程与线程池》,本篇是高性能、高并发系列的第 2 篇文章,在这里我们来到了 I/O 这一话题。你有没有想过,当我们执行文件 I/O、网络 I/O 操作时计算机底层到底发生了些什么?对于计算机来说 I/O 是极其重要的,本篇将带给你这个问的答案。

2、本文作者

应作者要求,不提供真名,也不提供个人照片。

本文作者主要技术方向为互联网后端、高并发高性能服务器、检索引擎技术,网名是“码农的荒岛求生”,公众号“码农的荒岛求生”。感谢作者的无私分享。

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

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

  • 1)当我们使用 C 语言中的 printf、C++中的"<<",Python 中的 print,Java 中的 System.out.println 等时;

  • 2)当我们使用各种语言读写文件时;

  • 3)当我们通过 TCP/IP 进行网络通信时;

  • 4)当我们使用鼠标龙飞凤舞时;

  • 5)当我们拿起键盘在评论区指点江山亦或是埋头苦干努力制造 bug 时;

  • 6)当我们能看到屏幕上的漂亮的图形界面时等等。

以上这一切,都是 I/O!

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

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

4、什么是 I/O?

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

这一点很重要!

既然是 copy 数据,那么又是从哪里 copy 到哪里呢?

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

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

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

5、I/O 与 CPU

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

我们知道现在的 CPU 其主频都是数 GHz 起步,这是什么意思呢?

简单说就是:CPU 执行机器指令的速度是纳秒级别的,而通常的 I/O 比如磁盘操作,一次磁盘 seek 大概在毫秒级别,因此如果我们把 CPU 的速度比作战斗机的话,那么 I/O 操作的速度就是肯德鸡。

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

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

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

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

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

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

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

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

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

在上一篇《深入计算机底层,理解线程与线程池》中,我们引入了进程和线程的概念。

在支持线程的操作系统中,实际上被调度的是线程而不是进程,为了更加清晰的理解 I/O 过程,我们暂时假设操作系统只有进程这样的概念,先不去考虑线程,这并不会影响我们的讨论。

现在内存中有两个进程,进程 A 和进程 B,当前进程 A 正在运行。

如下图所示:

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

就像这样:

read(buff);

这就是一种典型的 I/O 操作,当 CPU 执行到这段代码的时候会向磁盘发送读取请求。

注意:与 CPU 执行指令的速度相比,I/O 操作操作是非常慢的,因此操作系统是不可能把宝贵的 CPU 计算资源浪费在无谓的等待上的,这时重点来了,注意接下来是重点哦。

由于外部设备执行 I/O 操作是相当慢的,因此在 I/O 操作完成之前进程是无法继续向前推进的,这就是所谓的阻塞,即通常所说的 block。

操作系统检测到进程向 I/O 设备发起请求后就暂停进程的运行,怎么暂停运行呢?很简单:只需要记录下当前进程的运行状态并把 CPU 的 PC 寄存器指向其它进程的指令就可以了。

进程有暂停就会有继续执行,因此操作系统必须保存被暂停的进程以备后续继续执行,显然我们可以用队列来保存被暂停执行的进程。

如下图所示,进程 A 被暂停执行并被放到阻塞队列中(注意:不同的操作系统会有不同的实现,可能每个 I/O 设备都有一个对应的阻塞队列,但这种实现细节上的差异不影响我们的讨论)。

这时操作系统已经向磁盘发送了 I/O 请求,因此磁盘 driver 开始将磁盘中的数据 copy 到进程 A 的 buff 中。虽然这时进程 A 已经被暂停执行了,但这并不妨碍磁盘向内存中 copy 数据。

注意:现代磁盘向内存 copy 数据时无需借助 CPU 的帮助,这就是所谓的 DMA(Direct Memory Access)。

这个过程如下图所示:

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

实际上:操作系统中除了有阻塞队列之外也有就绪队列,所谓就绪队列是指队列里的进程准备就绪可以被 CPU 执行了。

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

现在进程 B 就位于就绪队列,万事俱备只欠 CPU。

如下图所示:

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

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

如下图所示:

注意:接下来的这段是重点中的重点!

注意观察上图:此时进程 B 在被 CPU 执行,磁盘在向进程 A 的内存空间中 copy 数据,看出来了吗——大家都在忙,谁都没有闲着,数据 copy 和指令执行在同时进行,在操作系统的调度下,CPU、磁盘都得到了充分的利用,这就是程序员的智慧所在。

现在你应该理解为什么操作系统这么重要了吧。

此后磁盘终于将全部数据都 copy 到了进程 A 的内存中,这时磁盘通知操作系统任务完成啦,你可能会问怎么通知呢?这就是中断。

操作系统接收到磁盘中断后发现数据 copy 完毕,进程 A 重新获得继续运行的资格,这时操作系统小心翼翼的把进程 A 从阻塞队列放到了就绪队列当中。

如下图所示:

注意:从前面关于就绪状态的讨论中我们知道,操作系统是不会直接运行进程 A 的,进程 A 必须被放到就绪队列中等待,这样对大家都公平。

此后进程 B 继续执行,进程 A 继续等待,进程 B 执行了一会儿后操作系统认为进程 B 执行的时间够长了,因此把进程 B 放到就绪队列,把进程 A 取出并继续执行。

注意:操作系统把进程 B 放到的是就绪队列,因此进程 B 被暂停运行仅仅是因为时间片到了而不是因为发起 I/O 请求被阻塞。

如下图所示:

进程 A 继续执行,此时 buff 中已经装满了想要的数据,进程 A 就这样愉快的运行下去了,就好像从来没有被暂停过一样,进程对于自己被暂停一事一无所知,这就是操作系统的魔法。

现在你应该明白了 I/O 是一个怎样的过程了吧。

这种进程执行 I/O 操作被阻塞暂停执行的方式被称为阻塞式 I/O,blocking I/O,这也是最常见最容易理解的 I/O 方式,有阻塞式 I/O 就有非阻塞式 I/O,在这里我们暂时先不考虑这种方式。

在本节开头我们说过暂时只考虑进程而不考虑线程,现在我们放宽这个条件,实际上也非常简单,只需要把前图中调度的进程改为线程就可以了,这里的讨论对于线程一样成立。

7、零拷贝(Zero-copy)

最后需要注意的一点就是:上面的讲解中我们直接把磁盘数据 copy 到了进程空间中,但实际上一般情况下 I/O 数据是要首先 copy 到操作系统内部,然后操作系统再 copy 到进程空间中。

因此我们可以看到这里其实还有一层经过操作系统的 copy,对于性能要求很高的场景其实也是可以绕过操作系统直接进行数据 copy 的,这也是本文描述的场景,这种绕过操作系统直接进行数据 copy 的技术被称为 Zero-copy,也就零拷贝,高并发、高性能场景下常用的一种技术,原理上很简单吧。

PS:对于搞即时通讯开发的 Java 程序员来说,著名的高性能网络框架 Netty 就使用了零拷贝技术,具体可以读《NIO框架详解:Netty的高性能之道》一文的第 12 节。如果对于 Netty 框架很好奇但不了解的话,可以因着这两篇文章入门:《新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析》、《史上最通俗Netty入门长文:基本介绍、环境搭建、动手实战》。

8、本文小结

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

接下来的一篇《从根上理解高性能、高并发(三):深入操作系统,彻底理解 I/O 多路复用》将要分享的是 I/O 技术的一大突破,正是因为它,才彻底解决了高并发网络通信中的 C10K 问题(见《高性能网络编程(二):上一个10年,著名的C10K并发连接问题》),敬请期待!

附录:更多高性能、高并发文章精选

高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少

高性能网络编程(二):上一个10年,著名的C10K并发连接问题

高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了

高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索

高性能网络编程(五):一文读懂高性能网络编程中的I/O模型

高性能网络编程(六):一文读懂高性能网络编程中的线程模型

高性能网络编程(七):到底什么是高并发?一文即懂!

以网游服务端的网络接入层设计为例,理解实时通信的技术挑战

知乎技术分享:知乎千万级并发的高性能长连接网关技术实践

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

一套原创分布式即时通讯(IM)系统理论架构方案

微信后台基于时间序的海量数据冷热分级架构设计实践

微信技术总监谈架构:微信之道——大道至简(演讲全文)

如何解读《微信技术总监谈架构:微信之道——大道至简》

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

17年的实践:腾讯海量产品的技术方法论

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

以微博类应用场景为例,总结海量社交系统的架构设计步骤

新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践

从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3280-1-1.html


用户头像

JackJiang

关注

还未添加个人签名 2019.08.26 加入

开源IM框架MobileIMSDK、BeautyEye的作者。

评论

发布
暂无评论
从根上理解高性能、高并发(二):深入操作系统,理解I/O与零拷贝技术