I/O 多路转接之 epoll:高并发服务器的性能利器

目录

[一、epoll 核心优势:解决 select/poll 的痛点](#一、epoll 核心优势:解决 select/poll 的痛点)

[二、epoll 工作原理:红黑树 + 就绪队列](#二、epoll 工作原理:红黑树 + 就绪队列)

核心流程

[三、epoll 关键系统调用](#三、epoll 关键系统调用)

[1. epoll_create:创建 epoll 实例](#1. epoll_create:创建 epoll 实例)

[2. epoll_ctl:管理监听的描述符](#2. epoll_ctl:管理监听的描述符)

[3. epoll_wait:等待就绪事件](#3. epoll_wait:等待就绪事件)

[四、epoll 的两种工作模式](#四、epoll 的两种工作模式)

[1. 水平触发(LT,默认模式)](#1. 水平触发(LT,默认模式))

[2. 边缘触发(ET)](#2. 边缘触发(ET))

[五、代码示例:基于 epoll 的多客户端服务器](#五、代码示例:基于 epoll 的多客户端服务器)

[六、epoll 的适用场景](#六、epoll 的适用场景)

七、总结


在高并发网络编程场景中,selectpoll 因自身缺陷(如描述符数量限制、遍历开销大等)逐渐力不从心。而 epoll 作为 Linux 下高性能的多路 I/O 复用技术,凭借其高效的事件通知机制,成为处理海量连接的 "性能利器"。

一、epoll 核心优势:解决 select/poll 的痛点

select/poll 相比,epoll 从根本上优化了高并发场景下的性能:

问题 select/poll 表现 epoll 表现
描述符数量限制 受限于 FD_SETSIZE(通常 1024) 无限制,仅受系统资源约束
遍历开销 线性扫描所有描述符(时间复杂度 O(n)) 直接获取就绪描述符(时间复杂度 O(1))
内存拷贝开销 每次调用需拷贝所有描述符到内核态 仅注册时拷贝,后续无额外开销

二、epoll 工作原理:红黑树 + 就绪队列

epoll 内部通过 "红黑树 + 就绪队列" 实现高效事件管理:

  • 红黑树 :存储所有需要监听的文件描述符(通过 epoll_ctl 注册)。
  • 就绪队列:当描述符就绪时,内核直接将其加入队列,避免遍历所有描述符。

核心流程

  1. 注册阶段 :通过 epoll_ctl 将描述符加入红黑树,内核为其注册回调函数。
  2. 就绪通知:当描述符就绪时,回调函数将其加入就绪队列。
  3. 获取就绪事件epoll_wait 直接从就绪队列中获取事件,无需遍历红黑树。

三、epoll 关键系统调用

1. epoll_create:创建 epoll 实例

复制代码
#include <sys/epoll.h>

int epoll_create(int size);
  • 作用 :创建一个 epoll 实例(本质是内核维护的红黑树和就绪队列)。
  • 参数size 已被废弃(只需传入大于 0 的值即可)。
  • 返回值epoll 实例的文件描述符(需通过 close 关闭)。

2. epoll_ctl:管理监听的描述符

复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 作用 :向 epoll 实例中添加、修改或删除监听的描述符。
  • 参数
    • epfdepoll_create 返回的实例描述符。
    • op:操作类型(EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL)。
    • fd:要监听的文件描述符。
    • event:监听的事件类型(如 EPOLLIN/EPOLLOUT 等)。

3. epoll_wait:等待就绪事件

复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 作用:等待并获取就绪的描述符事件。
  • 参数
    • events:用于存储就绪事件的数组。
    • maxeventsevents 数组的最大长度。
    • timeout:超时时间(-1 表示永久等待,0 表示非阻塞,正数为毫秒级超时)。
  • 返回值 :就绪事件的数量(0 表示超时,-1 表示出错)。

四、epoll 的两种工作模式

epoll 支持 水平触发(LT)边缘触发(ET) 两种模式,核心区别在于 "事件通知的时机"。

1. 水平触发(LT,默认模式)

  • 特点 :只要描述符就绪(如可读 / 可写),每次调用 epoll_wait 都会通知。
  • 场景:适合初学者或对性能要求不极致的场景,实现简单。

2. 边缘触发(ET)

  • 特点:仅在描述符 "从非就绪变为就绪" 时通知一次。
  • 优势:减少重复通知,性能更高(如 Nginx 默认使用 ET 模式)。
  • 注意 :需将描述符设为 非阻塞,并在一次通知中处理完所有数据(否则剩余数据不会再被通知)。

五、代码示例:基于 epoll 的多客户端服务器

下面是一个完整的 TCP 服务器示例,使用 epoll 处理多客户端连接:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>

#define MAX_CLIENTS 1024
#define BUFFER_SIZE 1024
#define PORT 8888

// 设置文件描述符为非阻塞
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags < 0) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    // 1. 创建服务器套接字
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 允许地址重用
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定地址和端口
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 3. 开始监听
    if (listen(server_fd, 5) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server started on port %d (epoll mode)\n", PORT);

    // 4. 创建 epoll 实例
    int epoll_fd = epoll_create(1);
    if (epoll_fd < 0) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }

    // 5. 添加服务器套接字到 epoll(监听新连接)
    struct epoll_event ev;
    ev.events = EPOLLIN;          // 监听可读事件
    ev.data.fd = server_fd;       // 存储服务器描述符
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) < 0) {
        perror("epoll_ctl");
        exit(EXIT_FAILURE);
    }

    struct epoll_event events[MAX_CLIENTS]; // 存储就绪事件
    int client_fds[MAX_CLIENTS] = {0};      // 存储客户端描述符

    while (1) {
        // 6. 等待事件就绪
        int ready = epoll_wait(epoll_fd, events, MAX_CLIENTS, -1);
        if (ready < 0) {
            perror("epoll_wait");
            continue;
        }

        // 7. 处理就绪事件
        for (int i = 0; i < ready; ++i) {
            int fd = events[i].data.fd;

            // 处理新连接
            if (fd == server_fd) {
                struct sockaddr_in client_addr;
                socklen_t addr_len = sizeof(client_addr);
                int new_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
                if (new_fd < 0) {
                    perror("accept");
                    continue;
                }

                // 设置客户端套接字为非阻塞(ET 模式需要)
                if (set_nonblocking(new_fd) < 0) {
                    perror("set_nonblocking");
                    close(new_fd);
                    continue;
                }

                // 添加客户端套接字到 epoll(监听可读事件,ET 模式)
                ev.events = EPOLLIN | EPOLLET; // ET 模式 + 可读事件
                ev.data.fd = new_fd;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &ev) < 0) {
                    perror("epoll_ctl");
                    close(new_fd);
                    continue;
                }

                // 存储客户端描述符
                for (int j = 0; j < MAX_CLIENTS; ++j) {
                    if (client_fds[j] == 0) {
                        client_fds[j] = new_fd;
                        printf("New client connected: fd = %d\n", new_fd);
                        break;
                    }
                }
            }
            // 处理客户端数据(ET 模式)
            else {
                char buffer[BUFFER_SIZE];
                int n;
                while ((n = read(fd, buffer, BUFFER_SIZE - 1)) > 0) {
                    buffer[n] = '\0';
                    printf("Received from client %d: %s", fd, buffer);
                    write(fd, buffer, n); // 回显数据
                }

                // 客户端断开或出错
                if (n <= 0) {
                    printf("Client %d disconnected\n", fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); // 从 epoll 中移除
                    close(fd);

                    // 清理客户端描述符数组
                    for (int j = 0; j < MAX_CLIENTS; ++j) {
                        if (client_fds[j] == fd) {
                            client_fds[j] = 0;
                            break;
                        }
                    }
                }
            }
        }
    }

    // 关闭 epoll 和服务器套接字(实际中不会执行到这里)
    close(epoll_fd);
    close(server_fd);
    return 0;
}

六、epoll 的适用场景

  • 高并发场景 :需要处理数千甚至数万连接时,epoll 的性能优势明显。
  • 性能敏感应用:如 Web 服务器(Nginx)、数据库连接池、实时通信系统等。
  • ET 模式优化:对延迟要求极高的场景,可通过 ET 模式进一步减少通知次数。

七、总结

epoll 是 Linux 下最强大的多路 I/O 复用技术,通过 "红黑树 + 就绪队列" 的设计,解决了 select/poll 的性能瓶颈。在高并发场景下,epoll 能高效处理海量连接,是构建高性能服务器的核心工具。


如果需要兼容多平台,select/poll 仍是备选;但在 Linux 专属的高并发场景中,epoll 几乎是唯一选择。

相关推荐
BingoGo2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
feifeigo1235 天前
matlab画图工具
开发语言·matlab
dustcell.5 天前
haproxy七层代理
java·开发语言·前端