IO--多路复用的三种机制Select,Poll和Epoll对比

IO--多路复用的三种机制Select,Poll和Epoll对比,第1张

select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。

多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

我们先分析一下select函数

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

【参数说明】

int maxfdp1 指定待测试的文件描述字个数,它的值是待测试的最大描述字加1。

fd_set *readset , fd_set *writeset , fd_set *exceptset

fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄。中间的三个参数指定我们要让内核测试读、写和异常条件的文件描述符集合。如果对某一个的条件不感兴趣,就可以把它设为空指针。

const struct timeval *timeout timeout告知内核等待所指定文件描述符集合中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

【返回值】

int 若有就绪描述符返回其数目,若超时则为0,若出错则为-1

select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。

下面是pll的函数原型:

poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024

【参数说明】

struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域

nfds_t nfds 记录数组fds中描述符的总数量

【返回值】

int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;

epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

Linux中提供的epoll相关函数如下:

1. epoll_create 函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。

2. epoll_ctl 函数注册要监听的事件类型。四个参数解释如下:

epoll_event 结构体定义如下:

3. epoll_wait 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。比如:0->1 就是Edge,1->1 就是Level。

ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。

一张图总结一下select,poll,epoll的区别:

epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。

既然select,poll,epoll都是I/O多路复用的具体的实现,之所以现在同时存在,其实他们也是不同 历史 时期的产物

关于IO会涉及到阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO等几个知识点。知识点虽然不难但平常经常容易搞混,特此Mark下,与君共勉。

阻塞IO情况下,当用户调用 read 后,用户线程会被阻塞,等内核数据准备好并且数据从内核缓冲区拷贝到用户态缓存区后 read 才会返回。可以看到是阻塞的两个部分。

非阻塞IO发出read请求后发现数据没准备好,会继续往下执行,此时应用程序会不断轮询polling内核询问数据是否准备好,当数据没有准备好时,内核立即返回EWOULDBLOCK错误。直到数据被拷贝到应用程序缓冲区,read请求才获取到结果。并且你要注意!这里最后一次 read 调用获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是 内核态的数据拷贝到用户程序的缓存区这个过程

非阻塞情况下无可用数据时,应用程序每次轮询内核看数据是否准备好了也耗费CPU,能否不让它轮询,当内核缓冲区数据准备好了,以事件通知当机制告知应用进程数据准备好了呢?应用进程在没有收到数据准备好的事件通知信号时可以忙写其他的工作。此时 IO多路复用 就派上用场了。

IO多路复用中文比较让人头大,IO多路复用的原文叫 I/O multiplexing,这里的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流. 发明它的目的是尽量多的提高服务器的吞吐能力。实现一个线程监控多个IO请求,哪个IO有请求就把数据从内核拷贝到进程缓冲区,拷贝期间是阻塞的!现在已经可以通过采用mmap地址映射的方法,达到内存共享效果,避免真复制,提高效率。

select、poll、epoll 都是I/O多路复用的具体的实现。

select是第一版IO复用,提出后暴漏了很多问题。

poll 修复了 select 的很多问题。

但是poll仍然不是线程安全的, 这就意味着不管服务器有多强悍,你也只能在一个线程里面处理一组 I/O 流。你当然可以拿多进程来配合了,不过然后你就有了多进程的各种问题。

epoll 可以说是 I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:

横轴 Dead connections 是链接数的意思,叫这个名字只是它的测试工具叫deadcon。纵轴是每秒处理请求的数量,可看到epoll每秒处理请求的数量基本不会随着链接变多而下降的。poll 和/dev/poll 就很惨了。但 epoll 有个致命的缺点是只有 linux 支持。

比如平常Nginx为何可以支持4W的QPS是因为它会使用目标平台上面最高效的I/O多路复用模型。

然后你会发现上面的提到过的操作都不是真正的异步,因为两个阶段总要等待会儿!而真正的异步 I/O 是内核数据准备好和数据从内核态拷贝到用户态这两个过程都不用等待。

很庆幸,Linux给我们准备了 aio_read aio_write 函数实现真实的异步,当用户发起aio_read请求后就会自动返回。内核会自动将数据从内核缓冲区拷贝到用户进程空间,应用进程啥都不用管。

我强力推荐C++后端开发免费学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

同步跟异步的区别在于 数据从内核空间拷贝到用户空间是否由用户线程完成 ,这里又分为同步阻塞跟同步非阻塞两种。

我们以同步非阻塞为例,如下可看到,在将数据从内核拷贝到用户空间这一过程,是由用户线程阻塞完成的。

可发现,用户在调用之后会立即返回,由内核完成数据的拷贝工作,并通知用户线程,进行回调。

在Java中,我们使用socket进行网络通信,IO主要有三种模式,主要看 内核支持 哪些。

同步阻塞IO ,每个客户端的Socket连接请求,服务端都会对应有个处理线程与之对应,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是 一个连接一个线程 。

BIO特点

常量:

主类:

服务端监听线程:

服务端处理线程:

客户端:

同步非阻塞IO之NIO :服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时说明读就绪,则调用该socket连接的相应读操作。如果发现某个 Socket端口上有数据可写时说明写就绪,则调用该socket连接的相应写操作。如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高,在进行IO操作请求时候再用个线程去处理,是 一个请求一个线程 。Java中使用Selector、Channel、Buffer来实现上述效果。

每个线程中包含一个 Selector 对象,它相当于一个通道管理器,可以实现在一个线程中处理多个通道的目的,减少线程的创建数量。远程连接对应一个channel,数据的读写通过buffer均在同一个 channel 中完成,并且数据的读写是非阻塞的。通道创建后需要注册在 selector 中,同时需要为该通道注册感兴趣事件(客户端连接服务端事件、服务端接收客户端连接事件、读事件、写事件), selector 线程需要采用 轮训 的方式调用 selector 的 select 函数,直到所有注册通道中有兴趣的事件发生,则返回,否则一直阻塞。而后循环处理所有就绪的感兴趣事件。以上步骤解决BIO的两个瓶颈:

下面对以下三个概念做一个简单介绍,Java NIO由以下三个核心部分组成:

channel和buffer有好几种类型。下面是Java NIO中的一些主要channel的实现:

正如你所看到的,这些通道涵盖了UDP和TCP网络IO,以及文件IO。以下是Java NIO里关键的buffer实现:

在微服务阶段,一个请求可能涉及到多个不同服务之间的跨服务器调用,如果你想实现高性能的PRC框架来进行数据传输,那就可以基于Java NIO做个支持长连接、自定义协议、高并发的框架,比如Netty。Netty本身就是一个基于NIO的网络框架, 封装了Java NIO那些复杂的底层细节,给你提供简单好用的抽象概念来编程。比如Dubbo底层就是用的Netty。

AIO是异步非阻塞IO,相比NIO更进一步,进程读取数据时只负责发送跟接收指令,数据的准备工作完全由操作系统来处理。

推荐一个零声教育C/C++后台开发的免费公开课程,个人觉得老师讲得不错,分享给大家:C/C++后台开发高级架构师,内容包括Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂 立即学习

原文:阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO 一锅端


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/471738.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-06-06
下一篇2023-06-06

发表评论

登录后才能评论

评论列表(0条)

    保存