linux epoll 机制

Linux epoll 机制

0 同步I/O,异步I/O

  • 同步(阻塞)I/O:在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
  • 异步(非阻塞)I/O:当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
💡 Linux epoll 机制是一种 I/O 事件通知机制,它可以用来监听多个文件描述符上的事件。相比于传统的 select 和 poll 机制,epoll 更加灵活,具有更高的效率和更好的扩展性,可以处理更多的并发连接。

epoll 机制的核心是 epoll_create 函数,它创建了一个 epoll 对象,用于存储文件描述符和事件信息。epoll 对象可以通过 epoll_ctl 函数来添加、修改和删除文件描述符和事件信息。当文件描述符上有事件发生时,epoll_wait 函数会返回事件信息,应用程序可以根据事件信息进行相应的处理。

1 epoll 基本概念

epoll 机制中有三个重要的概念:epoll 文件描述符、事件和事件源。

1.1 epoll 文件描述符

epoll 文件描述符是 epoll 的核心,它是一个指向内核数据结构的指针,用于管理事件源和事件。应用程序通过 epoll 文件描述符与内核进行交互,可以注册、修改和删除事件源,以及等待事件的发生。

1.2 事件

事件是 epoll 中的一个重要概念,它表示一个文件描述符上的某个事件。epoll 支持的事件包括:

  • EPOLLIN: 文件描述符可读

  • EPOLLPRI: 优先级数据可读

  • EPOLLOUT: 文件描述符可写

  • EPOLLRDNORM: 文件描述符可读

  • EPOLLRDBAND: 优先级数据可读

  • EPOLLWRNORM: 文件描述符可写

  • EPOLLWRBAND: 优先级数据可写

  • EPOLLMSG: 消息可读

  • EPOLLERR: 文件描述符发生错误

  • EPOLLHUP: 文件描述符被挂起

  • EPOLLRDHUP: 对端关闭了连接

  • EPOLLEXCLUSIVE: 独占模式

  • EPOLLWAKEUP: 唤醒模式

  • EPOLLONESHOT: 一次性事件

  • EPOLLET: 边缘触发模式

  • EPOLL_EVENTS 的源码定义

    enum EPOLL_EVENTS
      {
        EPOLLIN = 0x001, // 文件描述符可读
    #define EPOLLIN EPOLLIN
        EPOLLPRI = 0x002, // 优先级数据可读
    #define EPOLLPRI EPOLLPRI
        EPOLLOUT = 0x004, // 文件描述符可写
    #define EPOLLOUT EPOLLOUT
        EPOLLRDNORM = 0x040, // 文件描述符可读
    #define EPOLLRDNORM EPOLLRDNORM
        EPOLLRDBAND = 0x080, // 优先级数据可读
    #define EPOLLRDBAND EPOLLRDBAND
        EPOLLWRNORM = 0x100, // 文件描述符可写
    #define EPOLLWRNORM EPOLLWRNORM
        EPOLLWRBAND = 0x200, // 优先级数据可写
    #define EPOLLWRBAND EPOLLWRBAND
        EPOLLMSG = 0x400, // 消息可读
    #define EPOLLMSG EPOLLMSG
        EPOLLERR = 0x008, // 文件描述符发生错误
    #define EPOLLERR EPOLLERR
        EPOLLHUP = 0x010, // 文件描述符被挂起
    #define EPOLLHUP EPOLLHUP
        EPOLLRDHUP = 0x2000, // 对端关闭了连接
    #define EPOLLRDHUP EPOLLRDHUP
        EPOLLEXCLUSIVE = 1u << 28, // 独占模式
    #define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
        EPOLLWAKEUP = 1u << 29, // 唤醒模式
    #define EPOLLWAKEUP EPOLLWAKEUP
        EPOLLONESHOT = 1u << 30, // 一次性事件
    #define EPOLLONESHOT EPOLLONESHOT
        EPOLLET = 1u << 31 // 边缘触发模式
    #define EPOLLET EPOLLET
      };
    

1.3 事件源

事件源是 epoll 中的另一个重要概念,它表示一个文件描述符。事件源可以是一个 socket、一个文件、一个管道等等。

2 epoll api

epoll api 包括三个函数:epoll_create、epoll_ctl 和 epoll_wait。

2.1 epoll_create

epoll_create 函数用于创建一个 epoll 文件描述符,它的原型如下:

int epoll_create(int size);

其中,size 表示 epoll 文件描述符管理的事件源数量上限,它并不是一个硬性限制,只是一个提示。如果 size 小于等于 0,epoll_create 会返回一个错误。

