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() 时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。

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

当然,在 LT 模式下开发基于 epoll 的应用要简单一些,不太容易出错,而在 ET 模式下事件发生时,如果没有彻底地将缓冲区的数据处理完,则会导致缓冲区的用户请求得不到响应。注意,默认情况下 Nginx 采用 ET 模式使用 epoll 的。

2.2 I/O 多路复用

(1)阻塞OR非阻塞

我们知道,对于 linux 来说,I/O 设备为特殊的文件,读写和文件是差不多的,但是 I/O 设备因为读写与内存读写相比,速度差距非常大。与 cpu 读写速度更是没法比,所以相比于对内存的读写,I/O 操作总是拖后腿的那个。网络 I/O 更是如此,我们很多时候不知道网络 I/O 什么时候到来,就好比我们点了一份外卖,不知道外卖小哥们什么时候送过来,这个时候有两个处理办法:

第一个是我们可以先去睡觉,外卖小哥送到楼下了自然会给我们打电话,这个时候我们在醒来取外卖就可以了。
第二个是我们可以每隔一段时间就给外卖小哥打个电话,这样就能实时掌握外卖的动态信息了。

第一种方式对应的就是阻塞的 I/O 处理方式,进程在进行 I/O 操作的时候,进入睡眠,如果有 I/O 时间到达,就唤醒这个进程。第二种方式对应的是非阻塞轮询的方式,进程在进行 I/O 操作后,每隔一段时间向内核询问是否有 I/O 事件到达,如果有就立刻处理。

阻塞的原理

工作队列

阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select和epoll都是阻塞方法,以简单网络编程为例

下图中的计算机中运行着A、B、C三个进程,其中进程A执行着上述基础网络程序,一开始,这3个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行

图片

两个数据结构之间的关系

当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象(如下图)。这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。

图片

当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中(如下图)。由于工作队列只剩下了进程B和C,依据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。

图片

ps:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。

唤醒进程

当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。也由于socket的接收缓冲区已经有了数据,recv可以返回接收到的数据。

(2)线程池OR轮询

在现实中,我们当然选择第一种方式,但是在计算机中,情况就要复杂一些。我们知道,在 linux 中,不管是线程还是进程都会占用一定的资源,也就是说,系统总的线程和进程数是一定的。如果有许多的线程或者进程被挂起,无疑是白白消耗了系统的资源。而且,线程或者进程的切换也是需要一定的成本的,需要上下文切换,如果频繁的进行上下文切换,系统会损失很大的性能。一个网络服务器经常需要连接成千上万个客户端,而它能创建的线程可能之后几百个,线程耗光就不能对外提供服务了。这些都是我们在选择 I/O 机制的时候需要考虑的。这种阻塞的 I/O 模式下,一个线程只能处理一个流的 I/O 事件,这是问题的根源。

这个时候我们首先想到的是采用线程池的方式限制同时访问的线程数,这样就能够解决线程不足的问题了。但是这又会有第二个问题了,多余的任务会通过队列的方式存储在内存只能够,这样很容易在客户端过多的情况下出现内存不足的情况。

还有一种方式是采用轮询的方式,我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了。

(3)代理

采用轮询的方式虽然能够处理多个 I/O 事件,但是也有一个明显的缺点,那就是会导致 CPU 空转。试想一下,如果所有的流中都没有数据,那么 CPU 时间就被白白的浪费了。

为了避免CPU空转,可以引进了一个代理。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流,这就是 select 与 poll 所做的事情,可见,采用 I/O 复用极大的提高了系统的效率。

2.3内核接收网络数据全过程

如下图所示,进程在recv阻塞期间,计算机收到了对端传送的数据(步骤①)。数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知CPU有数据到达,CPU执行中断程序(步骤③)。此处的中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面(步骤④),再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。

图片

唤醒线程的过程如下图所示:

图片

三、协议栈如何与epoll通信?

协议栈和epoll模块之间的通信是异步的,没有耦合,不需要等待。

通知时机:

  • (1)协议栈三次握手完成,往accept全连接队列里加入这个节点时,通知epoll有事件来了epollin;

  • (2)客户端发了1个数据到协议栈,协议栈此时要返回ack给客户端的这里的时机,会通知epoll有事件可读 epollin。

四、epoll线程安全如何加锁?

1、对红黑树枷锁:一种是锁整棵树,另一种是锁子树。一般使用互斥锁。

2、对就绪队列枷锁:用自旋锁,队列操作比较简单,等到一些时间比让出线程更高效点。

4.1等待队列实现原理

(1)功能介绍

