JAVA面试题分享九十二:Epoll 底层原理?

一、简介

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本。

二、epoll的三个方法

  • epoll_create: 内核会创建一个eventpoll对象(专用的文件描述符,也就是程序中epfd所代表的对象)
    eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列。
  • epoll_ctl: 添加待监控的socket

如果通过spoll_ctl添加sock1、sock2和sock3的监视,内核会将三个socket添加到eventpoll监听队列

  • epoll_wait: 阻塞等待

进程A运行到了epoll_wait语句之后,进程A会等到eventpoll的等待队列。

三、epoll 原理详解

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

struct eventpoll {  ...  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,  也就是这个epoll监控的事件*/  struct rb_root rbr;  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/  struct list_head rdllist;  ...};

我们在调用 epoll_create 时,内核除了帮我们在 epoll 文件系统里建了个 file 结点,在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 rdllist 双向链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 rdllist 双向链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。所以,epoll_wait 非常高效。

所有添加到 epoll 中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做 ep_poll_callback,它会把这样的事件放到上面的 rdllist 双向链表中。

在 epoll 中对于每一个事件都会建立一个 epitem 结构体,如下所示:

struct epitem {  ...  //红黑树节点  struct rb_node rbn;  //双向链表节点  struct list_head rdllink;  //事件句柄等信息  struct epoll_filefd ffd;  //指向其所属的eventepoll对象  struct eventpoll *ep;  //期待的事件类型  struct epoll_event event;  ...}; // 这里包含每一个事件对应着的信息。

当调用 epoll_wait 检查是否有发生事件的连接时,只是检查 eventpoll 对象中的 rdllist 双向链表是否有 epitem 元素而已,如果 rdllist 链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此 epoll_waitx 效率非常高。epoll_ctl 在向 epoll 对象中添加、修改、删除事件时,从 rbr 红黑树中查找事件也非常快,也就是说 epoll 是非常高效的,它可以轻易地处理百万级别的并发连接。

【总结】:

一颗红黑树,一张准备就绪句柄链表,少量的内核 cache,就帮我们解决了大并发下的 socket 处理问题。

  • 执行 epoll_create()时,创建了红黑树和就绪链表;

  • 执行 epoll_ctl()时,如果增加 socket 句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;

  • 执行 epoll_wait()时立刻返回准备就绪链表里的数据即可。

 四、epoll 的两种触发模式

epoll 有 EPOLLLT 和 EPOLLET 两种触发模式,LT 是默认的模式,ET 是“高速”模式。

  • LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作;

  • ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果 ET 模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

还有一个特点是,epoll 使用“事件”的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用类似 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知。

【epoll 为什么要有 EPOLLET 触发模式?】:

如果采用 EPOLLLT 模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用 epoll_wait 都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用 EPOLLET 这种边缘触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

【总结】:

  • ET 模式(边缘触发)只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致 epoll_wait 返回;

  • LT 模式(水平触发,默认)只要有数据都会触发,缓冲区剩余未读尽的数据会导致 epoll_wait 返回。

五、epoll 反应堆模型

【epoll 模型原来的流程】:

epoll_create(); // 创建监听红黑树epoll_ctl(); // 向书上添加监听fdepoll_wait(); // 监听有监听fd事件发送--->返回监听满足数组--->判断返回数组元素--->lfd满足accept--->返回cfd---->read()读数据--->write()给客户端回应。

【epoll 反应堆模型的流程】:

epoll_create(); // 创建监听红黑树epoll_ctl(); // 向书上添加监听fdepoll_wait(); // 监听有客户端连接上来--->lfd调用acceptconn()--->将cfd挂载到红黑树上监听其读事件--->epoll_wait()返回cfd--->cfd回调recvdata()--->将cfd摘下来监听写事件--->epoll_wait()返回cfd--->cfd回调senddata()--->将cfd摘下来监听读事件--->...--->

六、epoll相对于select和poll的优化措施

优化措施一:

  • 功能分离: 进程到等待队列,进程阻塞
select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步揍合二为一。
大多数的应用场景中,需要监视的socket数量相对固定,并不需要每次都修改。

epoll将这两个操作分开,首先调用epoll_ctl维护监听队列,在调用epoll_wait阻塞进程。

 

优化措施二:

  • 引入了就绪队列rdlist
select低效的另一个原因在于程序不知道那些socket收到数据,只能一个一个遍历。如果内核维护一个“就绪队列”rdlist,引用收到数据的socket,就能避免遍历。

 

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值