调用该 API 后,操作系统内核会产生一个 eventpoll 实例的数据结构并返回一个 fd,这个 fd 就是 epoll 实例的句柄,下面的两个 API 都以它为中心。

2.2 epoll_ctl

epoll_ctl 函数用于注册、修改和删除事件源,它的原型如下:

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

其中,epfd 是 epoll 文件描述符,op 表示操作类型,可以是 EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DELfd 是事件源的文件描述符,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 表示监听的事件类型(EPOLLIN/EPOLLHUP/EPOLLOUT...)*/
  epoll_data_t data;	/* User data variable 用户自定义数据,当事件发生时将会原样返回给用户*/
} __EPOLL_PACKED;

2.3 epoll_wait

epoll_wait 函数用于等待事件的发生,它的原型如下:

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

其中,epfd 是 epoll 文件描述符,events 是一个 epoll_event 数组,“events”参数是一个缓冲区,将包含触发的事件(用于存储发生的事件),“maxevents” 是要返回的最大事件数(通常是 “events” 的大小),timeout 表示等待事件的超时时间(-1 == 无限),单位是毫秒。

  • 如果 timeout 为 -1,表示一直等待,直到有事件发生;
  • 如果 timeout 为 0,表示立即返回,不等待事件的发生;
  • 如果 timeout 大于 0,表示等待 timeout 毫秒,如果在这个时间内有事件发生,就返回;否则超时返回 0。

等待 epoll 实例 "epfd" 上的事件。返回在 "events" 缓冲区中返回的触发事件的数量。如果出现错误,则返回 -1,并将 "errno" 变量设置为特定的错误代码。

