IO多路复用之epoll

IO多路复用之epoll总结

基本概念

相对于之前介绍的select和poll, epoll更加的灵活而且没有描述符数量的限制。epoll使用一个文件描述符管理多个描述符,将用户关心的文件描述符以及事件存放到内核的一个事件表中,这样在用户空间和内核空间只需要copy一次。

epoll接口

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

    创建一个epoll句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select的第一个参数,select的第一个参数是最大的fd+1的值。epoll_create创建好之后也会占用一个fd,因此我们在使用完epoll之后需要调用close()来关闭句柄,否则的话会导致fd被耗尽。

  2. epoll_ctl

    epoll的事件注册函数,它有别于select。select是在监听事件的时候告诉内核要监听什么类型的事件,而epoll是在这里注册监听事件,当关心的事件类型发生的话,内核会调用该事件注册的callback函数。第一个参数是epoll_create的返回值,第二个参数表示工作,有如下几种:

    • EPOLL_CTL_ADD: 注册新的fd到epollfd中
    • EPOLL_CTL_MOD:修改已经注册的fd的监听事件
    • EPOLL_CTL_DEL:从epollfd中删除一个监听的fd

    第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么类型的事件,struct epoll_evet如下:

    struct epoll_event {
        __uint32_t events;  /*Epoll events*/
        epoll_data data; /*User data variable*/
    }
    
    struct epoll_data {
        void *ptr;
        int fd;
        __uint32_t u32;
        __uint64_t u64;
    }

    events可以是如下几种类型的宏的集合:

    • EPOLLIN:表示对应的文件描述符可读
    • EPOLLOUT:表示对应的文件描述符可写
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读
    • EPOLLERR: 表示对应的文件描述符发生错误
    • EPOLLHUP:表示对应的文件描述符被挂断
    • EPOLLET:将EPOLL设置为边缘触发,默认epoll是LT触发的
    • EPOLLONESHOT:只监听一次事件,如果还需要监听这个socket的话那么需要再次将这个socket加入到EPOLL队列里
  3. epoll_wait

    参数event用来从内核得到事件的集合,maxevents告诉内核这个events有多大,不能超过epoll_create的时候size。timeout是超时时间(0表示立即返回)。函数的返回结果是需要处理的事件数目。

epoll的工作模式

上面我们提到一个LT模式和ET模式。这是epoll工作的两种模式,LT模式是epoll的默认模式。

  • LT模式:当epoll_wait检测到有关心的事件发生并通知应用程序,应用程序可以不立即处理该事件,下次调用epoll_wait的时候epoll会再次通知此事件。还有一种情况即如果应用程序在处理该事件的时候没有将所有的数据读取完,那么之后调用epoll_wait的时候仍然会通知应用程序缓冲中有数据
  • ET模式:当epoll_wait检测到关心的事件发生并通知给应用程序,应用程序必须立即处理该事件,因为下一次调用epoll_wait不会再通知此事件了。

ET模式再很大程序减少了epoll事件被重复触发的次数,效率比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞接口,避免由于一个句柄的阻塞读/写把其他多个文件描述符的任务饿死。

epoll的工作原理

epoll比select/poll的优越之处就在于:后者每次调用的时候都要传递你所有监控的所有的socket给select/poll系统调用,这就需要将用户态的socket列表copy到内核态,如果句柄过多的话会非常低效。而我们调用epoll_wait的时候是不需要传递socket句柄给内核的,因为在epoll_ctl的时候已经拿到了句柄列表。

那么epoll是如何实现监听的呢?

我们在调用epoll_create的时候内核就已经开始分配必要的空间,创建必要的数据结构来为我们保存监控的句柄了。每次我们调用epoll_ctl的时候也只是往内核的数据结构中塞入新的socket句柄。

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

epoll在被内核初始化的时候,会申请开辟自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核的cache区,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页。

epoll的优势即当我们调用很多句柄的时候,epoll_wait仍然具有很高的效率,并将有效的发生事件的句柄给我们,由于我们在调用epoll_create时候,除了帮我们在epoll文件系统建立一个file结点,在内核cache里建立一个红黑树用于存储epoll_ctl传来的socket外,还会再建立一个list链表,用户存储准备就绪的事件。当epoll_wait调用的时候,仅仅只需要观察这个list链表里有没有数据即可。而且当有事件发生的时候,epoll_wait也只是从内核态copu少量的句柄到用户态而已。

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

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

上面说的LT模式中,如果上一次的事件没有处理完成那么下一次调用epoll_wait的时候仍然会触发事件。那么这个是怎么做到的呢? 我们在调用epoll_wait的时候会将准备就绪的socket拷贝到用户态,然后清空准备就绪list链表,而如果不是在ET模式的话,并且这些socket上确实有未处理的事件的话,epoll_wait会把这些句柄放回到刚刚清空的准备就绪链表中。这就实现了LT模式下只要有事件epoll_wait每次就会返回的功能。

参考链接:
http://www.cnblogs.com/Anker/p/3263780.html
http://blog.csdn.net/hdutigerkin/article/details/7517390

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值