从网络I/O模型到Netty,先深入了解下I/O多路复用

摘要:
特别是,I/O复用模型在高并发场景中具有非常好的优势。Netty还采用了I/O复用模型。Netty实际上是一个打包的框架。本质上,它使用Java的NIO包,该包使用I/O复用。因此,作为“知识先行+高频面试问题”的一章,让我们深入了解I/O复用模型。在IO复用模型中。事件表示事件阵列就绪。

微信搜索【阿丸笔记】,关注Java/MySQL/中间件各系列原创实战笔记,干货满满。

本文是Netty系列第3篇

上一篇文章我们了解了Unix标准的5种网络I/O模型,知道了它们的核心区别与各自的优缺点。尤其是I/O多路复用模型,在高并发场景下,有着非常好的优势。而Netty也采用了I/O多路复用模型。

那Netty是如何实现I/O多路复用的呢?

Netty实际上也是一个封装好的框架,它的本质上还是使用了Java的NIO包(New IO,不是网络I/O模型的NIO,Nonblocking IO)包,Java NIO包里面使用了I/O多路复用。

所以,本文作为一个 前置知识 + 高频面试题 章节(手动狗头),一起来深入了解下I/O多路复用模型吧。

 

本文预计阅读时间 5分钟,将重点回答以下两个问题:

  • I/O多路复用模式有哪些实现?select/poll/epoll
  • select/poll/epoll有什么区别
1.I/O多路复用模式的实现

这是我们上一篇讲I/O多路复用使用的图,可以再回顾一下I/O多路复用模型。

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

多个的进程的IO可以注册到一个复用器(selector)上,然后用一个进程调用select,select会监听所有注册进来的IO。

举个例子。
在BIO模式中,一个老师(应用进程/线程)只能同时处理一个同学(IO流)的问题。如果有10个同学,就需要配置10个老师来做一对一的讲解。

在IO多路复用模型中。我们给 老师 配置了一个 班长(复用器Selector)。班长 负责观察班级里的10个同学谁要提问,一旦有同学举手,班长就反馈老师去处理这个举手同学的问题。

这样一来,只需要1个老师,老师 只需要注意 班长 的反馈,就能及时处理对应的 同学 的问题了。

下面我们具体来看看I/O多路复用的三种实现:select、poll、epoll。

需要注意的是,select,poll,epoll都是IO多路复用的实现方式,而且本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

2. select

Linux系统提供了一个函数select来供开发者使用select多路复用机制。

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

该函数的作用是:

通过轮询,可以同时监视多个文件描述符是否发生了读、写、异常这三类IO事件。

最后返回发生IO事件的文件描述符数量,以及读事件、写事件、异常事件这三种事件分别发生在哪些文件描述符中(readfds、writefds、errorfds三个参数)。

文件描述符(File descriptor)是计算机中的一个术语,用于表述指向文件的引用的抽象化概念。

Linux下一切皆文件,包括IO设备也是。因此要对某个设备进行操作,就需要打开此设备文件,打开文件就会获得该文件的文件描述符fd( file discriptor),它就是一个很小的整数。

 

我们结合 老师-班长-同学 的模型来理解下这个过程。

  • 老师把学生名单(xxxxfds)给班长,让班长关注班级里的所有同学。
  • 班长时刻轮训班级里每个同学的状态(轮训所有fd_set),直到 超时 或者 有同学举手。
  • 一旦有同学举手,班长就会把学生名单上有变化的学生名字做标记,并把一共多少个学生有变化返回给 老师。
  • 老师可以获得举手同学的数量,并在学生名单(xxxxfds)上看的有哪几个同学发生了事件(读、写、异常)。
  • 老师拿到学生名单后,轮训班级里面的每个同学状态,根据具体的 读、写、异常事件 来进行IO处理。

特别注意,在select函数下,老师仅仅知道有学生发生变化了,但到底是哪些学生发生变化,他需要 轮训 一遍同学名单,找出举手的同学,然后倾听他的问题,并回答他的问题。

