多路复用I/O之Epoll

一、为什么要设计epoll

复制代码
read(socket_fd, buf, sizeof(buf));  // 如果没有数据,进程阻塞

在传统场景下,一个进程/线程往往只能对应一个链接,而一旦这个链接出现阻塞就容易出现被OS挂起的尴尬情况--整个进程/线程都会挂掉,这在工程中是难以接受的。

哪怕后续的select和poll在一定程度上解决部分问题,但依旧留下需要解决的新问题:

文件描述符数量限制(select)、效率不高O(n) -- 每次调用都要把全部描述符集合从用户态拷贝到内核态,内核需要线性扫描所有描述符找出就绪的。

epoll的设计初衷就是消除描述符数量限制、消除 O(n) 扫描 → 改为 O(1) 返回就绪列表、消除用户态/内核态频繁拷贝 → 通过内核共享内存。

二、epoll系统调用

epoll_create1(int flags)

复制代码
int epfd = epoll_create1(0);

作用:创建一个 epoll 实例,返回一个文件描述符指向该实例。

参数

  • flags:常用 0EPOLL_CLOEXEC(执行 exec 时自动关闭)

内核行为

  • 分配 struct eventpoll 对象。

  • 初始化红黑树、就绪链表、等待队列。

  • 分配一个未使用的文件描述符作为句柄。

注意

  • 最终必须调用 close(epfd) 释放内核资源。

  • epfd 本身也是一个文件描述符,可以用 epoll_ctl 监视另一个 epoll fd(较少用)。

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

复制代码
int epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

作用:控制 epoll 实例,添加/修改/删除要监视的文件描述符。

参数详解

参数 说明
epfd epoll_create 返回的描述符
op 操作类型
fd 要监视的目标文件描述符
event 告诉内核关心什么事件

op 取值

  • EPOLL_CTL_ADD:添加 fd 到红黑树(如果已存在则报错)。

  • EPOLL_CTL_MOD:修改 fd 上监听的事件(如从读改成写)。

  • EPOLL_CTL_DEL:从红黑树删除 fd(如果 fd 已关闭,内核会自动删除)。

struct epoll_event 结构

复制代码
struct epoll_event {
    uint32_t     events;   // 事件掩码(告诉内核关心什么)
    epoll_data_t data;     // 用户数据(告诉内核返回时带什么信息)
};

