select,poll,epoll(第二版)

背景:

全链路异步:

  • 应用层:编程模型的异步:响应式编程
  • 框架层:IO线程的异步:一个IO线程只能处理一个请求=====》一个IO线程只能处理多个请求,经典模型:reactor模型

     将Reactor分为两部分mainReactor和subReactor
     mainReactor负责处理新的连接事件,将后续的事件处理交给subReactor
     subReactor处理时间的方式,由阻塞变为多线程,引入了任务队列的模型

  • OS层:IO模型的异步
    很重要
    原因:一般的select和poll,都是同步IO,不是异步
             同步IO中,线程切换(会导致内核线程占比cpu很高,原因是线程上下文切换),IO事件的轮询,IO的操作都涉及到系统调用

 

IO的发展历程:

  1. 阻塞式IO(bio),一个连接对应一条线程
  2. 非阻塞式io:用户线程在发起IO请求后就可以返回
  3. io复用(nio):怎么做到的:把IO事件(单独线程)和IO操作(各自线程)分开,select阻塞直到有就绪事件,然后再遍历fd列表(O(n)
  4. 信号驱动式io:只是多了回调函数,在等待的过程中是异步的,但是在数据从内核复制到用户缓冲区是阻塞的
  5. 异步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)

通过两个手段解决了上面的不足

  1. 在内核中使用了红黑树来跟踪进程所有的待检测文件描述符(时间复杂度一般为O(logn)),减少了内核和用户空间大量的数据拷贝和内存分配。
  2. 使用了事件驱动的机制,内核里维护了一个链表来记录就绪事件。不需要轮询检测了。

epoll是Linux下高效的I/O多路复用机制之一,可以有效地处理大量的并发连接,提高网络应用程序的性能和吞吐量。epoll有三种工作模式:LT模式、ET模式和EPOLLONESHOT模式

  1. LT模式(Level Triggered,水平触发)

在LT模式下,当一个描述符上有可读或可写事件发生时,epoll_wait()函数会立即返回,应用程序可以在返回的events列表中处理这些事件。如果应用程序没有处理完所有的事件,下次调用epoll_wait()函数时会再次返回这些事件。

LT模式是epoll的默认模式,也是最常用的模式。它的优点是容易使用,可以直接处理每个事件,缺点是在高并发情况下,可能会导致频繁的epoll_wait()函数调用和事件处理,影响程序性能。

  1. ET模式(Edge Triggered,边缘触发)

在ET模式下,epoll_wait()函数只会在描述符上出现可读或可写事件时返回,而不是像LT模式一样每次调用都会返回所有的事件。当应用程序处理完这些事件后,必须立即读取或写入数据,直到返回EAGAIN错误,否则将无法再次接收到该描述符上的事件。

ET模式的优点是可以提高程序性能,减少epoll_wait()函数的调用次数。缺点是使用起来相对复杂,需要注意每个事件的处理方式和状态。

  1. 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,协议类型)

 

posted @ 2022-07-17 18:57  山野村夫01  阅读(42)  评论(0编辑  收藏  举报