select的缺点比较明显:

  • 具有O(n)的无差别轮询时间复杂度,每次调用需要轮训fd_set,同时处理得越多,轮询时间就越长。
  • 每次调用select函数,都需要把 所有 fd_set从 用户态 拷贝到 内核态 进行轮训,如果fd_set比较大,对性能影响就非常大。
3. poll

poll的实现和select非常相似,我们就不重复说明了,直接介绍一下区别。poll函数如下:

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

主要是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,pollfd结构使用链表而非数组,这导致pollfd的长度没有限制。但是如果pollfd长度过大,会导致性能下降。

除此之外,二者的原理基本一致,即对多个描述符也是进行轮询,根据描述符的状态进行处理。

因此,二者的缺陷也基本一致。

4. epoll

epoll的全称是eventpoll,它是基于event事件进行实现的,是linux特有的I/O复用函数。

它在实现和使用上和selectpoll有很大差别:

  • epoll通过一组函数来完成任务,而不是单个函数。
  • epoll把用户关心的文件描述符fd放在一个事件表中,而不是像select/poll那样把所有文件描述符集合(fds)传来传去。
  • epoll需要一个额外的文件描述符fd来表示这个事件表。

不同于select使用三个fd_set来对应读/写/异常的IO变化,epoll专门定义了一个epoll_event结构体,将其作为读/写/异常的IO变化的逻辑封装,称为事件(event)。

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

4.1 epoll的三个核心函数

epoll把原先的select/poll调用分成了3个函数。

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

  • 调用int epoll_create(int size)建立一个epoll句柄对象,返回一个文件描述符fd,指向 事件表。在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  • 参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
从网络I/O模型到Netty,先深入了解下I/O多路复用

 

  • 调用epoll_ctl向epoll对象中添加连接的套接字。
  • epfd就是epoll_creat返回的id。
  • op表示具体操作。包括添加fd的监听事件EPOLL_CTL_ADD、删除fd的监听事件EPOLL_CTL_DEL、修改fd的监听事件EPOLL_CTL_MOD。
  • fd是需要监听的fd(文件描述符)
  • event是告诉内核需要监听哪个事件
从网络I/O模型到Netty,先深入了解下I/O多路复用

 

  • 调用epoll_wait收集发生的事件的连接
  • 返回值表示已经准备继续的文件描述符的总数。
  • epfd表示事件表。
  • events表示 准备就绪的事件数组。event_wait如果检测到事件,就把就绪的事件从 事件表 中复制到这个数组中。(比select/poll高效的地方!!)
  • maxevents表示最多监听多少事件。
4.2 epoll的实现原理

当某一进程调用 epoll_create()方法 时,内核空间会创建一个eventpoll结构体,这个结构体中有两个成员变量与epoll的使用方式密切相关,结构体如下所示:

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

  • 红黑树根节点rbr:红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件
  • 链表rdlist:链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件

用 epoll_ctl()方法 将新添加的监控事件event加入到 红黑树rbr 中。还会给内核中断处理程序注册一个 回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。

一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,被触发的事件会被 回调函数 加入eventpoll的 链表rdlist 中。

当调用 epoll_wait()方法 检查是否有事件发生时,只需要检查eventpoll对象中的rdlist链表中是否有元素即可。如果链表中有数据的话,就把对应有修改的事件event复制到epoll_wait()方法的events数组变量中,用户就能获得了。

对比select/poll,我们可以看到此处不需要遍历监听的文件描述符,这正是epoll的魅力所在。

如此一来,epoll_wait的效率就非常高了。因为调用epoll_wait时,不需要向操作系统复制所有的连接的句柄数据,内核也不需要去遍历全部的连接。

4.3 epoll中有使用共享内存吗?

很多博客提到了这点:

epoll_wait返回时,对于就绪的事件,epoll使用的是共享内存的方式,即用户态和内核态都指向了就绪链表,所以就避免了内存拷贝消耗

但是事实确实如此吗?

源码面前无密码,我们直接看下源码吧。
参考eventpoll.c的源码。

https://github.com/torvalds/linux/blob/master/fs/eventpoll.c

具体的epoll_wait调用关系如下图所示。

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

我们可以在put_user中看到具体的说明。

从网络I/O模型到Netty,先深入了解下I/O多路复用

 