typedef union epoll_data {
    void    *ptr;   // 可以指向任意用户自定义结构
    int      fd;    // 最常用:直接存放 fd
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

常用事件掩码 events

掩码 含义
EPOLLIN 可读事件(socket 有数据、对端关闭连接)
EPOLLOUT 可写事件(发送缓冲区有空闲)
EPOLLERR 错误事件(通常不需要特意注册,会自动返回)
EPOLLHUP 挂断事件(对端关闭)
EPOLLET 边缘触发(Edge Triggered)
EPOLLONESHOT 一次性触发,处理完事件后自动删除,需要重新添加
EPOLLRDHUP 对端关闭连接(TCP 的 FIN 包,比 EPOLLHUP 更精细)

内核行为(ADD 操作)

  1. 检查 fd 是否已经在红黑树中。

  2. 分配一个 struct epitem(红黑树节点)。

  3. 将 fd 的信息、注册的事件、回调函数 ep_poll_callback 封装到 epitem 中。

  4. epitem 插入红黑树。

  5. 设置 fd 的回调函数为 ep_poll_callback

epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

复制代码
int nfds = epoll_wait(epfd, ready_events, MAX_EVENTS, -1);

作用:等待事件发生,返回就绪的文件描述符列表。

参数

  • epfd:epoll 实例描述符。

  • events:输出参数,数组用于存放就绪的事件(由用户分配)。

  • maxevents:数组大小,告诉内核最多返回多少个事件。

  • timeout:超时时间(毫秒),-1 表示无限等待,0 表示立即返回。

返回值

  • >0:就绪的文件描述符个数。

  • 0:超时,没有事件就绪。

  • -1:错误,errno 指示错误类型。

内核行为详解

复制代码
1. 检查就绪链表 rdllist 是否为空。
2. 如果非空:
   - 将就绪链表中的事件复制到用户空间的 events 数组(最多 maxevents 个)。
   - 根据触发模式决定是否从就绪链表中移除节点(LT 不移除,ET 移除)。
   - 返回复制的事件数量。
3. 如果为空且 timeout > 0:
   - 当前进程进入可中断睡眠状态,加入到 epoll 的等待队列。
   - 当有事件发生或超时到达时,进程被唤醒。
   - 唤醒后,重新检查就绪链表(可能多个事件同时到达)。
4. 如果 timeout == 0:
   - 立即返回 0。

关键点

  • events 数组由用户分配,内核只负责填充,避免了内核态分配内存的开销。

  • maxevents 建议设为略大于预期同时就绪的数量,典型值 1024。

三、epoll工作原理

一、底层核心内核结构

epoll 在内核中维护两个核心结构体,对应图中红黑树、双向链表结构:

  1. struct eventpoll(epoll 总控对象,epoll_create 创建)
    • rbr红黑树根节点 ,存储所有被监控的文件描述符 fd ,基于epitemrbn成员构建,增删查 fd 时时间复杂度为 \(O(logN)\);
    • rdllist就绪事件双向链表 ,存储已经就绪的 fd ,基于epitemrdllink成员构建,是 epoll \(O(1)\) 获取就绪事件的核心;
    • wq进程等待队列 ,无就绪事件时,调用epoll_wait的进程会挂载到该队列休眠;
  2. **struct epitem(单个 fd 的包装对象,epoll_ctl ADD 创建)**每一个被监控的 fd,都会被内核包装为epitem,包含 fd 标识、用户注册的监听事件、红黑树节点、就绪链表节点、回调关联等信息,是红黑树和就绪链表的基础单元。

二、三大系统调用的内核执行逻辑

epoll 通过 3 个系统调用完成创建实例、添加 / 修改 / 删除监控 fd、等待就绪事件全流程,每个调用的内核行为如下:

  1. epoll_create(size):创建 epoll 实例 内核分配struct eventpoll内核对象,初始化红黑树、就绪链表、等待队列,返回文件描述符epfd;入参size在 Linux 2.6.8 后废弃,仅需传入大于 0 的整数。
  2. epoll_ctl(epfd, op, fd, event):管理监控的 fd
    • EPOLL_CTL_ADD(添加 fd) :创建epitem并插入红黑树;核心操作:将 epoll 专属回调函数ep_poll_callback注册到 fd 底层驱动的等待队列,实现 "fd 就绪主动通知 epoll";
    • EPOLL_CTL_MOD(修改 fd 监听事件) :修改红黑树中epitem存储的监听事件掩码,不改变回调关联;
    • EPOLL_CTL_DEL(删除 fd) :从红黑树删除epitem,注销 fd 上注册的回调,解除监控关系。
  3. epoll_wait(epfd, events, maxevents, timeout):阻塞等待就绪事件
    1. 内核检查rdllist就绪链表;
    2. 链表非空:将就绪事件拷贝到用户态,直接返回就绪事件数量;
    3. 链表为空:将当前进程挂载到eventpoll->wq等待队列,进程休眠并让出 CPU;
    4. 超时 / 收到信号 / 被唤醒:进程退出休眠,再次检查就绪链表。

三、核心:回调 + 主动唤醒的事件流转机制

这是 epoll 区别于 select/poll 的核心,以 TCP socket 接收数据为例,流程分为 3 个阶段:

1. 注册阶段(epoll_ctl ADD

添加 socket fd 到 epoll 时,内核将epoll 的就绪回调函数ep_poll_callback注册到网卡 / 协议栈的等待队列,建立 "fd 就绪→触发回调" 的关联。

2. 就绪触发阶段(IO 事件发生)

网卡收到 TCP 数据,内核协议栈处理后将数据写入 socket 接收缓冲区;socket 缓冲区从空变为非空时,底层驱动触发等待队列中的ep_poll_callback回调函数;回调函数完成 2 个关键操作:

  1. 将该 fd 对应的epitem添加到rdllist就绪链表;
  2. 唤醒休眠在eventpoll->wq队列上的进程。

3. 事件获取阶段(epoll_wait返回)

进程被唤醒后,从休眠位置继续执行,读取rdllist就绪链表中的事件,仅将就绪的少量事件拷贝到用户空间,返回给应用程序。

四、epoll 高性能的本质:对比 select/poll

特性 select/poll epoll
就绪检测方式 主动轮询:每次调用遍历所有监控 fd,O(N) 被动回调:fd 就绪主动通知,仅处理就绪事件,O(1)
数据拷贝 全量拷贝:每次调用复制全部 fd 集合 增量拷贝:仅拷贝就绪事件,数据量极少
进程调度 无效轮询:进程唤醒后仍需遍历所有 fd 精准唤醒:仅就绪事件触发时唤醒,无无效检查
操作复杂度 每次调用 O(N) epoll_ctl O(logN),epoll_wait O(1)

关键细节

  1. 零拷贝误区 :epoll 并非完全零拷贝,epoll_wait仍需将内核就绪事件拷贝到用户空间,但仅拷贝就绪事件,远优于 select/poll 的全量拷贝;
  2. 触发模式:epoll 支持水平触发(LT,默认)和边缘触发(ET),LT 为只要事件未处理就持续通知,ET 为仅事件状态改变时通知;
  3. 休眠唤醒 :进程在epoll_wait的休眠是可中断的,仅就绪回调唤醒、定时器超时、接收信号时会退出休眠,无主动轮询行为。

四、水平触发(LT) vs 边缘触发(ET)

  • LT(水平触发,默认):只要条件满足,就一直提醒
  • ET(边缘触发):只有条件刚发生变化的一瞬间 提醒 1 次

我们把 socket 内核缓冲区 = 一个水桶epoll = 报警器,监控水桶里有没有水(有没有数据可读)

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

只要水桶里还有水 ,报警器就一直滴滴响、不停提醒

  • 例子:水桶进了 100ml 水,你舀出 50ml,桶里还剩 50ml;
  • 报警器不会停,只要有水,每次看都会提醒你:"还有水!快舀!"

对应 epoll 行为 :只要 socket 缓冲区还有未读完的数据 ,每次调用epoll_wait都会返回可读事件,反复通知你。

2. 边缘触发 ET

只有水刚流进水桶的那一瞬间 ,报警器只响 1 声 之后桶里就算剩很多水,报警器彻底闭嘴,不再提醒,直到下次新水进来

  • 例子:水桶进了 100ml 水,报警器响 1 声;你舀出 50ml,桶里剩 50ml;
  • 此时你再看报警器,完全不响了;必须等下次新水流进来,才会再响 1 声。

对应 epoll 行为 :只有数据刚到达、缓冲区从无→有(状态发生切换)的瞬间epoll_wait通知 1 次;缓冲区残留数据,永远不会再通知

场景 水平触发 (LT) 边缘触发 (ET)
数据到达 每次 epoll_wait 都返回 只返回一次
数据没读完 下次 epoll_wait 继续返回 不再返回(除非新数据到达)
编码难度 简单,不易出错 困难,需小心处理
性能 可能多次唤醒 一次唤醒处理完所有数据
适用场景 通用,低速设备 高速大量数据,文件服务器
模式 触发时机 核心特点 必须注意
LT 水平触发 缓冲区有数据就触发 有就一直喊,反复提醒 阻塞 IO 也能用,简单安全
ET 边缘触发 缓冲区状态变化瞬间触发(无→有) 变了才喊一次,之后沉默 必须用非阻塞 IO,必须一次性读完缓冲区

得益于ET这种"强硬模式"倒逼快速读取缓冲区内容,ET的效率明显会比LT模式高。

五、epoll 的惊群问题

什么是惊群?

多个进程 / 线程,同时阻塞在同一个 epoll 的 epoll_wait ;当一个 IO 事件就绪 时,内核会唤醒所有等待的进程 / 线程 ;但最终只有一个进程能处理这个事件 ,其余进程白醒一场,重新休眠。这种无效唤醒、浪费 CPU 资源 的现象,就是 epoll 惊群

解决方案

方案1:EPOLLEXCLUSIVE

复制代码
ev.events = EPOLLIN | EPOLLEXCLUSIVE;

给监听事件添加 EPOLLEXCLUSIVE 标志,内核只唤醒一个等待进程,不批量唤醒。适用于多个进程共享同一个监听 socket。

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

#define PORT 8080
#define MAX_EVENTS 10
#define PROC_NUM 4  // 开启4个子进程

// 创建监听socket
int create_listen_fd() {
    int lfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(lfd, 128);
    return lfd;
}

int main() {
    int lfd = create_listen_fd();
    // 1. 创建1个epoll实例(所有子进程共享)
    int epfd = epoll_create1(0);
    struct epoll_event ev, events[MAX_EVENTS];

    // ============== 核心:添加 EPOLLEXCLUSIVE 解决惊群 ==============
    ev.events = EPOLLIN | EPOLLEXCLUSIVE;  // 独占唤醒标志
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    // 2. fork 4个子进程,共享epfd
    for (int i = 0; i < PROC_NUM; i++) {
        if (fork() == 0) {
            printf("子进程 %d 启动\n", getpid());
            while (1) {
                // 所有子进程阻塞在同一个 epoll_wait
                int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
                for (int j = 0; j < n; j++) {
                    if (events[j].data.fd == lfd) {
                        // 仅一个进程会执行到这里!惊群解决
                        int cfd = accept(lfd, NULL, NULL);
                        printf("进程 %d 处理新连接\n", getpid());
                        close(cfd);
                    }
                }
            }
        }
    }
    wait(NULL);
    return 0;
}

**方案2:**主进程 accept,子进程独立 epoll

适合所有 Linux 版本 ,生产环境首选,从根源彻底杜绝惊群

主进程 :只负责监听 + accept 新连接;子进程 :每个进程创建独立的 epoll 实例,互不共享;主进程把新连接分发给子进程 → 无共享 = 无惊群

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

#define PORT 8080
#define PROC_NUM 4

int create_listen_fd() {
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {AF_INET, htons(PORT), INADDR_ANY};
    bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(lfd, 128);
    return lfd;
}

// 子进程工作函数:独立epoll,处理连接
void child_work() {
    // ============== 核心:每个子进程创建自己的 epoll 实例 ==============
    int epfd = epoll_create1(0);
    struct epoll_event ev, events[10];
    printf("子进程 %d 独立epoll创建完成\n", getpid());

    while (1) {
        int n = epoll_wait(epfd, events, 10, -1);
        // 处理自己epoll上的事件(无竞争,无惊群)
    }
}

int main() {
    int lfd = create_listen_fd();
    // 1. 创建4个子进程
    for (int i = 0; i < PROC_NUM; i++) {
        if (fork() == 0) child_work();
    }

    // 2. 主进程:只做accept,分发给子进程
    printf("主进程 %d 负责监听连接\n", getpid());
    while (1) {
        int cfd = accept(lfd, NULL, NULL);
        // 此处可将cfd发送给子进程(简化版,直接关闭演示)
        close(cfd);
    }
    return 0;
}

六、epoll 常见陷阱

陷阱 错误做法 后果 正确做法
ET模式忘记循环读取 read() 只调用一次 剩余数据永久滞留,无法处理 while(1) 循环读直到 EAGAIN
ET模式使用阻塞fd fd 未设置 O_NONBLOCK 最后一次 read 阻塞,进程卡死 必须 `fcntl(fd, F_SETFL, flags
EPOLLOUT滥用 同时注册 `EPOLLIN EPOLLOUT` CPU 100% 空转
忽略ERR/HUP 只处理 EPOLLIN/OUT fd 泄漏,事件反复触发 必须处理 `EPOLLERR
多线程不加保护 多线程同时操作同一个 epfd 数据竞争,事件错乱 使用 EPOLLONESHOT 或外部锁
data.ptr 忘记释放 ptr 指向堆内存,关闭时不释放 内存泄漏 close(fd)free(ptr)

陷阱一:ET模式忘记循环读取

❌ 错误示例

复制代码
// 注册 ET 模式
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

// 事件处理
void handle_event(int fd) {
    char buf[1024];
    // 只读一次,如果一次没读完,剩余数据永远无法处理
    int n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        process(buf, n);
    }
}

✅ 正确示例

复制代码
void handle_event(int fd) {
    char buf[4096];
    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n > 0) {
            process(buf, n);
        } else if (n == 0) {
            // 对端关闭
            close(fd);
            break;
        } else {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 数据已全部读完
                break;
            } else {
                // 真正错误
                close(fd);
                break;
            }
        }
    }
}

