从根上理解高性能、高并发(三):深刻操做系统,完全理解I/O多路复用

一、系列文章引言

1.1 文章目的

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

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

1.2 文章源起

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

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

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

1.3 文章目录

从根上理解高性能、高并发(一):深刻计算机底层,理解线程与线程池
从根上理解高性能、高并发(二):深刻操做系统,理解I/O与零拷贝技术
从根上理解高性能、高并发(三):深刻操做系统,完全理解I/O多路复用》(* 本文)
《从根上理解高性能、高并发(四):深刻操做系统,完全理解同步与异步 (稍后发布..)》
《从根上理解高性能、高并发(五):高并发高性能服务器究竟是如何实现的 (稍后发布..)》

1.4 本篇概述

接上篇《深刻操做系统,理解I/O与零拷贝技术》,本篇是高性能、高并发系列的第3篇文章,上篇里咱们讲到了I/O技术,本篇将以更具象的文件这个话题入手,带你一步步理解高性能、高并发服务端编程时没法回避的I/O多路复用及相关技术。编程

本文已同步发布于“即时通信技术圈”公众号,欢迎关注。公众号上的连接是:点此进入后端

二、本文做者

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

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

三、什么是文件?

在正式展开本文的内容以前,咱们须要先预习一下文件以及文件描述符的概念。网络

程序员使用I/O最终都逃不过文件这个概念。

在Linux世界中文件是一个很简单的概念,做为程序员咱们只须要将其理解为一个N byte的序列就能够了:

b1, b2, b3, b4, ....... bN

实际上全部的I/O设备都被抽象为了文件这个概念,一切皆文件(Everything is File),磁盘、网络数据、终端,甚至进程间通讯工具管道pipe等都被当作文件对待。

全部的I/O操做也均可以经过文件读写来实现,这一很是优雅的抽象可让程序员使用一套接口就能对全部外设I/O操做。

经常使用的I/O操做接口通常有如下几类:

  • 1)打开文件,open;
  • 2)改变读写位置,seek;
  • 3)文件读写,read、write;
  • 4)关闭文件,close。

程序员经过这几个接口几乎能够实现全部I/O操做,这就是文件这个概念的强大之处。

四、什么是文件描述符?

在上一篇《深刻操做系统,理解I/O与零拷贝技术》中咱们讲到:要想进行I/O读操做,像磁盘数据,咱们须要指定一个buff用来装入数据。

通常都是这样写的:

read(buff);

可是这里咱们忽略了一个关键问题:那就是,虽然咱们指定了往哪里写数据,可是咱们该从哪里读数据呢?

从上一节中咱们知道,经过文件这个概念咱们能实现几乎全部I/O操做,所以这里少的一个主角就是文件。

那么咱们通常都怎样使用文件呢?

举个例子:若是周末你去比较火的餐厅吃饭应该会有体会,通常周末人气高的餐厅都会排队,而后服务员会给你一个排队序号,经过这个序号服务员就能找到你,这里的好处就是服务员无需记住你是谁、你的名字是什么、来自哪里、喜爱是什么、是否是保护环境爱护小动物等等,这里的关键点就是:服务员对你一无所知,但依然能够经过一个号码就能找到你。

一样的:在Linux世界要想使用文件,咱们也须要借助一个号码,根据“弄不懂原则”,这个号码就被称为了文件描述符(file descriptors),在Linux世界中鼎鼎大名,其道理和上面那个排队号码同样。

所以:文件描述仅仅就是一个数字而已,可是经过这个数字咱们能够操做一个打开的文件,这一点要记住。

有了文件描述符,进程能够对文件一无所知,好比文件在磁盘的什么位置、加载到内存中又是怎样管理的等等,这些信息通通交由操做系统打理,进程无需关心,操做系统只须要给进程一个文件描述符就足够了。

所以咱们来完善上述程序:

int fd = open(file_name); // 获取文件描述符
read(fd, buff);

怎么样,是否是很是简单。

五、文件描述符太多了怎么办?

通过了这么多的铺垫,终于要到高性能、高并发这一主题了。

从前几节咱们知道,全部I/O操做均可以经过文件样的概念来进行,这固然包括网络通讯。

若是你有一个IM服务器,当三次握手建议长链接成功之后,咱们会调用accept来获取一个连接,调用该函数咱们一样会获得一个文件描述符,经过这个文件描述符就能够处理客户端发送的聊天消息而且把消息转发给接收者。

也就是说,经过这个描述符咱们就能够和客户端进行通讯了:

// 经过accept获取客户端的文件描述符
int conn_fd = accept(...);

Server端的处理逻辑一般是接收客户端消息数据,而后执行转发(给接收者)逻辑:

if(read(conn_fd, msg_buff) > 0) {
do_transfer(msg_buff);
}

是否是很是简单,然而世界终归是复杂的,固然也不是这么简单的。

接下来就是比较复杂的了。

