select,poll,epoll(第二版)
背景:
全链路异步:
- 应用层:编程模型的异步:响应式编程
- 框架层:IO线程的异步:一个IO线程只能处理一个请求=====》一个IO线程只能处理多个请求,经典模型:reactor模型
将Reactor分为两部分mainReactor和subReactor
mainReactor负责处理新的连接事件,将后续的事件处理交给subReactor
subReactor处理时间的方式,由阻塞变为多线程,引入了任务队列的模型 - OS层:IO模型的异步
很重要
原因:一般的select和poll,都是同步IO,不是异步
同步IO中,线程切换(会导致内核线程占比cpu很高,原因是线程上下文切换),IO事件的轮询,IO的操作都涉及到系统调用
IO的发展历程:
- 阻塞式IO(bio),一个连接对应一条线程
- 非阻塞式io:用户线程在发起IO请求后就可以返回
- io复用(nio):怎么做到的:把IO事件(单独线程)和IO操作(各自线程)分开,select阻塞直到有就绪事件,然后再遍历fd列表(O(n))
- 信号驱动式io:只是多了回调函数,在等待的过程中是异步的,但是在数据从内核复制到用户缓冲区是阻塞的
- 异步io(aio):在数据从内核复制到用户缓冲区是异步的
内核缓冲区和内核缓冲区:
目的:减少频繁地与设备之间的物理交换
select、poll、epoll 区别总结:
底层实现
select/poll
首先把关注的Socket集合从用户态拷贝到内核态,然后由内核检测事件,遍历整个集合(由于线性结构实现,时间复杂度为O(n))找到对应的socket,并改变状态为可读或者可写,然后再拷贝整个socket集合到用户态,继续遍历整个集合找到可读或可写的socket对其处理。
epoll
是直接内核支持的,通过create和ctl等函数,可以构造描述符(fd)相关的事件组合(event)
fd:每个链接,每个文件都有各自的文件描述符,比如端口号
event:当fd对应的资源变动,就会更新epoll_item结构,在没有变动时,epoll就阻塞等待,一旦有变动,就会通知各方
红黑树:所有fd的集合
队列:就绪fd的集合
相对于select,epoll有哪些改进?
- epoll不再需要像select一样对fd集合进行轮询,也不需要在调用时将fd集合在用户态和内核态进行交换
- 应用程序获得就绪fd的事件复杂度,epoll时O(1),select是O(n)
- select最大支持约1024个fd,epoll支持65535个
- select使用轮询模式检测就绪事件,epoll采用通知方式,更加高效
- select将事件注册和事件查询合在一起,但是epoll分开了,先用epoll_ctl维护等待队列,再用epoll_wait阻塞进程(空间换时间,存储就绪的fd)
通过两个手段解决了上面的不足
- 在内核中使用了红黑树来跟踪进程所有的待检测文件描述符(时间复杂度一般为O(logn)),减少了内核和用户空间大量的数据拷贝和内存分配。
- 使用了事件驱动的机制,内核里维护了一个链表来记录就绪事件。不需要轮询检测了。
epoll是Linux下高效的I/O多路复用机制之一,可以有效地处理大量的并发连接,提高网络应用程序的性能和吞吐量。epoll有三种工作模式:LT模式、ET模式和EPOLLONESHOT模式。
- LT模式(Level Triggered,水平触发)
在LT模式下,当一个描述符上有可读或可写事件发生时,epoll_wait()函数会立即返回,应用程序可以在返回的events列表中处理这些事件。如果应用程序没有处理完所有的事件,下次调用epoll_wait()函数时会再次返回这些事件。
LT模式是epoll的默认模式,也是最常用的模式。它的优点是容易使用,可以直接处理每个事件,缺点是在高并发情况下,可能会导致频繁的epoll_wait()函数调用和事件处理,影响程序性能。
- ET模式(Edge Triggered,边缘触发)
在ET模式下,epoll_wait()函数只会在描述符上出现可读或可写事件时返回,而不是像LT模式一样每次调用都会返回所有的事件。当应用程序处理完这些事件后,必须立即读取或写入数据,直到返回EAGAIN错误,否则将无法再次接收到该描述符上的事件。
ET模式的优点是可以提高程序性能,减少epoll_wait()函数的调用次数。缺点是使用起来相对复杂,需要注意每个事件的处理方式和状态。
- EPOLLONESHOT模式
在EPOLLONESHOT模式下,epoll_wait()函数只会返回一次该描述符上的事件,之后该描述符将被从epoll队列中删除,直到应用程序重新将该描述符添加到epoll队列中。
EPOLLONESHOT模式的优点是可以避免多个线程同时处理同一个描述符上的事件,确保事件的顺序和正确性。缺点是在重新添加描述符到epoll队列时,可能会出现并发访问的问题,需要进行额外的同步措施。
总之,不同的epoll工作模式各有特点,需要根据应用程序的具体需求进行选择和使用。常见的使用方式是使用LT模式处理普通的网络应用程序,使用ET模式处理高并发的网络应用程序,使用EPOLLONESHOT模式处理一些特定的情况,例如单线程处理多个描述符的情况。
- LT 模式下,读事件触发后,可以按需收取想要的字节数,不用把本次接收到的数据收取干净(即不用循环到 recv 或者 read 函数返回 -1,错误码为 EWOULDBLOCK 或 EAGAIN);ET 模式下,读事件必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使有机会,也可能存在上次没读完的数据没有及时处理,造成客户端响应延迟。
- LT 模式下,不需要写事件一定要及时移除,避免不必要的触发,浪费 CPU 资源;ET 模式下,写事件触发后,如果还需要下一次的写事件触发来驱动任务(例如发上次剩余的数据),你需要继续注册一次检测可写事件。
- LT 模式和 ET 模式各有优缺点,无所谓孰优孰劣。使用 LT 模式,我们可以自由决定每次收取多少字节(对于普通 socket)或何时接收连接(对于侦听 socket),但是可能会导致多次触发;使用 ET 模式,我们必须每次都要将数据收完(对于普通 socket)或必须理解调用 accept 接收连接(对于侦听socket),其优点是触发次数少
1、支持一个进程所能打开的最大连接数
select
单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
poll
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接
2、FD剧增后带来的IO效率问题
select
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll
同上
epoll
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
3、 消息传递方式
select
内核需要将消息传递到用户空间,都需要内核拷贝动作
poll
同上
epoll
epoll通过内核和用户空间共享一块内存来实现的。(错的) epoll_wait 实现的内核代码中调用了 __put_user
函数,这个函数就是将数据从内核拷贝到用户空间。
应用场景
不是用 epoll 就可以了,select 和 poll 都还没有过时,都有各自的使用场景。
1. select 应用场景
select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时要求更高的场景,比如核反应堆的控制。
select 可移植性更好,几乎被所有主流平台所支持。
2. poll 应用场景
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且epoll 的描述符存储在内核,不容易调试。
3. epoll 应用场景
只需要运行在 Linux 平台上,并且有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。
------摘自pdai
通过合理配置来支持百万级并发连接
文件句柄,也叫文件描述符。在Linux系统中,文件可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,它是一个非负整数(通常是小整数),用于指代被打开的文件。所有的IO系统调用,包括socket的读写调用,都是通过文件描述符完成的
在Linux下,通过调用ulimit命令,可以看到一个进程能够打开的最大文件句柄数量,这个命令的具体使用方法是:
ulimit -n
同步VS异步 阻塞VS非阻塞
首先要明白IO包括且只包括两个流程:等待数据,读取(拷贝)数据
阻塞IO VS 非阻塞IO
阻塞IO在等待数据和拷贝数据阶段都阻塞
非阻塞IO只在数据拷贝阶段阻塞
同步IO VS 异步IO
他俩的区别在数据拷贝阶段:
同步是IO线程发现数据准备好了,然后进行拷贝
异步是内核线程发现数据准备好了,然后内核线程发起数据拷贝,完成之后通知IO线程
同步IO VS 同步阻塞IO
同步io一定是阻塞的,不可能是不阻塞的,因为同步意味着必须拿到数据才可进行之后的操作
异步IO和异步阻塞/非阻塞IO
异步IO是指发起IO请求后,不用拿到IO的数据就可以继续执行。
异步阻塞IO:用户程序发起请求后,继续执行,拷贝数据过程中是阻塞的
异步非阻塞IO:用户程序发起请求后,继续执行,拷贝数据到用户空间的过程中用户程序也不会阻塞
总体:
分层:应用层(netty),框架层(reactor),OS层(nio)
内核会维护两个socket(包含发送、接收缓冲区,等待队列成员)队列:全链接队列(完成tcp三次握手的),半链接队列
怎么确定网络数据属于哪个socket(源端口,源ip,目的端口,目的ip,协议类型)