陷阱二:ET模式使用阻塞fd

❌ 错误示例

复制代码
// 没有设置非阻塞,直接 accept 后加入 epoll
int client_fd = accept(listen_fd, ...);
// 忘记调用 set_nonblocking()

ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

// 事件处理中循环读取
while (1) {
    ssize_t n = read(client_fd, buf, sizeof(buf));
    if (n == -1 && errno == EAGAIN) {
        break;  // 永远执行不到这里,因为阻塞 fd 不会返回 EAGAIN
    }
}

✅ 正确示例

复制代码
// 设置非阻塞函数
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

// 正确用法
int client_fd = accept(listen_fd, ...);
set_nonblocking(client_fd);  // 必须设置

ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

陷阱三:EPOLLOUT滥用

❌ 错误示例

复制代码
// 同时注册 IN 和 OUT
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLOUT;  // OUT 会一直触发
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

// 事件循环
while (1) {
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].events & EPOLLOUT) {
            // 每次 epoll_wait 都会进来,即使没有数据要发
            // CPU 瞬间 100%
        }
    }
}

✅ 正确示例

复制代码
// 按需注册 OUT
typedef struct {
    char* buffer;
    size_t len;
    size_t sent;
} pending_send_t;

pending_send_t pending[MAX_FD];

// 发送数据函数
void try_send(int fd, const char* data, size_t len) {
    ssize_t n = send(fd, data, len, 0);
    if (n == -1 && errno == EAGAIN) {
        // 缓冲区满,保存待发数据
        pending[fd].buffer = strdup(data);
        pending[fd].len = len;
        pending[fd].sent = 0;
        
        // 添加 OUT 监听
        struct epoll_event ev;
        ev.events = EPOLLIN | EPOLLOUT;
        ev.data.fd = fd;
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
    }
}

