简谈epoll

I/O编程关注的问题

epoll我们经常使用在网络编程I/O模型中,在此模型中我们主要关注的问题点是连接如何建立,连接何时断开,消息如何到达,消息是否发送完毕。 用户空间监测内核的消息得到这个结果。
在这里插入图片描述
在上图的蓝色框中,我们主要的关注的是读写是否被阻塞,数据是否达到,到达了怎么通知给用户空间。

阻塞io模型和⾮阻塞io模型

  • 阻塞在哪里?
  • 什么来决定阻塞还是非阻塞?
//连接的fd
fcntl(c->fd, F_SETFL, O_NONBLOCK);
  • 阻塞和非阻塞具体的差异是什么?
io函数在数据未到达时是否⽴刻返回

在这里插入图片描述
在这里插入图片描述
阻塞模型和非阻塞模型主要区别在数据准备阶段是否立刻返回,如果是在阻塞模型,数据准备阶段和数据拷贝阶段都会被阻塞,处理时间会比较长;在非阻塞模型中,如果在数据准备阶段,调用read/recv会立刻给一个结果(为准备好返回-1)。

I/O多路复用

I/O多路复用主要有slect, poll 和 epoll 三个主要的函数。这里我们主要介绍epoll函数。实现的机制主要是使用一个线程来检测多个io事件,把相应的事件fd添加到epoll中,使用epoll来管理。如果读写事件准备好时,epoll会触发相应的世间来通知到用户。
在这里插入图片描述

epoll的API

epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表组成。

struct eventpoll {
// ...
struct rb_root rbr; // 管理 epoll 监听的事件
struct list_head rdllist; // 保存着 epoll_wait 返回满⾜条件的事件
// ...
};
struct epitem {
// ...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对象
struct epoll_event event; // 注册的事件类型
// ...
};
struct epoll_event {
__uint32_t events;
epoll_data_t data; // 保存 关联数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;

主要函数

  • epoll_create系统调⽤
int epoll_create(int size);

size参数告诉内核这个epoll对象会处理的事件⼤致数量,⽽不是能够处理的事件的最⼤数(同时,size不要传0,会报invalid argument错误)。
在现在linux版本中,这个size参数已经没有意义了;
返回: epoll对象句柄;之后针对该epoll的操作需要通过该句柄来标识该epoll对象;

  • epoll_ctl系统调⽤
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

typedef union epoll_data {
    void *ptr; /* 指向用户自定义数据 */
    int fd; /* 注册的文件描述符 */
    uint32_t u32; /* 32-bit integer */
    uint64_t u64; /* 64-bit integer */
} epoll_data_t;

struct epoll_event {
    uint32_t events; /* 描述epoll事件 */
    epoll_data_t data; /* 见上面的结构体 */
};

epoll_ctl向epoll对象添加、修改或删除事件;
返回: 0表示成功, -1表示错误,根据errno错误码判断错误类型。
op类型:

EPOLL_CTL_ADD 添加新的事件到epoll中
EPOLL_CTL_MOD 修改epoll中的事件
EPOLL_CTL_DEL 删除epoll中的事件

event.events 取值:

EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包; FIN包就是read 返回 0)
EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起⾮阻塞tcp连接,连接建⽴成功事件相当于可写事件)
EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接
EPOLLPRI 表示连接上有紧急数据需要读
EPOLLERR 表示连接发⽣错误
EPOLLHUP 表示连接被挂起
EPOLLET 将触发⽅式设置为边缘触发,系统默认为⽔平触发
EPOLLONESHOT 表示该事件只处理⼀次,下次需要处理时需重新加⼊epoll
  • epoll_wait系统调⽤
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

收集 epoll 监控的事件中已经发⽣的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待timeout 毫秒后返回。
返回:表示当前发⽣的事件个数
返回0表示本次没有事件发⽣;
返回-1表示出现错误,需要检查errno错误码判断错误类型。

注意:
events 这个数组必须在⽤户态分配内存,内核负责把就绪事件复制到该数组中;
maxevents 表示本次可以返回的最⼤事件数⽬,⼀般设置为 events 数组的⻓度;
timeout表示在没有检测到事件发⽣时最多等待的时间;如果设置为0,检测到rdllist为空⽴刻返回;如果设置为-1,⼀直等待;
所有添加到epoll中的事件都会与⽹卡驱动程序建⽴回调关系,相应的事件发⽣时会调⽤这⾥的回调⽅法(ep_poll_callback) ,它会把这样的事件放在rdllist双向链表中。
在这里插入图片描述

epoll的两种触发方式

epoll监控多个文件描述符的I/O事件。epoll支持边缘触发(edge trigger,ET)或水平触发(level trigger,LT),通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程。ET模式可以理解为状态的改变(无数据->有数据, 有数据->无数据), 而LT可理解为一直持续的某种状态(数据不为空或者不满)。
select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式。

1. 水平触发的时机

  • 对于读操作,只要缓冲内容不为空,LT模式返回读就绪。
  • 对于写操作,只要缓冲区还不满,LT模式会返回写就绪。
    当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
    LT模式适合一次性读大数据。

2. 边缘触发的时机

  • 对于读操作
    当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
    当有新数据到达时,即缓冲区中的待读数据变多的时候。
    当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。

当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
ET模式适合读少数据。

在这里插入图片描述

epoll与select、poll的对比

1. 用户态将文件描述符传入内核的方式

  • select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。
  • poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。
  • epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。

2. 内核态检测文件描述符读写状态的方式

  • select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。

  • poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。

  • epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

3. 找到就绪的文件描述符并传递给用户态的方式

  • select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。

  • poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。

  • epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。

4. 重复监听的处理方式

  • select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。
  • poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。
  • epoll:无需重新构建红黑树,直接沿用已存在的即可。

epoll更高效的原因

  • select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。
  • select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。
  • select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销。
  • select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。
  • epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符

虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

Reference:
https://www.jianshu.com/p/31cdfd6f5a48

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值