EPOLLOUT 可写事件管理 + 非阻塞异步发送队列

摘要:本文详细讲解如何在 Linux epoll 网络编程中,通过 EPOLLOUT 事件和应用层异步发送队列,实现高效的非阻塞异步数据发送,解决 TCP 内核缓冲区满时的 EAGAIN 问题,构建高并发 Reactor 模型的核心发送机制。

关键词:epoll、EPOLLOUT、非阻塞IO、异步发送队列、Reactor模式、高并发网络编程、TCP发送缓冲区、EAGAIN处理## 一、核心背景与问题分析

1.1 原生问题:TCP 发送缓冲区的限制与挑战

TCP 内核发送缓冲区存在上限(通过 SO_SNDBUF 设置),这带来了以下挑战:

  • 缓冲区容量有限:当应用持续高速发送数据时,内核缓冲区很快被填满
  • 非阻塞发送返回 EAGAIN:非阻塞 send() 会立刻返回 -1 并设置 errno=EAGAIN/EWOULDBLOCK,表示缓冲区已满,暂时无法写入
  • 性能问题:如果直接反复调用 send() 轮询,会浪费 CPU 资源
  • 阻塞风险:不能在主线程 epoll 事件循环中阻塞等待### 解决方案

1.2 解决方案:异步发送队列 + EPOLLOUT 动态监听

针对上述问题,我们采用以下核心方案:

  1. 应用层异步发送队列:每个 Socket 单独维护一条待发数据队列,缓存 EAGAIN 时未发完的剩余数据

    • 队列采用链表结构,支持动态扩容
    • 每个数据块记录已发送偏移,支持断点续传
    • 队列长度可配置,防止内存无限增长
  2. EPOLLOUT 可写事件动态管理:智能控制 EPOLLOUT 监听状态

    • 常态:不监听 EPOLLOUT(减少无效事件触发,降低 CPU 开销)
    • 缓冲区满/有残留待发数据时:注册监听 EPOLLOUT
    • 队列全部发完后:立即取消监听 EPOLLOUT,避免持续触发可写事件
    • 核心优势:按需监听,避免 LT 模式下的 busy-loop
  3. 全程非阻塞 Reactor 模型

    • 主线程只负责 epoll 事件循环和回调分发
    • 发送操作完全异步,不阻塞事件循环
    • 支持单线程处理成千上万连接,实现高并发大流量传输
    • 可扩展为多线程 Reactor/IO 线程池模型### 1.3 前置条件与系统环境

在实现此方案前,需要确保以下基础组件已就绪:

  • 非阻塞 Socket:Socket 已设置为 O_NONBLOCK 非阻塞模式
  • epoll 实例:已有 epoll 实例(epoll_create1())和事件循环框架
  • 并发安全机制
    • 引用计数 refcount:管理 SocketSk 生命周期
    • per-socket 锁:保护发送队列的并发访问
  • 基础网络原语
    • recv_exact 非阻塞读原语
    • 半关闭标记处理(SHUT_WR/SHUT_RD)
    • TCP 状态管理(ESTABLISHED、CLOSE_WAIT 等)
  • 事件触发模式:基础 epoll 事件循环已跑通(默认使用 LT 水平触发,ET 边缘触发需特殊处理)- Socket 已设置为 O_NONBLOCK 非阻塞模式
  • 已有 epoll 实例、引用计数 refcount、per-socket 锁
  • 已有 recv_exact 非阻塞读原语、半关闭标记、TCP 状态管理
  • 基础 epoll 事件循环已跑通(LT水平触发)

二、核心数据结构定义(C)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <errno.h>
#include <pthread.h>

#define EPOLL_IN        EPOLLIN
#define EPOLL_OUT       EPOLLOUT
#define EPOLL_ERR       EPOLLERR
#define EPOLL_HUP       EPOLLHUP
#define MAX_EVENTS      1024
#define BUF_BLOCK_SIZE  4096

// 异步发送队列节点
typedef struct SendBlock {
    char* data;
    size_t len;
    size_t offset; // 已发送偏移
    struct SendBlock* next;
} SendBlock;

// 单个Socket控制块
typedef struct SocketSk {
    int fd;
    int type;               // SOCK_STREAM / SOCK_DGRAM
    int non_block;
    int write_closed;
    int refcount;
    pthread_mutex_t sk_lock;

    // 异步发送队列:待发送数据链表
    SendBlock* send_queue_head;
    SendBlock* send_queue_tail;
    int need_epoll_out;     // 是否需要监听EPOLLOUT

    // ... 原有字段:rx_buf, state, remote_addr, etc
} SocketSk;

// 全局epoll fd
static int g_epoll_fd = -1;

字段说明