进程有多种状态,当进程做好准备后,它就处于就绪状态(TASK_RUNNING),放入运行队列,等待内核调度器来调度。当然,同一时刻可能有多个进程进入就绪状态,但是却可能只有1个CPU是空闲的,所以最后能不能在CPU上运行,还要取决于优先级等多种因素。当进程进行外部设备的IO等待操作时,由于外部设备的操作速度一般是非常慢的,所以进程会从就绪状态变为等待状态(休眠),进入等待队列,把CPU让给其它进程。直到IO操作完成,内核“唤醒”等待的进程,于是进程再度从等待状态变为就绪状态。

在用户态,进程进行IO操作时,可以有多种处理方式,如阻塞式IO,非阻塞式IO,多路复用(select/poll/epoll),AIO(aio_read/aio_write)等等。这些操作在内核态都要用到等待队列。

(2)相关的结构体

typedef struct __wait_queue wait_queue_t;

struct __wait_queue
{
    unsigned int flags;
    #define WQ_FLAG_EXCLUSIVE 0x01
    struct task_struct * task; // 等待队列节点对应的进程
    wait_queue_func_t func;   // 等待队列的回调函数 ,在进程被唤醒
    struct list_head task_list;
};
这个是等待队列的节点,在很多等待队列里,这个func函数指针默认为空函数。
但是,在select/poll/epoll函数中,这个func函数指针不为空,并且扮演着重要的角色。

struct __wait_queue_head
{
    spinlock_t lock;
    struct list_head task_list;
};

typedef struct __wait_queue_head wait_queue_head_t;这个是等待队列的头部。其中task_list里有指向下一个节点的指针。为了保证对等待队列的操作是原子的,还需要一个自旋锁lock。

这里需要提一下内核队列中被广泛使用的结构体struct list_head。

struct list_head
{
    struct list_head *next, *prev;
};

(3)实现原理

可以看到,等待队列的核心是一个list_head组成的双向链表。

其中,第一个节点是队列的头,类型为wait_queue_head_t,里面包含了一个list_head类型的成员task_list。

接下去的每个节点类型为 wait_queue_t,里面也有一个list_head类型的成员task_list,并且有个指针指向等待的进程。通过这种方式,内核组织了一个等待队列。

那么,这个等待队列怎样与一个事件关联呢?

在内核中,进程在文件操作等事件上的等待,一定会有一个对应的等待队列的结构体与之对应。例如,等待管道的文件操作(在内核看来,管道也是一种文件)的进程都放在管道对应inode.i_pipe->wait这个等待队列中。这样,如果管道文件操作完成,就可以很方便地通过inode.i_pipe->wait唤醒等待的进程。

在大部分情况下(如系统调用read),当前进程等待IO操作的完成,只要在内核堆栈中分配一个wait_queue_t的结构体,然后初始化,把task指向当前进程的task_struct,然后调用add_wait_queue()放入等待队列即可。

但是,在select/poll中,由于系统调用要监视多个文件描述符的操作,因此要把当前进程放入多个文件的等待队列,并且要分配多个wait_queue_t结构体。这时候,在堆栈上分配是不合适的。因为内核堆栈很小。所以要通过动态分配的方式来分配wait_queue_t结构体。除了在一些结构体里直接定义等待队列的头部,内核的信号量机制也大量使用了等待队列。信号量是为了进行进程同步而引入的。与自旋锁不同的是,当一个进程无法获得信号量时,它会把自己放到这个信号量的等待队列中,转变为等待状态。当其它进程释放信号量时,会唤醒等待的进程。

epoll 关键结构体:

struct ep_pqueue
{
    poll_table pt;
    struct epitem *epi;
};

这个结构体类似于select/poll中的struct poll_wqueues。由于epoll需要在内核态保存大量信息,所以光光一个回调函数指针已经不能满足要求,所以在这里引入了一个新的结构体struct epitem。

struct epitem
{ 
    struct rb_node rbn;
    红黑树,用来保存eventpoll
 
    struct list_head rdllink;
    双向链表,用来保存已经完成的eventpoll
 
    struct epoll_filefd ffd;
    这个结构体对应的被监听的文件描述符信息
 
    int nwait;
    poll操作中事件的个数
 
    struct list_head pwqlist;
    双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table
 
    struct eventpoll *ep;
    指向eventpoll,多个epitem对应一个eventpoll
 
    struct epoll_event event;
    记录发生的事件和对应的fd
 
    atomic_t usecnt;
    引用计数
 
    struct list_head fllink;
    双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,
    用来保存所有监视这个文件的epoll节点
 
    struct list_head txlink;
    双向链表,用来保存传输队列
 
    unsigned int revents;
    文件描述符的状态,在收集和传输时用来锁住空的事件集合
};

