前言:为什么我们需要 Epoll?

在开始讲 Epoll 之前,我们先想象一个场景:

你开了一家奶茶店(服务器),有很多顾客(客户端)来点单。 如果你的经营模式是 Blocking I/O(阻塞 IO)

服务员只能接待一个顾客。顾客没想好喝什么,服务员就一直傻站在那里等,不能去服务其他人。想要服务 1000 个顾客,你就得雇 1000 个服务员(线程)。 后果:成本太高,内存爆表,店铺倒闭。

后来你学聪明了,用了 Select/Poll

你雇了一个可以在大厅巡逻的服务员。他拿了一张名单,不停地问每一桌顾客:“你要点单吗?”、“你要点单吗?”... 问完第 1000 桌,再跑回第 1 桌继续问。 后果:虽然只要一个服务员,但他把时间都浪费在“问”上了,效率极低。如果 1000 桌里只有 1 桌要点单,他还是得要把 1000 桌都问一遍。

👉 Select / Poll 的本质问题是:

  • 每次都要“遍历全部连接”

  • 大多数连接其实是“没事干的”

最后,Epoll 横空出世了:

服务员坐在柜台,不用出去跑。每张桌子上装了一个按铃。谁要点单,谁按铃。服务员只看墙上的“灯牌”,哪个灯亮了,就直接去服务哪一桌。 后果:极其高效!不管坐了 10 万人还是 100 万人,服务员只服务那些“按铃”的人。

这就是 Epoll(Event Poll),Linux 下最高效的 I/O 多路复用机制。

一、 Epoll 是什么?

Epoll 是 Linux 内核为处理大批量文件描述符(FD, File Descriptor)而作了改进的 poll

它的核心作用是:“监听”海量的 Socket 连接,当哪个连接有数据来了(读事件)或者可以发数据了(写事件),它就通知应用程序去处理。

它解决了 select/poll 的两个致命缺点:

  1. 无差别轮询:Select 每次都要把所有连接遍历一遍,Epoll 只看活跃的连接。

  2. 数据拷贝:Select 每次都要把所有 fd 从用户态拷贝到内核态,Epoll 利用内存映射等技术减少了拷贝。

二、 Epoll 的“黑科技”:它是如何实现的?

很多小白觉得 Epoll 难,是因为不知道它在内核里到底干了什么。其实它主要靠两个数据结构:红黑树双向链表

1. 存储介质:红黑树 (Red-Black Tree)

当你调用 epoll_ctl 往 Epoll 里添加一个 socket 时,内核会把这个 socket 存在一颗红黑树里。

  • 为什么用红黑树? 因为红黑树查找、插入、删除都很快。

  • 作用:用来存储所有我们正在监控的连接。再也不用像 Select 那样每次传一大个数组进去了,Epoll 已经在内核里记住了所有连接。

2. 就绪列表:双向链表 (Ready List)

这是 Epoll 高效的秘诀!内核维护了一个链表,这个链表里只存放**“有数据来了”**的那些连接。

👉 epoll_wait 并不是遍历所有连接
👉 而是直接读取这个就绪链表

3. 回调机制 (Callback)

这是 Epoll 能够“按铃”的关键! 当网卡收到数据包时,驱动程序会产生中断。Epoll 在内核中注册了一个回调函数。 一旦某个 socket 有数据到了:

  1. 内核通过回调函数被唤醒。

  2. 内核迅速找到红黑树里对应的 socket。

  3. 把这个 socket 丢到“就绪链表”里去。

总结 Epoll 的工作流程:

  1. epoll_create:在内核建个“红黑树”和“就绪链表”。

  2. epoll_ctl:把新来的连接(FD)挂到“红黑树”上,并告诉内核:“这哥们有消息了记得告诉我”。

  3. (网卡收包过程):数据来了 -> 触发回调 -> 对应的 FD 被自动复制到“就绪链表”。

  4. epoll_wait:应用程序调用这个函数,其实就是去检查**“就绪链表”**是不是空的。

    • 如果不空,直接把链表里的东西拿走(O(1) 效率)。

    • 如果空,就睡一会儿等链表有东西。

