Linux I/O

1.什么是直接 I/O

直接 I/O(Direct I/O)是一种绕过文件页缓存的文件访问方式。应用程序直接从磁盘读写数据,避免了文件页缓存与用户缓冲区之间的额外数据拷贝,常用于数据库等对数据一致性和缓存控制要求较高的场景。

直接 I/O 优点如下:

  • 绕过文件页缓存,节省内存。

  • 应用程序自主管理 IO 缓存与读写逻辑,灵活性强。

  • 减少数据拷贝次数。

  • 数据直接落盘,降低落盘延迟

直接 I/O 缺点如下:

  • 强制要求三重对齐,使用门槛高。

  • 不经过页缓存,无法享受内核预读、缓存命中带来的性能增益

  • 小粒度随机读写性能差,每次 I/O 都直接触发磁盘硬件读写

  • 仅同步文件数据块,不会自动持久化文件元数据,需手动调用 fsync()/fdatasync()

  • 不适合普通小文件、日志类简单读写场景。

2.从内核看直接 I/O

执行 open 系统调用传入 O_DIRECT 标志就开启了直接 I/O,否则便是缓存 I/O,具体情况如图1所示。

图1 直接 I/O 底层原理

用户程序1以直接 I/O 的方式打开测试文件,如下:

复制代码
int fd = open("test", O_RDWR | O_CREAT | O_DIRECT, 0644);

O_DIRECT 为直接 I/O 开启标志,内核打开文件时会创建一个 file 对象,file 对象的 f_flags 成员会记录打开文件时传入的打开标志(包括O_DIRECT),后续的读写操作会受到该标志的影响,即内核会根据该标志执行不同的读写逻辑。

以 ext4 文件系统写文件为例,内核源码如下:

复制代码
static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    struct inode *inode = file_inode(iocb->ki_filp);

    if (unlikely(ext4_forced_shutdown(inode->i_sb)))
        return -EIO;

#ifdef CONFIG_FS_DAX
    /* DAX写入路径处理 */
    if (IS_DAX(inode))
        return ext4_dax_write_iter(iocb, from);
#endif
    /* 直接I/O */
    if (iocb->ki_flags & IOCB_DIRECT)
        return ext4_dio_write_iter(iocb, from);
    else/* 缓冲I/O */
        return ext4_buffered_write_iter(iocb, from);
}

O_DIRECT 标志开启后,源码中的 iocb->ki_flags & IOCB_DIRECT 条件为真(注意:内核会将 O_DIRECT 转换成内核内部标志 IOCB_DIRECT ),内核会执行直接 I/O 读操作 ext4_dio_write_iter。该函数的内部会构建 bio ( struct bio 结构,描述一次块设备 I/O 操作,包括:起始扇区、扇区数量、读写方向等),并依次经过:通用块层->I/O调度层->块设备驱动层,将数据通过DMA方式拷贝至磁盘。直接 I/O 并没有将用户数据存储至文件页缓存。

用户程序2以普通方式(缓存 I/O)打开文件,该方式用户程序每次读写操作都要经过文件页缓存,文件页缓存详细介绍请查看我的这篇文章:7张图搞懂Linux文件页缓存(Page Cache),这里不再赘述。

3.直接 I/O 三重对齐

使用直接 I/O 必须满足三重对齐:

  • 内存地址对齐:用户态缓冲区起始虚拟地址必须是 逻辑块大小的整数倍(通常为 512 或 4096 字节)

  • 文件偏移对齐:lseek()/ pread()/pwrite()文件起始偏移,必须是逻辑块大小整数倍。

  • 读写长度对齐:read()/write()/pread()/pwrite()数据长度,必须是逻辑块大小整数倍。

直接 I/O 为什么需要满足三重对齐?笔者认真研究了 Linux 内核源码后找到了答案,如图2所示。

图2 直接 I/O 三重对齐

以 pread 和 pwrite 函数为例,用户程序调用这两个函数后,经过层层函数调用,最终读写操作都会调用 iomap_dio_rw 函数,该函数依次调用:

__iomap_dio_rw()->iomap_dio_iter()->iomap_dio_bio_iter()->iomap_dio_bio_iter(),iomap_dio_bio_iter 函数会进行三重对齐判断,判断语句为:

复制代码
if ((pos | length) & (bdev_logical_block_size(iomap->bdev) - 1) ||
        !bdev_iter_is_aligned(iomap->bdev, dio->submit.iter))
        return -EINVAL;
  • pos:表示文件偏移。

  • length:表示读写长度。

  • bdev_logical_block_size-1:逻辑块大小掩码。

条件1:(pos | length) & (bdev_logical_block_size(iomap->bdev) - 1) 用于检查:文件偏移 pos 必须是逻辑块大小整数倍;数据长度 length 必须是逻辑块大小整数倍。

条件2:!bdev_iter_is_aligned(iomap->bdev, dio->submit.iter)) 用于检查内存地址必须是逻辑块大小整数倍,具体实现为:

复制代码
staticinlinebool bdev_iter_is_aligned(struct block_device *bdev,
                    struct iov_iter *iter)
{
    return iov_iter_is_aligned(iter, bdev_dma_alignment(bdev), // 参数2:DMA 对齐要求(511或4095)
                   bdev_logical_block_size(bdev) - 1); // 参数3:逻辑块块大小掩码(511或4095)
}

bool iov_iter_is_aligned(conststruct iov_iter *i, unsigned addr_mask,
             unsigned len_mask)
{
    if (likely(iter_is_ubuf(i))) {
        // 检查 1:长度是否对齐(必须是 512/4K 整数倍)
        if (i->count & len_mask)
            returnfalse;
        // 检查 2:内存地址是否对齐,逻辑块大小整数倍
        if ((unsignedlong)(i->ubuf + i->iov_offset) & addr_mask)
            returnfalse;
        return true;
    }
    ......
}

4.直接I/O 测试

测试代码如下:

复制代码
#define _GNU_SOURCE
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>

#define ALIGN_SIZE 512 // 对齐要求
#define BUF_SIZE 4096 // 数据长度

intmain(){
    constchar *test_str = "hello";
    void *buf = NULL;

    int ret = posix_memalign(&buf, ALIGN_SIZE, 2 * BUF_SIZE);
    if (ret != 0) {
        perror("posix_memalign failed");
        return-1;
    }

    memcpy(buf, test_str, strlen(test_str));

    int fd = open("./mytest.txt", O_RDWR | O_CREAT | O_DIRECT, 0644);
    if (fd < 0) {
        perror("open O_DIRECT failed");
        free(buf);
        return1;
    }

    ret = pwrite(fd, buf, BUF_SIZE, 0);
    if (ret < 0) {
        perror("pwrite failed");
        goto out;
    }
    printf("直接I/O写入成功:%ld 字节\n", ret);
    memset(buf, 0, BUF_SIZE);
    ret = pread(fd, buf, BUF_SIZE, 0);
    if (ret < 0) {
        perror("pread failed");
        goto out;
    }
    printf("直接I/O读取成功:%ld 字节\n", ret);
    printf("读取内容:%s\n", (char *)buf);

    fdatasync(fd);
out:
    close(fd);
    free(buf);
    return0;
}

测试代码通过 posix_memalign 函数来分配用户缓冲区,该函数能够保证用户缓冲区内存地址和数据长度对齐要求。

直接 I/O 通常使用 pread 和 pwrite 函数进行读写,二者相较于 read 和 write 函数增加了 offset 参数(第四个参数,表示从文件 / 磁盘的第多少字节开始读写),offset 必须满足文件偏移对齐要求。pread 和 pwrite 函数传入的数据长度需要满足数据长度对齐要求。

值得注意的是,直接 I/O 只绕开了文件页缓存(用于缓存文件数据) ,文件元数据(如文件大小、访问时间等)仍由内核缓存和管理。直接 I/O 操作完毕后,需要调用 fsync() / fdatasync() 将文件元数据落盘。

编译并执行测试程序:

gcc direct_io.c

./a.out

输出示例如下:

我们可以直接查看磁盘来验证数据是否成功落盘,方法如下:

  • 安装autopsy / sleuthkit 取证工具

apt install sleuthkit

  • 解析 Ext4 文件系统,列出所有文件

fls -r /dev/<块设备文件> | grep 测试文件

fls会显示文件 inode 号,输出示例:

  • 找到文件 inode,直接提取文件内容

icat /dev/<块设备文件> inode号 | hexdump -C

输出示例:

磁盘中已经有了 "hello" 字符串记录。

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言