epoll最详细的介绍以及与epoll相关的问答题

参考自公众号:深度Linux

一、epoll的数据结构

epoll工作环境?

  • epoll工作在应用程序和内核协议栈之间。

  • epoll是在内核协议栈和vfs都有的情况下才有的。

图片

epoll 的核心数据结构是:1个红黑树和1个双向链表。还有3个核心API。

图片

可以看到,链表和红黑树使用的是同一个结点。实际上是红黑树管理所有的IO,当内部IO就绪的时候就会调用epoll的回调函数,将相应的IO添加到就绪链表上。数据结构有epitm和eventpoll,分别代表红黑树和单个结点,在单个结点上分别使用rbn和rblink使得结点同时指向两个数据结构。

1.1红黑树

  • 因为链表在查询,删除的时候毫无疑问时间复杂度是O(n);

  • 数组查询很快,但是删除和新增时间复杂度是O(n);

  • 二叉搜索树虽然查询效率是lgn,但是如果不是平衡的,那么就会退化为线性查找,复杂度直接来到O(n);

  • B+树是平衡多路查找树,主要是通过降低树的高度来存储上亿级别的数据,但是它的应用场景是内存放不下的时候能够用最少的IO访问次数从磁盘获取数据。比如数据库聚簇索引,成百上千万的数据内存无法满足查找就需要到内存查找,而因为B+树层高很低,只需要几次磁盘IO就能获取数据到内存,所以在这种磁盘到内存访问上B+树更适合。

因为我们处理上万级的fd,它们本身的存储空间并不会很大,所以倾向于在内存中去实现管理,而红黑树是一种非常优秀的平衡树,它完全是在内存中操作,而且查找,删除和新增时间复杂度都是lgn,效率非常高,因此选择用红黑树实现epoll是最佳的选择。

当然不选择用AVL树是因为红黑树是不符合AVL树的平衡条件的,红黑树用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决;而AVL树是严格平衡树,在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高。

1.2就绪socket列表-双向链表

就绪列表存储的是就绪的socket,所以它应能够快速的插入数据。

程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。(事实上,每个epoll_item既是红黑树节点,也是链表节点,删除红黑树节点,自然删除了链表节点)所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列(rdllist)。

红黑树和就绪队列的关系

红黑树的结点和就绪队列的结点的同一个节点,所谓的加入就绪队列,就是将结点的前后指针联系到一起。所以就绪了不是将红黑树结点delete掉然后加入队列。他们是同一个结点,不需要delete。

struct epitem {
RB_ ENTRY(epitem) rbn;
LIST_ ENTRY(epitem) rdlink;
int rdy; //exist in List
int sockfd;
struct epoll_ event event ;
};
struct eventpoll {
ep_ _rb_ tree rbr;
int rbcnt ;
LIST_ HEAD( ,epitem) rdlist;
int rdnum;
int waiting;
pthread_ mutex_ t mtx; //rbtree update
pthread_ spinlock_ t 1ock; //rdList update
pthread_ cond_ _t cond; //bLock for event
pthread_ mutex_ t cdmtx; //mutex for cond
};|

1.3三个API

int epoll_create(int size)

功能:内核会产生一个epoll 实例数据结构并返回一个文件描述符epfd,这个特殊的描述符就是epoll实例的句柄,后面的两个接口都以它为中心。同时也会创建红黑树和就绪列表,红黑树来管理注册fd,就绪列表来收集所有就绪fd。size参数表示所要监视文件描述符的最大值,不过在后来的Linux版本中已经被弃用(同时,size不要传0,会报invalid argument错误)。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

功能:将被监听的socket文件描述符添加到红黑树或从红黑树中删除或者对监听事件进行修改;同时向内核中断处理程序注册一个回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

功能:阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。

events: 用来记录被触发的events,其大小应该和maxevents一致

maxevents: 返回的events的最大个数处于ready状态的那些文件描述符会被复制进ready list中,epoll_wait用于向用户进程返回ready list(就绪列表)。

events和maxevents两个参数描述一个由用户分配的struct epoll event数组,调用返回时,内核将就绪列表(双向链表)复制到这个数组中,并将实际复制的个数作为返回值。

注意,如果就绪列表比maxevents长,则只能复制前maxevents个成员;反之,则能够完全复制就绪列表。

另外,struct epoll event结构中的events域在这里的解释是:在被监测的文件描述符上实际发生的事件。

调用epoll_create时,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,内部使用回调机制,红黑树中的节点通过回调函数添加到双向链表。

当epoll_wait调用时,仅仅观察这个双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。

epoll和poll/select区别?

  • (1)使用接口:select/poll需要把fds总集拷贝到内核协议栈中,epoll不需要。

  • (2)实现原理:select/poll在内核内循环 遍历是否有就绪io,epoll是单个加入红黑树。