既然咱们的主题是高并发,那么Server端就不可能只和一个客户端通讯,而是可能会同时和成千上万个客户端进行通讯。这时你须要处理再也不是一个描述符这么简单,而是有可能要处理成千上万个描述符。

为了避免让问题一上来就过于复杂,咱们先简单化,假设只同时处理两个客户端的请求。

有的同窗可能会说,这还不简单,这样写不就好了:

if(read(socket_fd1, buff) > 0) { // 处理第一个
do_transfer();
}
if(read(socket_fd2, buff) > 0) { // 处理第二个
do_transfer();

在上一篇《深刻操做系统,理解I/O与零拷贝技术》中咱们讨论过,这是很是典型的阻塞式I/O,若是此时没有数据可读那么进程会被阻塞而暂停运行。这时咱们就没法处理第二个请求了,即便第二个请求的数据已经就位,这也就意味着处理某一个客户端时因为进程被阻塞致使剩下的全部其它客户端必须等待,在同时处理几万客户端的server上。这显然是不能容忍的。

聪明的你必定会想到使用多线程:为每一个客户端请求开启一个线程,这样一个客户端被阻塞就不会影响处处理其它客户端的线程了。注意:既然是高并发,那么咱们要为成千上万个请求开启成千上万个线程吗,大量建立销毁线程会严重影响系统性能。

那么这个问题该怎么解决呢?

这里的关键点在于:咱们事先并不知道一个文件描述对应的I/O设备是不是可读的、是不是可写的,在外设的不可读或不可写的状态下进行I/O只会致使进程阻塞被暂停运行。

所以要优雅的解决这个问题,就要从其它角度来思考这个问题了。

六、“不要打电话给我,有须要我会打给你”

你们生活中确定会接到过推销电话,并且不止一个,一天下来接上十个八个推销电话你的身体会被掏空的。

这个场景的关键点在于:打电话的人并不知道你是否是要买东西,只能来一遍遍问你。所以一种更好的策略是不要让他们打电话给你,记下他们的电话,有须要的话打给他们,这样推销员就不会一遍一遍的来烦你了(虽然现实生活中这并不可能)。

在这个例子中:你,就比如内核,推销者就比如应用程序,电话号码就比如文件描述符,和你用电话沟通就比如I/O。

如今你应该明白了吧,处理多个文件描述符的更好方法其实就存在于推销电话中。

所以相比上一节中:咱们经过I/O接口主动问内核这些文件描述符对应的外设是否是已经就绪了,一种更好的方法是,咱们把这些感兴趣的文件描述符一股脑扔给内核,并霸气的告诉内核:“我这里有1万个文件描述符,你替我监视着它们,有能够读写的文件描述符时你就告诉我,我好处理”。而不是弱弱的问内核:“第一个文件描述能够读写了吗?第二个文件描述符能够读写吗?第三个文件描述符能够读写了吗?。。。”

这样:应用程序就从“繁忙”的主动变为了悠闲的被动,反正文件描述可读可写了内核会通知我,能偷懒我才不要那么勤奋。

这是一种更加高效的I/O处理机制,如今咱们能够一次处理多路I/O了,为这种机制起一个名字吧,就叫I/O多路复用吧,这就是 I/O multiplexing。

七、I/O多路复用(I/O multiplexing)

multiplexing一词其实多用于通讯领域,为了充分利用通讯线路,但愿在一个信道中传输多路信号,要想在一个信道中传输多路信号就须要把这多路信号结合为一路,将多路信号组合成一个信号的设备被称为Multiplexer(多路复用器),显然接收方接收到这一路组合后的信号后要恢复原先的多路信号,这个设备被称为Demultiplexer(多路分用器)。

以下图所示:

回到咱们的主题。

所谓I/O多路复用指的是这样一个过程:

  • 1)咱们拿到了一堆文件描述符(无论是网络相关的、仍是磁盘文件相关等等,任何文件描述符均可以);
  • 2)经过调用某个函数告诉内核:“这个函数你先不要返回,你替我监视着这些描述符,当这堆文件描述符中有能够进行I/O读写操做的时候你再返回”;
  • 3)当调用的这个函数返回后咱们就能知道哪些文件描述符能够进行I/O操做了。

也就是说经过I/O多路复用咱们能够同时处理多路I/O。那么有哪些函数能够用来进行I/O多路复用呢?

以Linux为例,有这样三种机制能够用来进行I/O多路复用:

  • 1)select;
  • 2)poll;
  • 3)epoll。

接下来咱们就来介绍一下牛掰的I/O多路复用三剑客。

八、I/O多路复用三剑客

本质上:Linux上的select、poll、epoll都是阻塞式I/O,也就是咱们常说的同步I/O。

缘由在于:调用这些I/O多路复用函数时若是任何一个须要监视的文件描述符都不可读或者可写那么进程会被阻塞暂停执行,直到有文件描述符可读或者可写才继续运行。

8.1 select:初出茅庐