三、 代码实战:三步走

在代码层面,Epoll 的操作非常简单,只需要三个 API。

第一步:创建 Epoll 实例 (epoll_create)

这就好比买了一本“记录本”,准备开始记账。

// 参数 1 现在的内核版本已经忽略了,只要大于 0 即可
int epfd = epoll_create(1); 
if (epfd == -1) {
    perror("epoll_create failed");
    exit(1);
}

第二步:管理监控项 (epoll_ctl)

这就好比往记录本上写名字(添加关注)或者划掉名字(不再关注)。

struct epoll_event ev;
ev.events = EPOLLIN;    // 关注“读事件”(有数据进来)
ev.data.fd = listenfd;  // 我们要关注的那个 socket(比如监听套接字)

// 参数操作:
// epfd: 第一步创建的实例
// EPOLL_CTL_ADD: 动作是“添加”
// listenfd: 目标 socket
// &ev: 我们关心的事件类型
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

第三步:等待事件发生 (epoll_wait)

这就好比老板坐在那里看“记录本”上的“就绪名单”。

struct epoll_event events[1024]; // 准备一个数组接“就绪名单”

while (1) {
    // 这一步是阻塞的。
    // 如果没有事件,程序停在这里休息,不消耗 CPU。
    // 如果有事件,内核把“就绪链表”里的数据填到 events 数组里,并返回数量 nready。
    int nready = epoll_wait(epfd, events, 1024, -1);

    // 遍历 nready,里面全是“有活干”的连接,没有一个是凑数的!
    for (int i = 0; i < nready; i++) {
        int connfd = events[i].data.fd;
        
        if (connfd == listenfd) {
            // 如果是监听 socket 就绪,说明有新连接来了 -> accept
        } else if (events[i].events & EPOLLIN) {
            // 如果是普通 socket 就绪,说明有数据发来了 -> recv
        }
    }
}

四、 补充知识:LT 模式与 ET 模式

Epoll 有两种工作模式,面试常考!

1. 水平触发 (LT - Level Triggered) —— 默认模式

  • 特点:只要缓冲区里还有数据,Epoll 就会一直通知你。

  • 比喻:快递员给你打电话:“你有快递!”。你嫌烦没下去拿。过了一会儿,快递员给你打电话:“你快递还在呢,快来拿!”直到你拿走为止。

  • 优点:编程简单,不易丢数据。

2. 边缘触发 (ET - Edge Triggered) —— 高速模式

  • 特点:只有数据从“无”变“有”的那一瞬间,Epoll 通知你一次。如果你不一次性读完,它再也不会通知你了。

  • 比喻:快递员发个短信:“快递放楼下了”。然后他就走了。如果你没看到或者忘了拿,快递员不会再提醒你,除非又来了新快递。

  • 优点:效率极高,减少了系统调用的次数。

  • 缺点:编程难度大,必须循环读取直到报错 EAGAIN,否则容易漏读数据。

👉 ET 快,是因为“减少了通知次数”
👉 代价是:程序必须写得非常严谨


五、 总结

  1. Epoll 是什么:Linux 下处理高并发网络连接的神器。

  2. 解决了什么:解决了 Select/Poll 轮询效率低、连接数受限的问题。

  3. 怎么实现的

    • 红黑树:快速存取监控的 socket。

    • 回调 + 就绪链表:数据一来自动加入链表,epoll_wait 直接拿链表,不需要遍历所有连接。

  4. 怎么用epoll_create (建本子) -> epoll_ctl (写名单) -> epoll_wait (等通知)。

希望这篇文章能帮你彻底搞懂 Epoll!在高性能网关(Nginx)、Redis、Node.js 底层,全都是 Epoll 在默默支撑着海量的请求。搞懂它,网络编程就算入门了一大半!

0voice · GitHub

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