文件系统是IO流程的起点,为了满足应用程序对IO操作的各种需求,在文件系统层设计了多种IO模式,上一篇介绍的bufferIO就是最常见的一种,本篇来梳理一下其他IO模式,它们的作用,流程以及是如何实现的。
iov_iter & kiocb
介绍各IO模式的实现前,先来看两个数据结构 iov_iter,kiocb
iov_iter
struct kvec {
void *iov_base; /* and that should *never* hold a userland pointer */
size_t iov_len;
};
struct iov_iter {
/*
* Bit 0 is the read/write bit, set if we're writing.
* Bit 1 is the BVEC_FLAG_NO_REF bit, set if type is a bvec and
* the caller isn't expecting to drop a page reference when done.
*/
unsigned int type; //标识读or写,以及其他属性
size_t iov_offset; //第一个iovec中,数据起始偏移
size_t count; //数据大小
union {
const struct iovec *iov; //结构与kvec一致,描述用户态的一段空间
const struct kvec *kvec; //描述内核态的一段空间
const struct bio_vec *bvec; //描述一个内存页中的一段空间
struct pipe_inode_info *pipe;
};
union {
unsigned long nr_segs; //iovec数量
struct {
int idx;
int start_idx;
};
};
}
“迭代器” 是内核中常见的设计,通常用来描述一个对象的处理进度。 iov_iter最初主要用于描述一次IO流程中用户空间的处理进度,以*iov保存用户空间的内存地址,iov_offset和count记录当前处理进度,这两个参数会随IO的进行会不断变化。随后该机制拓展到内核其他功能中,以union形式定义了更多属性。
参考:https://lwn.net/Articles/625077/
kiocb
struct kiocb {
struct file *ki_filp; //open文件创建的file结构
/* The 'ki_filp' pointer is shared in a union for aio */
randomized_struct_fields_start
loff_t ki_pos; //数据偏移
void (*ki_complete)(struct kiocb *iocb, long ret, long ret2); //IO完成回调
void *private;
int ki_flags; //IO属性
u16 ki_hint;
u16 ki_ioprio; /* See linux/ioprio.h */
unsigned int ki_cookie; /* for ->iopoll */
randomized_struct_fields_end
}
kiocb 中主要保存了一个file结构,以及记录读写偏移,相当于描述了一次IO中文件侧的处理进度。
iov_iter 和 kiocb 实际上分别描述了一次IO的两端,iov_iter描述内存侧,kiocb描述文件侧,文件系统提供两个接口基于这两个数据结构封装读写操作。
static inline ssize_t call_read_iter(struct file *file, struct kiocb *kio,
struct iov_iter *iter)
{
return file->f_op->read_iter(kio, iter);
}
将kiocb描述的文件数据,读到iov_iter描述的内存中。
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);
}
将iov_iter描述的内存数据,写到kiocb描述的文件中。
文件系统中所有IO模式的读写逻辑,最终都是基于这两个接口实现的。
DirectIO
上一篇提到的bufferIO,使用pagecache来缓存数据,但有时由于种种原因,应用不希望使用cache。directIO是用来解决这一需求的,它的实现是通过在open()调用时传入O_DIRECT参数,使得file->f_flags标记为O_DIRECT,在进行读写时根据该标记判断是否进行要绕过pagecache,当然底层文件系统驱动也需要实现directIO相关接口。还是以ext4为例看下directIO的流程。
directIO 也使用read(),write() 等系统调用,与bufferIO调用栈前面部分一致:
vfs_read() ->__vfs_read() ->call_read_iter() -> ext4_file_read_iter() -> generic_file_read_iter()
vfs_write() ->__vfs_write() -> call_write_iter() -> ext4_file_write_iter() -> __generic_file_write_iter()
主要区别在generic_file_read_iter() 和 __generic_file_write_iter()之后,来看下这两个函数的流程:
generic_file_read_iter 流程图
generic_file_write_iter 流程图
这两个函数在directIO模式下的流程很类似,可总结如下:
- 将pagecache中的“脏”页回写。
- 调用a_ops->directIO,执行磁盘到iov_iter中的用户内存地址的数据读写。
- 若上一步执行后,仍未完成所有数据的读写,以buffer IO模式读写剩下数据。
ext4 的a_ops->directIO 指向ext4_direct_IO() , 以read为例,调用栈为:
ext4_direct_IO()->ext4_direct_IO_read()->__blockdev_direct_IO()->do_blockdev_direct_IO()
do_blockdev_direct_IO() 中实现了directIO的核心逻辑,大致流程如下:
- 首先做一些基础信息校验,如数据是否超过inode大小,offset是否按blocksize对齐等
- 根据kiocb->ki_complete回调是否为NULL,设置本次directIO是同步or异步
- 调用do_direct_IO()