解释:poll/select每次都要把fds总集拷贝到内核协议栈内,内核采取轮询/遍历,返回就绪的fds集合。(大白话:poll/select的fds是存放在用户态协议栈,调用时拷贝到内核协议栈中并轮询,轮询完成后再拷贝到用户态协议栈)。而epoll是通过epoll_ctl每次有新的io就加入到红黑树里,有触发的时候用epoll_wait带出即可,不需要拷贝总集。

二、epoll的实现原理

为什么需要epoll?

epoll是Linux操作系统提供的一种事件驱动的I/O模型,用于高效地处理大量并发连接的网络编程。它相比于传统的select和poll方法,具有更高的性能和扩展性。使用epoll可以实现以下几个优势:

  1. 高效处理大量并发连接:epoll采用了事件驱动的方式,只有当有可读或可写事件发生时才会通知应用程序,避免了遍历所有文件描述符的开销。

  2. 内核与用户空间数据拷贝少:使用epoll时,内核将就绪的文件描述符直接填充到用户空间的事件数组中,减少了内核与用户空间之间数据拷贝次数。

  3. 支持边缘触发(Edge Triggered)模式:边缘触发模式下,仅在状态变化时才通知应用程序。这意味着每次通知只包含最新状态的文件描述符信息,可以有效避免低效循环检查。

  4. 支持水平触发(Level Triggered)模式:水平触发模式下,在就绪期间不断地进行通知,直到应用程序处理完该文件描述符。

select与poll的缺陷?

select 和 poll 都是Unix系统中用来监视一组文件描述符的变化的系统调用。它们可以监视文件描述符的三种变化:可读性、可写性和异常条件。select 和 poll 的主要缺陷如下:

  • 文件描述符数量限制:select 和 poll 都有一个限制,就是它们只能监视少于1024个文件描述符的变化。这对于现代的网络编程来说是不够的,因为一个进程往往需要监视成千上万的连接。

  • 效率问题:虽然 select 和 poll 可以监视多个文件描述符,但是它们在每次调用的时候都需要传递所有要监视的文件描述符集合,这会导致效率的降低。

  • 信息不足:select 和 poll 返回的只是哪些文件描述符已经准备好了,但是它们并不告诉你具体是哪一个。这就需要对所有要监视的文件描述符进行遍历,直到找到准备好的文件描述符为止。

  • 信号中断:select 和 poll 调用可以被信号中断,这可能会导致调用失败。

  • 为了解决这些问题,现代操作系统中引入了新的系统调用 epoll 来替代 select 和 poll。epoll 没有文件描述符的限制,它可以监视大量的文件描述符,并且可以实现即开即用,无需传递所有文件描述符集合。此外,epoll 可以直接告诉你哪些文件描述符已经准备好,这大大提高了处理效率。

2.1epoll操作

epoll 在 linux 内核中申请了一个简易的文件系统,把原先的一个 select 或者 poll 调用分为了三个部分:调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中给这个句柄分配资源)、调用 epoll_ctl 向 epoll 对象中添加连接的套接字、调用 epoll_wait 收集发生事件的连接。这样只需要在进程启动的时候建立一个 epoll 对象,并在需要的时候向它添加或者删除连接就可以了,因此,在实际收集的时候,epoll_wait 的效率会非常高,因为调用的时候只是传递了发生 IO 事件的连接。

epoll 实现

我们以 linux 内核 2.6 为例,说明一下 epoll 是如何高效的处理事件的,当某一个进程调用 epoll_create 方法的时候,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个重要的成员。

第一个是 rb_root rbr,这是红黑树的根节点,存储着所有添加到 epoll 中的事件,也就是这个 epoll 监控的事件。
第二个是 list_head rdllist 这是一个双向链表,保存着将要通过 epoll_wait 返回给用户的、满足条件的事件。

每一个 epoll 对象都有一个独立的 eventpoll 结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用 epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂到 rbr 红黑树中,这样就能够高效的识别重复添加的节点。

所有添加到 epoll 中的事件都会与设备(如网卡等)驱动程序建立回调关系,也就是说,相应的事件发生时会调用这里的方法。这个回调方法在内核中叫做 ep_poll_callback,它把这样的事件放到 rdllist 双向链表中。在 epoll 中,对于每一个事件都会建立一个 epitem 结构体。

当调用 epoll_wait 检查是否有发生事件的连接时,只需要检查 eventpoll 对象中的 rdllist 双向链表中是否有 epitem 元素,如果 rdllist 链表不为空,则把这里的事件复制到用户态内存中的同时,将事件数量返回给用户。通过这种方法,epoll_wait 的效率非常高。epoll-ctl 在向 epoll 对象中添加、修改、删除事件时,从 rbr 红黑树中查找事件也非常快。这样,epoll 就能够轻易的处理百万级的并发连接。

epoll工作模式

epoll 有两种工作模式,LT(水平触发)模式与 ET(边缘触发)模式。默认情况下,epoll 采用 LT 模式工作。两个的区别是:

  • Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait() 时,它还会通知你在

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值