Linux 文件 I/O 进化史(三):Direct I/O 和 Linux AIO

Direct I/O

前面介绍的 buffered I/O 和 mmap,访问文件都是需要经过内核的 page cache。这对于数据库这种 self-caching 的应用可能不是特别友好:

  1. 用户层的 cache 和内核的 page cache 其实是重复的,导致内存浪费。
  2. 数据的传输:disk -> page cache -> user buffer 需要两次内存拷贝。

为此,Linux 提供了一种绕过 page cache 读写文件的方式:direct I/O。

要使用 direct I/O 需要在 open 文件的时候加上 O_DIRECT 的 flag。比如:

int fd = open(fname, O_RDWR | O_DIRECT);
复制代码

使用 direct I/O 有一个很大的限制:buffer 的内存地址、每次读写数据的大小、文件的 offset 三者都要与底层设备的逻辑块大小对齐(一般是 512 字节)。

Since Linux 2.6.0, alignment to the logical block size of the underlying storage (typically 512 bytes) suffices. The logical block size can be determined using the ioctl(2) BLKSSZGET operation or from the shell using the command: blockdev --getss

$ blockdev --getss /dev/vdb1 
512
复制代码

如果不满足对齐要求,系统会报 EINVAL(Invalid argument。) 错误:

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
  int fd = open("/tmp/direct_io_test", O_CREAT | O_RDWR | O_DIRECT);
  if (fd < 0) {
    perror("open");
    return -1;
  }

  constexpr int kBlockSize = 512;

  // buffer 地址不对齐
  {
    char* p = nullptr;
    int ret = posix_memalign(
        (void**)&p, kBlockSize / 2,
        kBlockSize);  // 这里有一定概率可以分配出与 kBlockSize 对齐的内存
    assert(ret == 0);
    int n = write(fd, p, kBlockSize);
    assert(n < 0);
    perror("write");
    free(p);
  }

  // buffer 大小不对齐
  {
    char* p = nullptr;
    int ret = posix_memalign((void**)&p, kBlockSize, kBlockSize / 2);
    assert(ret == 0);
    int n = write(fd, p, kBlockSize / 2);
    assert(n < 0);
    perror("write");
    free(p);
  }

  // 文件 offset 不对齐
  {
    char* p = nullptr;
    int ret = posix_memalign((void**)&p, kBlockSize, kBlockSize);
    assert(ret == 0);
    off_t offset = lseek(fd, kBlockSize / 2, SEEK_SET);
    assert(offset == kBlockSize / 2);
    int n = write(fd, p, kBlockSize);
    assert(n < 0);
    perror("write");
    free(p);
  }

  // 三者对齐
  {
    char* p = nullptr;
    int ret = posix_memalign((void**)&p, kBlockSize, kBlockSize);
    assert(ret == 0);
    off_t offset = lseek(fd, 0, SEEK_SET);
    assert(offset == 0);
    int n = write(fd, p, kBlockSize);
    assert(n == kBlockSize);
    printf("write ok\n");
    free(p);
  }

  return 0;
}
复制代码

Direct I/O 与数据持久化

使用 buffered I/O 写入数据,write 返回之后,数据实际上只到达 page cache —— 还在内存中,需要等内核线程周期性将脏页刷新到持久化存储上,或应用程序调用 fsync 主动将数据刷新脏页。

理论上,使用 direct I/O 不会经过 page cache,当 write 返回之后,数据应该达到持久化存储。 但是,除了文件数据本身,文件的一些重要的元数据,比如文件的大小,也会影响数据的完整性。而 direct I/O 只是对文件数据本身有效,文件的元数据读写还是会经过内核缓存 —— 使用 direct I/O 读写文件,依然需要使用 fsync 来刷新文件的元数据。

但是,并不是所有文件元数据都会影响到数据的完整性,比如文件的修改时间。为此,MySQL 提供了一个新的刷盘参数:O_DIRECT_NO_FSYNC —— 使用 O_DIRECT  读写数据,只有在必要的使用进行 fsync。

As of MySQL 8.0.14, fsync() is called after creating a new file, after increasing file size, and after closing a file, to ensure that file system metadata changes are synchronized. The fsync() system call is still skipped after each write operation.

Linux AIO

前面介绍的,无论是调用 read/write 读写文件(包括 buffered I/O 和 direct I/O),还是使用 mmap 映射文件,都是属于同步文件 I/O。

同步 I/O 接口的优点是:简单易用、逻辑清晰。但是除了这个,同步 I/O 似乎没别的优点了。同步的 I/O 接口,如果没有命中 page cache,会导致线程阻塞 => 这种情况下,往往会使用多线程来解决 => 随着 SSD 等持久化设备的性能提升,继续增加线程数 => 大量线程的基础开销 + 大量上下文切换 + 内核调度器的负载 => 系统性能很差 => 需要异步文件 I/O 来提升高并发读写时的性能。

