NIO&epoll

  说到NIO,涉及到的知识点有很多,我们来一一捋一捋。

IO

  IO(InputStream/OutputStream)指的是读出/写入数据,IO可以分为磁盘IO和网络IO,围绕我们今天主题讲的是网络IO。网络IO包括了等待数据传输和读写数据的过程,等待数据传输其实就是等待数据经由网线、网卡、内核空间的过程,读写数据的过程是内核空间和用户空间的互相拷贝的过程。
  举个例子,在read发生时,很关键的两点是:

  1. 等待准备数据。
  2. 将数据从内核拷贝到用户进程中去。
    正如万事万物并不是你想要的时候就有的一样,当内核空间想要拿到数据时,可能数据还没有传进来,这时,它只能等着数据的到达,而用户进程也因此而阻塞。当内核空间一直等到数据准备好了,它就会将数据从内核空间中拷贝到用户内存,然后内核空间返回结果,用户进程才解除阻塞状态,重新运行起来。那网络IO的网络通信是借由谁完成的呢?

Socket

  Java中的网络通信是通过Socket实现的,我们都熟悉TCP/IP协议、Http协议等,Socket是TCP/IP协议的一个具体的实现。Socket分为ServerSocket和Socket两大类,ServerSocket用于服务端,可以通过方法监听请求,监听到请求后返回Scoket,Socket用于具体完成数据传输,客户端直接使用Socket发起请求并传输数据。 说白了,网络IO就是通过socket来进行通信的。

如何得知已经收到数据

  Socket是我们传输数据的工具,那我们如何得知已经收到数据呢?数据从网线传过来,在我们电脑上第一步是到达网卡,此时控制网卡的驱动(网卡驱动程序就是CPU控制和使用网卡的程序)开始发挥作用,网卡通过中断通知CPU数据已达的消息。
  中断允许让设备,如键盘,串口卡,并口等设备表明它们需要CPU。一旦CPU接收了中断请求,CPU就会暂时停止执行正在运行的程序,并调用一个称为中断服务程序(interrupt service routine)的特定程序。之后,CPU会恢复执行之前被中断的程序。那么接下来会发生什么?内核空间还在等着它需要的数据呢,我们继续往下走。

阻塞

  操作系统也已经知道,数据到达了。我们现在来到内核空间,操作系统为了实现进程调度,会把进程分为“运行”、“等待”几种不同的状态:
运行的进程能轮流获得CPU的资源,“等待”状态其实不难理解,回溯一下,我们  前文里一直在说的就是等待网络数据的到来啊,很好理解,拥有CPU资源的当前进程执行到创建socket语句时,操作系统会创建一个由文件系统管理的对象,这个对象包含了发送缓冲区、接收缓冲区、等待队列等,这个等待队列指向所有需要等待该socket的进程。当程序执行到recv()方法时(不论是客户端还是服务器应用程序都用recv函数从TCP连接的另一端接收数据),如果等待的数据还未到达,当前进程将会被加入等待队列,它的状态也就变成了阻塞。
  此时,可能你会疑问?操作系统如何知道数据到底对应的是哪个socket?我们知道网络数据包里都包含了IP和端口号,操作系统会给每个socket对象的索引维护一个端口号以变快速读取,从而内核通过端口号找到对应的socket。
  阻塞一般就是用等待队列来实现,这个等待队列是给进程添加“等待中”队列的引用,将进程停止在此处并睡眠下,直到条件满足时,才可通过此处,继续运行。在睡眠等待期间,wake up时,唤起来检查条件,条件满足解除阻塞,不满足继续睡下去。
所以我们来总结一下刚刚说的过程:

  1. 等待数据:进程A拥有CPU资源,当它执行到socket生成一个socket对象(包含接收buffer、发送buffer、等待队列),继续执行到recv()方法时,操作系统讲进程A从工作队列移到等待队列,其他进程继续轮流执行,A被阻塞,不往下执行代码,也不占用CPU资源。
  2. 数据来了:网络数据经由网卡传进内存,网卡驱动中断信号,CPU做出响应,执行中断程序,socket接收数据。
  3. 唤醒进程:socket收到数据后,操作系统将该socket的等待队列中的进程A的状态改为“运行中”,进程回到工作队列中继续执行代码。由于socket的接收buffer有了数据,recv()方法返回接收到的数据。
      总算是完整梳理了内核接收数据的全过程,但你有发现问题吗?如何做到监视全部的socket?

