多路IO复用:epoll

epoll简介

epoll在linux 2.5.44内被引入,FreeBSD, MacOS中类似的实现是kqueueepoll主要解决selectpoll对于fd或事件结构需要O(n)的循环遍历。epoll在注册事件时,会将事件添加到红黑树中并注册回调事件,对红黑树搜索寻找就绪的事件,并添加就绪链表中,因此epoll查找就绪事件的事件复杂度为O(1),在大量事件注册和监控中,epoll会有更好的性能。不同于select, pollepoll使用一组函数完成任务:epoll_createepoll_ctlepoll_wait

epoll_create

epoll_create用于创建一个epoll实例的fd, 后续的函数可以使用这个fd,函数签名:

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
  • size:自从Linux 2.6.8之后size参数被忽略了,在最开始时候,size参数会通知内核后续添加到epoll实例期望添加的fd数量。内核使用这个信息作为fd事件的内部数据结构中分配空间量的提示(内核可能会预留分配更多的空间以免添加fd超过给定size)。目前这个size的提示不再被需要了(内核对动态地分配数据结构的空间大小),但size参数依然需要给一个大于0的数组,这是为了保证在新内核开发的程序在旧内核中前向兼容。

  • flags:如果flags设置成为0,epoll_create1epoll_create没有什么区别,如果flags设置为EPOLL_CLOEXEC,类似于openO_CLOEXEC(Linux 2.6.23+),设置fd标志FD_CLOEXEC为1,即“执行时关闭”(close-on-exec),在调用exec时,该描述符会关闭。避免fd被exec执行进程修改和访问,造成可能的竞争问题。相关内容查阅APUE P60,P66,P201

  • return:返回fd指向新的epoll实例,后续epoll的操作接口会调用这个fd,创建失败返回-1。

epoll_clt

对epoll的事件表进行操作,函数签名:

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:操作的目标epoll实例fd

  • op:具体epoll_clt对epoll实例进行的操作,基本都是和fd事件注册有关系,主要有三种类型:

op操作
EPOLL_CLT_ADD往事件表中注册fd事件
EPOLL_CTL_MOD修改fd上注册的事件
EPOLL_CLT_DEL删除fd上注册的事件
  • fd:表示操作的fd

  • event指定注册的事件,是一个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:具体epoll的事件类型,和poll的事件类型一致,在前面加上"E"就行,比如EPOLLINEPOLLOUT。另外包括了两个额外的事件类型EPOLLETEPOLLONESHOT
    • data:一个联合体,fd成员最多被使用,指定事件所属的目标fd,ptr可以用于指定fd相关用户数据。由于union只能保留一个成员,同时需要fdptr可以在ptr指向用户数据中包含fd
  • return:返回操作成功(0)或者是失败(-1)

epoll_wait

在超时时间内等待一组fd的事件触发,函数签名

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
               int maxevents, int timeout,
               const sigset_t *sigmask);
  • epfd:等待的epoll实例fd

  • events:包含了等待结束后,对调用者可用的就绪事件数组

  • maxevents:指定了最多监听的事件数量,可以设置常量MAX_EVENT_NUMBER

  • timeout:epoll等待的超时时间,和selectpoll类似

为什么事件表构建为RB-Tree

摘录自该博客

epoll和poll的一个很大的区别在于,poll每次调用时都会存在一个将pollfd结构体数组中的每个结构体元素从用户态向内核态中的一个链表节点拷贝的过程,而内核中的这个链表并不会一直保存,当poll运行一次就会重新执行一次上述的拷贝过程,这说明一个问题:poll并不会在内核中为要监听的文件描述符长久的维护一个数据结构来存放他们,而epoll内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用epoll_ctl函数使用EPOLL_CTL_ADD宏来插入,epoll_wait也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当我现在要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的选择。

换句话说,epoll_clt中fd事件注册的增删改查都是能在O(logN)完成的,因此选择红黑树

LT/ET

概念

epoll包含两种对fd的操作模式:LT(Level Trigger,电平触发);ET(Edge Trigger,边沿触发)。对于每个注册事件的fd,epoll默认是工作于LT模式下,在fd对应event注册时如果event->events设置了EPOLLET,epoll会按照ET模式操作该fd。

  • LT:epoll_wait上检测道有事件发生,并通知调用者后,调用者可以不立即处理该事件。在下一次调用epoll_wait时,epoll_wait还是会向调用者通知该事件,直到事件被处理。selectpoll同样是工作于这个模式
  • ET:epoll_wait检测到有事件发生,并通知调用者后,调用者必须立即处理该事件,如果该次事件没有处理,epoll_wait将不会向调用者通知这一事件。该操作方式降低了重复调用epoll_wait的次数

ET工作机制