Linux AIO 是内核提供的一套支持文件异步 I/O 的接口(只支持使用 O_DIRECT 的方式来读写文件):

int io_setup(unsigned nr_events, aio_context_t *ctx_idp);
int io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp);
int io_getevents(aio_context_t ctx_id, long min_nr, long nr,
                 struct io_event *events, struct timespec *timeout);
int io_cancel(aio_context_t ctx_id, struct iocb *iocb,
                     struct io_event *result);
int io_destroy(aio_context_t ctx_id);
复制代码
  1. io_setup 创建一个能支持 nr_events 个操作的异步 I/O context。
  2. io_submit 提交异步 I/O 请求。
  3. io_getevents 获取已完成的异步 I/O 结果。
  4. io_cancel 取消之前提交的某个异步 I/O 请求。
  5. io_destroy 取消所有提交的异步 I/O 请求,并销毁异步 I/O context。

正常情况下,Linux AIO 的流程如下:

  1. 调用 io_setup 创建一个 I/O context 用于提交和收割 I/O 请求。
  2. 创建 1~n 和 I/O 请求,调用 io_submit 提交请求。
  3. I/O 请求执行完成,通过 DMA 直接将数据传输到 user buffer。
  4. 调用 io_getevents 收割已完成的 I/O。
  5. 重新执行第 2 步,或者确认不需要继续执行 AIO,调用 io_destroy 销毁 I/O context。

(图片来自 https://www.scylladb.com/2017/10/05/io-access-methods-scylla/)

这里用一个简单的例子介绍一下 Linux AIO 接口的基本使用方法。各个 Linux AIO 接口的详细介绍,建议参考相关的 Linux Manual Page。 这里要注意的是:

  1. glibc 并没有提供 Linux AIO 系统调用接口的封装,使用的时候需要使用 syscall 简单封装一下。
  2. libaio 是对 Linux AIO 的封装,接口很像但是不完全一样,不要混淆。
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/aio_abi.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>

int io_setup(unsigned nr, aio_context_t *ctxp) {
  return syscall(__NR_io_setup, nr, ctxp);
}

int io_destroy(aio_context_t ctx) { return syscall(__NR_io_destroy, ctx); }

int io_submit(aio_context_t ctx, long nr, struct iocb **iocbpp) {
  return syscall(__NR_io_submit, ctx, nr, iocbpp);
}

int io_getevents(aio_context_t ctx, long min_nr, long max_nr,
                 struct io_event *events, struct timespec *timeout) {
  return syscall(__NR_io_getevents, ctx, min_nr, max_nr, events, timeout);
}

int main(int argc, char *argv[]) {
  int fd = open("/tmp/linux_aio_test", O_RDWR | O_CREAT | O_DIRECT, 0666);
  if (fd < 0) {
    perror("open");
    return -1;
  }

  aio_context_t ctx = 0;
  int ret = io_setup(128, &ctx);
  if (ret < 0) {
    perror("io_setup");
    return -1;
  }

  struct iocb cb;
  memset(&cb, 0, sizeof(cb));
  cb.aio_fildes = fd;
  cb.aio_lio_opcode = IOCB_CMD_PWRITE;
  char *data = nullptr;
  ret = posix_memalign((void **)&data, 512, 4096);
  assert(ret == 0);
  memset(data, 'A', 4096);
  cb.aio_buf = (uint64_t)data;
  cb.aio_offset = 0;
  cb.aio_nbytes = 4096;

  struct iocb *cbs[1];
  cbs[0] = &cb;
  ret = io_submit(ctx, 1, cbs);
  if (ret != 1) {
    free(data);
    perror("io_submit");
    return -1;
  }

  struct io_event events[1];
  ret = io_getevents(ctx, 1, 1, events, nullptr);
  assert(ret == 1);
  printf("%ld %ld\n", events[0].res, events[0].res2);
  free(data);
  ret = io_destroy(ctx);
  if (ret < 0) {
    perror("io_destroy");
    return -1;
  }
  return 0;
}
复制代码

小结

Linux AIO 是 Linux 下一个尝试解决文件异步 I/O 的解决方案,但是这个方案并不彻底、不完美。

  • Linux AIO 只支持 direct I/O。 这意味着使用 Linux AIO 的所有读写操作都要受到 direct I/O 的限制:1)buffer 地址、buffer 大小、文件 offset 的对齐限制;2)无法使用 page cache。
  • 不完备的异步,仍然有可能被阻塞。 比如在 ext4 文件系统上,如果需要读取文件的元数据,此时调用可能会被阻塞。
  • 较大的参数拷贝开销。 每个 I/O 提交需要拷贝一个 64 字节的 struct iocb 对象;每个 I/O 完成需要拷贝一个 32 字节的 struct io_event 对象;一个 I/O 请求总共需要 96 字节的拷贝。这个拷贝开销是否可以承受,和单次 I/O 的大小有关:如果单次 I/O 本身就很大,相较之下,这点消耗可以忽略;而在大量小 I/O 的场景下,这样的拷贝影响比较大。
  • 多线程提交或收割 I/O 请求会对 io_context_t 造成比较大的锁竞争。 
  • 每个 I/O 需要两次系统调用才能完成( io_submit  和 io_getevents ),大量小 I/O 难以接受 —— 不过 io_submit 和 io_getevents 都支持批量操作,可以通过批量提交和批量收割来减少系统调用。

