对 Linux 的 AIO 一直是一些碎片化的知识,没有好好总结。我们知道,AIO 的推荐使用场景是访问块设备、结合 O_DIRECT
一起使用。比如,CephBlockDevice.h.
Jens Axboe 在 fio 的 issues 里写道:
如果去看 libaio 的测试用例,及其 man 手册中的示例代码,就会发现:
- 示例中访问的只是普通文件(不是块设备);
- 而且打开文件的模式并不是都加了
O_DIRECT
普通文件可以用 AIO 接口吗? 🔗
显然可以,比如 libaio 的示例代码,或者看 seastar 的源代码。
不启用 O_DIRECT 时的行为是什么? 🔗
仔细阅读上面 Jens Axboe 的回复,libaio is still only async for O_DIRECT
.
这句话的意思是,打开 O_DIRECT 才能获得 async 语义,但不是说必须使用 O_DIRECT
.
vfs 对 io_submit 的处理 🔗
vfs 对 AIO 请求的处理逻辑可以在fs/aio.c
中找到。以写请求为例,req
经过 io_submit()
入口一直到达文件操作指针 f_op
中注册的 write_iter()
方法。
/* file: fs/aio.c */
static int aio_write(struct kiocb *req, const struct iocb *iocb,
bool vectored, bool compat)
{
/* ... omitted ... */
req->ki_flags |= IOCB_WRITE;
aio_rw_done(req, call_write_iter(file, req, &iter));
/* ... omitted ... */
}
/* file: include/linux/fs.h */
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
unsigned int flags);
/* ... omitted ... */
};
static inline ssize_t call_write_iter(struct file *file, struct kiocb *kio,
struct iov_iter *iter)
{
return file->f_op->write_iter(kio, iter);
}
内核对 write_iter()
接口的定义:
write_iter
possibly asynchronous write with iov_iter as source
这里有个关键字 possibly. 我们看看 XFS 的实现:
/* file: fs/xfs/xfs_file.c */
STATIC ssize_t
xfs_file_write_iter(
struct kiocb *iocb,
struct iov_iter *from)
{
/* ... omitted ... */
if (iocb->ki_flags & IOCB_DIRECT) {
/*
* Allow a directio write to fall back to a buffered
* write *only* in the case that we're doing a reflink
* CoW. In all other directio scenarios we do not
* allow an operation to fall back to buffered mode.
*/
ret = xfs_file_dio_write(iocb, from);
if (ret != -ENOTBLK)
return ret;
}
return xfs_file_buffered_write(iocb, from);
}
进一步阅读 xfs_file_buffered_write()
得到的简单结论是:对于 buffered
IO, XFS 会默默地进行同步写。Ext4 也有类似逻辑。1 其实,只要分别检查block/fops.c
, net/socket.c
里read_iter()
和 write_iter()
的实现,就能顺藤摸瓜找到块设备、套接字对 io_submit()
的处理。
同步的 io_submit() 有啥好处? 🔗
既然未打开 O_DIRECT
时,调用io_submit()
会同步阻塞,为啥不直接用read/write
接口?io_submit()
有个好处:不仅可以对单个 fd 进行批量请求(类似 readv/writev
),它还可以批量提交针对多个 fd 的请求,从而进一步节省系统调用的次数。2
struct iocb cb[2] = {{.aio_fildes = fd1,
.aio_lio_opcode = IOCB_CMD_PWRITE,
.aio_buf = (uint64_t)&buf[0],
.aio_nbytes = 0},
{.aio_fildes = fd2,
.aio_lio_opcode = IOCB_CMD_PREAD,
.aio_buf = (uint64_t)&buf[0],
.aio_nbytes = BUF_SZ}};
struct iocb *list_of_iocb[2] = {&cb[0], &cb[1]};
io_submit(ctx, 2, list_of_iocb);
O_DIRECT 隐含的要求 🔗
man (2) open
的 NOTES
小节里写了有整整一页关于 O_DIRECT
的相关要求。比如:
- 地址和长度的对齐要求 - 必须按照逻辑块的大小(blockdev –getss)对齐。
- 如果 DIO 请求的地址来自 mmap(2) MAP_PRIVATE、堆上分配的内存、静态分配的内存,则该请求不能和 fork(2) 同时并发运行。
- 等等。
如果使用 O_DIRECT
方式操纵文件,则读写的粒度以逻辑块为大小。因此,不可能写出一个只包含 hello world
的 11 字节大小的文件。
简单总结 🔗
下面内容主要来自网络:3
AIO 的工作场景 🔗
- 块设备上的 DIO;
- 某些支持 DIO 的文件系统,如:ext4, jfs, xfs 等等。
需要注意的场景 🔗
采用 buffered IO 时,io_submit()
可能并不会报错(取决于文件系统的实现),而是会悄悄变成同步 IO – 也就是 IO 请求完成才会返回。
展望 🔗
自从 2019 年 5 月 io_uring 进入 Linux 5.1 内核,如今终于有了同时支持 DIO、Buffered IO 的异步 IO 接口。随着 3.10.x, 4.18.x 慢慢退役,也许采用 liburing 的项目会渐渐超过 libaio 吧。
彩蛋 🔗
GNOME indexer 也不是那么一无是处。
$ tracker3 search -f -t address_space_operations
Files:
file:///home/live4thee/Docuements/io/Linux-VFS-and-Block.pdf
又因为terminal 会识别 URL, 鼠标一点就会直接通过 xdg-open
打开文档。
-
执行 ext4_dio_write_iter() 的过程中甚至可能回退到 ext4_buffered_write_iter(). ↩︎
-
参考 https://blog.cloudflare.com/io_submit-the-epoll-alternative-youve-never-heard-about/ ↩︎