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

本文原题“读取文件时,程序经历了什么?”,本次发布时有少量改动。php

一、系列文章引言

1.1 文章目的

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

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

1.2 文章源起

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

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

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

1.3 文章目录

1.4 本篇概述

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

二、本文做者

应做者要求,不提供真名,也不提供我的照片。服务器

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

三、不能执行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呢?

四、什么是I/O?

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

这一点很重要!

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

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

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

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

五、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操做时底层都发生了什么。

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

六、执行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,在这里咱们暂时先不考虑这种方式。

在本节开头咱们说过暂时只考虑进程而不考虑线程,如今咱们放宽这个条件,实际上也很是简单,只须要把前图中调度的进程改成线程就能够了,这里的讨论对于线程同样成立。

七、零拷贝(Zero-copy)

最后须要注意的一点就是:上面的讲解中咱们直接把磁盘数据copy到了进程空间中,但实际上通常状况下I/O数据是要首先copy到操做系统内部,而后操做系统再copy到进程空间中。

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

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

八、本文小结

本文讲解的是程序员经常使用的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模型

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

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

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

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