Linux AIO 从诞生之日(内核 2.5 版本)起到现在(2021 年),一直在不断完善。但大部分情况下,都是一些修修补补的工作,Linux AIO 一直都没有被实现完整 —— 特别是只支持 direct I/O,异步接口仍然可能被阻塞。

为了彻底解决 Linux AIO 在设计上的不完善,Linux 5.1 开始引入了全新的异步 I/O 接口:io_uring —— 希望 io_uring 能彻底解决 Linux 下的异步 I/O 问题。


作者:linjinhe
链接:https://juejin.cn/post/6938034371580395527
 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
深圳精敏工业级别20点I/O工控板JMDM-12DIO8AIOrar,一、简介 JMDM-12DIO8AIO是深圳市精敏数字机器有限公司自主研发的一款具有8路光电隔离数字量输入和4路模拟量输入,4路继电器输出和4路模拟量输出,高可靠稳定性的工业单片机串口控制器,也叫20点单片机I/O工控板。二、功能特性 1、工作电源:DC12V~24V 1A或AC9V~18V 1A;控制板自带防雷击保护电路,稳定可靠。 2、8位高性能单片机控制:程序存储空间:32K;数据存储空间:16K (若有特定需要),保存数据,断电数据不丢失。3、一种通信接口:一路RS232接口,可用来下载程序和与电脑等上位机通信。4、I/O: 1) 8路光电隔离数字量输入:输入电压范围为直流12V~24V;也可定制为5V(购买前需特别说明),输入电流为5~10MA;可用于检测外部各种信号的传感器输入,如光电传感器、红外传感器、霍尔传感器、接近开关、点动开关等数字量输入器件等; 2) 4路继电器输出:可以有效防止该产品上电时瞬间产生误动作,稳定可靠。继电器的触点可承受的交流电压为125V~277V,最大输出电流为10A~12A。 3)4路模拟量输入:外接4路模拟量电压信号,采集电压范围是0~10V,采集精度可达到18位; 4)4路模拟量输出:可外接4路模拟量控制装置(如比例阀等),电压输出精度可达到12位; 5、程序下载说明:连接好电脑和控制器之间的串口通讯线。使用STC-ISP下载软件将编译好的HEX文件下载到控制器。(注意:下载的过程中有一个手动断电和上电的过程)。 6、系统稳定性:具有工业级防雷击、抗强电磁干扰、高可靠性能,无死机现象。7、2种工作方式:支持独立控制(根据KEIL C语言编写的程序逻辑自动控制)、RS232串口监控(需编写上位机、下位机程序);8、数字量工作状态指示灯:每路数字量输入或输出都有一个指示灯,方便观察输入或输出点工作状态。9、带有2位拨码开关,以便进行地址或其它功能的模式选择。10、外形尺寸: 线路板尺寸:长×宽×高: 122mm×86mm×23mm。 外壳尺寸: 长×宽×高: 145mm×90mm×40mm。 安装尺寸: 长×宽: 135mm×70mm。、编程说明 在KEIL C编程环境下,本公司提供编程范例,客户可在此基础上二次开发应用程序。1.控制单个继电器输出点:sbit OUT1 = P2^4; 第一个继电器输出端口定义;sbit OUT2 = P2^5; 第二个继电器输出端口定义;sbit OUT3 = P2^6; 第个继电器输出端口定义;sbit OUT4 = P2^7; 第四个继电器输出端口定义; OUT3 =0; 打开第个继电器; OUT3 =1; 关闭第个继电器;2.读输入点命令:sbit IN1 = P0^0; 第一个输入点定义;sbit IN2 = P0^1; 第二个输入点定义;sbit IN3 = P0^2; 第个输入点定义;sbit IN4 = P0^3; 第四个输入点定义;sbit IN5 = P0^4; 第五个输入点定义;sbit IN6 = P0^5; 第六个输入点定义;sbit IN7 = P0^6; 第七个输入点定义;sbit IN8 = P0^7; 第八个输入点定义; if((IN3 ==0)&& (IN3 ==0)) 判断第个输入点有没有信号输入;3.读拨码开关命令:sbit SW1 = P1^2; //拨码开关1sbit SW2 = P1^1; //拨码开关2

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一口Linux

众筹植发

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值