epoll的详解

epoll之前是select

PPC(Process per connection)和TPC(thread per connection)是进程和线程被相继提出来之后,面对高并发问题时首先被提出的方案,也是相对自然的IO方案,但是随着io并发数量的提高,对内存的高消耗和创建销毁时cpu造成的效率损失,这种方案的适应性受到质疑。于是新的io管理策略被提出来,也就是io多路复用,所谓的多路复用指的是用一个进程或者一个线程来同时管理多路IO(socket),实现一个高并发的管理策略,而select就是linux中的IO多路复用方案:

select的实现

select的实现是通过一个select系统调用和多个宏指令完成io管理过程的,其实现过程很精巧,接下来我们从源码角度分析一下该模型的优势和存在的问题。

关键词:最大连接受限,select系统调用,线性轮询,内存拷贝

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
/****
select系统调用有五个参数
maxfdp-->返回就绪队列的fd的最大数量,也就是进程当前监听的socket的数量+1,后面会介绍为什么要加一;
readset-->等待读入的fds,传入监听的fds和返回就绪的fds
writeset-->等待写入的fds,同上
exceptset-->异常等待的事件
timeout-->轮询的时间(unit:ms),如果设置成null,如果没有就绪的fds就一直等待
select所有过程都是通过这一个系统调用来实现的,所以需要在用户态和内核态之间不断的复制数据,并且仅仅维护一个监听io线性链表,内部需要不断的轮询寻找就绪的socket,检查io的状态,注意和伪IO的区别,给fd设置回调函数,以设置就绪io
****/

我们需要重点关注一下fd_set数据类型,该数据类型表征监听的文件描述符状态的位图,其中最大连接数量受限于sizeof(fd_set), 在当前内核中是1024;

源码剖析

1. 准备工作

#include <sys/select.h>
//fd_set-->关键数据类型,用来表征文件描述符的活动状态
 typedef struct
 {
     /* XPG4.2 requires this member name.  Otherwise avoid the name
        from the global namespace.  */
 #ifdef __USE_XOPEN
     __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
 # define __FDS_BITS(set) ((set)->fds_bits)
 #else
     __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
 # define __FDS_BITS(set) ((set)->__fds_bits)
 #endif
} fd_set;
//在/usr/include/bits/typesizes.h指定为1024;
#define FD_SETSIZE  _FD_SETSIZE
//几个用于操作fd_set的宏
//将各位清零
void FD_CLR(int fd, fd_set *set);
//判断当前描述符是否置位
int  FD_ISSET(int fd, fd_set *set);
//将当前描述符置位
void FD_SET(int fd, fd_set *set);
//取出所有为零的位
void FD_ZERO(fd_set *set);

2. 主调函数

SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
        fd_set __user *, exp, struct timeval __user *, tvp)
{
    struct timespec end_time, *to = NULL;
    struct timeval tv;
    int ret;
    if (tvp) {
        //将timeout从用户态复制进内核态,并计算出绝对时间
        if (copy_from_user(&tv, tvp, sizeof(tv)))
            return -EFAULT;
        to = &end_time;
        if (poll_select_set_timeout(to,
                tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
                (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
            return -EINVAL;
    }
 //核心例程------------------>>>>>>>>>>
    ret = core_sys_select(n, inp, outp, exp, to);
//
    ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
 
    return ret;
}
/**转向core_sys_select的调用
*
*
**/
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
               fd_set __user *exp, struct timespec *end_time)
{
    fd_set_bits fds;//用于维护六种fds的位图,
    void *bits;
    int ret, max_fds;
    unsigned int size;
    struct fdtable *fdt;//文件描述符表
    /* Allocate small arguments on the stack to save memory and be faster */
    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
 
    ret = -EINVAL;
    if (n < 0)
        goto out_nofds;
 
    /* max_fds can increase, so grab it once to avoid race */
    rcu_read_lock();//rcu锁的使用,适合一写多读,读锁很轻量级,仅仅关闭抢占即可
    fdt = files_fdtable(current->files);
    max_fds = fdt->max_fds;
    rcu_read_unlock();
    if (n > max_fds)
        n = max_fds;
 
    /*
     * We need 6 bitmaps (in/out/ex for both incoming and outgoing),
     * since we used fdset we need to allocate memory in units of
     * long-words. 
     */
    size = FDS_BYTES(n);//将最大文件数需要的位图转化为以字节为单位的大小
    bits = stack_fds;//栈上开辟空间的首地址
      //栈上空间如果不够,需要到slab内存里面额外分配
    if (size > sizeof(stack_fds) / 6) {
        /* Not enough space in on-stack array; must use kmalloc,kmalloc在 */

        ret = -ENOMEM;
        bits = kmalloc(6 * size, GFP_KERNEL);
        if (!bits)
            goto out_nofds;
    }
    fds.in      = bits;
    fds.out     = bits +   size;
    fds.ex      = bits + 2*size;
    fds.res_in  = bits + 3*size;
    fds.res_out = bits + 4*size;
    fds.res_ex  = bits + 5*size;
 
    if ((ret = get_fd_set(n, inp, fds.in)) ||
        (ret = get_fd_set(n, outp, fds.out)) ||
        (ret = get_fd_set(n, exp, fds.ex)))
        goto out;
    zero_fd_set(n, fds.res_in);
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);
 
    ret = do_select(n, &fds, end_time);
 
    if (ret < 0)
        goto out;
    if (!ret) {
        ret = -ERESTARTNOHAND;
        if (signal_pending(current))
            goto out;
        ret = 0;
    }
 
    if (set_fd_set(n, inp, fds.res_in) ||
        set_fd_set(n, outp, fds.res_out) ||
        set_fd_set(n, exp, fds.res_ex))
        ret = -EFAULT;
 
out:
    if (bits != stack_fds)
        kfree(bits);
out_nofds:
    return ret;
}

fds中的各个成员分别按照最大fds的数量来获取位图在栈上的位置,并通过get_fd_set()将用户空间的位图复制到内核态,然后将返回缓冲区置零,等待设置,接下来最重要的就是do_select函数,该函数将不断轮询各fds的状态(通过file->poll()),将就绪的fd加入到返回队列中,并同时检查等待时间,这里是通过被动检查的方式获取fd上的io事件,也就是poll函数的返回值。注意最后函数返回的时候,也需要使用用户传进来的地址对返回的内容进行装载,也就是这里的set_fd_set操作;

/**转向do_select的调用
*
*
**/
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
    ktime_t expire, *to = NULL;
    struct poll_wqueues table;
    poll_table *wait;
    int retval, i, timed_out = 0;
    unsigned long slack = 0;
 
    rcu_read_lock();
    retval = max_select_fd(n, fds);//根据给定的文件描述符表修正n的值
    rcu_read_unlock();
 
    if (retval < 0)
        return retval;
    n = retval;
 
    poll_initwait(&table);//初始化文件等待队列
    wait = &table.pt;//将文件等待队列和打开文件表关联起来
    if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
        wait->_qproc = NULL;
        timed_out = 1;
    }
 
    if (end_time && !timed_out)
        slack = select_estimate_accuracy(end_time);//时间修正算法
 
    retval = 0;
    for (;;) {
        unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
 
        inp = fds->in; outp = fds->out; exp = fds->ex;
        rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
 
        for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
            unsigned long in, out, ex, all_bits, bit = 1, mask, j;
            unsigned long res_in = 0, res_out = 0, res_ex = 0;
     //获取每一sizeof(unsigned long)32位长度的位图
            in = *inp++; out = *outp++; ex = *exp++;
            all_bits = in | out | ex;
            if (all_bits == 0) {
                i += BITS_PER_LONG;
                continue;
            }
   //处理每一long的位图
            for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
                struct fd f;
                if (i >= n)
                    break;
//这里可以看出来是从第一位开始,所以需要加一
                if (!(bit & all_bits))
                    continue;
                f = fdget(i);//i是从外循环计数的,所以这里i就是文件描述符
                if (f.file) {
                    const struct file_operations *f_op;
                    f_op = f.file->f_op;
                    mask = DEFAULT_POLLMASK;
//检查看看这里的文件对象支不支持poll选项
                    if (f_op && f_op->poll) {
                        wait_key_set(wait, in, out, bit);
//将文件注册入相应的等待队列,如果文件已经注册,则只获取并获取状态
                        mask = (*f_op->poll)(f.file, wait);
                    }
                    fdput(f);
//将就绪的文件描述符加入到用户态队列中
                    if ((mask & POLLIN_SET) && (in & bit)) {
                        res_in |= bit;
                        retval++;
                        wait->_qproc = NULL;
                    }
                    if ((mask & POLLOUT_SET) && (out & bit)) {
                        res_out |= bit;
                        retval++;
                        wait->_qproc = NULL;
                    }
                    if ((mask & POLLEX_SET) && (ex & bit)) {
                        res_ex |= bit;
                        retval++;
                        wait->_qproc = NULL;
                    }
                }
            }
            if (res_in)
                *rinp = res_in;
            if (res_out)
                *routp = res_out;
            if (res_ex)
                *rexp = res_ex;
            cond_resched();
        }
        wait->_qproc = NULL;