首先这里列出ET工作正常的两个必要条件,有一个条件不满足,epoll在处理完一次就可能出现阻塞:

  • fd对应文件状态标志O_NONBLOCK
  • fd在上次处理触发事件时,IO直到遇到EWOULDBLOCK错误(所谓的边沿,就是从EWOULDBLOCK错误清空清空的突变)

为更好地理解ET,首先举一个select + blocking服务器读取数据的例子:

ssize_t nbytes;
for (;;) {
    /* select call happens here */
    if (select(FD_SETSIZE, &read_fds, NULL, NULL, NULL) < 0) {
        perror("select");
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < FD_SETSIZE; i++) {
        if (FD_ISSET(i, &read_fds)) {
            /* read call happens here */
            if ((nbytes = read(i, buf, sizeof(buf))) >= 0) {
                handle_read(nbytes, buf);
            } else {
                /* real version needs to handle EINTR correctly */
                perror("read");
                exit(EXIT_FAILURE);
            }
        }
    }
}

假如此时buf大小1024B,一次读入数据64KB,一次读不完,又要select再继续读剩余的部分,会造成128次的系统调用。
一种优化就是把blocking fd设置成O_NONBLOCK,select一次invoke后,允许循环调用read,直到遇到EWOULDBLOCK错误:

ssize_t nbytes;
for (;;) {
    /* select call happens here */
    if (select(FD_SETSIZE, &read_fds, NULL, NULL, NULL) < 0) {
        perror("select");
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < FD_SETSIZE; i++) {
        if (FD_ISSET(i, &read_fds)) {
            /* NEW: loop until EWOULDBLOCK is encountered */
            for (;;) {
                /* read call happens here */
                nbytes = read(i, buf, sizeof(buf));
                if (nbytes >= 0) {
                    handle_read(nbytes, buf);
                } else {
                    if (errno != EWOULDBLOCK) {
                        /* real version needs to handle EINTR correctly */
                        perror("read");
                        exit(EXIT_FAILURE);
                    }
                    break;
                }
            }
        }
    }
}

具体之前说的错误码含义:

  • EWOULDBLOCK: 操作可能会阻塞

  • EAGAIN: 资源暂时不可用(可能和EWOULDBLOCK有相同的errno值)

APUE P389:如果对一个非阻塞fd的操作不能无阻塞地完成(比如数据读完),4.3 BSD返回EWOULDBLOCK,如今基于BSD提供POSIX.1的O_NONBLOCK标志,并且将EWOULDBLOCK定义为和POSIX.1的EAGAIN定义含义相同。

最后总结一下LT/ET。

LT:

  • ET模式和LT模式最大的不同在于调用epoll_wait时。LT模式会遍历每个fd,判断fd的状态是否匹配了event注册的目标条件(如EPOLLIN)。只要有一个fd满足条件,epoll就会解除阻塞。

ET:

  • 需要理解)不会对fd检查,调用epoll_wait后会立刻sleep。当有新的数据进入内核,由于epoll维护了fd的事件表,O(1)时间内可以唤醒进程。

  • ET模式要求程序员完全read/write数据,直到返回EWOULDBLOCK,否则容易造成死锁。比如200B待处理数据,一次ET模式的epoll_wait+read 100B后,epoll_wait不会再次invoke,server一直在等新数据,client一直在等剩余100B数据处理完。

EPOLLONESHOT (Linux 2.6.2+)

应用场景

即使是ET模式下,也不能保证只有一个线程被唤醒(需要理解:像回复所说,问题场景是有多个线程epoll_wait同一个fd?我的理解是当一个线程在处理触发的fd时候,最后一部分数据处理完设置EWOULDBLOCK,但在判断errno之前又有新数据进来,导致fd对应的事件又被触发并由其他线程处理,本线程去判断errno时又没有问题,也会变成继续去处理该fd)。对一个可用fd,如果有多个进程被唤醒并开始对fd做IO,可能会导致竞争问题。

EPOLLONESHOT:和EPOLLET一样设置于epoll_event.events中的事件类型。设置对应fd事件为单次触发,这意味着当一个事件被epoll_wait触发,相关的fd在内部被禁用,和它相关的事件不会再被epoll接口通知。当一个线程的epoll_wait触发了一个fd事件并开始处理,那么不管之后该fd是否满足触发条件,fd对应的事件都不会再被触发,保持了本线程对fd做IO时的独占性。

触发设置EPOLLONESHOT的fd这种独占性会一直保持下去,除非显示地使用epoll_cltEPOLL_CLT_MOD恢复对应事件的epoll_event.events事件类型。

#实例

书上代码实例:GitHub

参考文献

[1]. UNIX环境高级编程
[2]. Linux高性能服务器编程
[3]. https://zh.wikipedia.org/wiki/Epoll
[4]. https://linux.die.net/man/4/epoll
[5]. https://blog.csdn.net/Mr_H9527/article/details/99745659

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值