netty的epoll和linux的epoll是如何实现的

1.linux的epoll

epoll 是Linux内核中的一种可扩展IO事件处理机制,最早在 Linux 2.5.44内核中引入,可被用于代替POSIX select 和 poll 系统调用,并且在具有大量应用程序请求时能够获得较好的性能( 此时被监视的文件描述符数目非常大,与旧的 select 和 poll 系统调用完成操作所需 O(n) 不同, epoll能在O(1)时间内完成操作,所以性能相当高),epoll 与 FreeBSD的kqueue类似,都向用户空间提供了自己的文件描述符来进行操作。

一、高效的epoll

epoll 是Linux下,高效的多路复用技术,也是Linux下高性能网络服务器的关键技术。 通过网络socket链接,把远程资源加载到本地内存中。如何来优化这个性能呢?

二、epoll提出原因

2.1.poll和select 相对之前的AIO有很大的提高,但是由于需要监视着“等待队列”与及“阻塞进程”,性能还是未完全释放。 这个时候CPU又被别的进程给抢走,上下文切换的性能又被消耗。

2.2.select 要进行遍历,才能感知到那个socket来了数据,因此select只能一个一个遍历,来唤醒每个socketChanel。

三、epoll原理

3.1 epoll监视多个socket, 改进了select维护等待队列和阻塞进程进一步改进。把这两步动作给拆分开来。

3.2 epoll_ctl 维护等待队列

3.3 epoll_wait 阻塞队列

四、eventpoll

4.1 调用epoll_create方法,另外会创建一个eventpoll对象。

4.2 eventpoll维护着就绪列表,如果有socket来数据,就把socket添加到就绪列表。

4.3 阻塞进程收到就绪列表的回调,既可以开始socket数据传输工作。

2.netty的epoll

使用Netty构建服务器时,需要指定parent线程池和child线程池,parent线程负责监听端口,一旦有连接接入,则注册到child线程池中的一个线程上,该连接的IO操作/任务都由该线程完成。 换句话说,一个线程会负责多个连接的IO操作,也就是多路复用。Netty底层是使用系统提供的select或者epoll来实现多路复用的。

先来科普下select/poll/epoll。

select/poll

服务端建立每个连接,相当于打开文件,会获得对应的文件描述符(fd),相同的源IP/源端口/目标IP/目标端口对应同一个fd。

select和poll是相似的,不一样的地方是,select是使用数组,有连接数限制,而poll使用链表,无连接数限制。

监听连接时,从用户层的角度看, (1)会构建3个fd数组,分别关注读/写/异常事件,设置超时时间,调用系统提供的select方法。 (2)调用select方法时,需要将fd数组传到内核态,等待部分fd就绪后,把fd数组(包含就绪状态)返回到用户态 (3)用户程序对fd数组进行遍历,处理就绪的fd (4)重新调用select方法。

可以看出不好的地方是(1)每次都要传入fd数组,返回整个fd数组,导致了大量在用户空间和内核空间的相互拷贝。 (2)用户程序仍需要遍历fd数组才能找出就绪的fd

从系统层的角度看,调用select方法时 (1)遍历fd数组,对于每个fd,调用其对应的poll方法(由设备对应的驱动程序实现),将fd所在线程加入等待队列,并且检查就绪状态,记录感兴趣的就绪状态。 (2)如果存在感兴趣的就绪状态,直接返回 (3)如果不存在感兴趣的就绪状态,进入休眠,等待fd就绪后,会唤醒等待队列中的线程 (4)被唤醒后,重复1-4的操作。

可以看出不好的地方是每次都需要检查所有fd。

epoll

epoll相对select改善了很多。 (1)在使用epoll时,首先会构建epoll对象。 (2)有连接接入时,会插入到epoll对象中,epoll对象里实际是一个红黑树+双向链表,fd插入到红黑树中,通过红黑树查找到是否重复 (3)一旦fd就绪,会触发回调把fd的插入到就绪链表中,并唤醒等待队列中的线程。 (4)调用epoll_wait方法时只需要检查就绪链表,如有则返回给用户程序,如没有进入等待队列。

由于epoll把fd管理起来,不需要每次都重复传入,而且只返回就绪的fd,因此减少了用户空间和内核空间的相互拷贝,在fd数量庞大的时候更加高效。

Netty可以选择使用不同的多路复用技术。

NioEventLoop

NioEventLoop底层会根据系统选择select或者epoll。如果是windows系统,则底层使用WindowsSelectorProvider(select)实现多路复用;如果是linux,则使用epoll

当为select模式,在NioEventLoop对应的Selector中会维护着newKeys,updateKeys,cancelledKeys,分别是新增的fd,更新fd的感兴趣状态,取消fd监听。每当连接接入,或者断连,都会调用NioEventLoop的注册/解除注册方法,更新这几个集合。(这里是JDK11的实现,在JDK8中则直接更新PollArrayWrapper)

NioEventLoop在运行的时候,会不断的监听注册的连接,核心逻辑在doSelect方法中,主要的几个操作是 (1)processUpdateQueue,将newKeys,updateKeys,cancelledKeys中的key更新到PollArrayWrapper中 (2)subSelector.poll(),这里实际是调用native方法,会将PollArrayWrapper的fd拷贝至readFds/writeFds/exceptFds,并监听这些fd。

private native int poll0(long pollAddress, int numfds,
             int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
​

(3)updateSelectedKeys,遍历freadFds/writeFds/exceptFds,将就绪的fd存储到selectedKey中

当存在fd就绪后,doSelect方法返回,应用程序可以遍历selectedKey进行处理。

EpollEventLoop

EpollEventLoop底层使用epoll实现多路复用。

EpollEventLoop中会初始化epollFd、eventFd、timerFd。

  • epollFd是调用系统方法生成的epoll对象,后续会使用其管理所有需要监听的fd

  • eventFd是用于线程通讯,程序会把eventFd添加到epollFd中,监听eventFd,一旦eventFd有操作,则会唤醒调用epoll_wait的线程。

  • timerFd是用于计时,同样的监听timerFd,一旦时间到达,则会唤醒调用epoll_wait的线程。

每当连接接入或者断连,都会调用epoll_ctl_add/epoll_ctl_del方法来操作epoll对象。

在EpollEventLoop的run方法中,会调用epoll_wait来监听所有fd,一旦有fd就绪,会拷贝至EpollEventArray中,应用程序遍历EpollEventArray处理所有就绪事件。