因此,事件确实是从内核空间拷贝到用户空间的,并没有使用共享内存。

5.三种实现对比

通过上面的分析,相信大家都已经了解了select/poll/epoll的实现。
下面通过一个表格来总结他们的主要区别。

select

poll

epoll

事件集合

用户通过传递3个参数来关注 可读、可写、异常 3类事件

统一所有事件类型,只用1个参数来关注

通过1个事件表来管理用户订阅的事件

事件传递效率

每次等待socket事件,都需要把所有socket从用户态拷贝至内核态

同select

只需将socket添加一次到红黑树上即可

内核检测就绪效率

每次调用需要扫描整个注册的文件描述符集合,然后将其中已经就绪的文件描述符设置在传入的数组中

同select

通过回调机制,将就绪的事件加入链表rdlist

应用检查已经就绪的 事件 效率

遍历,O(n)复杂度

遍历, O(n)复杂度

只获取已经就绪的事件rdlist,O(1)复杂度

最大支持文件描述符

数组存放事件,有最大限制

链表存放事件,无最大限制(受系统最大限制)

红黑树存放事件,无最大限制(受系统最大限制)

从整体来看,epoll的实现性能是比select/poll更好的。

当然,如果保持活跃的连接一直非常多,epoll_wait的效率就不一定高了,因为此时epoll_wait的回调函数触发过于频繁。

因此,epoll最适合的场景是连接数量很多,但是活跃连接数量不多的情况。

 


参考书目:
《Linux高性能服务器编程》

都看到最后了,原创不易,点个关注,点个赞吧~
文章持续更新,可以微信搜索「阿丸笔记 」第一时间阅读,回复【笔记】获取Canal、MySQL、HBase、JAVA实战笔记,回复【资料】获取一线大厂面试资料。
知识碎片重新梳理,构建Java知识图谱:github.com/saigu/JavaK…(历史文章查阅非常方便)

免责声明:文章转载自《从网络I/O模型到Netty,先深入了解下I/O多路复用》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇jmeter的jmx脚本结构解析Ubuntu 16.09开启iptables的日志实现调试下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

oracle sqlplus及常用sql语句

常用sql语句 有需求才有动力 http://blog.csdn.net/yitian20000/article/details/6256716 常用sql语句 创建表空间:create tablespace 表空间名字 filedata 'd:db.dbf' size 20m uniform size 128k;说明:创建一个20M的表空间,且表空间的区...

论文笔记:(2021CVPR)PAConv: Position Adaptive Convolution with Dynamic Kernel Assembling on Point Clouds

目录 摘要 1、引言 2、相关工作 将点云映射到常规二维或三维栅格(体素) 基于MLPs的点表示学习 基于点卷积的点表示学习 动态卷积和条件卷积 3、方法 3.1 回顾 3.2 动态内核组装 Weight Bank ScoreNet. Kernel generation 3.3 权重正则化 3.4 与前期工作的关系 4、骨干网体系结...

Oracle序列号

转载:https://www.cnblogs.com/laipDIDI/articles/2620971.html 例1:创建序列:CREATE SEQUENCE ABC INCREMENT BY1 START WITH1 MAXVALUE 9999999999 NOCYCLE NOCACHE; 语法详解CREATE SEQUENCE 序列名 [INCRE...

UI自动化的API总结

断言:assert 1 == 2unittest里的断言:assertEqual(title, u"百度一下,你就知道", "页面title属性值错误!")前进:driver.forward()后退:driver.back()刷新:driver.refresh()最大化:driver.maximize_window()获取坐标:driver.get_win...

sql-优化建议

1. 查询 SQL 尽量不要使用 select *,而是 select 具体字段 反例: select * from employee; 正例: select id,name from employee; 理由: 只取需要的字段,节省资源、减少网络开销; select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询。 2. 如果知道...

在clickhouse中更新和删除

ck 目前支持了更新和删除,但是与传统sql语法 略有不同,我也记录下来,防止后面忘记。 测试数据 :) select count(*) from system.columns where table='test_update'; ┌─count()─┐ │ 332 │ └─────────┘ :) select count(*) from t...