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 几乎是唯一选择。

相关推荐
郝学胜-神的一滴2 小时前
深入理解 C++ 中的 `std::bind`:功能、用法与实践
开发语言·c++·算法·软件工程
zhangfeng11333 小时前
wgcna 相关性热图中4个颜色 4个共表达模块 的模块基因是否都要做GO/KEGG分析”,核心取决于你的**研究目标和模块的生物学意义*
开发语言·r语言·生物信息
come112343 小时前
Go 语言中的结构体
android·开发语言·golang
Dream_Ji3 小时前
Swift 入门(一 - 基础语法)
开发语言·ios·swift
勇闯逆流河3 小时前
【C++】AVL详解
开发语言·c++
一口面条一口蒜4 小时前
R语言中的获取函数与替换函数
开发语言·r语言
程序员烧烤4 小时前
【Java初学基础10】一文讲清反射
java·开发语言
长安——归故李4 小时前
【PLC程序学习】
java·c语言·javascript·c++·python·学习·php
大飞pkz4 小时前
【设计模式】状态模式
开发语言·设计模式·c#·状态模式