epoll详解

epoll 是 Linux 提供的 I/O 多路复用机制,

用于在一个进程中同时监听多个文件描述符(如 socket),当某些文件描述符发生 I/O 事件时,内核会通知应用程序进行处理。

epoll三个核心函数

int epoll_create(int size);

在内核中创建一个 eventpoll 对象,并返回对应的文件描述符 epfd。

size 参数在早期 Linux 内核中表示:预计要监听的 fd 数量,但在现代 Linux 内核中已经没有实际作用,只要大于 0 即可。

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

用来管理 epoll 实例里监听的文件描述符。

常见操作类型:

  • EPOLL_CTL_ADD
  • EPOLL_CTL_MOD
  • EPOLL_CTL_DEL
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

/*
调用后执行的操作
1 检查 rdllist
2 如果为空,则把当前线程加入 wq 睡眠
3 如果有 ready 事件,则从 rdllist 取 epitem
4 把事件拷贝到用户态的 events 数组
5 返回实际事件个数
*/

等待事件发生,并把就绪事件拷贝到用户态。

epoll_wait 是否会阻塞,取决与timeout

timeout = -1,一直阻塞,直到有事件
timeout = 0,不阻塞,立即返回
timeout > 0,最多等 timeout 毫秒

epoll的工作原理

在这里插入图片描述
内核部分源码eventpoll结构

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 */
	spinlock_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_send_events or __ep_eventpoll_poll 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 */
	u64 gen;
	struct hlist_head refs;
	u8 loop_check_depth;

	/*
	 * usage count, used together with epitem->dying to
	 * orchestrate the disposal of this struct
	 */
	refcount_t refcount;

#ifdef CONFIG_NET_RX_BUSY_POLL
	/* used to track busy poll napi_id */
	unsigned int napi_id;
	/* busy poll timeout */
	u32 busy_poll_usecs;
	/* busy poll packet budget */
	u16 busy_poll_budget;
	bool prefer_busy_poll;
#endif

#ifdef CONFIG_DEBUG_LOCK_ALLOC
	/* tracks wakeup nests for lockdep validation */
	u8 nests;
#endif
};

epitem结构:epoll内核中的节点结构,将epoll实例和一个监听fd进行绑定,并记录该fd的事件信息。

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;

	/*
	 * Protected by file->f_lock, true for to-be-released epitem already
	 * removed from the "struct file" items list; together with
	 * eventpoll->refcount orchestrates "struct eventpoll" disposal
	 */
	bool dying;

	/* List containing poll wait queues */
	struct eppoll_entry *pwqlist;

	/* The "container" of this item */
	struct eventpoll *ep;

	/* List header used to link this item to the "struct file" items list */
	struct hlist_node 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;
};

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

简单示例

/*
	回声服务器
*/
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>

#define MAX_EVENTS 100
#define PORT 8888

int main(){
    // 1 创建 socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    bind(listenfd, (sockaddr*)&addr, sizeof(addr));
    listen(listenfd, 5);
    // 2 创建 epoll
    int epfd = epoll_create(1);
    epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
    while (1){
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++){
            int fd = events[i].data.fd;
            // 新连接
            if (fd == listenfd){
                int connfd = accept(listenfd, NULL, NULL);
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }
            else{
                char buf[1024] = {0};
                int len = read(fd, buf, sizeof(buf));
                if (len <= 0){
                    close(fd);
                }
                else{
                    write(fd, buf, len); // 回显
                }
            }
        }
    }
}

常见面试问题

1、select、poll、epoll的区别

监听文件描述符的限制:

  • select有上限值,默认是1024
  • poll 没有固定上限,理论上只受系统打开文件描述符数量限制
  • epoll 没有固定上限,同样理论上只受系统打开文件描述符数量限制

底层的数据结构不同:

  • select 是基于位图
  • poll 是基于数组
  • epoll底层是红黑树加一个双向链表

遍历fd的不同:

  • select 不会通知我们哪些fd上有事件发生,返回后需要遍历所有监听的 fd,通过 FD_ISSET 判断哪些 fd 就绪
  • poll 同样不会通知我们哪些fd上有事件发生,返回后同样需要遍历整个 pollfd 数组
  • epoll会告诉我们哪些fd上有事件发生,只返回就绪的fd,不需要遍历全部监听对象
2、epoll比poll更快吗

不一定,在并发量不高的场景下,

poll 遍历一个结构体数组未必满(O(n)),

在高并发场景下 epoll 性能明显更好,因为 epoll 只返回就绪的 fd,而 poll 需要遍历所有 fd。具体多高的连接算高并发,只能仁者见仁智者见智了

3、epoll 的底层为什么使用的是红黑树,而不是hash表或者B+树

因为 epoll 底层管理监听 fd 的核心需求需要满足:

高效插入
高效删除
高效查找
有序稳定
内核实现成熟
最坏情况可控

红黑树在内核中实现成熟,性能稳定,提供 O(log n) 的插入、删除和查找性能。

Hash 表虽然平均查找快(O(1)),但存在哈希冲突和扩容带来的最坏情况退化为(O(n))问题,

B+ 树更适合面向磁盘或大规模范围查询场景

4、epoll的ET和LT模式

主要区别是事件通知方式不同。

ET:边缘触发,只有状态发生变化时才通知(无数据->有数据),只触发 一次事件。ET 模式必须配合非阻塞,因为必须一次性把数据读完。

LT:水平触发,默认模式,只要 fd 上还有数据,就会一直通知。

5、为什么ET必须配合非阻塞

读数据时,可能一直阻塞在 read,后面的连接和事件就处理不了了,而 ET 又只通知一次,所以必须循环读,直到返回 EAGAIN。

6、epoll中的惊群问题是什么

如果多个线程/进程同时阻塞在同一个 epoll 实例上,一个事件到来时,可能把多个等待者一起唤醒,这就叫 惊群

大家都去抢同一个fd,产生大量无效唤醒和上下文切换。

常见解决方法包括:让一个线程专门负责 epoll_wait、使用线程池分发任务、在监听 socket 上使用 EPOLLEXCLUSIVE,以及配合 EPOLLONESHOT 避免同一连接

被多个线程重复处理。

T必须配合非阻塞

读数据时,可能一直阻塞在 read,后面的连接和事件就处理不了了,而 ET 又只通知一次,所以必须循环读,直到返回 EAGAIN。

6、epoll中的惊群问题是什么

如果多个线程/进程同时阻塞在同一个 epoll 实例上,一个事件到来时,可能把多个等待者一起唤醒,这就叫 惊群

大家都去抢同一个fd,产生大量无效唤醒和上下文切换。

常见解决方法包括:让一个线程专门负责 epoll_wait、使用线程池分发任务、在监听 socket 上使用 EPOLLEXCLUSIVE,以及配合 EPOLLONESHOT 避免同一连接

被多个线程重复处理。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