/* Wait for events on an epoll
 instance "epfd". Returns the number of
   triggered events returned in "events" buffer. Or -1 in case of
   error with the "errno" variable set to the specific error code. The
   "events" parameter is a buffer that will contain triggered
   events. The "maxevents" is the maximum number of events to be
   returned ( usually size of "events" ). The "timeout" parameter
   specifies the maximum wait time in milliseconds (-1 == infinite).

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int epoll_wait (int __epfd, struct epoll_event *__events,
		       int __maxevents, int __timeout)
	__attr_access ((__write_only__, 2, 3));

以下是 epoll 相关函数的定义和内容详解:

#include <sys/epoll.h>

int epoll_create(int size);
// 创建一个 epoll 对象,size 参数指定了 epoll 对象中存储文件描述符和事件信息的数量上限。返回 epoll 对象的文件描述符,失败返回 -1。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 向 epoll 对象中添加、修改或删除文件描述符和事件信息。epfd 参数是 epoll 对象的文件描述符,op 参数指定操作类型,fd 参数是需要添加、修改或删除的文件描述符,event 参数是事件信息。返回 0 表示成功,-1 表示失败。

struct epoll_event {
    uint32_t events;    // 表示事件类型,可以是 EPOLLIN、EPOLLOUT、EPOLLRDHUP、EPOLLPRI、EPOLLERR、EPOLLHUP 和 EPOLLET 的组合。
    epoll_data_t data;  // 表示事件数据,可以是文件描述符或指针。
};

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 等待文件描述符上的事件发生,events 参数用于存储事件信息,maxevents 参数指定 events 数组的大小,timeout 参数指定等待时间。返回事件数量,失败返回 -1。

3 触发机制

epoll 有三种触发机制:LT(Level Triggered)、ET(Edge Triggered)和 ONESHOT。

1、水平触发:level trigger(LT)

LT(Level Triggered)是 epoll 的默认触发机制。当 epoll_wait() 检测到文件描述符上有事件发生,epoll_wait就会以非阻塞的方式返回,并将此事件通知应用程序后,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,它还会再次通知应用程序该事件。

  • 这种触发机制的优点是应用程序可以不必处理所有事件,只处理它感兴趣的事件。
  • 缺点是如果应用程序处理事件的速度太慢,该epoll事件没有被处理完(没有返回 EWOULDBLOCK
    ),那么 epoll_wait() 将不断通知应用程序同一个事件,这将导致 CPU 占用率过高。

2、边缘触发:edge trigger(ET)

ET(Edge Triggered)是一种更高效的触发机制。

epoll_wait() 检测到文件描述符上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件(立即返回,并且sleep这一事件的epoll_wait,不管该事件有没有结束)。如果不处理,下次调用 epoll_wait() 时将不会再次通知应用程序该事件

  • 这种触发机制的优点是可以避免 epoll_wait() 不断通知同一个事件,从而降低 CPU 占用率。
  • 缺点是应用程序必须立即处理事件,否则将会丢失事件。

在使用ET模式时,必须要保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);并且每次调用readwrite的时候都必须等到它们返回EWOULDBLOCK(确保所有数据都已读完或写完)。

3、ONESHOT

ONESHOT 触发机制是一种特殊的 ET 触发机制。当 epoll_wait() 检测到文件描述符上有事件发生并将此事件通知应用程序后,应用程序 必须调用 epoll_ctl() 函数重新注册该文件描述符 ,否则下次调用 epoll_wait() 时将不会再次通知应用程序该事件。

  • 这种触发机制的优点是可以避免 epoll_wait() 不断通知同一个事件,从而降低 CPU 占用率。
  • 缺点是应用程序必须重新注册文件描述符,否则将会丢失事件。

4 epoll 与 select 和 poll 对比

select 和 poll 是 Linux 下的两种 I/O 多路复用机制,相比于 epoll,它们在用户态和内核态的切换上更加耗时,同时还存在文件描述符数量限制的问题。

1、文件描述符数量

  • select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
  • poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目
  • epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效

2、将 fd 传入内核的方式

  • select:从用户态创建拷贝到内核态,每次调用都需要拷贝
  • poll:从用户态拷贝到内核态,每次调用都需要拷贝
  • epoll:通过 epoll_create 直接在内核态创建一棵红黑树,通过 epoll_ctl 将要监听的文件描述符注册到红黑树上。

3、内核态检测 fd 就绪状态的方式

  • select:轮询机制遍历所有的 fd,判断哪个文件描述符上有事件发生
  • poll:轮询机制遍历所有的 fd,判断哪个文件描述符上有事件发生
  • epoll:回调机制,调用 epoll_ctl 时会在内核态注册回调函数,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后 epoll_ctl 传来的 fd 外,还会再建立一个list链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 list 链表里有没有数据即可。
    • epoll 是根据每个 fd 上面的回调函数(中断函数)判断,只有发生了事件的 socket 才会主动的去调用 callback 函数,其他空闲状态 socket 则不会,若是就绪事件,插入 list。

4、应用程序索引就绪文件描述符

  • select/poll 只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历。
  • epoll 返回的发生了事件的个数和结构体数组,结构体包含 socket 的信息,因此直接处理返回的数组即可。

5、工作模式

  • select 和 poll 都只能工作在相对低效的 LT 模式下
  • epoll 则可以工作在 ET 高效模式,并且 epoll 还支持 EPOLLONESHOT 事件,该事件能进一步减少可读、可写和异常事件被触发的次数。

5 epoll 更高效的原因

  • 对于select和poll来说,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态;epoll 则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需要执行一个系统调用。系统调用的开销是很大的,而且在有很多短期活跃连接的情况下,epoll 可能会慢于 select 和 poll 由于这些大量的系统调用开销。
  • select 和 poll 的动作基本一致,只是 poll 采用链表的方式来存储 fd,而 select 采用 fd 标注位来存放,所以 select 会受到最大连接数的限制而 poll 不会。epoll 底层通过红黑树来描述,并且维护一个 ready list,将事件表中已经就绪的事件添加到这里,在使用 epoll_wait 调用时,仅观察这个 list 中有没有数据即可。
  • select、poll、epoll 虽然都会返回就绪的 fd 数量,但是 select 和 poll 并不会明确指出是哪些 fd 就绪,而 epoll 会。这造成的区别就是:系统调用返回后,调用 select 和 poll 的程序需要遍历监听的整个文件描述符找到是哪些处于就绪状态,产生了大量的开销,而 epoll 则不需要去以这种方式检查,当有活动产生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理。随着 fd 数量的增加,select 和 poll 的效率会越来越低,而 epoll 则不会受到太大影响。
  • select 和 poll 都只能工作在相对低效的LT模式下,而 epoll 同时支持 LT 和 ET 模式。epoll 的 ET 模式效率高,系统不会充斥大量不关心的就绪 fd。

6 具体应用场景

  • 当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如 selece 和 poll。
  • 当监测的 fd 数目较小,且各个 fd 都比较活跃,建议使用 select 或者 poll。
  • 当监测的 fd 数目非常大,成千上万,且单位时间只有其中的一部分 fd 处于就绪状态,这个时候使用 epoll 能够明显提升性能。

参考资料

linux epoll 机制

小白视角:一文读懂社长的TinyWebServer

最新版Web服务器项目详解 - 04 http连接处理(上)

1 打赏
打赏 20 积分后可见