该结构体用来保存与epoll节点关联的多个文件描述符,保存的方式是使用红黑树实现的hash表。至于为什么要保存,下文有详细解释。它与被监听的文件描述符一一对应。

struct eventpoll
{ 
    spinlock_t lock;
    读写锁
 
    struct mutex mtx;
    读写信号量
 
    wait_queue_head_t wq; 
    wait_queue_head_t poll_wait;
 
    struct list_head rdllist;
    已经完成的操作事件的队列。
 
    struct rb_root rbr;
    保存epoll监视的文件描述符
    struct epitem *ovflist;
    struct user_struct *user;
};

这个结构体保存了epoll文件描述符的扩展信息,它被保存在file结构体的private_data中。它与epoll文件节点一一对应。通常一个epoll文件节点对应多个被监视的文件描述符。所以一个eventpoll结构体会对应多个epitem结构体。

那么,epoll中的等待事件放在哪里呢?见下面

struct eppoll_entry
{ 
    struct list_head llink;
    void *base;
    wait_queue_t wait;
    wait_queue_head_t *whead;
};
与select/poll的struct poll_table_entry相比,epoll的表示等待队列节点的结构体只是稍有不同,
与struct poll_table_entry比较一下。
struct poll_table_entry
{
    struct file * filp;
    wait_queue_t wait;
    wait_queue_head_t * wait_address;
};

由于epitem对应一个被监视的文件,所以通过base可以方便地得到被监视的文件信息。又因为一个文件可能有多个事件发生,所以用llink链接这些事件。

相关内核代码:fs/eventpoll.c

判断一个tcp套接字上是否有激活事件:net/ipv4/tcp.c:tcp_poll函数,每个epollfd在内核中有一个对应的eventpoll结构对象。

其中关键的成员是一个readylist(eventpoll:rdllist)和一棵红黑树(eventpoll:rbr),eventpoll的红黑树中,红黑树的作用是使用者调用EPOLL_MOD的时候可以快速找到fd对应的epitem。

epoll_ctl的功能是实现一系列操作,如把文件与eventpollfs文件系统的inode节点关联起来。这里要介绍一下eventpoll结构体,它保存在file->f_private中,记录了eventpollfs文件系统的inode节点的重要信息,其中成员rbr保存了该epoll文件节点监视的所有文件描述符。组织的方式是一棵红黑树,这种结构体在查找节点时非常高效。首先它调用ep_find()从eventpoll中的红黑树获得epitem结构体。然后根据op参数的不同而选择不同的操作。如果op为EPOLL_CTL_ADD,那么正常情况下epitem是不可能在eventpoll的红黑树中找到的,所以调用ep_insert创建一个epitem结构体并插入到对应的红黑树中。

ep_insert()首先分配一个epitem对象,对它初始化后,把它放入对应的红黑树。此外,这个函数还要作一个操作,就是把当前进程放入对应文件操作的等待队列。这一步是由下面的代码完成的。

init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
......
revents = tfile->f_op->poll(tfile, &epq.pt);

函数先调用init_poll_funcptr注册了一个回调函数ep_ptable_queue_proc,ep_ptable_queue_proc函数会在调用f_op->poll时被执行。

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
           poll_table *pt)
{

struct epitem *epi = ep_item_from_epqueue(pt);

struct eppoll_entry *pwq;

if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {

 init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);

 pwq->whead = whead;

pwq->base = epi;

add_wait_queue(whead, &pwq->wait);

list_add_tail(&pwq->llink, &epi->pwqlist);

 epi->nwait++;

} else {
 epi->nwait = -1;

 }
}

该函数分配一个epoll等待队列结点eppoll_entry:一方面把它挂到文件操作的等待队列中,另一方面把它挂到epitem的队列中。此外,它还注册了一个等待队列的回调函数ep_poll_callback。当文件操作完成,唤醒当前进程之前,会调用ep_poll_callback(),把eventpoll放到epitem的完成队列中(注释:通过查看代码,此处应该是把epitem放到eventpoll的完成队列,只有这样才能在epoll_wait()中只要看eventpoll的完成队列即可得到所有的完成文件描述符),并唤醒等待进程。

如果在执行f_op->poll以后,发现被监视的文件操作已经完成了,那么把它放在完成队列中了,并立即把等待操作的那些进程唤醒。

if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
ep_rbtree_insert(ep, epi);

调用epoll_wait的时候,将readylist中的epitem出列,将触发的事件拷贝到用户空间.之后判断epitem是否需要重新添加回readylist。

epitem重新添加到readylist必须满足下列条件:

