登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

epoll EPOLLONESHOT 怎样正确重新武装事件

来源:17golang原创

时间:2026-10-25 14:16:11 279浏览 收藏

使用 EPOLLONESHOT 后,描述符的一次事件被 epoll_wait 返回,相关通知就会被禁用。处理完成且连接仍需继续监听时,调用 epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &event),重新提交关注掩码与用户数据,才能恢复通知。

不要把“重新进入 epoll_wait”当成重武装。本文先给最小写法,再用事件数量和读取字节数说明差异;原创示例没有在本机执行,也不提供虚构的性能提升或延迟数据。

Linux man-pages 官方项目资料:https://man7.org/linux/man-pages/man7/epoll.7.html。本地连接对接口:https://man7.org/linux/man-pages/man2/socketpair.2.html。

先看计数:首次通知后是否还有后续事件

典型现象是首次收到可读事件后,连接后续仍有输入,却再也没有处理机会。诊断时把 epoll_wait 返回事件数、实际读取字节数和 MOD 调用结果分开记录。仅看“对端成功写入”,不能证明当前监听已经重新启用。

本例只关注一个本地连接、两个单字节写入和一次重武装。计数用于看清通知语义,不用于推断真实服务器能承载多少连接。正式负载还涉及调度、业务处理与缓冲管理,不能由这个短程序得出吞吐结论。

行为假设:ONESHOT 暂停的是通知

ONESHOT 并不替你读走数据,也不代表关闭 fd。它限制的是此项监听后续的通知。应用仍然拥有连接状态,必须决定这次处理后继续监听、等待写就绪,还是关闭连接。

把处理读到暂时无数据和恢复监听作为明确的交接点,有助于避免遗漏后续通知。多线程场景中,这个交接还必须配合连接所有权;ONESHOT 不能代替应用对对象生命期的管理。

图1:epoll 实例、连接描述符与关注事件的静态注册关系
图1:epoll 实例、连接描述符与关注事件的静态注册关系。静态结构说明图,不是执行时序或运行截图。

改动点:完整掩码配合 EPOLL_CTL_MOD

/* 工作线程完成当前连接处理后,更新完整的关注掩码。 */
struct epoll_event interest = {0};
interest.events = EPOLLIN | EPOLLONESHOT;
interest.data.fd = fd;
if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &interest) == -1) {
    /* 不忽略失败:由调用方记录并处理连接生命周期。 */
    perror("epoll_ctl MOD");
}

这个片段假设 epfd 和 fd 已经有效、fd 已在该 epoll 实例中注册。MOD 是更新已有注册项,不是再次 ADD。每次都构造本轮需要的完整掩码,并保留 EPOLLONESHOT;若原设计还采用 EPOLLET 等标志,也要按原有模式明确提交。

data.fd 是本例选择的事件识别字段。若实际项目使用 data.ptr 指向连接对象,应继续提交正确且有效的对象指针,不能照抄 fd 字段后丢掉应用的分发信息。失败时要记录并进入明确的恢复或关闭路径,不能假设重武装总会成功。

复现方法:用 socketpair 隔离外部网络

下面的完整程序使用非阻塞 AF_UNIX socketpair,避开远端服务和监听端口。第一轮读到 EAGAIN 后写入第二个字节,先做一次零超时等待观察未武装状态,再 MOD 并观察已有可读数据。本例采用水平触发加 ONESHOT,刻意不混入 EPOLLET。

#include 
#include 
#include 
#include 
#include 

static int wait_event(int epfd, struct epoll_event *event, int timeout) {
    int count;
    do {
        count = epoll_wait(epfd, event, 1, timeout);
    } while (count == -1 && errno == EINTR); /* 中断后重新等待。 */
    return count;
}

static int drain_read(int fd, int *bytes) {
    char buffer[64];
    for (;;) {
        ssize_t n = read(fd, buffer, sizeof buffer);
        if (n > 0) {
            *bytes += (int)n;
            continue;
        }
        if (n == 0) return 0; /* 对端关闭,不再继续重武装读取。 */
        if (errno == EINTR) continue;
        if (errno == EAGAIN || errno == EWOULDBLOCK) return 1;
        return -1;
    }
}

