首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >腾讯一面:epoll不支持文件IO,kqueue怎么做到的!

腾讯一面:epoll不支持文件IO,kqueue怎么做到的!

作者头像
早起的鸟儿有虫吃
修改2025-08-15 16:22:54
修改2025-08-15 16:22:54
16700
代码可运行
举报
文章被收录于专栏:吊打面试官吊打面试官
运行总次数:0
代码可运行

技术

事件检测精度

文件 I/O 支持

极限 IOPS (NVMe)

epoll

1ms-10ms

❌ 不支持

-

kqueue

1ms-10ms

✅ 支持(EVFILT_AIO)

500K

io_uring

1微妙(us)

✅ 原生支持

1.5M - 10M+

unsetunset一、背景unsetunset

面试官:提问谈谈你对kqueue理解?

小义:这个范围太广了,如何缩小回答范围呢? 选择一个参照物 epoll,哪怕没有真正用过,你至少了解,

如下信息:

在 Linux 系统中,虽然说一切皆是文件,但是不同文件不同处理方式

epoll 是一种高效的 I/O 多路复用技术,主要用于提高网络服务器的性能。 然而,它并不支持本地文件(如磁盘 I/O)

相比之下,FreeBSD 及其衍生系统中的 kqueue 机制不仅支持网络 I/O,还支持对本地文件的监控

这个时候需要 日常工作时候提前去准备 ,临场发挥这个事情根本不可能,刨根问题才是解决办法

  • epoll为什么不支持磁盘
  • 为什么kqueue支持磁盘

unsetunset二、epoll不支持磁盘I/Ounsetunset

2.1 从技术发展角度,回顾IO流程

图片
图片

来源:一文掌握网络编程精华

磁盘I/O
  • POSIX规范要求:磁盘文件描述符始终被视为可读/可写

Regular files are always readable and they are also always writeable. This is clearly stated in the relevant POSIX specifications

磁盘I/O:读写延迟由硬件性能决定(寻道时间、旋转延迟),判断是否就绪 无用。

  • 磁盘 I/O 操作通常是阻塞的
  • 2016年10月11日,阿里云华东地区部分ECS服务器出现IO HANG问题,导致部分网站瘫痪,一些用户无法连接云服务器

疑问:普通的文件始终是就绪状态,异步IO怎么实现的呢 aio_read aio_write?

参考 https://zhuanlan.zhihu.com/p/1915501761361313970

方式1: Linux POSIX AIO 是一个用户级别的实现,它在多线程中执行常规的阻塞 I/O 操作,从而模拟异步 I/O 操作。

方式2: Linux libaio(通常称为 Linux Native Asynchronous I/O)是 Linux 内核原生的异步 I/O 接口,专为高性能存储场景设计,适用于 Direct I/O 操作。它通过内核级支持实现了真正的异步非阻塞 I/O

网络I/O:
  • 网络读写操依赖 遇网络协议栈,例如Tcp滑动窗口, 依赖对方操作
  • 数据到达时间不确定,需异步通知机制

2.1 poll机制(内核就绪不等于用户态完成)不支持磁盘I/O

  • 编译直接报错epoll_ctl接口不支持监控文件
代码语言:javascript
代码运行次数:0
运行
复制
//三板斧api
int epoll_create(int size); 

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

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
                 
result = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

[vagrant@localhost epoll]$ gcc epoll.c -o epoll
[vagrant@localhost epoll]$ ./epoll
epoll_ctl(): Operation not permitted

• 内核实现约束

epoll_ctl(): Operation not permitted 函数报的错,

所以我们首先应该从 epoll_ctl 的源码入手,如下:

代码语言:javascript
代码运行次数:0
运行
复制
     epoll_ctl()调用时检查file_operations->poll函数:  
     if (!tfile->f_op || !tfile->f_op->poll) 
         return -EPERM;  // 普通文件无poll方法,直接报错

所以,出现 Operation not permitted 的原因就是:被监听的文件没有提供 poll 接口。

例如ext4文件系统未实现.poll接口。