2.1 字段详细说明

  1. SendBlock 结构体

    • data:指向待发送数据的缓冲区指针
    • len:数据总长度(字节)
    • offset:已发送字节数,支持分段逐步发送(断点续传)
    • next:指向下一个发送块的指针,形成单向链表
  2. SocketSk 结构体关键字段

    • send_queue_head / send_queue_tail:每个 Socket 独立的异步发送队列头尾指针
    • need_epoll_out:核心状态标志,控制 EPOLLOUT 监听开关
      • 1:存在待发数据,需要监听 EPOLLOUT
      • 0:队列为空,无需监听 EPOLLOUT(避免无效事件)
    • sk_lock:互斥锁,保护发送队列的并发修改
      • 必须使用 per-socket 锁而非全局锁,避免锁竞争
    • write_closed:写方向关闭标志,防止向已关闭连接发送数据
  3. 设计考量

    • 内存管理:每个 SendBlock 独立分配,便于细粒度释放
    • 并发安全:锁粒度控制在 Socket 级别,减少锁竞争
    • 性能优化:队列操作 O(1) 时间复杂度,避免遍历开销
    • 可扩展性:结构体预留扩展字段,支持未来功能增强## 三、基础工具函数

3.1 创建/释放发送块

SendBlock* send_block_create(const char* data, size_t len) {
    SendBlock* blk = (SendBlock*)malloc(sizeof(SendBlock));
    blk->data = (char*)malloc(len);
    memcpy(blk->data, data, len);
    blk->len = len;
    blk->offset = 0;
    blk->next = NULL;
    return blk;
}

void send_block_free(SendBlock* blk) {
    free(blk->data);
    free(blk);
}

3.2 添加数据到异步发送队列

static void sk_enqueue_send(SocketSk* sk, const char* data, size_t len) {
    pthread_mutex_lock(&sk->sk_lock);

    SendBlock* blk = send_block_create(data, len);
    if (sk->send_queue_head == NULL) {
        sk->send_queue_head = blk;
        sk->send_queue_tail = blk;
    } else {
        sk->send_queue_tail->next = blk;
        sk->send_queue_tail = blk;
    }

    pthread_mutex_unlock(&sk->sk_lock);
}

3.3 修改 epoll 监听事件(核心:动态增删 EPOLLOUT)

static int epoll_modify(int epoll_fd, int fd, uint32_t mask) {
    struct epoll_event ev;
    ev.data.fd = fd;
    ev.events = mask;
    return epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev);
}

3.4 非阻塞 write 发送单个块数据

static ssize_t sk_send_block_nonblock(SocketSk* sk) {
    if (sk->send_queue_head == NULL) return 0;

    SendBlock* blk = sk->send_queue_head;
    ssize_t sent = send(sk->fd, blk->data + blk->offset, blk->len - blk->offset, MSG_NOSIGNAL);

    if (sent > 0) {
        blk->offset += sent;
        // 当前块发送完毕,移除
        if (blk->offset >= blk->len) {
            sk->send_queue_head = blk->next;
            send_block_free(blk);
        }
        return sent;
    } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 内核缓冲区满,停止发送,等待EPOLLOUT
        return -1;
    } else {
        // 真实错误
        return -2;
    }
}

四、上层异步发送接口(非阻塞异步 write)

核心逻辑

  • 优先尝试直接非阻塞发送;发不完则进队列 + 开启 EPOLLOUT 监听
  • 主线程调用时立刻返回,不阻塞事件循环
int async_send(SocketSk* sk, const char* data, size_t len) {
    if (sk == NULL || sk->write_closed) return -1;

    pthread_mutex_lock(&sk->sk_lock);
    // 尝试直接非阻塞发送
    ssize_t sent = send(sk->fd, data, len, MSG_NOSIGNAL);
    if (sent > 0 && (size_t)sent == len) {
        pthread_mutex_unlock(&sk->sk_lock);
        return len;
    } else if (sent > 0) {
        // 发了一部分,剩余入队
        sk_enqueue_send(sk, data + sent, len - (size_t)sent);
        sk->need_epoll_out = 1;
        epoll_modify(g_epoll_fd, sk->fd, EPOLL_IN | EPOLL_OUT | EPOLL_ERR | EPOLL_HUP);
        pthread_mutex_unlock(&sk->sk_lock);
        return (int)len;
    } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 缓冲区已满,全部入队,开启EPOLLOUT监听
        sk_enqueue_send(sk, data, len);
        sk->need_epoll_out = 1;
        epoll_modify(g_epoll_fd, sk->fd, EPOLL_IN | EPOLL_OUT | EPOLL_ERR | EPOLL_HUP);
        pthread_mutex_unlock(&sk->sk_lock);
        return (int)len;
    } else {
        pthread_mutex_unlock(&sk->sk_lock);
        return -1;
    }
}

五、EPOLLOUT 事件处理主逻辑(epoll 回调)

核心规则

  1. 收到 EPOLLOUT:循环发送队列剩余数据,直到缓冲区满或队列为空
  2. 队列为空时立刻关闭 EPOLLOUT 监听,防止无效事件持续触发
  3. 全程非阻塞、不阻塞 epoll 主线程
