epoll 详解

用法描述       

epoll是Linux内核为处理大批量文件描述符而设计的IO多路复用机制,它能显著提高程序在存在大量并发连接而只有少部分活跃连接情况下的系统CPU利用率。epoll之所以可以做到如此高的效率是因为它在获取就绪事件的时候,并不会遍历所有被监听的文件描述符集,而只会遍历那些被设备IO事件异步唤醒(CPU中断机制)而加入就绪链表的文件描述符集。
epoll机制提供了三个系统调用给用户态应用程序管理活跃请求:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
  • epoll_create:创建一个epoll的句柄。参数size是内核保证能够正确处理的最大文件描述符数目(现在内核使用红黑树组织epoll相关数据结构,自从linux2.6.8之后,size参数是被忽略的,>0的数即可)。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  • epoll_ctl:操作上面建立的epoll fd,例如,将刚建立的socket fd加入到epoll中让其监控,或者把 epoll正在监控的某个socket fd移出epoll,不再监控它等等。这个函数用于向epoll注册一个事件,而且明确监听的事件类型;第一个参数为epoll句柄,第二个参数表示对这个fd监听事件操作,用宏来表示,有以下三种:
  • EPOLL_CTL_ADD 将fd监听事件添加进epfd中;
  • EPOLL_CTL_MOD 修改已经注册的fd监听事件;
  • EPOLL_CTL_DEL 从epfd中删除fd事件 第三个参数为监听的套接字,第四个参数为监听fd的事件.

第三个参数是需要监听的fd。 第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;
 //感兴趣的事件和被触发的事件
struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

events可以是以下几个宏的集合:  

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); 
EPOLLOUT:表示对应的文件描述符可以写; 
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); 
EPOLLERR:表示对应的文件描述符发生错误; 
EPOLLHUP:表示对应的文件描述符被挂断; 
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

  •  epoll_wait在调用时,在给定的timeout时间内,当在监控的这些文件描述符中的某些文件描述符上有事件发生时,就返回用户态的进程。这个函数用于等待事件的发生.第二个参数是用户自己开辟的一块事件数组,用于存储就绪的事件,第三个参数为这个数组的最大值,就是第二个参数事件数组的最大值,用户想监听fd的个数,第四个参数为超时时间(0表示立即返回,-1表示永久阻塞,直到有就绪事件)

是否所有的文件类型都可以被 epoll 监视? NO。 只有底层驱动实现了 file_operations 中 poll 函数的文件类型才可以被 epoll 监视!socket 类型的文件驱动是实现了 poll 函数的,因此才可以被 epoll 监视。struct file_operations 声明位置是在 include/linux/fs.h 中。

数据结构

一个struct和一个union,分别为struct epoll_event和union epoll_data。

在epoll自身内部,还涉及了多个重要的数据结构,分别为struct eventpoll、struct epitem、struct eppoll_entry、struct ep_pqueue、struct poll_table_struct等。核心数据结构之间的关系如下图所示:

 

epoll实现原理

在linux,一切皆文件。当调用epoll_create时,内核给这个epoll分配一个file,但是这个不是普通的文件,而是只服务于epoll。所以当内核初始化epoll时,会开辟一块内核高速cache区,用于安置我们监听的socket,这些socket会以红黑树的形式保存在内核的cache里,以支持快速的查找、插入和删除,同时建立了一个list链表,用于存储准备就绪的事件。调用epoll_wait时,在timeout时间内,只是简单的观察这个list链表是否有数据,如果没有,则睡眠至超时时间到返回;如果有数据,则在超时时间到时,拷贝至用户态events数组中.

那么,这个准备就绪list链表是怎么维护的呢?

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

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

epoll有两种模式LT(水平触发)和ET(边缘触发)

  • LT模式:epoll默认使用。主要缓冲区数据一次没有处理完,那么下次epoll_wait返回时,还会返回这个句柄;
  • ET模式:缓冲区数据一次没处理结束,那么下次是不会再通知了,只在第一次返回。在ET模式下,一般是通过while循环,一次性读完全部数据。

源码分析 

static int __init eventpoll_init(void)
{
   mutex_init(&pmutex);
   ep_poll_safewake_init(&psw);
   epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem), 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC, NULL);
   pwq_cache = kmem_cache_create("eventpoll_pwq", sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL);
   return 0;
}

epoll用kmem_cache_create(slab分配器)分配内存用来存放struct epitem和struct eppoll_entry。当向系统中添加一个fd时,就创建一个epitem结构体,这是内核管理epoll的基本数据结构:

