技术 | 事件检测精度 | 文件 I/O 支持 | 极限 IOPS (NVMe) |
---|---|---|---|
epoll | 1ms-10ms | ❌ 不支持 | - |
kqueue | 1ms-10ms | ✅ 支持(EVFILT_AIO) | 500K |
io_uring | 1微妙(us) | ✅ 原生支持 | 1.5M - 10M+ |
面试官:提问谈谈你对kqueue理解?
小义:这个范围太广了,如何缩小回答范围呢? 选择一个参照物 epoll,哪怕没有真正用过,你至少了解,
如下信息:
在 Linux 系统中,虽然说一切皆是文件,但是不同文件不同处理方式
epoll 是一种高效的 I/O 多路复用技术,主要用于提高网络服务器的性能。 然而,它并不支持本地文件(如磁盘 I/O)。
相比之下,FreeBSD 及其衍生系统中的 kqueue 机制不仅支持网络 I/O,还支持对本地文件的监控。
这个时候需要 日常工作时候提前去准备 ,临场发挥这个事情根本不可能,刨根问题才是解决办法
来源:一文掌握网络编程精华
Regular files are always readable and they are also always writeable. This is clearly stated in the relevant POSIX specifications
磁盘I/O:读写延迟由硬件性能决定(寻道时间、旋转延迟),判断是否就绪 无用。
疑问:普通的文件始终是就绪状态,异步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
//三板斧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 的源码入手,如下:
epoll_ctl()调用时检查file_operations->poll函数:
if (!tfile->f_op || !tfile->f_op->poll)
return -EPERM; // 普通文件无poll方法,直接报错
所以,出现 Operation not permitted 的原因就是:被监听的文件没有提供 poll 接口。
例如ext4文件系统未实现.poll接口。
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()仍阻塞等待磁盘加载 事件就绪 ≠ 操作完成
queue抽象出了高级的event事件,相比于epoll只能等io是否ready,kqueue可以直接监听类似“文件数据已经加载到内存”这样的事件,这看上去就比epoll有意义多了。所以kqueue可以支持磁盘io。
它相当于托管了整个磁盘IO操作,做到让磁盘io看上去是“非阻塞”的。
与 epoll 的本质区别:
https://people.freebsd.org/~jlemon/papers/kqueue.pdf
文档: kqueue, kevent -- kernel event notification mechanism
EVFILT_AIO:
AIO 操作(如 aio_read())完成后,内核通过此过滤器通知用户态
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).
EVFILT_AIO 的实现本质是 将 AIO 操作的生命周期注入 kqueue 事件流,通过三个核心步骤完成:
aio_return()
,获取该 AIO 操作的最终返回状态或读取的字节数。EVFILT_AIO
主要职责是通知 AIO 操作的完成状态,不直接处理实际的 I/O 数据传输。knote
信息(以 kevent
结构的形式)拷贝回用户空间。kevent()
系统调用注册一个 EVFILT_AIO
类型的事件
aio_read
提交请求 → kqueue
监听 EVFILT_AIO
→ 事件触发后通过回调处理数据#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;
}
微秒级延迟在网络事件中的意义
操作 | 典型耗时 |
---|---|
CPU 执行 1 条指令 | ≈ 0.3 ns |
L1 缓存访问延迟 | ≈ 0.5 ns |
内存访问延迟 | ≈ 100 ns |
网络数据包进入内核队列 | 1–10 μs |
固态硬盘(NVMe)随机读 | ≈ 10 μs |
注:内核在 1–10 μs 内将网络数据从网卡搬运到协议栈缓冲区,并加入 epoll 就绪队列。
技术 | 单事件内核处理延迟 | 100万并发连接事件处理总耗时 |
---|---|---|
poll | ≥ 50 μs | 50秒 |
epoll(优化) | 8 μs | 8秒 |
io_uring | 2 μs | 2秒 |
结论:每事件节省 1μs,100万连接可减少 >16分钟 CPU 占用!
特性 | epoll(Linux) | kqueue(BSD/macOS) |
---|---|---|
通知类型 | 就绪通知(Readiness) | 完成通知(Completion) |
内核支持 | 仅检测状态,不执行 I/O | 可执行 I/O 并返回结果 |
用户态负担 | 需主动读写数据 | 直接消费结果 |
适用 I/O 类型 | 网络 I/O(非阻塞) | 网络 + 磁盘 I/O + 信号等 |
read()
/write()
完成数据搬运。这种模型符合Linux“机制与策略分离”的设计原则——内核提供基础能力,具体操作由用户态控制
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
io_uring_enter()
),减少上下文切换。性能影响:
io_uring
在10万+并发场景下延迟降低50%+,系统调用次数减少90%