代码语言:javascript
代码运行次数:0
运行
复制
const struct file_operations ext4_file_operations = {
.llseek         = generic_file_llseek,
.read           = do_sync_read,
.write          = do_sync_write,
.aio_read       = generic_file_aio_read,
.aio_write      = ext4_file_write,
.unlocked_ioctl = ext4_ioctl,
.mmap           = ext4_file_mmap,
.open           = ext4_file_open,
.release        = ext4_release_file,
.fsync          = ext4_sync_file,
.splice_read    = generic_file_splice_read,
.splice_write   = generic_file_splice_write,

数据不在Page Cache read()仍阻塞等待磁盘加载 事件就绪 ≠ 操作完成

unsetunset三、kqueue支持磁盘I/Ounsetunset

queue抽象出了高级的event事件,相比于epoll只能等io是否ready,kqueue可以直接监听类似“文件数据已经加载到内存”这样的事件,这看上去就比epoll有意义多了。所以kqueue可以支持磁盘io。

它相当于托管了整个磁盘IO操作,做到让磁盘io看上去是“非阻塞”的。

3.1 完成通知模型(Completion Notification)

与 epoll 的本质区别:

  • epoll 采用就绪通知(Readiness Notification),仅告知“当前可执行 I/O”(如缓冲区有数据可读)。
  • kqueue 支持完成通知(Completion Notification),可监听“I/O 操作已完成”的事件(如磁盘数据已加载到内存)。

3.2 怎么做到呢?

https://people.freebsd.org/~jlemon/papers/kqueue.pdf

文档: kqueue, kevent -- kernel event notification mechanism

EVFILT_AIO:

  • 专用于异步磁盘 I/O(AIO)的完成通知(如磁盘数据已读入内存)。
  • 触发条件:

AIO 操作(如 aio_read())完成后,内核通过此过滤器通知用户态

代码语言:javascript
代码运行次数:0
运行
复制
  EVFILT_AIO    Events for this  filter  are  not  registered  with
      kevent()   directly but  are  registered  via  the
      aio_sigevent member of an asynchronous I/O  request
      when it is scheduled via an asynchronous I/O system
      call  such as aio_read().  The filter returns under
      the same conditions as aio_error(). For  more  de-
      tails on this filter see sigevent(3) and aio(4).
  • kqueue.pdf 第6章节描述

EVFILT_AIO 的实现本质是 将 AIO 操作的生命周期注入 kqueue 事件流,通过三个核心步骤完成:

  1. 注册绑定:将 aiocb 地址作为事件标识关联到 kqueue。
  2. 内核回调:AIO 完成时通过 aio_complete() 触发事件入队。
  3. 用户处理:kevent() 批量获取事件并解析结果

4. 用户读取数据(用户态)

4.1 EVFILT_AIO 的职责
4.2 数据传递流程
  • 应用程序调用 aio_return(),获取该 AIO 操作的最终返回状态读取的字节数
  • 表示 AIO 操作已经完成
  • 如果涉及读取操作,数据此时已经位于应用程序指定的用户空间缓冲区中。
  • 应用程序会提供一个用户空间的缓冲区,用于接收或提供数据。
  • EVFILT_AIO 主要职责通知 AIO 操作的完成状态不直接处理实际的 I/O 数据传输
  • Kqueue 会将活跃列表中的 knote 信息(以 kevent 结构的形式)拷贝回用户空间
  • Kqueue 机制本身是在内核态实现的,不在用户态实现。
  • 当应用程序通过 kevent() 系统调用注册一个 EVFILT_AIO 类型的事件
  • 核心流程:aio_read 提交请求 → kqueue 监听 EVFILT_AIO → 事件触发后通过回调处理数据
代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/event.h>
#include <aio.h>
#include <string.h>
#define BUFFER_SIZE 4096
#define AIO_EVENT_ID 12345  // 自定义事件标识符
// 封装 AIO 上下文
struct aio_context {
    struct aiocb aio_cb;    // AIO 控制块
    char buffer[BUFFER_SIZE]; // 数据缓冲区
    void (*handler)(struct aio_context*); // 完成回调函数
};
// AIO 完成回调函数
void aio_completion_handler(struct aio_context *ctx) {
    // 检查 AIO 操作状态
    if (aio_error(&ctx->aio_cb) != 0) {
        perror("aio_error");
        return;
    }
    // 获取实际读取字节数
    ssize_t bytes_read = aio_return(&ctx->aio_cb);
    if (bytes_read <= 0) {
        perror("aio_return");
        return;
    }
    // 处理数据(此处打印内容)
    printf("Read %zd bytes: %.*s\n", bytes_read, (int)bytes_read, ctx->buffer);
}
int main() {
    int kq = kqueue();  // 创建 kqueue 实例
    if (kq == -1) {
        perror("kqueue");
        exit(EXIT_FAILURE);
    }
    // 打开目标文件(非阻塞模式)
    int fd = open("test.txt", O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("open");
        close(kq);
        exit(EXIT_FAILURE);
    }
    // 初始化 AIO 上下文
    struct aio_context ctx;
    memset(&ctx, 0, sizeof(ctx));
    ctx.handler = aio_completion_handler;
    ctx.aio_cb.aio_fildes = fd;          // 目标文件描述符
    ctx.aio_cb.aio_buf = ctx.buffer;      // 数据缓冲区
    ctx.aio_cb.aio_nbytes = BUFFER_SIZE;   // 缓冲区大小
    ctx.aio_cb.aio_offset = 0;            // 文件偏移量
    // 配置 kqueue 事件通知
    ctx.aio_cb.aio_sigevent.sigev_notify = SIGEV_KEVENT; // 使用 kqueue 通知
    ctx.aio_cb.aio_sigevent.sigev_notify_kqueue = kq;    // 指定 kqueue 实例
    ctx.aio_cb.aio_sigevent.sigev_value.sival_ptr = &ctx; // 传递上下文指针
    ctx.aio_cb.aio_sigevent.sigev_filter = EVFILT_AIO;    // 事件类型为 AIO
    ctx.aio_cb.aio_sigevent.sigev_ident = AIO_EVENT_ID;  // 自定义事件标识符
    // 提交异步读请求
    if (aio_read(&ctx.aio_cb) == -1) {
        perror("aio_read");
        close(fd);
        close(kq);
        exit(EXIT_FAILURE);
    }
    // 监听 kqueue 事件
    struct kevent event;
    struct timespec timeout = { .tv_sec = 5, .tv_nsec = 0 }; // 5秒超时
    while (1) {
        int nev = kevent(kq, NULL, 0, &event, 1, &timeout);
        if (nev == -1) {
            perror("kevent");
            break;
        } else if (nev == 0) {
            printf("Timeout: No events\n");
            break;
        }
        // 检查是否为 EVFILT_AIO 事件
        if (event.filter == EVFILT_AIO && event.ident == AIO_EVENT_ID) {
            struct aio_context *ctx_ptr = (struct aio_context *)event.udata;
            ctx_ptr->handler(ctx_ptr); // 调用回调处理数据
            break;
        }
    }
    // 清理资源
    close(fd);
    close(kq);
    return 0;
}

unsetunset耗时对比

微秒级延迟在网络事件中的意义

1. 硬件操作耗时参考

操作

典型耗时

CPU 执行 1 条指令

≈ 0.3 ns

L1 缓存访问延迟

≈ 0.5 ns

内存访问延迟

≈ 100 ns

网络数据包进入内核队列

1–10 μs

固态硬盘(NVMe)随机读

≈ 10 μs

:内核在 1–10 μs 内将网络数据从网卡搬运到协议栈缓冲区,并加入 epoll 就绪队列。


为什么 μs 级延迟至关重要?

高并发场景性能对比

技术

单事件内核处理延迟

100万并发连接事件处理总耗时

poll

≥ 50 μs

50秒

epoll(优化)

8 μs

8秒

io_uring

2 μs

2秒

结论:每事件节省 1μs,100万连接可减少 >16分钟 CPU 占用!

  1. μs 是网络优化的黄金尺度
    • 10μs 延迟 ≈ 万兆网卡传输 1.5KB 数据的时间
    • 若处理逻辑超 10μs,网卡将被堵死(100Gbps 网卡每 μs 需处理 12.5KB)
  2. 延迟敏感场景的 μs 级要求
    • 高频交易系统:从网卡到交易指令 ≤ 50 μs
    • 5G 基站调度:端到端延迟 ≤ 1 ms(=1000 μs

一句话总结区别:unsetunset

特性

epoll(Linux)

kqueue(BSD/macOS)

通知类型

就绪通知(Readiness)

完成通知(Completion)

内核支持

仅检测状态,不执行 I/O

可执行 I/O 并返回结果

用户态负担

需主动读写数据

直接消费结果

适用 I/O 类型

网络 I/O(非阻塞)

网络 + 磁盘 I/O + 信号等

  • 就绪通知(epoll模型) Linux的I/O多路复用(如epoll)采用就绪通知:内核仅告知用户态“某个文件描述符已就绪(例如socket缓冲区有数据)”,用户态仍需主动调用read()/write()完成数据搬运。这种模型符合Linux“机制与策略分离”的设计原则——内核提供基础能力,具体操作由用户态控制
  • 完成通知(kqueue模型) kqueue支持完成通知:内核直接执行I/O操作(如磁盘数据读入内存),完成后通过事件(如EVFILT_AIO)通知用户态直接消费结果。这种模型将I/O全流程托管给内核,更适合BSD“端到端集成”的设计哲学

本质差异: epoll是“监工”(通知可操作),用户态是“搬运工”; kqueue是“全流程工人”,用户态是“验收员”。

维度

io_uring (Linux)

kqueue (BSD/macOS)

核心架构

双环形队列(SQ/CQ)生产-消费模型

统一事件集(kevent)监听模型

数据流

共享内存零拷贝

内核-用户态数据拷贝

磁盘I/O实现

直接提交操作指令(如IORING_OP_READ)

通过EVFILT_AIO转发AIO事件

性能天花板

更低延迟(微秒级)、更高吞吐(百万QPS+)

受限于系统调用和拷贝开销

设计目标

全场景异步I/O统一接口

跨事件类型统一通知机制

  • EVFILT_AIO (kqueue)
    • 事件转发模型:通过EVFILT_AIO过滤器监听异步I/O(如aio_read())的完成事件,本质是事件通知代理。用户需先提交AIO请求,内核完成后生成事件,再由kqueue转发给用户态。
    • 系统调用依赖:每次提交请求需调用aio_read(),监听事件需调用kevent(),高频操作时系统调用开销显著。
    • 数据拷贝:事件结果需从内核拷贝到用户态缓冲区,增加内存操作开销。
  • io_uring
    • 零拷贝共享队列:通过双环形队列(SQ/CQ) 实现用户态与内核态共享内存。用户直接写入SQ(提交队列),内核消费后结果写入CQ(完成队列),全程无系统调用与数据拷贝。
    • 批处理支持:单次系统调用可提交/收割多个I/O请求(如io_uring_enter()),减少上下文切换。

性能影响io_uring在10万+并发场景下延迟降低50%+,系统调用次数减少90%

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • unsetunset一、背景unsetunset
  • unsetunset二、epoll不支持磁盘I/Ounsetunset
    • 2.1 从技术发展角度,回顾IO流程
      • 磁盘I/O
    • 2.1 poll机制(内核就绪不等于用户态完成)不支持磁盘I/O
  • unsetunset三、kqueue支持磁盘I/Ounsetunset
    • 3.1 完成通知模型(Completion Notification)
    • 3.2 怎么做到呢?
    • 4. 用户读取数据(用户态)
      • 4.1 EVFILT_AIO 的职责
      • 4.2 数据传递流程
  • unsetunset耗时对比
    • 1. 硬件操作耗时参考
    • 为什么 μs 级延迟至关重要?
      • 高并发场景性能对比
  • 一句话总结区别:unsetunset
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档