struct epitem {
    struct rb_node  rbn;        //用于主结构管理的红黑树
    struct list_head  rdllink;  //事件就绪队列
    struct epitem  *next;       //用于主结构体中的链表
    struct epoll_filefd  ffd;   //这个结构体对应的被监听的文件描述符信息
    int  nwait;                 //poll操作中事件的个数
    struct list_head  pwqlist;  //双向链表,保存着被监视文件的等待队列,功能类似select/poll中的poll_table
    struct eventpoll  *ep;      //该项属于哪个主结构体(多个epitm从属于一个eventpoll)
    struct list_head  fllink;   //双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点
    struct epoll_event  event;  //注册的感兴趣的事件,也就是用户空间的epoll_event
}
  • epitem->pwqlist :用来保存这个 epitem 的 poll 等待队列。epitem 是 epoll 中很重要的一种数据结构, 是红黑树和 rdllist 的基本组成元素。需要监听的文件和事件信息,都被包装在 epitem 结构里。每当用户调用 epoll_ctl()新增一个监视文件,都要给这个文件注册一个回调函数 ep_poll_callback, 当网卡收到数据后软中断会调用这个 ep_poll_callback 把这个 epitem 加入到 ep->rdllist 中。
  • pwdlist 就是跟 ep_poll_callback 注册相关的。当调用 epoll_ctl()新增一个监视文件后,内核会为这个 epitem 创建一个 eppoll_entry 对象,通过 eppoll_entry->wait_queue_t->wait_queue_func_t 来设置 ep_poll_callback。pwdlist 为什么要做成一个队列呢,直接设置成 eppoll_entry 对象不就行了吗?实际上不同文件类型实现 file_operations->poll 用到等待队列数量可能不同。虽然大多数都是 1 个,但也有例外。比如“scullpipe”类型的文件就用到了 2 个等待队列。 
  • pwqlist、epitem、fd、epoll_entry、ep_poll_callback 间的关系是这样:

通过十个问题助你彻底理解linux epoll工作原理

 而每个epoll fd(epfd)对应的主要数据结构为:

struct eventpoll {
    spin_lock_t       lock;        //对本数据结构的访问
    struct mutex      mtx;         //防止使用时被删除
    wait_queue_head_t     wq;      //sys_epoll_wait() 使用的等待队列
    wait_queue_head_t   poll_wait;       //file->poll()使用的等待队列
    struct list_head    rdllist;        //事件满足条件的链表
    struct rb_root      rbr;            //用于管理所有fd的红黑树(树根)
    struct epitem      *ovflist;       //将事件到达的fd进行链接起来发送至用户空间
}
  • ep->wq wq 是一个等待队列,用来保存对某一个 epoll 实例调用 epoll_wait()的所有进程。一个进程调用 epoll_wait()后,如果当前还没有任何事件发生,需要让当前进程挂起等待(放到 ep->wq 里);当 epoll 实例监视的文件上有事件发生后,需要唤醒 ep->wq 上的进程去继续执行用户态的业务逻辑。之所以要用一个等待队列来维护关注这个 epoll 的进程,是因为有时候调用 epoll_wait()的不只一个进程,当多个进程都在关注同一个 epoll 实例时,休眠的进程们通过这个等待队列就可以逐个被唤醒了。多个进程关注同一个 epoll 实例,那么有事件发生后先唤醒谁?后唤醒谁?还是一起全唤醒?这涉及到一个称为“惊群效应”的问题。
  • ep->poll_wait 是 epoll 实例中另一个等待队列。当被监视的文件是一个 epoll 类型时,需要用这个等待队列来处理递归唤醒。 epoll 也是一种文件类型,其底层驱动也实现了 file_operations 中的 poll 函数,因此一个 epoll 类型的 fd 可以被其他 epoll 实例监视。而 epoll 类型的 fd 只会有“读就绪”的事件。当 epoll 所监视的非 epoll 类型文件有“读就绪”事件时,当前 epoll 也会进入“读就绪”状态