// 事件处理
if (events[i].events & EPOLLOUT) {
    int fd = events[i].data.fd;
    pending_send_t* p = &pending[fd];
    
    ssize_t n = send(fd, p->buffer + p->sent, p->len - p->sent, 0);
    if (n > 0) {
        p->sent += n;
        if (p->sent >= p->len) {
            // 发送完成,移除 OUT 监听
            free(p->buffer);
            memset(p, 0, sizeof(*p));
            
            struct epoll_event ev;
            ev.events = EPOLLIN;  // 只保留 IN
            ev.data.fd = fd;
            epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
        }
    }
}

陷阱四:忽略ERR/HUP

❌ 错误示例

复制代码
// 只处理 EPOLLIN 和 EPOLLOUT
for (int i = 0; i < nfds; i++) {
    if (events[i].events & EPOLLIN) {
        handle_read(events[i].data.fd);
    }
    if (events[i].events & EPOLLOUT) {
        handle_write(events[i].data.fd);
    }
    // 忽略 EPOLLERR 和 EPOLLHUP -> fd 永远不会被关闭
}

✅ 正确示例

复制代码
for (int i = 0; i < nfds; i++) {
    int fd = events[i].data.fd;
    
    // 先检查错误和挂断
    if (events[i].events & (EPOLLERR | EPOLLHUP)) {
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        continue;
    }
    
    if (events[i].events & EPOLLIN) {
        handle_read(fd);
    }
    
    if (events[i].events & EPOLLOUT) {
        handle_write(fd);
    }
}

