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

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

1.I/O多路复用模式的实现


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


55.jpg


多个的进程的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多路复用机制。


56.png


该函数的作用是:


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


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


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

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



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


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


特别注意,在select函数下,老师仅仅知道有学生发生变化了,但到底是哪些学生发生变化,他需要 轮询 一遍同学名单(xxxfds),找出举手的同学,然后和他进行交流。


select的缺点比较明显:


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


3. poll


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

57.png


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


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


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


4. epoll


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


它在实现和使用上和select\poll有很大差别:


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


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

58.png


4.1 epoll的三个核心函数


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

59.png


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


60.png

  • 调用epoll_ctl向epoll对象中添加连接的套接字。
  • epfd就是epoll_creat返回的事件表id。
  • op表示具体操作。包括添加fd的监听事件EPOLL_CTL_ADD、删除fd的监听事件EPOLL_CTL_DEL、修改fd的监听事件EPOLL_CTL_MOD。
  • fd是需要监听的fd(文件描述符)
  • event是告诉内核需要监听哪个事件


60.png


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


4.2 epoll的实现原理


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

61.png


  • 红黑树根节点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调用关系如下图所示。

62.png


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

63.png


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


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高性能服务器编程》

目录
相关文章
|
2月前
|
存储 网络协议 算法
从HPACK到多路复用,揭秘HTTP/2如何终结网络拥堵
HTTP/2通过HPACK压缩头部冗余信息,提升传输效率;并利用多路复用技术,在单个TCP连接上并行处理多个请求,避免队头阻塞,显著提升性能。同时支持服务器推送和流优先级设置,优化资源加载体验。
146 7
|
2月前
|
监控 前端开发 安全
Netty 高性能网络编程框架技术详解与实践指南
本文档全面介绍 Netty 高性能网络编程框架的核心概念、架构设计和实践应用。作为 Java 领域最优秀的 NIO 框架之一,Netty 提供了异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。本文将深入探讨其 Reactor 模型、ChannelPipeline、编解码器、内存管理等核心机制,帮助开发者构建高性能的网络应用系统。
208 0
|
6月前
|
监控 应用服务中间件 Linux
掌握并发模型:深度揭露网络IO复用并发模型的原理。
总结,网络 I/O 复用并发模型通过实现非阻塞 I/O、引入 I/O 复用技术如 select、poll 和 epoll,以及采用 Reactor 模式等技巧,为多任务并发提供了有效的解决方案。这样的模型有效提高了系统资源利用率,以及保证了并发任务的高效执行。在现实中,这种模型在许多网络应用程序和分布式系统中都取得了很好的应用成果。
178 35
|
6月前
|
安全 Java 调度
Netty源码—3.Reactor线程模型二
本文主要介绍了NioEventLoop的执行总体框架、Reactor线程执行一次事件轮询、Reactor线程处理产生IO事件的Channel、Reactor线程处理任务队列之添加任务、Reactor线程处理任务队列之执行任务、NioEventLoop总结。
|
6月前
|
安全 Java
Netty源码—2.Reactor线程模型一
本文主要介绍了关于NioEventLoop的问题整理、理解Reactor线程模型主要分三部分、NioEventLoop的创建和NioEventLoop的启动。
|
6月前
|
弹性计算 网络协议 Java
Netty基础—2.网络编程基础二
本文介绍了网络编程的基本概念和三种主要模式:BIO(阻塞IO)、AIO(异步IO)和NIO(非阻塞IO)。BIO模型通过为每个客户端连接创建一个线程来处理请求,适合客户端较少的情况,但在高并发下性能较差。AIO模型通过异步IO操作,允许操作系统处理IO,适合高并发场景,但编码复杂且Linux支持有限。NIO模型通过Selector实现多路复用,适合高并发且性能要求高的场景。文章还详细介绍了NIO中的Buffer、Selector、Channel等核心组件,并提供了NIO的实战开发流程和代码示例。
|
6月前
|
监控 网络协议 Java
Netty基础—1.网络编程基础一
本文详细介绍了网络通信的基础知识,涵盖OSI七层模型、TCP/IP协议族及其实现细节。首先解释了OSI模型各层功能,如物理层负责数据通路建立与传输,数据链路层提供无差错传输等。接着探讨了TCP/IP协议,包括TCP和UDP的特点、三次握手与四次挥手过程,以及如何通过确认应答和序列号确保数据可靠性。还分析了HTTP请求的传输流程和报文结构,并讨论了短连接与长连接概念。 此外,解析了Linux下的IO模型,包括阻塞IO、非阻塞IO、IO复用(select/poll/epoll)、信号驱动IO和异步IO的特点与区别,强调了epoll在高并发场景下的优势及其水平触发和边缘触发两种工作模式。
|
6月前
|
网络协议 算法 Java
Netty基础—3.基础网络协议
本文详细梳理了计算机网络的基础知识,涵盖从物理层到应用层的各层协议及其功能。内容包括七层模型与四层模型对比、IP地址与子网划分、TCP三次握手及四次挥手过程、Socket编程原理、HTTP/HTTPS协议的工作机制等。同时深入探讨了Linux IO模型(阻塞、非阻塞、IO多路复用)及其应用场景,并分析了select、poll、epoll的区别。此外,还涉及Java IO读写的底层流程及同步异步、阻塞非阻塞的概念。这些知识点为理解网络通信和高性能服务器开发提供了全面的理论支持。
|
11月前
|
JSON 算法 Java
Nettyの网络聊天室&扩展序列化算法
通过本文的介绍,我们详细讲解了如何使用Netty构建一个简单的网络聊天室,并扩展序列化算法以提高数据传输效率。Netty的高性能和灵活性使其成为实现各种网络应用的理想选择。希望本文能帮助您更好地理解和使用Netty进行网络编程。
150 12
|
11月前
|
存储 缓存 监控
Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
本文介绍了Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
1034 7