乐趣区

关于高并发:从根上理解高性能高并发三深入操作系统彻底理解IO多路复用

1、系列文章引言

1.1 文章目标

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

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

1.2 文章源起

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

越往常识的深处走,越感觉对即时通讯技术理解的太少。于是起初,为了让开发者门更好地从根底电信技术的角度了解网络(尤其挪动网络)个性,我跨专业收集整理了《IM 开发者的零根底通信技术入门》系列高阶文章。这系列文章未然是一般即时通讯开发者的网络通信技术常识边界,加上之前这些网络编程材料,解决网络通信方面的常识盲点根本够用了。

对于即时通讯 IM 这种零碎的开发来说,网络通信常识的确十分重要,但回归到技术实质,实现网络通信自身的这些技术特色:包含下面提到的线程池、零拷贝、多路复用、事件驱动等等,它们的实质是什么?底层原理又是怎么?这就是整顿本系列文章的目标,心愿对你有用。

1.3 文章目录

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

1.4 本篇概述

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

本文已同步公布于“即时通讯技术圈”公众号,欢送关注。公众号上的链接是:点此进入。

2、本文作者

应作者要求,不提供真名,也不提供集体照片。

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

3、什么是文件?

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

程序员应用 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 操作,这就是文件这个概念的弱小之处。

4、什么是文件描述符?

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

个别都是这样写的:

read(buff);

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

从上一节中咱们晓得,通过文件这个概念咱们能实现简直所有 I / O 操作,因而这里少的一个配角就是文件。

那么咱们个别都怎么应用文件呢?

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

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

因而:文件形容仅仅就是一个数字而已,然而通过这个数字咱们能够操作一个关上的文件,这一点要记住。

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

因而咱们来欠缺上述程序:

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

怎么样,是不是非常简单。

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

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

从前几节咱们晓得,所有 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 只会导致过程阻塞被暂停运行。

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

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

大家生存中必定会接到过采购电话,而且不止一个,一天下来接上十个八个采购电话你的身材会被掏空的。

这个场景的关键点在于:打电话的人并不知道你是不是要买货色,只能来一遍遍问你。因而一种更好的策略是不要让他们打电话给你,记下他们的电话,有需要的话打给他们,这样推销员就不会一遍一遍的来烦你了(尽管现实生活中这并不可能)。

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

当初你应该明确了吧,解决多个文件描述符的更好办法其实就存在于采购电话中。

因而相比上一节中:咱们通过 I / O 接口被动问内核这些文件描述符对应的外设是不是曾经就绪了,一种更好的办法是,咱们把这些感兴趣的文件描述符一股脑扔给内核,并霸气的通知内核:“我这里有 1 万个文件描述符,你替我监督着它们,有能够读写的文件描述符时你就通知我,我好解决”。而不是弱弱的问内核:“第一个文件形容能够读写了吗?第二个文件描述符能够读写吗?第三个文件描述符能够读写了吗?。。。”

这样:应用程序就从“忙碌”的被动变为了安闲的被动,反正文件形容可读可写了内核会告诉我,能偷懒我才不要那么怠惰。

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

7、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 多路复用三剑客。

8、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 基本上就是高并发的代名词。

9、本文小结

基于所有皆文件的设计哲学,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

退出移动版