找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

481

积分

0

好友

64

主题
发表于 前天 01:20 | 查看: 4| 回复: 0

异步I/O允许用户空间程序在提交一个I/O请求后立即返回,无需等待操作完成。当操作在后台完成后,内核会通知用户程序。在Linux系统中,主要存在两种AIO机制:传统的Linux AIO和革命性的高性能框架io_uring。

1. 传统 Linux AIO (Legacy AIO)

传统的Linux AIO(由libaio库支持)是早期为满足数据库等场景的高性能I/O需求而设计的。但其自身存在一些设计和性能上的限制,导致其在普通文件I/O及某些设备驱动中的应用并不广泛。

  • 用户空间操作:通常使用io_submit()提交请求,再通过io_getevents()来检查完成事件。
  • 驱动程序实现:驱动程序需要实现file_operations中的aio_readaio_write等方法,并处理I/O请求块struct kiocb

2. io_uring (现代AIO)

io_uring是Linux内核引入的一种革命性的高性能异步I/O框架,旨在彻底解决传统AIO的限制,提供真正的非阻塞异步操作。其核心在于通过内核和用户空间共享的两个环形队列(提交队列SQ和完成队列CQ)来实现极低的开销。

  • 用户空间操作
    1. 应用程序将请求描述放入提交队列(SQ)
    2. 调用io_uring_enter()系统调用(或类似的库函数封装)来通知内核处理。
    3. 应用程序从完成队列(CQ)中消费已完成的事件。
  • 驱动程序实现:驱动需要集成到io_uring框架中,这通常涉及更底层的块设备层或文件系统集成。对于普通的字符设备驱动,集成io_uring相对复杂,但能带来巨大的性能提升。

在学习路径上,通常建议先掌握阻塞I/Opoll/select/epoll多路复用机制,再学习异步通知(信号驱动I/O)。传统Linux AIO在现今的驱动开发中已较少使用,而io_uring无疑代表了Linux高性能I/O的未来方向。

io_uring的核心思想是利用用户空间和内核空间共享的两个环形缓冲区(Ring Buffers)来管理I/O请求和完成通知,从而最大限度地避免每次操作都进行昂贵的系统调用

io_uring接口围绕两个通过共享内存映射到用户空间的关键环形队列运行:

组件 英文全称 简称 作用
提交队列 Submission Queue SQ 用户空间向内核提交I/O请求的地方。
完成队列 Completion Queue CQ 内核通知用户空间I/O操作已完成的地方。

io_uring的原生系统调用接口较为复杂,在实际开发中,我们通常使用liburing库来简化操作。下面是一个使用liburing异步读取文件的简明示例,展示了SQE提交流程、user_data传递请求上下文以及CQE消费的完整流程。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <liburing.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

#define QD 64     // 队列深度 (Queue Depth)
#define BLOCK_SIZE 1024

// 结构体用于存储每个 I/O 请求的上下文信息
struct read_data {
    off_t offset;
    size_t length;
    char *buffer;
};

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return 1;
    }

    const char *filename = argv[1];
    struct io_uring ring;
    int ret;
    int fd;
    struct stat st;

    // 1. 初始化 io_uring 实例
    ret = io_uring_queue_init(QD, &ring, 0);
    if (ret < 0) {
        fprintf(stderr, "io_uring_queue_init failed: %s\n", strerror(-ret));
        return 1;
    }

    // 2. 打开文件
    fd = open(filename, O_RDONLY);
    if (fd < 0) {
        perror("open failed");
        io_uring_queue_exit(&ring);
        return 1;
    }

    // 获取文件大小
    if (fstat(fd, &st) < 0) {
        perror("fstat failed");
        close(fd);
        io_uring_queue_exit(&ring);
        return 1;
    }

    // 计算需要多少个块来读取整个文件
    int num_blocks = (st.st_size + BLOCK_SIZE - 1) / BLOCK_SIZE;
    printf("Reading file '%s' (Size: %ld bytes) in %d blocks.\n",
           filename, st.st_size, num_blocks);

    // 3. 提交所有读取请求
    for (int i = 0; i < num_blocks; ++i) {
        struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); // 获取一个 SQE
        if (!sqe) {
            fprintf(stderr, "io_uring_get_sqe failed\n");
            break;
        }

        // 分配并设置请求上下文
        struct read_data *data = malloc(sizeof(*data));
        data->offset = (off_t)i * BLOCK_SIZE;
        data->length = (i == num_blocks - 1) ? (st.st_size % BLOCK_SIZE) : BLOCK_SIZE;
        if (data->length == 0) data->length = BLOCK_SIZE; // 确保最后一小块大于0
        data->buffer = malloc(data->length);
        if (!data->buffer) {
            perror("malloc buffer failed");
            free(data);
            break;
        }

        // 设置 SQE:这是一个异步读取请求
        io_uring_prep_read(sqe, fd, data->buffer, data->length, data->offset);
        // 将请求上下文指针设置到 SQE 的 user_data 字段,以便完成时能取回
        io_uring_sqe_set_data(sqe, data);
    }

    // 4. 提交请求到内核
    // io_uring_submit() 执行一次系统调用,将所有准备好的 SQE 提交给内核。
    ret = io_uring_submit(&ring);
    if (ret < 0) {
        fprintf(stderr, "io_uring_submit failed: %s\n", strerror(-ret));
        close(fd);
        io_uring_queue_exit(&ring);
        return 1;
    }
    printf("%d requests submitted.\n", ret);

    // 5. 等待所有请求完成并消费结果
    struct io_uring_cqe *cqe;
    int completed = 0;
    while (completed < num_blocks) {
        // 等待一个或多个完成事件 (可能会阻塞,直到有事件完成)
        ret = io_uring_wait_cqe(&ring, &cqe);
        if (ret < 0) {
            fprintf(stderr, "io_uring_wait_cqe failed: %s\n", strerror(-ret));
            break;
        }

        // 从 CQE 中取回请求上下文
        struct read_data *data = io_uring_cqe_get_data(cqe);

        // 检查结果
        if (cqe->res < 0) {
            fprintf(stderr, "Read failed for offset %ld: %s\n",
                    data->offset, strerror(-cqe->res));
        } else if (cqe->res != data->length) {
            // 简单处理:实际读取字节数与请求不符
            fprintf(stderr, "Short read for offset %ld: expected %zu, got %d\n",
                    data->offset, data->length, cqe->res);
        } else {
            // 成功:现在 data->buffer 中包含了从文件中读取的数据
            // 在实际应用中,你会在成功的回调中处理这些数据
            // printf("Successfully read %d bytes at offset %ld.\n", cqe->res, data->offset);
        }

        // 清理内存
        free(data->buffer);
        free(data);

        // 标记该 CQE 已被消费,并释放 CQE
        io_uring_cqe_seen(&ring, cqe);
        completed++;
    }

    // 6. 清理
    close(fd);
    io_uring_queue_exit(&ring);
    printf("All %d reads completed and cleaned up.\n", completed);
    return 0;
}

此示例清晰地演示了io_uring的基础应用模式,它是理解和运用这一高性能框架的绝佳起点。




上一篇:C++20编译期编程实战:constexpr、consteval、constinit核心解析
下一篇:模拟电路27个核心知识点解析:从入门到实战应用指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-6 23:53 , Processed in 0.100608 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表