1) epitem上有用户关注的事件触发. 2) epitem被设置为水平触发模式(如果一个epitem被设置为边界触发则这个epitem不会被重新添加到readylist中,在什么时候重新添加到readylist请继续往下看)。 注意,如果epitem被设置为EPOLLONESHOT模式,则当这个epitem上的事件拷贝到用户空间之后,会将这个epitem上的关注事件清空(只是关注事件被清空,并没有从epoll中删除,要删除必须对那个描述符调EPOLL_DEL),也就是说即使这个epitem上有触发事件,但是因为没有用户关注的事件所以不会被重新添加到readylist中。

epitem被添加到readylist中的各种情况(当一个epitem被添加到readylist如果有线程阻塞在epoll_wait中,那个线程会被唤醒):

1)对一个fd调用EPOLL_ADD,如果这个fd上有用户关注的激活事件,则这个fd会被添加到readylist. 2)对一个fd调用EPOLL_MOD改变关注的事件,如果新增加了一个关注事件且对应的fd上有相应的事件激活,则这个fd会被添加到readylist. 3)当一个fd上有事件触发时(例如一个socket上有外来的数据)会调用ep_poll_callback(见eventpoll::ep_ptable_queue_proc),

如果触发的事件是用户关注的事件,则这个fd会被添加到readylist中,了解了epoll的执行过程之后,可以回答一个在使用边界触发时常见的疑问.在一个fd被设置为边界触发的情况下,调用read/write,如何正确的判断那个fd已经没有数据可读/不再可写.epoll文档中的建议是直到触发EAGAIN错误.而实际上只要你请求字节数小于read/write的返回值就可以确定那个fd上已经没有数据可读/不再可写,最后用一个epollfd监听另一个epollfd也是合法的,epoll通过调用eventpoll::ep_eventpoll_poll来判断一个epollfd上是否有触发的事件(只能是读事件)。

以下是个人读代码总结:

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,

int, maxevents, int, timeout)
 SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,

struct epoll_event __user *, event)

epoll_ctl的机制大致如下:

mutex_lock(&ep->mtx);

epi = ep_find(ep, tfile, fd); //这里就是去ep->rbr 红黑树查找

error = -EINVAL;

switch (op) {

case EPOLL_CTL_ADD:

if (!epi) {
             epds.events |= POLLERR | POLLHUP;
            error = ep_insert(ep, &epds, tfile, fd);

        } else
            error = -EEXIST;

        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);

4.2源码分析

(1)sys_epoll_wait()函数:

/* 
 * Implement the event wait interface for the eventpoll file. It is the kernel 
 * part of the user space epoll_wait(2). 
 */  
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,  
        int, maxevents, int, timeout)  
{  
    int error;  
    struct file *file;  
    struct eventpoll *ep;  
  
    /* The maximum number of event must be greater than zero */  
    /* 
     * 检查maxevents参数。 
     */  
    if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)  
        return -EINVAL;  
  
    /* Verify that the area passed by the user is writeable */  
    /* 
     * 检查用户空间传入的events指向的内存是否可写。参见__range_not_ok()。 
     */  
    if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))) {  
        error = -EFAULT;  
        goto error_return;  
    }  
  
    /* Get the "struct file *" for the eventpoll file */  
    /* 
     * 获取epfd对应的eventpoll文件的file实例,file结构是在epoll_create中创建 
     */  
    error = -EBADF;  
    file = fget(epfd);  
    if (!file)  
        goto error_return;  
  
    /* 
     * We have to check that the file structure underneath the fd 
     * the user passed to us _is_ an eventpoll file. 
     */  
    /* 
     * 通过检查epfd对应的文件操作是不是eventpoll_fops 
     * 来判断epfd是否是一个eventpoll文件。如果不是 
     * 则返回EINVAL错误。 
     */  
    error = -EINVAL;  
    if (!is_file_epoll(file))  
        goto error_fput;  
  
    /* 
     * At this point it is safe to assume that the "private_data" contains 
     * our own data structure. 
     */  
    ep = file->private_data;  
  
    /* Time to fish for events ... */  
    error = ep_poll(ep, events, maxevents, timeout);  
  
error_fput:  
    fput(file);  
error_return:  
  
    return error;  
}

sys_epoll_wait()是epoll_wait()对应的系统调用,主要用来获取文件状态已经就绪的事件,该函数检查参数、获取eventpoll文件后调用ep_poll()来完成主要的工作。在分析ep_poll()函数之前,先介绍一下使用epoll_wait()时可能犯的错误(接下来介绍的就是我犯过的错误):

返回EBADF错误

除非你故意指定一个不存在的文件描述符,否则几乎百分百肯定,你的程序有BUG了!从源码中可以看到调用fget()函数返回NULL时,会返回此错误。fget()源码如下:

struct file *fget(unsigned int fd)  
{  
    struct file *file;  
    struct files_struct *files = current->files;  
  
    rcu_read_lock();  
    file = fcheck_files(files, fd);  
    if (file) {  
        if (!atomic_long_inc_not_zero(&file->f_count)) {  
            /* File object ref couldn't be taken */  
            rcu_read_unlock();  
            return NULL;  
        }  
    }  
    rcu_read_unlock();  
  
    return file;  
}

主要看这句(struct files_struct *files = current->files;),这条语句是获取描述当前进程已经打开的文件的files_struct结构,然后从这个结构中查找传入的fd对应的file实例,如果没有找到,说明当前进程中打开的文件不包括这个fd,所以几乎百分百肯定是程序设计的问题。我的程序出错,就是因为在父进程中创建了文件描述符,但是将子进程变为守护进程了,也就没有继承父进程中打开的文件。

死循环(一般不会犯,但是我是第一次用,犯了)

epoll_wait()中有一个设置超时时间的参数,所以我在循环中没有使用睡眠队列的操作,想依赖epoll的睡眠操作,所以在返回值小于等于0时,直接进行下一次循环,没有充分考虑epoll_wait()的返回值小于0时的不同情况,所以代码写成了下面的样子:

for(;;) {  
    ......  
    events = epoll_wait(fcluster_epfd, fcluster_wait_events,   
            fcluster_wait_size, 3000);  
        if (unlikely(events <= 0)) {  
            continue;  
        }  
    .......  
}

当epoll_wait()返回EBADF或EFAULT时,就会陷入死循环,因此此时还没有进入睡眠的操作。

(2)ep_poll()函数

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,  
           int maxevents, long timeout)  
{  
    int res, eavail;  
    unsigned long flags;  
    long jtimeout;  
    wait_queue_t wait;  
  
    /* 
     * Calculate the timeout by checking for the "infinite" value (-1) 
     * and the overflow condition. The passed timeout is in milliseconds, 
     * that why (t * HZ) / 1000. 
     */  
    /* 
     * timeout是以毫秒为单位,这里是要转换为jiffies时间。 
     * 这里加上999(即1000-1),是为了向上取整。 
     */  
    jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?  
        MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;  
  
retry:  
    spin_lock_irqsave(&ep->lock, flags);  
  
    res = 0;  
    if (list_empty(&ep->rdllist)) {  
        /* 
         * We don't have any available event to return to the caller. 
         * We need to sleep here, and we will be wake up by 
         * ep_poll_callback() when events will become available. 
         */  
        init_waitqueue_entry(&wait, current);  
        wait.flags |= WQ_FLAG_EXCLUSIVE;  
        /* 
         * 将当前进程加入到eventpoll的等待队列中, 
         * 等待文件状态就绪或直到超时,或被 
         * 信号中断。 
         */  
        __add_wait_queue(&ep->wq, &wait);  
  
        for (;;) {  
            /* 
             * We don't want to sleep if the ep_poll_callback() sends us 
             * a wakeup in between. That's why we set the task state 
             * to TASK_INTERRUPTIBLE before doing the checks. 
             */  
            set_current_state(TASK_INTERRUPTIBLE);  
            /* 
             * 如果就绪队列不为空,也就是说已经有文件的状态 
             * 就绪或者超时,则退出循环。 
             */  
            if (!list_empty(&ep->rdllist) || !jtimeout)  
                break;  
            /* 
             * 如果当前进程接收到信号,则退出 
             * 循环,返回EINTR错误 
             */  
            if (signal_pending(current)) {  
                res = -EINTR;  
                break;  
            }  
  
            spin_unlock_irqrestore(&ep->lock, flags);  
            /* 
             * 主动让出处理器,等待ep_poll_callback()将当前进程 
             * 唤醒或者超时,返回值是剩余的时间。从这里开始 
             * 当前进程会进入睡眠状态,直到某些文件的状态 
             * 就绪或者超时。当文件状态就绪时,eventpoll的回调 
             * 函数ep_poll_callback()会唤醒在ep->wq指向的等待队列中的进程。 
             */  
            jtimeout = schedule_timeout(jtimeout);  
            spin_lock_irqsave(&ep->lock, flags);  
        }  
        __remove_wait_queue(&ep->wq, &wait);  
  
        set_current_state(TASK_RUNNING);  
    }  
    /* Is it worth to try to dig for events ? */  
    /* 
     * ep->ovflist链表存储的向用户传递事件时暂存就绪的文件。 
     * 所以不管是就绪队列ep->rdllist不为空,或者ep->ovflist不等于 
     * EP_UNACTIVE_PTR,都有可能现在已经有文件的状态就绪。 
     * ep->ovflist不等于EP_UNACTIVE_PTR有两种情况,一种是NULL,此时 
     * 可能正在向用户传递事件,不一定就有文件状态就绪, 
     * 一种情况时不为NULL,此时可以肯定有文件状态就绪, 
     * 参见ep_send_events()。 
     */  
    eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;  
  
    spin_unlock_irqrestore(&ep->lock, flags);  
  
    /* 
     * Try to transfer events to user space. In case we get 0 events and 
     * there's still timeout left over, we go trying again in search of 
     * more luck. 
     */  
    /* 
     * 如果没有被信号中断,并且有事件就绪, 
     * 但是没有获取到事件(有可能被其他进程获取到了), 
     * 并且没有超时,则跳转到retry标签处,重新等待 
     * 文件状态就绪。 
     */  
    if (!res && eavail &&  
        !(res = ep_send_events(ep, events, maxevents)) && jtimeout)  
        goto retry;  
  
    /* 
     * 返回获取到的事件的个数或者错误码 
     */  
    return res;  
}

