epoll模型讲解/源码分析

epoll模型

在select/poll出现之前我们只能通过read/write的IO操作来从流中读取数据,当然在少量IO操作的时候完全是可靠的,但是当IO操作快速增长时甚至到了大规模并发阶段,这样的IO就显得捉襟见肘了。
首先我们来理解一个内核缓冲区的概念,假设A,B两个分别作为写入方与读出方,假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。
假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满
也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了,我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。

为了避免非阻塞轮询时CPU的空转,引入了select,select会将当前线程阻塞,当有一个或多个IO事件发生时,当前线程就被唤醒,但这时我们仍需遍历一遍所有的流以确定哪些流的事件到达了。也就是说使用select会带来O(n)时间复杂度的轮询时间,epoll可理解为event poll,即epoll将有IO事件发生的流放入一个链表中,并一次性返回到用户空间,把轮询复杂度复杂度降低到了O(1)

系统调用

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提供的API这里就简单介绍一下:

  • epoll_create用于创建一个句柄,自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  • epoll_ctl可以操作上面建立的epoll fd,例如,将刚建立的socket fd加入到epoll中让其监控,或者把 epoll正在监控的某个socket fd移出epoll,不再监控它等等。
  • epoll_wait在调用时,在给定的timeout时间内,当在监控的这些文件描述符中的某些文件描述符上有事件发生时,就返回用户态的进程。

深入内部

数据组织

epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。即epoll用了更适合小内存数据块I/O的slab分配器,这也大大提升了epoll的性能。
epoll有两个个重要的数据结构,一个用于存储外来fd的红黑树,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。此外epoll还会再建立一个就绪链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。

实现原理

首先我们先来看一下epoll源代码功能的实现流程图:
epoll.png-46kB
接下来我们讨论epoll解决高并发的细节问题:
首先看一下epoll源码内部两个主要的数据结构,第一个是struct eventpoll,每创建一个epollfd, 内核就会分配一个eventpoll与之对应。

struct eventpoll {
    spinlock_t lock;                /* 自旋锁*/
    struct mutex mtx;               /* 互斥锁*/
    wait_queue_head_t wq;           /* 调用epoll_wait()时的等待队列*/
    wait_queue_head_t poll_wait;    /* file->poll()使用的等待队列 */
    struct list_head rdllist;       /* 所有已经ready的epitem都在这个链表里面 */
    struct rb_root rbr;             /* 所有要监听的epitem都在这里 */
    struct epitem *ovflist;         /*这是一个单链表链接着所有的struct epitem当event转移到用户空间时*/
    struct user_struct *user;       /* 这里保存了一些用户变量, 比如fd监听数量的最大值等等 */
};

第二个是struct epitem结构,当向系统中添加一个fd时,就创建一个epitem结构体,这是内核管理epoll的基本数据结构。

struct epitem {
    struct rb_node rbn;             /* 用于主结构管理的红黑树 */
    struct list_head rdllink;       /* 链表节点, 所有已经ready的epitem都会被链到eventpoll的rdllist中 */
    struct epitem *next;            /* 用于主结构体中的链表 */
    struct epoll_filefd ffd;        /* 这个结构体对应的被监听的文件描述符信息 */
    int nwait;                      /* poll操作中事件的个数 */
    struct list_head pwqlist;       /* 双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table */
    struct eventpoll *ep;           /* 当前epitem属于哪个eventpoll */
    struct list_head fllink;        /* 双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点 */
    struct epoll_event event;       /* 当前的epitem关系哪些events, 这个数据是调用epoll_ctl时从用户态传递过来 */
};

1.执行epoll_create时,创建了红黑树就绪list链表,struct eventpoll在调用epoll_create时被创建

/* 你没看错, 这就是epoll_create()的真身, 基本啥也不干直接调用epoll_create1了,
 * 另外你也可以发现, size这个参数其实是没有任何用处的... */
SYSCALL_DEFINE1(epoll_create, int, size)
{
        if (size <= 0)
                return -EINVAL;
        return sys_epoll_create1(0);
}
/* 这里是真正的sys_epoll_create*/
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    int error;
    struct eventpoll *ep = NULL;    /* 主描述符 */

    ...

    error = ep_alloc(&ep);          /* ep_alloc(struct eventpoll **pep)为pep分配内存,并初始化 */

    ...

    /*
     * Creates all the items needed to setup an eventpoll file. That is,
     * a file structure and a free file descriptor.
     */
    /* 这里是创建一个匿名fd, 说起来就话长了...长话短说:
     * epollfd本身并不存在一个真正的文件与之对应, 所以内核需要创建一个
     * "虚拟"的文件, 并为之分配真正的struct file结构, 而且有真正的fd.
     * 因此我们可以看到epoll_create会返回一个fd.
     * 这里2个参数比较关键:
     * eventpoll_fops, fops就是file operations, 就是当你对这个文件(这里是虚拟的)进行操作(比如读)时,
     * fops里面的函数指针指向真正的操作实现, 类似C++里面虚函数和子类的概念.
     * epoll只实现了poll和release(就是close)操作, 其它文件系统操作都有VFS全权处理了.
     * ep, ep就是struct epollevent, 它会作为一个私有数据保存在struct file的private指针里面.
     * 其实说白了, 就是为了能通过fd找到struct file, 通过struct file能找到eventpoll结构.
     */
    err
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值