void handle_epoll_out(SocketSk* sk) {
    pthread_mutex_lock(&sk->sk_lock);

    // 循环发送直到缓冲区满或队列为空
    while (sk->send_queue_head != NULL) {
        ssize_t ret = sk_send_block_nonblock(sk);
        if (ret == -1) {
            // EAGAIN,缓冲区满,暂停发送,保留EPOLLOUT监听
            break;
        }
        if (ret == -2) {
            // 致命错误,关闭连接
            sk->write_closed = 1;
            break;
        }
    }

    // 队列为空 → 取消EPOLLOUT监听
    if (sk->send_queue_head == NULL && sk->need_epoll_out) {
        sk->need_epoll_out = 0;
        epoll_modify(g_epoll_fd, sk->fd, EPOLL_IN | EPOLL_ERR | EPOLL_HUP);
    }

    pthread_mutex_unlock(&sk->sk_lock);
}

六、完整 epoll 主循环

void epoll_event_loop() {
    struct epoll_event events[MAX_EVENTS];
    while (1) {
        int n = epoll_wait(g_epoll_fd, events, MAX_EVENTS, -1);
        if (n < 0) break;

        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            uint32_t mask = events[i].events;
            SocketSk* sk = get_sk_by_fd(fd); // 通过fd查找SocketSk

            if (mask & (EPOLL_ERR | EPOLL_HUP)) {
                // 异常关闭连接,做清理
                close_socket(sk);
                continue;
            }

            if (mask & EPOLL_IN) {
                // 可读事件:非阻塞读取数据、业务处理、回显/转发
                handle_read(sk);
            }

            if (mask & EPOLL_OUT) {
                // 可写事件:消费异步发送队列
                handle_epoll_out(sk);
            }
        }
    }
}

七、关键原理 & 避坑指南

7.1 核心原理

  • 常态只监听 EPOLLIN,减少 epoll 事件开销
  • 仅存在待发数据时启用 EPOLLOUT,内核缓冲区有空时自动通知
  • 应用层队列做流量削峰:把突发大流量先缓存,由内核缓冲区节奏控制发送速率,防止丢包/反复EAGAIN
  • 实现 Reactor 模型异步发送:主线程只做事件分发,不做阻塞IO、不做长时间拷贝

7.2 关键坑点

  1. 长期监听 EPOLLOUT = CPU 暴涨
    • 缓冲区空闲时 LT 模式会持续反复触发 EPOLLOUT,造成死循环空跑
    • ✅ 必须发完数据立刻移除 EPOLLOUT 监听
  2. 非阻塞发送必须处理 MSG_NOSIGNAL
    • 防止向已关闭连接发数据触发 SIGPIPE,导致程序崩溃
  3. 并发安全
    • 异步队列必须加 per-socket 锁,防止主线程/工作线程同时修改队列
    • 结合 refcount 引用计数:确保 epoll 回调执行期间 SocketSk 不会被提前 free
  4. 边缘触发ET特殊处理
    • ET模式必须在 EPOLLOUT 回调里循环发送直到 EAGAIN,否则残留数据卡住
    • 学习原型优先使用 LT 水平触发
  5. TCP 流控联动
    • 配合 TCP 滑动窗口:当接收窗口为 0 时,内核缓冲区会被填满,EPOLLOUT 事件会暂停触发,符合 TCP 拥塞/流控机制
    • 可增加发送队列长度上限,防止内存无限堆积

7.3 大流量优化方向

  1. 批量缓冲区/内存池:减少 malloc/free 频繁分配开销
  2. 分片发送:避免单次超大块写入造成延迟抖动
  3. 限速/限流:设置最大发送队列长度,防止慢连接占用过多内存
  4. 零拷贝:结合 sendfile/mmap,减少用户/内核态拷贝开销
  5. 定时器:增加空闲超时、慢连接清理机制

八、整体数据流

  1. 上层业务调用 async_send() → 尝试非阻塞发送
    • ✅ 可一次发完:直接写入内核缓冲区,返回成功
    • ✅ 缓冲区满:剩余数据进入 per-socket 异步发送队列,开启 EPOLLOUT 监听
  2. 内核 TCP 缓冲区被对方 ACK 释放 → 有空余空间
    • epoll 触发 EPOLLOUT 可读事件
    • handle_epoll_out 循环消费异步队列,逐步写入内核缓冲区
    • 队列为空 → 关闭 EPOLLOUT 监听
  3. 全程 epoll 主线程无阻塞,可同时处理成千上万连接,实现高并发大流量传输

九、和整体协议栈的定位

  • 属于 Socket 层 Reactor 事件调度层,不改动底层 fd映射、SocketSk基础结构、TCP/UDP传输层逻辑
  • 解决原生非阻塞 IO 的 EAGAIN 反复轮询问题,适配真实 Linux epoll 异步网络编程模型
  • 后续可进一步改造为多线程 Reactor/IO 线程池模型,适配多核服务器
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