ep_poll()的主要过程是:首先将超时时间(以毫秒为单位)转换为jiffies时间,然后检查是否有事件发生,如果没有事件发生,则将当前进程加入到eventpoll中的等待队列中,直到事件发生或者超时。如果有事件发生,则调用ep_send_events()将发生的事件传入用户空间的内存。ep_send_events()函数将用户传入的内存简单封装到ep_send_events_data结构中,然后调用ep_scan_ready_list()将就绪队列中的事件传入用户空间的内存。

(3)ep_scan_ready_list()函数

/** 
 * ep_scan_ready_list - Scans the ready list in a way that makes possible for 
 *                      the scan code, to call f_op->poll(). Also allows for 
 *                      O(NumReady) performance. 
 * 
 * @ep: Pointer to the epoll private data structure. 
 * @sproc: Pointer to the scan callback. 
 * @priv: Private opaque data passed to the @sproc callback. 
 * 
 * Returns: The same integer error code returned by the @sproc callback. 
 */  
static int ep_scan_ready_list(struct eventpoll *ep,  
                  int (*sproc)(struct eventpoll *,  
                       struct list_head *, void *),  
                  void *priv)  
{  
    int error, pwake = 0;  
    unsigned long flags;  
    struct epitem *epi, *nepi;  
    LIST_HEAD(txlist);  
  
    /* 
     * We need to lock this because we could be hit by 
     * eventpoll_release_file() and epoll_ctl(). 
     */  
    /* 
     * 获取互斥锁,该互斥锁在移除eventpoll文件(eventpoll_release_file() )、 
     * 操作文件描述符(epoll_ctl())和向用户传递事件(epoll_wait())之间进行互斥 
     */  
    mutex_lock(&ep->mtx);  
  
    /* 
     * Steal the ready list, and re-init the original one to the 
     * empty list. Also, set ep->ovflist to NULL so that events 
     * happening while looping w/out locks, are not lost. We cannot 
     * have the poll callback to queue directly on ep->rdllist, 
     * because we want the "sproc" callback to be able to do it 
     * in a lockless way. 
     */  
    spin_lock_irqsave(&ep->lock, flags);  
    /* 
     * 将就绪队列中就绪的文件链表暂存在临时 
     * 表头txlist中,并且初始化就绪队列。 
     */  
    list_splice_init(&ep->rdllist, &txlist);  
    /* 
     * 将ovflist置为NULL,表示此时正在向用户空间传递 
     * 事件。如果此时有文件状态就绪,不会放在 
     * 就绪队列中,而是放在ovflist链表中。 
     */  
    ep->ovflist = NULL;  
    spin_unlock_irqrestore(&ep->lock, flags);  
  
    /* 
     * Now call the callback function. 
     */  
    /* 
     * 调用ep_send_events_proc()将就绪队列中的事件 
     * 存入用户传入的内存中。 
     */  
    error = (*sproc)(ep, &txlist, priv);  
  
    spin_lock_irqsave(&ep->lock, flags);  
    /* 
     * During the time we spent inside the "sproc" callback, some 
     * other events might have been queued by the poll callback. 
     * We re-insert them inside the main ready-list here. 
     */  
    /* 
     * 在调用sproc指向的函数将就绪队列中的事件 
     * 传递到用户传入的内存的过程中,可能有文件 
     * 状态就绪,这些事件会暂存在ovflist链表中, 
     * 所以这里要将ovflist中的事件移到就绪队列中。 
     */  
    for (nepi = ep->ovflist; (epi = nepi) != NULL;  
         nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {  
        /* 
         * We need to check if the item is already in the list. 
         * During the "sproc" callback execution time, items are 
         * queued into ->ovflist but the "txlist" might already 
         * contain them, and the list_splice() below takes care of them. 
         */  
        if (!ep_is_linked(&epi->rdllink))  
            list_add_tail(&epi->rdllink, &ep->rdllist);  
    }  
    /* 
     * We need to set back ep->ovflist to EP_UNACTIVE_PTR, so that after 
     * releasing the lock, events will be queued in the normal way inside 
     * ep->rdllist. 
     */  
    /* 
     * 重新初始化ovflist,表示传递事件已经完成, 
     * 之后再有文件状态就绪,这些事件会直接 
     * 放在就绪队列中。 
     */  
    ep->ovflist = EP_UNACTIVE_PTR;  
  
    /* 
     * Quickly re-inject items left on "txlist". 
     */  
    /* 
     * 如果sproc指向的函数ep_send_events_proc()中处理出错或者某些文件的 
     * 触发方式设置为水平触发(Level Trigger),txlist中可能还有事件,需要 
     * 将这些就绪的事件重新添加回eventpoll文件的就绪队列中。 
     */  
    list_splice(&txlist, &ep->rdllist);  
  
    if (!list_empty(&ep->rdllist)) {  
        /* 
         * Wake up (if active) both the eventpoll wait list and 
         * the ->poll() wait list (delayed after we release the lock). 
         */  
        if (waitqueue_active(&ep->wq))  
            wake_up_locked(&ep->wq);  
        if (waitqueue_active(&ep->poll_wait))  
            pwake++;  
    }  
    spin_unlock_irqrestore(&ep->lock, flags);  
  
    mutex_unlock(&ep->mtx);  
  
    /* We have to call this outside the lock */  
    if (pwake)  
        ep_poll_safewake(&ep->poll_wait);  
  
    return error;  
}

ep_scan_ready_list()函数的参数sproc指向的函数是ep_send_events_proc(),参见ep_send_events()函数。

(4)ep_send_events_proc()函数

/* 
 * @head:已经就绪的文件列表 
 * @priv:用来存储已经就绪的文件 
 */  
static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,  
                   void *priv)  
{  
    struct ep_send_events_data *esed = priv;  
    int eventcnt;  
    unsigned int revents;  
    struct epitem *epi;  
    struct epoll_event __user *uevent;  
  
    /* 
     * We can loop without lock because we are passed a task private list. 
     * Items cannot vanish during the loop because ep_scan_ready_list() is 
     * holding "mtx" during this call. 
     */  
    for (eventcnt = 0, uevent = esed->events;  
         !list_empty(head) && eventcnt < esed->maxevents;) {  
        epi = list_first_entry(head, struct epitem, rdllink);  
  
        list_del_init(&epi->rdllink);  
  
        /* 
         * 调用文件的poll函数有两个作用,一是在文件的唤醒 
         * 队列上注册回调函数,二是返回文件当前的事件状 
         * 态,如果第二个参数为NULL,则只是查看文件当前 
         * 状态。 
         */  
        revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) &  
            epi->event.events;  
  
        /* 
         * If the event mask intersect the caller-requested one, 
         * deliver the event to userspace. Again, ep_scan_ready_list() 
         * is holding "mtx", so no operations coming from userspace 
         * can change the item. 
         */  
        if (revents) {  
            /* 
             * 向用户内存传值失败时,将当前epitem实例重新放回 
             * 到链表中,从这里也可以看出,在处理失败后,head指向的 
             * 链表(对应ep_scan_ready_list()中的临时变量txlist)中 
             * 有可能会没有完全处理完,因此在ep_scan_ready_list()中 
             * 需要下面的语句 
             *    list_splice(&txlist, &ep->rdllist); 
             * 来将未处理的事件重新放回到eventpoll文件的就绪队列中。 
             */  
            if (__put_user(revents, &uevent->events) ||  
                __put_user(epi->event.data, &uevent->data)) {  
                list_add(&epi->rdllink, head);  
                /* 
                 * 如果此时已经获取了部分事件,则返回已经获取的事件个数, 
                 * 否则返回EFAULT错误。 
                 */  
                return eventcnt ? eventcnt : -EFAULT;  
            }  
            eventcnt++;  
            uevent++;  
            if (epi->event.events & EPOLLONESHOT)  
                epi->event.events &= EP_PRIVATE_BITS;  
            /* 
             * 如果是触发方式不是边缘触发(Edge Trigger),而是水平 
             * 触发(Level Trigger),需要将当前的epitem实例添加回 
             * 链表中,下次读取事件时会再次上报。 
             */  
            else if (!(epi->event.events & EPOLLET)) {  
                /* 
                 * If this file has been added with Level 
                 * Trigger mode, we need to insert back inside 
                 * the ready list, so that the next call to 
                 * epoll_wait() will check again the events 
                 * availability. At this point, noone can insert 
                 * into ep->rdllist besides us. The epoll_ctl() 
                 * callers are locked out by 
                 * ep_scan_ready_list() holding "mtx" and the 
                 * poll callback will queue them in ep->ovflist. 
                 */  
                list_add_tail(&epi->rdllink, &ep->rdllist);  
            }  
        }  
    }  
  
    return eventcnt;  
}

