论epoll的实现

  1. 上一篇博客 论select的实现 里面已经说了为什么 select 比较慢。poll 的实现和 select 类似,只是少了最大 fd 限制,如果有兴趣可以自己去看代码。我这里来简单来过一下 epoll 的实现。

1) 一次添加

select / poll 为了实现简单,不对已有的 fd 进行管理。每次需要传入最大的轮询 fd, 然后每个监听 fd 挂到设备一次导致性能不佳。epoll 对于监听的 fd,通过 epoll_ctl 来把对应的 fd 添加到红黑树,实现快速的查询和添加,删除。

1.1) epoll 实例创建

使用 epoll 之前会使用 epoll_create 创建一个 epoll 实例,它实际上是一个文件, 只是存在于内存中的文件。下面实现来自 linux-2.6.24 的 fs/eventpoll.c:

asmlinkage long sys_epoll_create(int size) {...
struct eventpoll *ep;...// 创建 eventpoll 实例if (size <= 0 || (error = ep_alloc(&ep)) != 0)
goto error_return;...// 为 epoll 文件添加文件操作函数
error = anon_inode_getfd(&fd, &inode, &file, "[eventpoll]",&eventpoll_fops, ep);}

epoll_create 的参数 size 是老版本的实现,使用的是 hash 表, size 应该是用来算 bucket 数目,后面因为使用红黑树,这个参数不再使用, 可以忽略。

1.2) 添加 fd

asmlinkage long sys_epoll_ctl(int epfd, int op, int fd,
struct epoll_event __user *event) {...// 先查找 fd 是否已经存在
epi = ep_find(ep, tfile, fd); error = -EINVAL;
switch (op) {// 如果是添加,就插入到 eventpoll 实例的红黑树
case EPOLL_CTL_ADD:if (!epi) {
epds.events |= POLLERR | POLLHUP;// 添加监听的 fd 到epoll
error = ep_insert(ep, &epds, tfile, fd);} else
error = -EEXIST;break;...}}

接着调用 ep_insert 是添加 fd 到红黑树以及把进程的回调函数添加文件句柄的监听队列,当有事件到来时,会唤醒进程。

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd){...// 创建 epitem 并设置回调函数 ep_ptable_queue_procinit_poll_funcptr(&epq.pt, ep_ptable_queue_proc);...// 这里会回调 ep_ptable_queue_proc, 并查询 fd 可读写状态
revents = tfile->f_op->poll(tfile, &epq.pt);...// epitem 添加到 eventpoll 的红黑树ep_rbtree_insert(ep, epi);...}

ep_ptable_queue_proc 这个回调函数, 除了把进程添加文件句柄的监听列表,并注册回调函数为 ep_poll_callback。 这个函数会查询 fd 的读写状态, 如果当前文件句柄可以读写,就把当前的 fd 添加到就绪队列。后续查询是否有 fd 可以读写,只要只要拷贝这个就绪列表,不用查询。我们下面会来看看 epoll_wait 的实现。

2) 快速查询

epoll 之所以快,除了没有多次重复挂载事件之外,在有读写事件到来的实现,也是很高效。没有像 select/poll 那样, 需要轮询所有的fd, 才能知道哪些 fd 有事件需要处理。

epoll 有一个专门的链表用来存放哪些 fd 有事件到来,用户空间需要查询哪些 fd 有读写等待处理,只需要拷贝这个链表即可。

我们使用系统调用 epoll_wait, 会到内核调用 sys_epoll_wait, 这个函数的主要实现就是调用了 ep_poll:

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout){...
res = 0;if (list_empty(&ep->rdllist)) {// 如果没有事件, 不断等待读写事件到来// 直到超时,或者有读写事件for (;;) {/*
* We don't want to sleep if the ep_poll_callback() sends us
* a wakeup in between. That's why we set the task state
* to TASK_INTERRUPTIBLE before doing the checks.
*/// 当前进程设置为可中断set_current_state(TASK_INTERRUPTIBLE);// 有连接就绪if (!list_empty(&ep->rdllist) || !jtimeout)break;if (signal_pending(current)) {
res = -EINTR;break;}}}//如果 rdllist 不为空, 说明有事件到来。
eavail = !list_empty(&ep->rdllist);spin_unlock_irqrestore(&ep->lock, flags);// 拷贝到用户空间if (!res && eavail &&!(res = ep_send_events(ep, events, maxevents)) && jtimeout)
goto retry;return res;}

我们可以看到,epoll 在实现 epoll_wait的时候,并不会去查询 fd 的可读写状态。 而是等待 fd 有读写到来时, 通过回调函数把有事件到来的 fd 主动拷贝到 rdllist。

另外一个有一个细节点就是, 当用户在拷贝事件到用户空间时,刚好有事件到来,那么这些读写事件会不会正好丢了。答案是当然不会,epoll
准备了另外一个链表,叫 overflow list, 当检查正在拷贝时,会把这些 fd 临时放到这个链表,下次再拷贝到 rdllist.

3) 总结

select/poll 还有比较坑的是,每次查询到 fd 读写事件结果之后,需要把所有 fd 对应的结果的 bitmap 拷贝到用户空间。 比如监听 100w 的 fd, 只有一个 fd 有读写事件, 却要拷贝 100w fd的结果 bitmap。

对比来看,select/poll 实现极为简单,但并不适合用来维护大量的连接。