通过十个问题助你彻底理解linux epoll工作原理

  • ep->rdllist:epoll 实例中包含就绪事件的 fd 组成的链表。通过扫描 ep->rdllist 链表,内核可以轻松获取当前有事件触发的 fd。而不是像 select()/poll() 那样全量扫描所有被监视的 fd,再从中找出有事件就绪的。因此可以说这一点决定了 epoll 的性能是远高于 select/poll 的。(为什么 epoll 中事件就绪的 fd 会“主动”跑到 rdllist 中去,而不用全量扫描就能找到它们呢? 这是因为每当调用 epoll_ctl 新增一个被监视的 fd 时,都会注册一下这个 fd 的回调函数 ep_poll_callback, 当网卡收到数据包会触发一个中断,中断处理函数再回调 ep_poll_callback 将这个 fd 所属的“epitem”添加至 epoll 实例中的 rdllist 中)
  • ep->ovflist:在 rdllist 被占用时,用来在不持有 ep->lock 的情况下收集有就绪事件的 fd。当 epoll 上已经有了一些就绪事件的时候,内核需要扫描 rdllist 将就绪的 fd 返回给用户态。这一步通过 ep_scan_ready_list 函数来实现。其中 sproc 是一个回调函数(也就是 ep_send_events_proc 函数),来处理数据从内核态到用户态的复制。由于 rdllist 链表业务非常繁忙(epoll 增加监视文件、修改监视文件、有事件触发...等情况都需要操作 rdllist),所以在复制数据到用户空间时,加了一个 ep->mtx 互斥锁来保护 epoll 自身数据结构线程安全,此时其他执行流程里有争抢 ep->mtx 的操作都会因命中 ep->mtx 进入休眠。
     

使用问题

1. epoll 惊群

多个进程等待在 ep->wq 上,事件触发后所有进程都被唤醒,但只有其中 1 个进程能够成功继续执行的现象。其他被白白唤起的进程等于做了无用功,可能会造成系统负载过高的问题。为了解决 epoll 惊群,内核后续的高版本又提供了 EPOLLEXCLUSIVE 选项和 SO_REUSEPORT 选项,我个人理解两种解决方案思路上的不同点在于:EPOLLEXCLUSIVE 是在唤起进程阶段起作用,只唤起排在队列最前面的 1 个进程;而 SO_REUSEPORT 是在分配连接时起作用,相当于每个进程自己都有一个独立的 epoll 实例,内核来决策把连接分配给哪个 epoll

2. epmutex、ep->mtx、ep->lock 3 把锁的区别

锁的粒度和使用目的不同。

  1. epmutex 是一个全局互斥锁,epoll 中一共只有 3 个地方用到这把锁。分别是 ep_free() 销毁一个 epoll 实例时、eventpoll_release_file() 清理从 epoll 中已经关闭的文件时、epoll_ctl() 时避免 epoll 间嵌套调用时形成死锁。我的理解是 epmutex 的锁粒度最大,用来处理跨 epoll 实例级别的同步操作。
  2. ep->mtx 是一个 epoll 内部的互斥锁,在 ep_scan_ready_list() 扫描就绪列表、eventpoll_release_file() 中执行 ep_remove()删除一个被监视文件、ep_loop_check_proc()检查 epoll 是否有循环嵌套或过深嵌套、还有 epoll_ctl() 操作被监视文件增删改等处有使用。可以看出上述的函数里都会涉及对 epoll 实例中 rdllist 或红黑树的访问,因此我的理解是 ep->mtx 是一个 epoll 实例内的互斥锁,用来保护 epoll 实例内部的数据结构的线程安全。
  3. ep->lock 是一个 epoll 实例内部的自旋锁,用来保护 ep->rdllist 的线程安全。自旋锁的特点是得不到锁时不会引起进程休眠,所以在 ep_poll_callback 中只能使用 ep->lock,否则就会丢事件。

3. epoll 使用红黑树的目的是什么

用来维护一个 epoll 实例中所有的 epitem。

用户态调用 epoll_ctl()来操作 epoll 的监视文件时,需要增、删、改、查等动作有着比较高的效率。尤其是当 epoll 监视的文件数量达到百万级的时候,选用不同的数据结构带来的效率差异可能非常大。

通过十个问题助你彻底理解linux epoll工作原理

从时间(增、删、改、查、按序遍历)、空间(存储空间大小、扩展性)等方面考量,红黑树都是非常优秀的数据结构(当然这以红黑树比较高的实现复杂度作为代价)。epoll 红黑树中的 epitem 是按什么顺序组织的。阅读代码可以发现是先比较 2 个文件指针的地址大小,如果相同再比较文件 fd 的大小。

通过十个问题助你彻底理解linux epoll工作原理

4. epoll 相关数据结构间的关系

  • 通过十个问题助你彻底理解linux epoll工作原理

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值