4.3如何加锁

3个api做什么事情

epoll_create() ===》创建红黑树的根节点
epoll_ctl() ===》add,del,mod 增加、删除、修改结点
epoll_wait() ===》把就绪队列的结点copy到用户态放到events里面,跟recv函数很像

分析加锁

  • 如果有3个线程同时操作epoll,有哪些地方需要加锁?我们用户层面一共就只有3个api可以使用

  • 如果同时调用 epoll_create() ,那就是创建三颗红黑树,没有涉及到资源竞争,没有关系。

  • 如果同时调用 epoll_ctl() ,对同一颗红黑树进行,增删改,这就涉及到资源竞争需要加锁了,此时我们对整棵树进行加锁。

  • 如果同时调用epoll_wait() ,其操作的是就绪队列,所以需要对就绪队列进行加锁。

我们要扣住epoll的工作环境,在应用程序调用 epoll_ctl() ,协议栈会不会有回调操作红黑树结点?调用epoll_wait() copy出来的时候,协议栈会不会操作操作红黑树结点加入就绪队列?综上所述:

epoll_ctl() 对红黑树加锁epoll_wait()对就绪队列加锁回调函数()   对红黑树加锁,对就绪队列加锁

那么红黑树加什么锁,就绪队列加什么锁呢?

对于红黑树这种节点比较多的时候,采用互斥锁来加锁。就绪队列就跟生产者消费者一样,结点是从协议栈回调函数来生产的,消费是epoll_wait()来消费。那么对于队列而言,用自旋锁(对于队列而言,插入删除比较简单,cpu自旋等待比让出的成本更低,所以用自旋锁)。