陷阱五:多线程不加保护

❌ 错误示例

复制代码
// 线程1:不断调用 epoll_wait
void thread1() {
    while (1) {
        int nfds = epoll_wait(epfd, events, 1024, -1);
        // 处理事件...
    }
}

// 线程2:同时修改 epoll
void thread2() {
    // 没有同步机制,直接修改
    epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &ev);
}

✅ 正确示例(使用 EPOLLONESHOT)

复制代码
// 主线程:注册时使用 EPOLLONESHOT
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLONESHOT;  // 触发一次后自动禁用
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

// 工作线程:处理完事件后重新注册
void worker_thread(int fd) {
    // 处理这个 fd 上的所有数据
    handle_read_until_eagain(fd);
    
    // 重新注册,让 epoll 可以再次触发
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLONESHOT;
    ev.data.fd = fd;
    epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}

✅ 正确示例(使用互斥锁)

复制代码
pthread_mutex_t epoll_mutex = PTHREAD_MUTEX_INITIALIZER;

void modify_epoll(int op, int fd, struct epoll_event* ev) {
    pthread_mutex_lock(&epoll_mutex);
    epoll_ctl(epfd, op, fd, ev);
    pthread_mutex_unlock(&epoll_mutex);
}

陷阱六:data.ptr 忘记释放