Select

  任何事情都不可能一蹴而就,解决问题先从笨办法开始——select,它的思路是:维护一个名为fds的数组,数组里放了全部需要监视的socket对象,等待数据时,进程挂起,任意socket收到数据后,进程被唤醒。看起来不难,再捋一下它的过程:

  1. 等待数据:维护一个名为fds的数组,数组里加入所有需要监视的socket对象。调用select() 方法,操作系统会把A加入到数组中所有socket对象的等待队列中。
  2. 数据来了:网卡收到数据后,网卡驱动发出中断,CPU响应,启动中断程序,socket接收数据。
  3. 唤醒进程:select()返回,进程A被唤醒,重回工作队列。
    补充
      当程序调用select时,内核会先遍历一遍socket,如果有一个以上的socket接收缓冲区有数据,那么select直接返回,不会阻塞。这也是为什么select的返回值有可能大于1的原因之一。如果没有socket有数据,进程才会阻塞。
      监视所有的socket是做到了,又有新问题来了:如何得知具体是哪个socket接收到了数据?是需要遍历一遍fds才能得知的,此外,进程被唤醒,要从所有的socket对象的等待队列中移除,又需要去遍历fds并移除被唤醒的进程,再加上补充情况里说的那一次遍历,简直可以凑成遍历三连。所以,不用我再明说,你肯定知道了,接下来,是这个系统需要再次进化的时候了。

epoll

  Select遍历次数多,开销大的原因有以下两点:

  1. 维护等待队列和阻塞进程关联在一起
解决方案:功能分离

将维护等待队列和阻塞进程分离开,在epoll方法中,使用epoll_create创建一个epoll对象epdf,epoll_ctl将需要监视的socket加入epdf中,调用epoll_wait接收数据。

  1. 需要遍历才能知道接收到数据的socket
解决方案:就绪列表

epoll维护一个rdlist,list引用收到socket的对象,这样就避免了为寻找接收到数据的socket而去遍历全部socket的低效。进程被唤醒后,只要获取rdlist的内容,就能获取接收到数据的socket对象。

epoll的过程:
  1. 创建epoll对象epdf:当某个进程调用epoll_create方法时,内核会创建一个epoll对象epdf(eventpoll),eventpoll跟socket一样,是文件系统中的一员,也有等待队列,还维护了rdlist作为它的成员。
  2. 维护监视列表:创建epoll对象后,epoll_ctl方法可以添加和删除所监听的socket,内核将eventpoll需要的socket加入监视列表。所以,socket收到数据后,中断程序会直接操作eventpoll对象,而不是直接操作进程。
  3. 接收数据:当socket接收到数据后,中断程序会给rdlist里添加收到数据的socket的引用。eventpoll相当于socket和进程的中介,socket接收数据并不直接影响进程,而是通过rdlist来改变进程的状态。rdlist:有引用,epoll_wait返回;无引用,rdlist空,阻塞进程A。
  4. 阻塞和唤醒进程:
    阻塞:进程运行到epoll_wait时,内核将进程加入eventpoll的等待队列。
    唤醒:socket接收到数据后,中断程序会做两件事:(1)修改rdlist(知道是哪个socket发生改变)(2) 唤醒eventpoll中的等待队列中的进程。
      epoll值得配图一张,引用自知乎罗培羽


    epoll.png
值得思考的两个问题:
rdlist的数据结构:

  需要满足的条件是,能够快速添加socket,而且epoll_ctl还要监听,需要频繁添加、删除socket,无疑是双向链表最为合适。

索引结构:

  维护等待队列和进程阻塞分离,所以需要一个便于监听管理socket的数据结构,需要满足便于查找、防止重复添加、方便删除的数据结构。之前二叉树家族的文章里讲过的数据结构很满足这个要求,红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。epoll使用了红黑树作为索引结构。

NIO及epoll

NIO

  NIO(new IO) ,这个new是相当于BIO来说的,BIO(Blocking IO)是同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。为了降低开销的问题,就产生了NIO,同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

epoll

  epoll是在select的基础上,改良了几个不够高效的点,引用了先进的数据结构,实现了更高效的多路复用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342