五、ET与LT的实现

  • ET边沿触发,只触发一次

  • LT水平触发,如果没有读完就一直触发

代码如何实现ET和LT的效果呢?水平触发和边沿触发不是故意设计出来的,这是自然而然,水到渠成的功能。水平触发和边沿触发代码只需要改一点点就能实现。从协议栈检测到接收数据,就调用一次回调,这就是ET,接收到数据,调用一次回调。而LT水平触发,检测到recvbuf里面有数据就调用回调。所以ET和LT就是在使用回调的次数上面的差异。

那么具体如何实现呢?协议栈流程里面触发回调,是天然的符合ET只触发一次的。那么如果是LT,在recv之后,如果缓冲区还有数据那么加入到就绪队列。那么如果是LT,在send之后,如果缓冲区还有空间那么加入到就绪队列。那么这样就能实现LT了。

  • 24
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,接下来我会详细介绍 epollepollLinux 内核提供的一种 I/O 事件通知机制,与 select 和 poll 相比,它具有更高的性能和更强的扩展性。 epoll 的特点如下: 1. 支持边缘触发和水平触发两种模式。 2. 采用基于事件驱动的方式,可以同时监听大量的文件描述符。 3. 支持添加、修改和删除事件,可以动态地改变监听的文件描述符。 4. 可以通过 epoll_wait 函数等待事件的发生,避免了轮询的效率问题。 epoll 的使用流程如下: 1. 调用 epoll_create 函数创建一个 epoll 实例。 2. 调用 epoll_ctl 函数向 epoll 实例添加、修改或删除事件。 3. 调用 epoll_wait 函数等待事件的发生。 4. 处理已经发生的事件。 下面是一个简单的使用 epoll 的示例: ```c #include <sys/epoll.h> int main() { int fd = socket(AF_INET, SOCK_STREAM, 0); struct epoll_event event, events[10]; int epfd, nfds; epfd = epoll_create(1); event.data.fd = fd; event.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); while(1) { nfds = epoll_wait(epfd, events, 10, -1); for(int i = 0; i < nfds; i++) { if(events[i].data.fd == fd) { // 处理文件描述符 fd 的事件 } } } return 0; } ``` 在这个示例,我们创建了一个 epoll 实例,并向其添加了一个文件描述符,然后使用 epoll_wait 函数等待事件的发生,并处理已经发生的事件。 需要注意的是,epoll 的事件驱动模型并不是线程安全的,因此在多线程环境下需要进行同步处理。 希望这份介绍对你有所帮助。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值