epoll实现原理的理解

  Epoll是Linux IO的多路复用的机制,是select/poll的增强版本,在Linux内核fs/eventpoll.c中可以查看epoll的具体的实现。

一、epoll数据结构

  学习任何组件,首先得知道它有什么数据结构或者数据类型,epoll主要有两个结构体:eventpoll和epitem。epitem是每一个IO对应的事件,比如EPOLL_CTL_ADD操作时,就需要创建一个epitem;eventpoll是每一个epoll所对应的,比如epoll_create就是创建一个eventpoll。

struct epitem {
    union {
        /* RB tree node links this structure to the eventpoll RB tree */
        struct rb_node rbn;
        /* Used to free the struct epitem */
        struct rcu_head rcu;
    };

    /* List header used to link this structure to the eventpoll ready list */
    struct list_head rdllink;

    /*
     * Works together "struct eventpoll"->ovflist in keeping the
     * single linked chain of items.
     */
    struct epitem *next;

    /* The file descriptor information this item refers to */
    struct epoll_filefd ffd;//sockfd

    /* List containing poll wait queues */
    struct eppoll_entry *pwqlist;

    /* The "container" of this item */
    struct eventpoll *ep;

    /* List header used to link this item to the "struct file" items list */
    struct hlist_node fllink;

    /* wakeup_source used when EPOLLWAKEUP is set */
    struct wakeup_source __rcu *ws;

    /* The structure that describe the interested events and the source fd */
    struct epoll_event event;
};

struct eventpoll {

    struct mutex mtx;

    /* Wait queue used by sys_epoll_wait() */
    wait_queue_head_t wq;

    /* Wait queue used by file->poll() */
    wait_queue_head_t poll_wait;

    /* List of ready file descriptors */
    struct list_head rdllist;

    /* Lock which protects rdllist and ovflist */
    rwlock_t lock;

    /* RB tree root used to store monitored fd structs */
    struct rb_root_cached rbr;

    /*
     * This is a single linked list that chains all the "struct epitem" that
     * happened while transferring ready events to userspace w/out
     * holding ->lock.
     */
    struct epitem *ovflist;

    /* wakeup_source used when ep_scan_ready_list is running */
    struct wakeup_source *ws;

    /* The user that created the eventpoll descriptor */
    struct user_struct *user;

    struct file *file;

    /* used to optimize loop detection check */
    u64 gen;
    struct hlist_head refs;

#ifdef CONFIG_NET_RX_BUSY_POLL
    /* used to track busy poll napi_id */
    unsigned int napi_id;
#endif

#ifdef CONFIG_DEBUG_LOCK_ALLOC
    /* tracks wakeup nests for lockdep validation */
    u8 nests;
#endif
};

   数据结构如下图所示。

   list用来存储就绪的IO,rbtree用来存储所有IO,方便快速查找fd,这两种数据结构我们都从inster和remove来讨论。对于list,当内核IO准备就绪时,则执行epoll_event_callback的回调函数,将epitem添加到list中;当epoll_wait激活重新运行时,将list的epitem 逐一拷贝到events中,并删除list中被拷贝出来的epitem。

对于rbtree又该何时添加何时删除呢?当app执行epoll_ctl(EPOLL_CTL_ADD)操作,将epitem添加到rbtree中;当app执行epoll_ctl(EPOLL_CTL_DEL)操作,将对应的epitem从rbtree中删除。那么list和rbtree又如何做到线程安全呢?

二、epoll锁的机制

  list使用最小粒度的锁spinlock,便于在SMP下添加操作的时候,能够快速操作list。避免SMP体系下,多核竞争,此处采用自旋锁,不适合采用睡眠锁;添加操作如下。

    (1)获取spinlock

    (2)epitem的rdy置为1,代表epitem已在就绪队列中

    (3)添加到list

    (4)将eventpoll的rdnum加1

    (5)释放spinlock

  删除则与添加类似。

  对于rbtree的操作使用互斥锁,过程如下:

    (1)获取互斥锁

    (2)查找sockid的epitem是否存在,不存在可以添加

    (3)分配epitem

    (4)sockid赋值

    (5)设置event添加到epitem的event域

    (6)将epitem添加到rbtree

    (7)释放互斥锁

  这里的互斥锁,锁的是整颗树,而不是节点;删除则与之类似操作。

三、epoll回调

  首先要知道回调函数何时执行,此部分需要与tcp的协议栈联系起来理解。

  (1)三次握手完成时,把fd加入到就绪队列,把event置为EPOLLIN可读,此时标识可以进入到accept读取socket数据;

  (2)recvbuffer有数据的时候(可读数据),找到对应的fd加入到就绪队列,把event置EPOLLIN为可读;

    (3)sendbuffer有空隙的时候(可发数据),找到对应的fd加入到就绪队列,把event置EPOLLOUT为可写;

    (4)接收到fin的时候(断开连接),找到对应的fd加入到就绪队列,把event置EPOLLIN为可读;

 

四、LT与ET

   (1)LT是水平出发,有数据就一直触发,只要recvBuffer里面有数据就一直触发,直到数据读取完,适用于大块;

  (2)ET是边沿触发,从没有数据到有数据,才触发,只触发一次,即使没读完数据,也不会再触发去读取剩余的数据,剩余的数据等待下一次触发再读,适用于小块;

 

思考:

  就绪集合为啥使用队列而不适用栈?

    首先就绪集合的数据本身就需要遍历所有,肯定使用链式的数据结构,如果使用栈,就会存在就绪节点一次那不完的情况,导致上一次没被取出的节点,在下一次epoll_wait再拿的时候也可能拿不到,导致出现一些就绪节点永远都不被处理。

   epoll的误区:
  (1)epoll性能高,里面有内存映射,mmap
  (2)epoll比select/poll要高,在fd很少时select/poll比epoll更好

 

posted @ 2021-10-17 20:34  MrJuJu  阅读(339)  评论(0编辑  收藏  举报