在select这种I/O多路复用机制下,咱们须要把想监控的文件描述集合经过函数参数的形式告诉select,而后select会将这些文件描述符集合拷贝到内核中。

咱们知道数据拷贝是有性能损耗的,所以为了减小这种数据拷贝带来的性能损耗,Linux内核对集合的大小作了限制,并规定用户监控的文件描述集合不能超过1024个,同时当select返回后咱们仅仅能知道有些文件描述符能够读写了,可是咱们不知道是哪个。所以程序员必须再遍历一边找到具体是哪一个文件描述符能够读写了。

所以,总结下来select有这样几个特色:

  • 1)我能照看的文件描述符数量有限,不能超过1024个;
  • 2)用户给个人文件描述符须要拷贝的内核中;
  • 3)我只能告诉你有文件描述符知足要求了,可是我不知道是哪一个,你本身一个一个去找吧(遍历)。

所以咱们能够看到,select机制的这些特性在高并发网络服务器动辄几万几十万并发连接的场景下无疑是低效的。

8.2 poll:小有所成

poll和select是很是类似的。

poll相对于select的优化仅仅在于解决了文件描述符不能超过1024个的限制,select和poll都会随着监控的文件描述数量增长而性能降低,所以不适合高并发场景。

8.3 epoll:独步天下

在select面临的三个问题中,文件描述数量限制已经在poll中解决了,剩下的两个问题呢?

针对拷贝问题:epoll使用的策略是各个击破与共享内存。

实际上:文件描述符集合的变化频率比较低,select和poll频繁的拷贝整个集合,内核都快被烦死了,epoll经过引入epoll_ctl很体贴的作到了只操做那些有变化的文件描述符。同时epoll和内核还成为了好朋友,共享了同一块内存,这块内存中保存的就是那些已经可读或者可写的的文件描述符集合,这样就减小了内核和程序的拷贝开销。

针对须要遍历文件描述符才能知道哪一个可读可写这一问题,epoll使用的策略是“当小弟”。

在select和poll机制下:进程要亲自下场去各个文件描述符上等待,任何一个文件描述可读或者可写就唤醒进程,可是进程被唤醒后也是一脸懵逼并不知道究竟是哪一个文件描述符可读或可写,还要再从头至尾检查一遍。

但epoll就懂事多了,主动找到进程要当小弟替大哥出头。

在这种机制下:进程不须要亲自下场了,进程只要等待在epoll上,epoll代替进程去各个文件描述符上等待,当哪一个文件描述符可读或者可写的时候就告诉epoll,epoll用小本本认真记录下来而后唤醒大哥:“进程大哥,快醒醒,你要处理的文件描述符我都记下来了”,这样进程被唤醒后就无需本身从头至尾检查一遍,由于epoll小弟都已经记下来了。

所以咱们能够看到:在epoll这种机制下,实际上利用的就是“不要打电话给我,有须要我会打给你”这种策略,进程不须要一遍一遍麻烦的问各个文件描述符,而是翻身作主人了——“大家这些文件描述符有哪一个可读或者可写了主动报上来”。

这种机制实际上就是大名鼎鼎的事件驱动——Event-driven,这也是咱们下一篇的主题。

实际上:在Linux平台,epoll基本上就是高并发的代名词。

九、本文小结

基于一切皆文件的设计哲学,I/O也能够经过文件的形式实现,高并发场景下要与多个文件交互,这就离不开高效的I/O多路复用技术。

本文咱们详细讲解了什么是I/O多路复用以及使用方法,这其中以epoll为表明的I/O多路复用(基于事件驱动)技术使用很是普遍,实际上你会发现但凡涉及到高并发、高性能的场景基本上都能见到事件驱动的编程方法,固然这也是下一篇咱们要重点讲解的主题《从根上理解高性能、高并发(四):深刻操做系统,完全理解同步与异步》,敬请期待!

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

高性能网络编程(一):单台服务器并发TCP链接数到底能够有多少
高性能网络编程(二):上一个10年,著名的C10K并发链接问题
高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了
高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索
高性能网络编程(五):一文读懂高性能网络编程中的I/O模型
高性能网络编程(六):一文读懂高性能网络编程中的线程模型
高性能网络编程(七):到底什么是高并发?一文即懂!
以网游服务端的网络接入层设计为例,理解实时通讯的技术挑战
知乎技术分享:知乎千万级并发的高性能长链接网关技术实践
淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路
一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
一套原创分布式即时通信(IM)系统理论架构方案
微信后台基于时间序的海量数据冷热分级架构设计实践
微信技术总监谈架构:微信之道——大道至简(演讲全文)
如何解读《微信技术总监谈架构:微信之道——大道至简》
快速裂变:见证微信强大后台架构从0到1的演进历程(一)
17年的实践:腾讯海量产品的技术方法论
腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面
以微博类应用场景为例,总结海量社交系统的架构设计步骤
新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践
重新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

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

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

相关文章
相关标签/搜索