❌ 错误示例

复制代码
struct connection {
    int fd;
    char* buffer;
    size_t buffer_len;
};

// 添加连接
struct connection* conn = malloc(sizeof(struct connection));
conn->fd = client_fd;
conn->buffer = malloc(8192);

struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = conn;  // 使用 ptr 携带指针
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

// 关闭连接时 - 错误:只关闭 fd,忘记释放 conn
if (error_occurred) {
    close(client_fd);
    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
    // 内存泄漏!conn 和 conn->buffer 都没有释放
}

✅ 正确示例

复制代码
// 关闭连接时 - 正确
if (error_occurred) {
    // 先获取指针
    struct connection* conn = (struct connection*)events[i].data.ptr;
    
    // 释放内部资源
    if (conn->buffer) {
        free(conn->buffer);
    }
    
    // 释放连接结构体
    free(conn);
    
    // 关闭 fd
    close(client_fd);
    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
}
相关推荐
开开心心就好10 小时前
免费无广告的批量卸载与系统清理工具
linux·服务器·网络·智能手机·rabbitmq·excel·memcached
wanhengidc10 小时前
高防服务器中的数据安全
运维·服务器·网络
艾莉丝努力练剑10 小时前
【Linux网络】Linux 网络编程:HTTP(五)HTTP收尾,从Cookie会话保持、抓包问题到 HTTPS 初识
linux·运维·服务器·网络·c++
时夜_Ryan10 小时前
JumpServer堡垒机:一键部署运维安全审计
linux·运维·服务器·网络·安全·centos
车软派开发学长10 小时前
零基础学习车软嵌入式AUTOSAR,以一帧CAN报文实战讲解AUTOSAR的学习
网络·stm32·车载系统·autosar·嵌入式实时数据库
源远流长jerry10 小时前
LVS 与 Nginx 负载均衡:从原理到生产实战
运维·网络·网络协议·tcp/ip·nginx·负载均衡·lvs
yyuuuzz10 小时前
境外云服务器使用常见问题梳理
运维·服务器·网络·aws
暗夜猎手-大魔王11 小时前
转载--Hermes Agent 02 | 模型无关的秘密:200+ 模型的统一接入层
网络
c++逐梦人11 小时前
多路转接epoll
linux·网络·epoll