linux epoll 机制
Linux epoll 机制
0 同步I/O,异步I/O
- 同步(阻塞)I/O:在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
- 异步(非阻塞)I/O:当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
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_ADD
、EPOLL_CTL_MOD
或 EPOLL_CTL_DEL
,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 表示监听的事件类型(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模式时,必须要保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);并且每次调用read
和write
的时候都必须等到它们返回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 能够明显提升性能。
不错,赞le