//如果有事件准备就绪,等待时间用完,当前进程有挂起信号则返回
        if (retval || timed_out || signal_pending(current))
            break;
        if (table.error) {
            retval = table.error;
            break;
        }
 
        /*
         * If this is the first loop and we have a timeout
         * given, then we convert to ktime_t and set the to
         * pointer to the expiry value.
         */
        if (end_time && !to) {
            expire = timespec_to_ktime(*end_time);
            to = &expire;
        }
 //时间片没有用完,则重新获取等待时间
        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
                       to, slack))
            timed_out = 1;
    }
 
    poll_freewait(&table);
 
    return retval;
}

这个函数实现整个轮询现场,首先外层的无限循环构成整个wait事件到来的主体,内部两个循环是用来分片检查各个fd对应事件是否到来(就绪?),并同时检查等待时间,一旦有事件到来或者时间用完就直接返回。内核对用户态采取绝不信任的原则,所以内核不会直接使用用户传进来的地址值,而是使用相应的函数将用户态内容复制到内核态,同样的,返回给用户态时就要进行一步将就绪的fds位图按值复制给用户态(绝不信任意味着,用户态的地址不能在内核态使用(共享内存除外),一切传入和返回必须按值拷贝,这样就增加了系统的负担,所以就出现了后来的各种零拷贝技术

至此我们的select源码部分就分析的差不多了,接下来我们做个简短的总结

  • select依赖于fd_set这个结构体,而支持的连接数量受限于系统的__FD_SETBITS参数,一般为1024,当然这种位图的实现就降低了用户态与内核态之间的拷贝开销;
  • select的实现依赖于内存在内核和用户空间之间的拷贝,需要支持相应的开销,当连接数增加时这种开销会变得难以负担;
  • select使用主动轮询的方式来获取所有被监听io上就绪的事件,即轮询的代价随着连接数量的增长呈线性增加
    select有效的支持了高并发请求,并使得单线程或者单进程能够同时管理监听多个网络io,而之前都是以多线程的方式支持这种多连接。这大大提高了编程的效率,和系统性能的可扩展性,但是由于上述的三个主要问题的存在,随着网络io的爆发式增长,这种io管理方式仍然无法有效的适应,所有了一个改进版poll

poll

poll是对select的一个简单的改进版本,性能上不存在明显的差异,只是有效的解决了io数量受限的问题,接下来我们了解一下poll是如何改进这个问题的。
poll重新定义了一个用于存放fd的结构体,而不是使用fd_set这样的位图结构

struct pollfd

{

int fd;               /* 文件描述符 */

short events;        /* 等待的事件 */

short revents;       /* 实际发生了的事件 */

} ;

typedef unsigned long   nfds_t;
  • struct pollfd * fds:是一个struct pollfd结构类型的数组,用于存放需要检测其状态的socket描述符;每当调用这个函数之后,系统不需要清空这个数组,操作起来比较方便;特别是对于 socket连接比较多的情况下,在一定程度上可以提高处理的效率;这一点与select()函数不同,调用select()函数之后,select() 函数需要清空它所检测的socket描述符集合,导致每次调用select()之前都必须把socket描述符重新加入到待检测的集合中;因此,select()函数适合于只检测少量socket描述符的情况,而poll()函数适合于大量socket描述符的情况;
  • 如果待检测的socket描述符为负值,则对这个描述符的检测就会被忽略,也就是不会对成员变量events进行检测,在events上注册的事件也会被忽略,poll()函数返回的时候,会把成员变量revents设置为0,表示没有事件发生;

1. 合法的事件如下:

  • POLLIN 有数据可读。
  • POLLRDNORM 有普通数据可读。
  • POLLRDBAND 有优先数据可读。
  • POLLPRI 有紧迫数据可读。
  • POLLOUT 写数据不会导致阻塞。
  • POLLWRNORM 写普通数据不会导致阻塞。
  • POLLWRBAND 写优先数据不会导致阻塞。
  • POLLMSG SIGPOLL 消息可用。

此外,revents域中还可能返回下列事件:

  • POLLER 指定的文件描述符发生错误;
  • POLLHUP 指定的文件描述符挂起事件。
  • POLLNVAL 指定的文件描述符非法。

这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。使用poll()和select()不一样,你不需要显式地请求异常情况报告。

POLLIN | POLLPRI等价于select()的读事件,
POLLOUT |POLLWRBAND等价于select()的写事件。
POLLIN等价于POLLRDNORM |POLLRDBAND,
而POLLOUT则等价于POLLWRNORM

如果是对一个描述符上的多个事件感兴趣的话,可以把这些常量标记之间进行按位或运算就可以了;
比如:对socket描述符fd上的读、写、异常事件感兴趣,就可以这样做:
struct pollfd fds;
fds[nIndex].events=POLLIN | POLLOUT | POLLERR;

当 poll()函数返回时,要判断所检测的socket描述符上发生的事件,可以这样做:
  • 检测可读TCP连接请求:if((fds[nIndex].revents & POLLIN) == POLLIN){//接收数据/调用accept()接收连接请求}
  • 检测可写:
    if((fds[nIndex].revents & POLLOUT) == POLLOUT){//发送数据}
  • 检测异常:
    if((fds[nIndex].revents & POLLERR) == POLLERR){//异常处理}
    nfds_t nfds:用于标记数组fds中的结构体元素的总数量;
    timeout:是poll函数调用阻塞的时间,单位:毫秒
    如果timeout==0,那么 poll() 函数立即返回而不阻塞,
    如果timeout==INFTIM,那么poll() 函数会一直阻塞下去,直到所检测的socket描述符上的感兴趣的事件发 生是才返回,如果感兴趣的事件永远不发生,那么poll()就会永远阻塞下去;

2. 返回值:

大于0:数组fds中准备好读、写或出错状态的那些socket描述符的总数量;
等于0:数组fds中没有任何socket描述符准备好读、写,或出错;此时poll超时,超时时间是timeout毫秒;换句话说,如果所检测的 socket描述符上没有任何事件发生的话,那么poll()函数会阻塞timeout所指定的毫秒时间长度之后返回,
-1: poll函数调用失败,同时会自动设置全局变量errno;errno为下列值之一:

3. 错误代码

EBADF: 一个或多个结构体中指定的文件描述符无效。
EFAULTfds : 指针指向的地址超出进程的地址空间。
EINTR : 请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds : 参数超出PLIMIT_NOFILE值。
ENOMEM : 可用内存不足,无法完成请求。

4. 实现机制

poll是一个系统调用,其内核入口函数为sys_poll,sys_poll几乎不做任何处理直接调用do_sys_poll,do_sys_poll的执行过程可以分为三个部分:

  • 将用户传入的pollfd数组拷贝到内核空间,因此拷贝操作和数组长度相关,时间上这是一个O(n)操作,这一步的代码在do_sys_poll中包括从函数开始到调用do_poll前的部分。
  • 查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时,挂起操作是通过调用schedule_timeout执行的。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是O(n),这里面不包括等待时间。相关代码在do_poll函数中。
  • 将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是O(n),具体代码包括do_sys_poll函数中调用do_poll后到结束的部分。

5. 注意事项

  • poll() 函数不会受到socket描述符上的O_NDELAY标记和O_NONBLOCK标记的影响和制约,也就是说,不管socket是阻塞的还是非阻塞 的,poll()函数都不会受到影响;
  • poll()函数则只有个别的的操作系统提供支持(如:SunOS、Solaris、AIX、HP提供 支持,但是Linux不提供支持),可移植性差;

接下来我们看看epoll是如何解决select和poll是仍然存在的问题


content

  • epoll的概念
  • 和epoll相关的结构体
  • epoll_create
  • epoll_ctl
  • epoll_wait

1. epoll的概念

epoll 全称 eventpoll,是 linux 内核2.6以后IO多路复用(IO multiplexing)的一个改进版实现,顾名思义,我们可以理解epoll是一种基于事件被动响应机制的io管理方案。IO多路复用的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。在select和poll的基础上,epoll主要在以下几个方面做出了改进:
  • 更高级的内存机制(slab分配,内存共享)避免了内核和用户之间的数据拷贝;
  • 被动响应diss主动轮询;
  • 数据结构级的性能优化(红黑树提高查询效率,就绪链表存储就绪时间,中间链表用于存储epoll_wait返回后产生的就绪事件)

2. epoll相关的结构体

  • 大总管eventpoll
// epoll的核心实现对应于一个epoll描述符  
struct eventpoll {  
    spinlock_t lock;  //自旋锁,用于进程间同步,忙等
    struct mutex mtx;  //互斥锁,用于临界区互斥访问,会pending,c++里面会配合条件变量使用
    wait_queue_head_t wq; // epoll_wait中pending的进程就被加入到该等待队列里面
    wait_queue_head_t poll_wait;  // f_op->poll()时,监听的fds事件在这里等待
    struct list_head rdllist;   //已就绪的epitem 列表,将它与未就绪的事件分开,有助于快速给epoll_wait做出响应 
    struct rb_root rbr;   //保存所有加入到当前epoll的文件对应的epitem,快速查询  
    struct epitem *ovflist;  // 当正在向用户空间复制数据时, 产生的就绪事件,所以在下一次epoll_wait到来之时需要将该链表中的数据加入到返回队列中去;  
    struct user_struct *user;  
    struct file *file;  //该文件描述符会被映射至一个匿名文件,用于建立自己的文件系统
    int visited;  
    struct list_head visited_list_link;  //用于避免重复查询,深度递归查询,优化查询结构
}  

epoll_create就是用于创建该结构体并获得一个指针epfd

  • 普通员工epitem
struct epitem {  
    struct rb_node rbn;  
    struct list_head rdllink;  
    struct epitem *next;  
    struct epoll_filefd ffd;  //文件描述符fd+file信息,公共构成了红黑树节点的key
    int nwait;  //被挂载的poll_wait数量
    struct list_head pwqlist;  //一个文件可能被监听多个事件,所以需要用链表把这多个事件所在的wait_queue串起来
    // 当前epitem 的所有者  
    struct eventpoll *ep;  //指向父节点
    struct list_head fllink;  //文件链表啥的,没搞懂暂时
    struct epoll_event event;  //ctl传入的文件事件,两个域,第一个域是events,第二个域data是一个联合体,但是一般情况下我们使用void *ptr指向用户创建的结构体,便于内核给用户返回数据用
};  
  • 被操作的实体eppoll_entry
// 与一个文件上的一个wait_queue_head 相关联,因为同一文件可能有多个等待的事件,
//这些事件可能使用不同的等待队列 
struct eppoll_entry {  
    struct list_head llink;  //pwq_list的节点
    struct epitem *base; //父节点  
    wait_queue_t wait;  //wait_queue中的节点
    wait_queue_head_t *whead;  //wait_queue的头结点
};

这些结构体的设计很精巧,既赋予epoll足够的io管理能力(fd的多事件等待队列,用于管理io的红黑树,用于转载就绪事件的链表),同时也为用户提供了便利的数据传递接口(poll_event便于就绪队列的返回,epoll_ctl接口使得内核和用户态成功解耦)。他们从某种程度上相当于重建了一个epoll文件系统,其中的目录项(epitem)和文件节点(eppoll_entry)均在slab高速cache中分配;

2. epoll_create

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    int error, fd;
    struct eventpoll *ep = NULL;
    struct file *file;
 
    /* 持久化设置  */
    BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
 
    if (flags & ~EPOLL_CLOEXEC)
        return -EINVAL;
    /*
     * Create the internal data structure ("struct eventpoll").
     */
    error = ep_alloc(&ep);//分配相应的结构体
    if (error < 0)
        return error;
    /*
     * Creates all the items needed to setup an eventpoll file. That is,
     * a file structure and a free file descriptor.
     */
    fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
    if (fd < 0) {
        error = fd;
        goto out_free_ep;

    }
    file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
                 O_RDWR | (flags & O_CLOEXEC));//创建匿名文件inode
    if (IS_ERR(file)) {
        error = PTR_ERR(file);
        goto out_free_fd;
    }
    ep->file = file;
    fd_install(fd, file);//将file和fd连接起来
    return fd;
 
out_free_fd:
    put_unused_fd(fd);
out_free_ep:
    ep_free(ep);
    return error;
}
epoll_create里面主要创建event_poll和其内部数据,并构建一个匿名文件用于映射当前的文件系统,便于将加入进来的fd装载进这样的文件系统,并返回匿名文件的文件描述符;注意:文件对象(匿名)属于当前进程内核数据,只能被内核态访问,用户态访问必须使用相应接口,这里借助文件系统的思维解耦了用户应用层和内核底层。

3. epoll_ctl

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
        struct epoll_event __user *, event)
{
    int error;
    int did_lock_epmutex = 0;
    struct file *file, *tfile;
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;
 
    error = -EFAULT;
    if (ep_op_has_event(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto error_return;
 
    /* Get the "struct file *" for the eventpoll file */
    error = -EBADF;
    file = fget(epfd);
    if (!file)
        goto error_return;
 
    /* Get the "struct file *" for the target file */
    tfile = fget(fd);
    if (!tfile)
        goto error_fput;
 
    /* The target file descriptor must support poll */
    error = -EPERM;
    if (!tfile->f_op || !tfile->f_op->poll)
        goto error_tgt_fput;
 
    /* Check if EPOLLWAKEUP is allowed */
    if ((epds.events & EPOLLWAKEUP) && !capable(CAP_BLOCK_SUSPEND))
        epds.events &= ~EPOLLWAKEUP;
 
    /*
     * We have to check that the file structure underneath the file descriptor
     * the user passed to us _is_ an eventpoll file. And also we do not permit
     * adding an epoll file descriptor inside itself.
     */
    error = -EINVAL;
    if (file == tfile || !is_file_epoll(file))
        goto error_tgt_fput;
 
    /*
     * At this point it is safe to assume that the "private_data" contains
     * our own data structure.
     */
    ep = file->private_data;
 
    /*
     * When we insert an epoll file descriptor, inside another epoll file
     * descriptor, there is the change of creating closed loops, which are
     * better be handled here, than in more critical paths. While we are
     * checking for loops we also determine the list of files reachable
     * and hang them on the tfile_check_list, so we can check that we
     * haven't created too many possible wakeup paths.
     *
     * We need to hold the epmutex across both ep_insert and ep_remove
     * b/c we want to make sure we are looking at a coherent view of
     * epoll network.
     */
    if (op == EPOLL_CTL_ADD || op == EPOLL_CTL_DEL) {
        mutex_lock(&epmutex);
        did_lock_epmutex = 1;
    }
    if (op == EPOLL_CTL_ADD) {
        if (is_file_epoll(tfile)) {
            error = -ELOOP;
            if (ep_loop_check(ep, tfile) != 0) {
                clear_tfile_check_list();
                goto error_tgt_fput;
            }
        } else
            list_add(&tfile->f_tfile_llink, &tfile_check_list);
    }
 
    mutex_lock_nested(&ep->mtx, 0);
 
    /*
     * Try to lookup the file inside our RB tree, Since we grabbed "mtx"
     * above, we can be sure to be able to use the item looked up by
     * ep_find() till we release the mutex.
     */
    epi = ep_find(ep, tfile, fd);
 
    error = -EINVAL;
    switch (op) {
    case EPOLL_CTL_ADD:
        if (!epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_insert(ep, &epds, tfile, fd);
        } else
            error = -EEXIST;
        clear_tfile_check_list();
        break;
    case EPOLL_CTL_DEL:
        if (epi)
            error = ep_remove(ep, epi);
        else
            error = -ENOENT;
        break;
    case EPOLL_CTL_MOD:
        if (epi) {
            epds.events |= POLLERR | POLLHUP;
            error = ep_modify(ep, epi, &epds);
        } else
            error = -ENOENT;
        break;
    }
    mutex_unlock(&ep->mtx);
 
error_tgt_fput:
    if (did_lock_epmutex)
        mutex_unlock(&epmutex);
 
    fput(tfile);
error_fput:
    fput(file);
error_return:
 
    return error;
}
epoll_ctl主要干以下几件事
  • 检查动作是否存在,将用户空间的epoll_event复制进内核空间,注意event的data结是一个指向用户态的指针,不可信任;
  • 根据传进来的fd获取文件对象,然后判断当前文件对象是否支持poll操作;
  • 检查文件对象和当前epoll指向的匿名文件是否相同,避免循环递归;
  • 检查当前文件是否被重复访问(只在当前ctl接口被访问时),之前的check_list被使用到(这种情况什么时候会发生??);
  • 加锁访问红黑树,使用fd和file指针;
  • 根据操作类型选择epoll接口,ep_insert,ep_modify,ep_remove;
  • 然后根据新加入的fd事件构建epitem节点,并将其加入到红黑树, 注册初始化poll回调函数指针ep_ptable_queue_proc(这里只需要初始化一次,而select需要多次初始化),该回调函数进一步将epitem加入到相应的等待队列上,然后给给其注册一个回调函数ep_poll_callback,用于有事件到来的时候将其添加进就绪队列,并给epoll_wait所在的等待队列发送wakeup通知,唤醒所有等待就绪事件的epoll_wait;

4. epoll_wait

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
        int, maxevents, int, timeout)
////
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)

epoll_wait做那几件事

epoll_wait-->ep_poll中等待检查就绪队列,睡眠,唤醒--->ep_send_events--->ep_scan_ready_list,将rdlist数据拷贝到txlist(用于用户数据的拷贝),清空rdlist,准备ovflist --->ep_send_events_proc, 将txlist数据拷贝到用户空间,并*检查是否是LT模式,如果是将event重新加入到rdlist--->并将ovflist在这一过程中收到的数据加入到rdlist中,供下次访问;

spin_lock_irqsave(&ep->lock, flags);
    /* 这一步要注意, 首先, 所有监听到events的epitem都链到rdllist上了,
     * 但是这一步之后, 所有的epitem都转移到了txlist上, 而rdllist被清空了,
     * 要注意哦, rdllist已经被清空了! */
list_splice_init(&ep->rdllist, &txlist);
    /* ovflist, 在ep_poll_callback()里面我解释过, 此时此刻我们不希望
     * 有新的event加入到ready list中了, 如果调用ep_poll_callback()函
     *数的时候发现epoll对象eventpoll的ovflist成员不等于EP_UNACTIVE_PTR
     *的话,说明此时正在扫描rdllist链表,这个时候会将就绪事件对应的epitem
     *对象加入到ovflist链表暂存起来,等rdllist链表扫描完之后在将ovflist链表中
     *的内容移动到rdllist链表中保存后下次再处理... */
ep->ovflist = NULL;
spin_unlock_irqrestore(&ep->lock, flags);
  • 如果未指定超时直接跳转到check_events,通常用于非阻塞fd。指定超时,走正常流程fetch_events:将当前进程挂在等待队列睡眠,当相应待监听事件就绪时会有回调ep_poll_callback唤醒。唤醒时调用ep_events_available检查就绪链表,不像select每次都需要轮询,这里epoll只需要检查下就绪链表是否为空(时间复杂度O(1))。ep_send_events调用ep_scan_ready_list执行回调ep_read_events_proc遍历就绪链表并传入用户空间以及回调执行期间发生的事件通过eventpoll的ovflist(回调执行前置空)将就绪fd去重插入就绪链表以便下一次epoll_wait调用处理,这里还涉及ET和LT模式下的不同处理,暂时不作分

总结:

  • epoll使用匿名文件映射的方式实现了用户态和内核态高效的消息传递
  • 使用红黑树提高查询效率,使用就绪链表存储需要返回给用户态的fds,这个数量相对较小,所以拷贝带来的开销就相应降低
  • 使用AIO的方式给每一个监听事件注册一个ep_poll_callback的回调函数,该函数主动将就绪事件加入到就绪队列,无需epoll主动轮询,是epoll_wait能够快速返回
references:

https://blog.csdn.net/baiye_xing/article/details/76352935
https://tqr.ink/2017/10/05/implementation-of-epoll/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容