Linux epoll的深入剖析:关键要点与应用总览
什么是epoll?
epoll是自Linux 2.6版本开始,由Linux内核提供的一种高效I/O事件通知机制,用于解决传统select和poll在处理大量并发连接时存在的遍历效率低下、最大数量限制以及数据频繁拷贝等问题。它能够对多种文件描述符(诸如socket、管道、eventfd、timerfd等)上的I/O事件进行监听。
核心特性
高效的事件驱动模型
- 时间复杂度优势:无论监控多少个文件描述符(FD),epoll进行事件检测的时间复杂度几乎稳定在O(1),而select和poll的时间复杂度为O(n)。
- 仅返回活跃的FD:与select和poll需要遍历所有FD不同,epoll只会把发生事件的FD返回,从而减少了无效遍历。
支持大并发连接
- 单个进程能够轻松管理数十万乃至百万级别的并发连接,像Nginx、Redis就是借助epoll实现高并发的。
- 采用红黑树(RB – tree)来存储FD,使得查找、插入、删除操作都十分高效。
边缘触发与水平触发模式
- 水平触发(LT,默认模式):只要FD处于可读或可写状态,epoll就会持续向应用程序发送通知,这与poll的机制类似。
- 边缘触发(ET):仅在FD状态发生变化时才通知一次,这种模式效率更高,但需要正确处理,否则可能会丢失事件。
epoll的使用方法
epoll_create
int epoll_create(int size);
其作用是创建一个epoll实例,得到用于后续对epoll进行所有调用的epoll文件描述符。当不再需要时,调用close()进行关闭,当所有引用该epoll实例的文件描述符都关闭后,内核会销毁该实例并释放相关资源以供重用。返回值方面,成功时返回文件描述符,出错时返回 – 1并设置errno。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
该函数用于对要监听的fd进行添加、修改或删除操作。通过对由epfd引用的epoll实例执行控制操作,对目标文件描述符fd执行op操作。event参数描述了与文件描述符fd相关联的对象。
op参数
op | 功能 | 说明 |
---|---|---|
EPOLL_CTL_ADD |
将新的fd添加到epoll实例中 | 如果fd已存在则返回EEXIST |
EPOLL_CTL_MOD |
修改fd的监听事件 | 如果fd不存在则返回ENOENT |
EPOLL_CTL_DEL |
从epoll实例中移除fd | event 参数可以为NULL |
事件
头文件eventpoll.h
中的宏定义:
#define EPOLLIN (__force __poll_t)0x00000001
#define EPOLLOUT (__force __poll_t)0x00000004
#define EPOLLERR (__force __poll_t)0x00000008
#define EPOLLHUP (__force __poll_t)0x00000010
#define EPOLLRDHUP (__force __poll_t)0x00002000
#define EPOLLEXCLUSIVE ((__force __poll_t)(1U << 28))
#define EPOLLET ((__force __poll_t)(1U << 31))
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 */
epoll_data_t data; /* User data variable */
};
epoll的事件类型是在epoll_event.events
中设置的,epoll会依据这些事件类型来判断是否将fd加入活跃队列,最终由epoll_wait()
返回。
宏定义含义
宏定义 | 含义 |
---|---|
EPOLLIN |
表示对应的文件描述符可以进行读操作(recv/read) |
EPOLLOUT |
表示对应的文件描述符可以进行写操作(send/write) |
EPOLLRDHUP |
表示对方关闭了写端或者处于半关闭状态(对TCP很有用) |
EPOLLERR |
表示对应的文件描述符发生错误 |
EPOLLHUP |
表示对应的文件描述符被挂断(对方断开连接) |
EPOLLET |
边缘触发(Edge Trigger) |
EPOLLONESHOT |
事件只触发一次,触发后自动从epoll中移除 |
返回值:成功时返回文件描述符,出错时返回 – 1并设置errno。
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
该函数用于等待由epfd指向的epoll实例上的事件。events指向的内存区域将包含调用者可用的事件。epoll_wait()最多返回maxevents个事件,maxevents参数必须大于0。
timeout参数指定epoll_wait()阻塞的最小毫秒数。将timeout指定为 – 1会使epoll_wait()无限期阻塞,指定为0会使epoll_wait()立即返回,即便没有可用事件。
返回值:成功时返回已就绪的I/O文件描述符数量;如果在请求的timeout毫秒数内没有文件描述符就绪,则返回0;出错时返回 – 1并设置errno。
核心数据结构
eventpoll
eventpoll是epoll的控制中心,它用红黑树来管理监听的fd,rdllist是触发事件队列,wq是epoll_wait阻塞队列,poll_wait供file->poll使用,通过锁来保护多核并发。事件触发依靠回调,回调会唤醒epoll_wait,事件会被放到rdllist中。
/*
* This structure is stored inside the "private_data" member of the file
* structure and represents the main data structure for the eventpoll
* interface.
*/
struct eventpoll {
/*
* This mutex is used to ensure that files are not removed
* while epoll is using them. This is held during the event
* collection loop, the file cleanup path, the epoll file exit
* code and the ctl operations.
*/
struct mutex mtx;
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
struct list_head rdllist;
/* Lock which protects rdllist and ovflist */
rwlock_t lock;
/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;
/*
* This is a single linked list that chains all the "struct epitem" that
* happened while transferring ready events to userspace w/out
* holding ->lock.
*/
struct epitem *ovflist;
/* wakeup_source used when ep_scan_ready_list is running */
struct wakeup_source *ws;
/* The user that created the eventpoll descriptor */
struct user_struct *user;
struct file *file;
/* used to optimize loop detection check */
int visited;
struct list_head visited_list_link;
#ifdef CONFIG_NET_RX_BUSY_POLL
/* used to track busy poll napi_id */
unsigned int napi_id;
#endif
};
成员描述
- mtx:全局互斥锁,确保在事件收集、关闭fd、epoll_ctl操作时epoll的一致性。
- wq:epoll_wait阻塞队列,没有事件时,调用
epoll_wait
的进程会挂在这个队列上。 - poll_wait:供
file->poll()
使用的等待队列,内核中poll实现底层回调时会用到。 - rdllist:就绪事件链表,fd触发事件时,内核会把对应
epitem
放到这里,供epoll_wait
返回给用户。 - ovflist:单链表,当rdllist被锁定遍历,向用户空间发送数据时,rdllist不允许被修改,新触发的就绪epitem会被ovflist串联起来,等待rdllist处理完后,再将ovflist数据写入rdllist。具体可查看ep_scan_ready_list逻辑。
- user:拥有这个epoll的用户,用于内核权限检查、资源限制等。
- lock:锁,保护rdllist和ovflist。
- rbr:红黑树根节点,用来管理所有注册到epoll的fd(
epitem
)。 - file:eventpoll对应的文件结构,Linux中一切皆文件,用vfs管理数据。
- napi_id:应用于中断缓解技术。
epitem
epitem是epoll中管理单个fd事件状态的核心单元,它挂在红黑树上的rbn,就绪事件挂在rdllist,等待队列挂在pwqlist,将用户关心的事件拷贝到event
里,容器指针是ep,fd信息是ffd,这样能高效组织并高效唤醒。
/*
* Each file descriptor added to the eventpoll interface will
* have an entry of this type linked to the "rbr" RB tree.
* Avoid increasing the size of this struct, there can be many thousands
* of these on a server and we do not want this to take another cache line.
*/
struct epitem {
union {
/* RB tree node links this structure to the eventpoll RB tree */
struct rb_node rbn;
/* Used to free the struct epitem */
struct rcu_head rcu;
};
/* List header used to link this structure to the eventpoll ready list */
struct list_head rdllink;
/*
* Works together "struct eventpoll"->ovflist in keeping the
* single linked chain of items.
*/
struct epitem *next;
/* The file descriptor information this item refers to */
struct epoll_filefd ffd;
/* Number of active wait queue attached to poll operations */
int nwait;
/* List containing poll wait queues */
struct list_head pwqlist;
/* The "container" of this item */
struct eventpoll *ep;
/* List header used to link this item to the "struct file" items list */
struct list_head fllink;
/* wakeup_source used when EPOLLWAKEUP is set */
struct wakeup_source __rcu *ws;
/* The structure that describe the interested events and the source fd */
struct epoll_event event;
};
成员描述
- rbn:挂在
eventpoll->rbr
红黑树上。 - rcu:需要释放epitem时用RCU延迟删除。
- rdllink:链接到
eventpoll->rdllist
,表示就绪事件。fd有事件触发时,内核会把它放到rdllist
里。 - next:用于维护
eventpoll->ovflist
单向链表的next指针,防止拷贝事件到用户空间时遗漏新事件。 - ffd:记录节点对应的fd和file文件信息。
- nwait:等待队列个数。
- pwqlist:等待事件回调队列。数据进入网卡时,底层中断会执行ep_poll_callback。
- ep:eventpoll指针,epitem关联eventpoll。
- fllink:epoll文件链表结点,与epoll文件链表进行关联file.f_ep_links。参考fs.h, struct file结构。
- ws:EPOLLWAKEUP模式下使用。
- event:用户关注的事件。
epoll的网络服务器流程图
初始化阶段
socket()
:创建server_fd
bind()
:绑定IP和端口fcntl()
:设置非阻塞模式(O_NONBLOCK)listen()
:监听端口,准备接收连接
epoll创建与注册
epoll_fd = epoll_create()
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, EPOLLIN)
epoll_create()
:创建epoll实例,得到epoll_fd
epoll_ctl(EPOLL_CTL_ADD)
:将server_fd
加入epoll_fd
,监听EPOLLIN
(监听是否有新连接)
事件循环
epoll_wait(epoll_fd, events, ...)
epoll_wait()
:阻塞等待事件,若有事件触发(新连接/客户端读写事件),返回事件数组
处理新连接(server_fd可读)
if (server_fd == events[i].data.fd) {
client_fd = accept(server_fd); // 非阻塞accept
fcntl(client_fd, O_NONBLOCK); // 设置非阻塞
epoll_ctl(EPOLL_CTL_ADD, client_fd, EPOLLIN); // 监控client_fd
}
accept()
接收新连接,得到client_fd
- 设置
client_fd
非阻塞 epoll_ctl(EPOLL_CTL_ADD)
,监听client_fd
的EPOLLIN
处理客户端数据(client_fd可读)
if (client_fd 有 EPOLLIN 事件) {
ret = read(client_fd, buf, size); // 非阻塞read
if (ret > 0) {
// 正常读取数据,处理业务逻辑
} else if (ret == 0 || (ret == -1 && errno != EAGAIN)) {
// 客户端关闭连接或出错
close(client_fd);
epoll_ctl(EPOLL_CTL_DEL, client_fd); // 移除监控
}
// EAGAIN 表示数据未就绪,继续等待
}
- 如果
EPOLLIN
:说明client_fd
有数据可读,调用read()
ret > 0
:正常读取数据,进行业务处理。ret == 0
:客户端关闭连接,关闭client_fd
并从epoll移除。ret == -1 && errno == EAGAIN
:数据未就绪(非阻塞模式),继续等待。
处理客户端写入(EPOLLOUT事件)
if (需要向 client_fd 写入数据) {
epoll_ctl(EPOLL_CTL_MOD, client_fd, EPOLLOUT); // 关注可写事件
}
if (client_fd 有 EPOLLOUT 事件) {
ret = write(client_fd, buf, size); // 非阻塞write
if (ret == -1 && errno == EAGAIN) {
// 缓冲区满,稍后重试
} else if (ret >= 0) {
// 写入成功,恢复监控 EPOLLIN
epoll_ctl(EPOLL_CTL_MOD, client_fd, EPOLLIN);
} else {
// 写入失败,关闭连接
close(client_fd);
epoll_ctl(EPOLL_CTL_DEL, client_fd);
}
}
- 如果
EPOLLOUT
:说明client_fd
可写,调用write()
ret >= 0
:写入成功,恢复监控EPOLLIN
。ret == -1 && errno == EAGAIN
:缓冲区满,稍后重试。
关闭连接
close()
关闭server_fd
和所有client_fd
,结束服务
TCP + epoll流程图
这是一张基于Linux 5.0.1内核下,关于epoll的网络编程以及TCP连接建立相关的流程图,涵盖了从客户端连接请求到服务器端处理的多个环节,下面简单介绍流程:
① epoll_create
- 创建
eventpoll
结构体,初始化红黑树rbr
和就绪队列rdlist
- 返回
epoll_fd
② epoll_ctl
EPOLL_CTL_ADD
把目标fd
包装成epitem
节点- 挂到
eventpoll->rbr
红黑树里 - 建立
sock->sk_wq
的回调ep_poll_callback
③ 监听事件队列
- 将
epitem
挂到socket->wq->wait_queue_head
上 - 事件触发时由回调唤醒挂在
wait_queue
上的epitem
④ epoll_wait
- 把当前线程挂到
eventpoll->wq
上,进入TASK_INTERRUPTIBLE
- 调用
schedule()
让出CPU等待事件
⑤ 网络事件到达(TCP三次握手)
- 驱动收到网卡中断,触发
tcp_v4_rcv
- 调用
sock_def_wakeup
唤醒wait_queue
上的epitem
⑥ ep_poll_callback
- 将就绪的
epitem
节点挂到eventpoll->rdlist
- 设置
wake_up_flag
唤醒`epoll