开发高性能网络程序时,windows开发者们言必称iocp,linux开发者们则言必称epoll。大家都明白epoll是一种IO多路复用技
术,可以非常高效的处理数以百万计的socket句柄,比起以前的select和poll效率高大发了。我们用起epoll来都感觉挺爽,确实快,那么,
它到底为什么可以高速处理这么多并发连接呢?

先简单回顾下如何使用C库封装的3个epoll系统调用吧。

  1. int epoll_create(int size);
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  3. int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

使用起来很清晰,首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。

epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。

epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。

从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给
select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存
到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已
经在epoll_ctl中拿到了要监控的句柄列表。

所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。

在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。

epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些
socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上
建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

  1. static int __init eventpoll_init(void)
  2. {
  3. ... ...
  4. /* Allocates slab cache used to allocate "struct epitem" items */
  5. epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),
  6. 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,
  7. NULL, NULL);
  8. /* Allocates slab cache used to allocate "struct eppoll_entry" */
  9. pwq_cache = kmem_cache_create("eventpoll_pwq",
  10. sizeof(struct eppoll_entry), 0,
  11. EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);
  12. ... ...

epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄
给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树
用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这
个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非
常高效。

而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!

那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的
红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个
socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create
时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然
后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。

这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,
会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是
ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要
它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从
epoll_wait返回的。

论epoll的实现的更多相关文章

  1. 从I/O复用谈epoll为什么高效

    上一篇文章中,谈了一些网络编程的基本概念.在现实使用中,用的最多的就是I/O复用了,无非就是select,poll,epoll 很多人提到网络就说epoll,认为epoll效率是最高的.单纯的这么认为 ...

  2. select、poll、epoll之间的区别总结

    select.poll.epoll之间的区别总结 05/05. 2014 select,poll,epoll都是IO多路复用的机制.I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪 ...

  3. (转载) Linux IO模式及 select、poll、epoll详解

    注:本文是对众多博客的学习和总结,可能存在理解错误.请带着怀疑的眼光,同时如果有错误希望能指出. 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案 ...

  4. linux下select/poll/epoll机制的比较

    select.poll.epoll简介 epoll跟select都能提供多路I/O复用的解决方案.在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSI ...

  5. epoll LT/ET 深度剖析

    EPOLL事件的两种模型: Level Triggered (LT) 水平触发 .socket接收缓冲区不为空 有数据可读 读事件一直触发 .socket发送缓冲区不满 可以继续写入数据 写事件一直触 ...

  6. 非阻塞/异步(epoll) openssl

    前段时间在自己的异步网络框架handy中添加openssl的支持,当时在网络上搜索了半天也没有找到很好的例子,后来自己慢慢的摸索,耗费不少时间,终于搞定.因此把相关的资料整理一下,并给出简单的例子,让 ...

  7. select,epoll,poll比较

    介绍和比较 http://www.cnblogs.com/maociping/p/5132583.html 比较 http://www.dataguru.cn/thread-336032-1-1.ht ...

  8. Linux epoll

    一. epoll函数集 epoll主要有三个函数: 1. int epoll_create(int size); 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大.这个参数不同于 ...

  9. linux下epoll实现机制

    linux下epoll实现机制 原作者:陶辉 链接:http://blog.csdn.net/russell_tao/article/details/7160071 先简单回顾下如何使用C库封装的se ...

  10. select、poll、epoll之间的区别总结[整理]

    select,poll,epoll都是IO多路复用的机制.I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作.但select ...

随机推荐

  1. (转发)InputAccessoryView的使用方法

    转自:http://blog.sina.com.cn/s/blog_45e2b66c01015we9.html UITextFields and UITextViews have an inputAc ...

  2. 从 Objective-C 里的 Alloc 和 AllocWithZone 谈起

    一.问题起源 一切起源于Apple官方文档里面关于单例(Singleton)的示范代码:Creating a Singleton Instance.主要的争议集中在下面这一段: static MyGi ...

  3. iOS9适配总结

    每年iOS升级,都会带来一些坑,这次iOS9也不例外.本文总结了微信在适配iOS9上遇到的问题和解决方案. 一.iOS9问题汇总   1. 编译问题(Bitcode) 大部分人升级到Xcode7后,首 ...

  4. 51nod——2502最多分成多少块

    数据范围好小... 题目中没说要升序降序,不过样例解释里可以看出是要升序. #include <bits/stdc++.h> using namespace std; ],b[],visi ...

  5. 深入理解 hashcode 和 hash 算法

    深入理解 hashcode 和 hash 算法 2017年12月30日 23:06:07 阅读数:5197 标签: hashhashmaphashcode二进制 更多 个人分类: jdk-源码  ht ...

  6. matplotlib绘图(三)

    matplotlib中2D图形的绘制 直方图 直方图的参数只有一个x,不像条形图需要传入x,y 直方图作用:是统计x在某个区间上出现的次数 直方图是条形图的一种形式 hist()的参数: #bins ...

  7. Node项目实战-静态资源服务器

    打开github,在github上创建新项目: Repository name: anydoor Descripotion: Tiny NodeJS Static Web server 选择:publ ...

  8. python模块之pickle

    和json不同的是: json只支持str,int,tuple,list,dict. pickle支持python里所有的数据类型,但是只能在python里序列化,不跨平台,python独有. 代码示 ...

  9. python模块之sys

    sys.argv 命令行参数List,第一个元素是程序本身路径 sys.exit(n) 退出程序,正常退出时exit(0) sys.version 获取Python解释程序的版本信息 sys.maxi ...

  10. 字符串:HDU5371-Hotaru's problem(manacher 的应用)

    Hotaru's problem Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others) Pr ...