int main(void) {
    int pair[2] = {-1, -1}, epfd = -1, result = 1, bytes = 0;
    struct epoll_event interest = {0}, event = {0};
    /* 本地连接对均为非阻塞,不依赖监听端口或远程服务。 */
    if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0, pair) == -1)
        goto error;
    epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) goto error;
    interest.events = EPOLLIN | EPOLLONESHOT; /* 本例不使用 EPOLLET。 */
    interest.data.fd = pair[0];
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, pair[0], &interest) == -1)
        goto error;
    if (write(pair[1], "A", 1) != 1) goto error;
    int first = wait_event(epfd, &event, 1000);
    if (first == -1) goto error;
    if (first != 1 || !(event.events & EPOLLIN)) goto unexpected;
    if (drain_read(pair[0], &bytes) != 1) goto unexpected;

    /* 写入第二个字节时,ONESHOT 的通知尚未恢复。 */
    if (write(pair[1], "B", 1) != 1) goto error;
    int disabled = wait_event(epfd, &event, 0);
    if (disabled == -1) goto error;
    /* MOD 重新提交掩码和 data,恢复后续就绪通知。 */
    if (epoll_ctl(epfd, EPOLL_CTL_MOD, pair[0], &interest) == -1)
        goto error;
    int rearmed = wait_event(epfd, &event, 1000);
    if (rearmed == -1) goto error;
    if (rearmed != 1 || !(event.events & EPOLLIN)) goto unexpected;
    if (drain_read(pair[0], &bytes) != 1) goto unexpected;
    printf("first=%d disabled=%d rearmed=%d bytes=%d\n",
           first, disabled, rearmed, bytes);
    result = 0;
    goto cleanup;
error:
    perror("演示系统调用失败");
    goto cleanup;
unexpected:
    fprintf(stderr, "未观察到本例要求的可读状态,请检查运行环境\n");
cleanup:
    /* 所有出口都释放本例拥有的描述符。 */
    if (epfd != -1) close(epfd);
    if (pair[0] != -1) close(pair[0]);
    if (pair[1] != -1) close(pair[1]);
    return result;
}

保存为 oneshot_demo.c,在 Linux 环境中可使用以下命令自行运行。本文未执行这些命令,图片也不作为运行证据。

# 编译原创示例,打开常用警告。
cc -std=c11 -Wall -Wextra -O2 oneshot_demo.c -o oneshot_demo
# 运行后查看事件数与读取总字节数。
./oneshot_demo
图2:MOD 调用、完整事件掩码与用户数据字段的静态关系
图2:MOD 调用、完整事件掩码与用户数据字段的静态关系。静态结构说明图,不是执行时序或运行截图。

结果对比:缺少重武装与恢复通知

观察项本例正常条件下的语义预期说明
first1首次就绪被返回
disabled0未重武装时没有后续通知
rearmed1MOD 后再次取得可读通知
bytes2两个单字节写入均由读循环消费

这些数字是示例输入与 API 语义推导,不是实测结果。若删掉 MOD,不能继续期待第二次等待必然获得输入通知。若连接在实际业务中已关闭,则不应机械重武装;先判断连接是否继续存在和需要哪些方向的事件。

读取循环处理 EINTR、EAGAIN 与 EOF,避免把暂时无数据当成断连。此处的缓冲只做计数,没有实现应用协议;真实程序还要把字节组装成消息,而不是认为一次可读通知对应一条完整消息。

边界条件和常见问题

必须把 EPOLLONESHOT 和 EPOLLET 一起使用吗?

不是同一个开关。本文没有使用 EPOLLET。若采用边沿触发,非阻塞读写与处理到 EAGAIN 尤其重要,不能只增加 ONESHOT 就忽略原有读写约定。

为什么不先重武装再让工作线程处理?

如果当前线程仍在修改连接缓冲和状态,过早恢复可能让别的等待线程取得后续事件。应先定义谁拥有连接和何时交还监听,再安排 MOD,而不是把它当作随处可调用的解锁操作。

EPOLLONESHOT 是否保证一个 fd 永远只由一个线程接触?

不能。其他应用代码仍可能直接读写、关闭或重复提交这个连接。对象释放、fd 复用和陈旧事件都需要自己的同步与身份管理。

每次都监听 EPOLLOUT 会更稳吗?

不应为重武装无条件添加写关注。根据实际待发送缓冲决定是否需要写就绪,避免没有业务工作却频繁处理可写状态。重武装的目标是恢复正确的关注集合。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>