一、页缓存

Linux 内核使用 页缓存(Page Cache) 机制来对文件中的数据进行缓存。

1.什么是页缓存?

  • 为了提升对文件的读写效率,Linux 内核会以页大小(4KB)为单位,将文件划分为多数据块。
    当用户对文件中的某个数据块进行读写操作时,内核首先会申请一个内存页(称为 页缓存)与文件中的数据块进行绑定。
  • 如下图所示:
    Linux页缓存、LinuxVFS中文件打开、读、写底层逻辑_b树
  • 如上图所示,当用户对文件进行读写时,实际上是对文件的 页缓存 进行读写。
    所以对文件进行读写操作时,会分以下两种情况进行处理:
    (1)当从文件中读取数据时,如果要读取的数据所在的页缓存已经存在,那么就直接把页缓存的数据拷贝给用户即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户
    (2)当向文件中写入数据时,如果要写入的数据所在的页缓存已经存在,那么直接把新数据写入到页缓存即可。
    否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把新数据写入到页缓存中。
    对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。

2.页缓存的实现

address_space

  • 在 Linux 内核中,使用 file 对象来描述一个被打开的文件,其中有个名为 f_mapping 的字段,定义如下:
struct file {
    ...
    struct address_space *f_mapping;
};

从上面代码可以看出,f_mapping 字段的类型为 address_space 结构,其定义如下:
struct address_space {
    struct inode           *host;      /* owner: inode, block_device */
    struct radix_tree_root page_tree;  /* radix tree of all pages */
    rwlock_t               tree_lock;  /* and rwlock protecting it */
    ...
};
address_space 结构其中的一个作用就是用于存储文件的页缓存,下面介绍一下各个字段的作用:
host:指向当前 address_space 对象所属的文件 inode 对象(每个文件都使用一个 inode 对象表示)。
page_tree:用于存储当前文件的页缓存。
tree_lock:用于防止并发访问page_tree导致的资源竞争问题。

从 address_space 对象的定义可以看出,文件的 页缓存 使用了 radix树 来存储。
radix树:又名基数树,它使用键值(key-value)对的形式来保存数据,并且可以通过键(文件偏移量)快速查找到其对应的页缓存。
(内核以文件读写操作中的数据 偏移量 作为键,以数据偏移量所在的 页缓存 作为值,存储在 address_space 结构的 page_tree 字段中。)
  • 下图显示了各个结构的关系
    Linux页缓存、LinuxVFS中文件打开、读、写底层逻辑_c语言_02
二、LinuxVFS之文件打开、读、写逻辑

Linux中所有文件系统都是依靠VFS系统进行协同工作的

  • 使用VFS可以利用标准的Unix系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作。
    Linux页缓存、LinuxVFS中文件打开、读、写底层逻辑_缓存_03
     - Unix使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和安装点。
    VFS中共有四个主要对象类型分别是:
    (1)超级块对象,代表一个具体的已安装文件系统,操作对象为super_operations
    (2)索引节点对象,代表一个具体文件,操作对象为inode_operations
    (3)目录项对象,代表一个目录项,是路径的一个组成部分,操作对象为dentry_operations
    (4)文件对象,代表由进程打开的文件,操作对象为file_operations

1.文件打开操作

eg:

#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>

 

int main ()
{

  int i, f;
  FILE *fp;
  char string[24];

  fp = fopen ("test.dat", "w+");
  sprintf (string, "helloworld\n");
  fwrite (string, 11, 1, fp);
  fclose (fp);
}

内核入口

  • 使用strace ./io后,可以发现会调用系统调用open来实现文件的打开
open("test.dat", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3

这个系统调用才是内核中的函数,该函数定义在如下:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
        if (force_o_largefile())
                flags |= O_LARGEFILE;
		//系统调用,会调用do_sys_open函数
        return do_sys_open(AT_FDCWD, filename, flags, mode);
}

do_sys_open()功能如下:
通过函数build_open_flags()来设置需要打开文件的flags(其结构体为open_flags);
通过函数get_unused_fd_flags()获取一个可用的fd;
调用alloc_fd()函数从fd_table中获取一个可用fd,并做些简单初始化得到一个文件描述符;
调用do_filp_open()函数获取file对象;
最后通过fd_install(),建立文件描述符和file之间的关联,即安装在进程的fd数组中。


do_filp_open()功能如下:
要根据文件名字进行搜索,如果不存在需要进行文件创建。
这里相关数据结构是ext4_dir_inode_operations,不同的文件系统会有不同的数据结构,从而指定不同的函数。
const struct inode_operations ext4_dir_inode_operations = {
        .create         = ext4_create,
        .lookup         = ext4_lookup,
        .link           = ext4_link,
        .unlink         = ext4_unlink,
        .symlink        = ext4_symlink,
        .mkdir          = ext4_mkdir,
        .rmdir          = ext4_rmdir,
        .mknod          = ext4_mknod,
        .tmpfile        = ext4_tmpfile,
        .rename         = ext4_rename2,
        .setattr        = ext4_setattr,
        .getattr        = ext4_getattr,
        .listxattr      = ext4_listxattr,
        .get_acl        = ext4_get_acl,
        .set_acl        = ext4_set_acl,
        .fiemap         = ext4_fiemap,
};
//等价于下列操作
//const struct inode_operations ext4_dir_inode_operations = {ext4_create,ext4_lookup};
//const struct inode_operations ext4_dir_inode_operations;
//ext4_dir_inode_operations.create=ext4_create;
  • 逻辑流程如下:
    到submit_bio(红色框内)后会调用generic_make_request从而进入块层:
    Linux页缓存、LinuxVFS中文件打开、读、写底层逻辑_数据_04

2.文件读操作

read系统调用读取文件中的数据,调用链如下:

read()
└→ sys_read()
   └→ vfs_read()
      └→ do_sync_read()
         └→ generic_file_aio_read()
            └→ do_generic_file_read()
               └→ do_generic_mapping_read()

内核中的读文件基于页的,内核总是一次传送几个完整的数据页。
如果数据不在RAM 中,内核会分配一个新页框,并将文件适当部分填充并放入到页高速缓存,最后把所需读字节复制到进程地址空间中。
我们从系统调用read开始,其系统调用实现如下,相比之前版本使用了ksys_read函数进行重新封装。
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
        return ksys_read(fd, buf, count);
}

ksys_read函数如下:
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
		//fdget_pos和fdput_pos是锁相关操作
        struct fd f = fdget_pos(fd);
        ssize_t ret = -EBADF;
        if (f.file) {
        		//file_pos_read和file_pos_write是读写文件中读写位置
                loff_t pos = file_pos_read(f.file);
                ret = vfs_read(f.file, buf, count, &pos);
                if (ret >= 0)
                        file_pos_write(f.file, pos);
                fdput_pos(f);
        }
        return ret;
}

然后调用了vfs_read函数,该函数是read的具体实现,也是虚拟文件系统读的总开始,很多关于文件系统的监控点都会设置在此函数上.

//入参分别是文件句柄的file结构,用户空间缓存,读取数量和读取位置。
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
        ssize_t ret;
        if (!(file->f_mode & FMODE_READ))
                return -EBADF;
        if (!(file->f_mode & FMODE_CAN_READ))
                return -EINVAL;
		//先通过函数rw_verify_area做一些入参的基本检测,如读写位置是否为负,或者读的数量超过文件自身字节数上限,
		//如果该函数执行出错就直接就退出读操作了。
		//如果要读取数量大于系统最大读取数量,则设置读取数量为系统的值。
        if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
                return -EFAULT;

        ret = rw_verify_area(READ, file, pos, count);
        if (!ret) {
                if (count > MAX_RW_COUNT)
                        count =  MAX_RW_COUNT;
                //
                ret = __vfs_read(file, buf, count, pos);
                if (ret > 0) {
                		//fsnotify_access通知文件被读取
                        fsnotify_access(file);
                        //add_rchar来增加当前进程读取字节数
                        add_rchar(current, ret);
                }
                //inc_syscr来增加进程的系统调用次数
                inc_syscr(current);
        }
        return ret;
}

主要的核心是__vfs_read()函数,其定义如下:
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,loff_t *pos)
{
        if (file->f_op->read)
                return file->f_op->read(file, buf, count, pos);
        else if (file->f_op->read_iter)
                return new_sync_read(file, buf, count, pos);
        else
                return -EINVAL;
}
先是使用file的f_op函数集,ext4则是结构体ext4_ ,定义如下,
const struct file_operations ext4_file_operations = {
        .llseek         = ext4_llseek,
        .read_iter      = ext4_file_read_iter,
        .write_iter     = ext4_file_write_iter,
        .unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = ext4_compat_ioctl,
#endif 
        .mmap           = ext4_file_mmap,
        .mmap_supported_flags = MAP_SYNC,
        .open           = ext4_file_open,
        .release        = ext4_release_file,
        .fsync          = ext4_sync_file,
        .get_unmapped_area = thp_get_unmapped_area,
        .splice_read    = generic_file_splice_read,
        .splice_write   = iter_file_splice_write,
        .fallocate      = ext4_fallocate,
};

非文件系统的操作函数集如下def_blk_fops,在没有文件系统的时候会使用此处的函数:
const struct file_operations def_blk_fops = {
        .open           = blkdev_open,
        .release        = blkdev_close,
        .llseek         = block_llseek,
        .read_iter      = blkdev_read_iter,
        .write_iter     = blkdev_write_iter,
        .mmap           = generic_file_mmap,
        .fsync          = blkdev_fsync,
        .unlocked_ioctl = block_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = compat_blkdev_ioctl,
#endif
        .splice_read    = generic_file_splice_read,
        .splice_write   = iter_file_splice_write,
        .fallocate      = blkdev_fallocate,
};

此外xfs文件系统的操作函数集如下:
const struct file_operations xfs_file_operations = {
        .llseek         = xfs_file_llseek,
        .read_iter      = xfs_file_read_iter,
        .write_iter     = xfs_file_write_iter,
        .splice_read    = generic_file_splice_read,
        .splice_write   = iter_file_splice_write,
        .unlocked_ioctl = xfs_file_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = xfs_file_compat_ioctl,
#endif         
        .mmap           = xfs_file_mmap,
        .mmap_supported_flags = MAP_SYNC,
        .open           = xfs_file_open,
        .release        = xfs_file_release,
        .fsync          = xfs_file_fsync,
        .get_unmapped_area = thp_get_unmapped_area,
        .fallocate      = xfs_file_fallocate,
        .clone_file_range = xfs_file_clone_range,
        .dedupe_file_range = xfs_file_dedupe_range,
};

其read函数并没有定义,所以调用new_sync_read()函数
static ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{              
        struct iovec iov = { .iov_base = buf, .iov_len = len };
        //函数中kiocb表示io control block. 用来跟踪记录IO操作的完成状态
        struct kiocb kiocb;
        //iov_iter表示用户和内核之间传递的数据
        struct iov_iter iter;
        ssize_t ret;
                       
        init_sync_kiocb(&kiocb, filp);
        kiocb.ki_pos = *ppos;
        iov_iter_init(&iter, READ, &iov, 1, len);

        ret = call_read_iter(filp, &kiocb, &iter);
        BUG_ON(ret == -EIOCBQUEUED);
        *ppos = kiocb.ki_pos;
        return ret;
 }
通过init_sync_kiocb()来初始化kiocb
static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
{         
        *kiocb = (struct kiocb) {
                .ki_filp = filp,
                .ki_flags = iocb_flags(filp),
                .ki_hint = file_write_hint(filp), 
        };
}
iov_iter_init()用来初始化iov_iter

接着就是调用call_read_iter函数,如下,其实就是调用ext4_file_operations中的 ext4_file_read_iter。
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);
} 

ext4_file_read_iter函数
static ssize_t ext4_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
		//ext4_forced_shutdown会获取ext4超级块的信息,来检测下相关flag中的EXT4_FLAGS_SHUTDOWN位                                   
        if (unlikely(ext4_forced_shutdown(EXT4_SB(file_inode(iocb->ki_filp)->i_sb))))
                return -EIO;
        //iov_iter_count检测下iov_iter的成员count变量
        if (!iov_iter_count(to))
                return 0; /* skip atime */
 
 //然后判断内核是否配置了CONFIG_FS_DAX(Direct access),
 //以及文件的打开方式是否是直接访问设备,这个直接影响访问是否绕过pagecache
 //如果配置了CONFIG_FS_DAX,且文件打开方式指定了直接访问,那么则调用ext4_dax_read_iter函数
//因为CONFIG_FS_DAX默认系统是不设置的,并不是常用的配置项,
//而且就算配置在函数ext4_dax_read_iter中还会判断inode是否支持直接访问,否则还是会调用函数generic_file_read_iter。
#ifdef CONFIG_FS_DAX
        if (IS_DAX(file_inode(iocb->ki_filp)))
                return ext4_dax_read_iter(iocb, to);
#endif
        return generic_file_read_iter(iocb, to);
} 

如果是xfs文件系统则调用函数xfs_file_buffered_aio_read(),继而调用generic_file_read_iter函数。

generic_file_read_iter()函数是文件系统的读路径
该函数比较长不列出来了,可以自行观察mm/filemap.c.
该函数是会先根据iocb中打开文件的flag来判断是否是Direct IO,如果是则进入到Direct IO分支,
判断上次写操作是否需要filemap_write_and_wait_range函数同步,确保读到的数据是最新的;
然后调用mapping->a_ops->direct_IO来访问数据,其中dirct_IO是address_space_operations函数集指定的函数,在ext4中是ext4_direct_IO。
static const struct address_space_operations ext4_aops = {
        .readpage               = ext4_readpage,
        .readpages              = ext4_readpages,
        .writepage              = ext4_writepage,
        .writepages             = ext4_writepages,
        .write_begin            = ext4_write_begin,
        .write_end              = ext4_write_end,
        .set_page_dirty         = ext4_set_page_dirty,
        .bmap                   = ext4_bmap,
        .invalidatepage         = ext4_invalidatepage,
        .releasepage            = ext4_releasepage,
        .direct_IO              = ext4_direct_IO,
        .migratepage            = buffer_migrate_page,
        .is_partially_uptodate  = block_is_partially_uptodate,
        .error_remove_page      = generic_error_remove_page,
};
默认的操作函数集是def_blk_aops

static const struct address_space_operations def_blk_aops = {
        .readpage       = blkdev_readpage,
        .readpages      = blkdev_readpages,
        .writepage      = blkdev_writepage,
        .write_begin    = blkdev_write_begin,
        .write_end      = blkdev_write_end,
        .writepages     = blkdev_writepages,
        .releasepage    = blkdev_releasepage,
        .direct_IO      = blkdev_direct_IO,
        .is_dirty_writeback = buffer_check_dirty_writeback,
};
如果不是直接IO则调用generic_file_buffered_read。
该函数是通用文件读路径。
循环在内存中寻找所读取内容是否在内存中缓存,如果cache命中失败,使用
page_cache_async_readahead/page_cache_sync_readahead会从磁盘中读取页,并进行预读;

此外,还要判断页是否是最新,以免读到脏数据;
如果非最新,则需要调用address_space_operations中readpage函数进行读操作获取最新页,读页的函数最后都会调用submit_bio;
  
此外,如果内存已经没有page cache,则需要调用函数page_cache_alloc来进行申请一个page并加入到page_cache_lru,最后通过copy_page_to_iter将内存中数据复制到用户空间。

最后通过函数file_accessed来更新文件访问时间
  • 从上面的调用链可以看出,read 系统调用最终会调用 do_generic_mapping_read 函数来读取文件中的数据,其实现如下:
    (1)通过调用 find_get_page 函数查找要读取的文件偏移量所对应的页缓存是否存在,如果存在就把页缓存中的数据拷贝到应用程序的内存中。
    (2)否则调用 page_cache_alloc_cold 函数申请一个空闲的内存页作为新的页缓存,并且通过调用add_to_page_cache_lru函数把新申请的页缓存添加到文件页缓存和 LRU 队列中。
    (3)通过调用 readpage 接口从文件中读取数据到页缓存中,并且把页缓存的数据拷贝到应用程序的内存中。
void
do_generic_mapping_read(struct address_space *mapping,
                        struct file_ra_state *_ra,
                        struct file *filp,
                        loff_t *ppos,
                        read_descriptor_t *desc,
                        read_actor_t actor)
{
    struct inode *inode = mapping->host;
    unsigned long index;
    struct page *cached_page;
    ...

    cached_page = NULL;
    index = *ppos >> PAGE_CACHE_SHIFT;
    ...

    for (;;) {
        struct page *page;
        ...

find_page:
        // 1. 查找文件偏移量所在的页缓存是否存在
        page = find_get_page(mapping, index);
        if (!page) {
            ...
            // 2. 如果页缓存不存在, 那么跳到 no_cached_page 进行处理
            goto no_cached_page; 
        }
        ...

page_ok:
        ...
        // 3. 如果页缓存存在, 那么把页缓存的数据拷贝到用户应用程序的内存中
        ret = actor(desc, page, offset, nr);
        ...
        if (ret == nr && desc->count)
            continue;
        goto out;
        ...

readpage:
        // 4. 从文件读取数据到页缓存中
        error = mapping->a_ops->readpage(filp, page);
        ...
        goto page_ok;
        ...

no_cached_page:
        if (!cached_page) {
            // 5. 申请一个内存页作为页缓存
            cached_page = page_cache_alloc_cold(mapping);
            ...
        }

        // 6. 把新申请的页缓存添加到文件页缓存中
        error = add_to_page_cache_lru(cached_page, mapping, index, GFP_KERNEL);
        ...
        page = cached_page;
        cached_page = NULL;
        goto readpage;
    }

out:
    ...
}

  • 从上面代码可以看出,当页缓存不存在时会申请一块空闲的内存页作为页缓存,并且通过调用
    (1)add_to_page_cache_lru 函数把其添加到文件的页缓存和 LRU 队列中。
    (2)add_to_page_cache_lru 函数主要完成两个工作:
    通过调用 add_to_page_cache 函数把页缓存添加到文件页缓存中,也就是添加到 address_space 结构的 page_tree 字段中。
    通过调用 lru_cache_add 函数把页缓存添加到 LRU 队列中。LRU 队列用于当系统内存不足时,对页缓存进行清理时使用。
 int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
                           pgoff_t offset, gfp_t gfp_mask)
{
    // 1. 把页缓存添加到文件页缓存中
    int ret = add_to_page_cache(page, mapping, offset, gfp_mask);
    if (ret == 0)
        lru_cache_add(page); // 2. 把页缓存添加到 LRU 队列中
    return ret;
}
  • 逻辑流程如下:
    Linux页缓存、LinuxVFS中文件打开、读、写底层逻辑_数据_05

3.文件写操作

//和系统中的读操作一样,系统的写操作也是从系统调用write开始,其系统调用如下:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
{
        return ksys_write(fd, buf, count);
}

其逻辑同读操作基本是一致的,此处也是调用ksys_write()函数,该函数如下,逻辑同读操作并无二:
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
        struct fd f = fdget_pos(fd);
        ssize_t ret = -EBADF;
        if (f.file) {
                loff_t pos = file_pos_read(f.file);
                ret = vfs_write(f.file, buf, count, &pos);
                if (ret >= 0)
                        file_pos_write(f.file, pos);
                fdput_pos(f);
        }
        return ret;
}
函数接着是调用vfs_write,如下,会做一些写之前的检测,最后会更新进程中的静态统计:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
        ssize_t ret;
        if (!(file->f_mode & FMODE_WRITE))
                return -EBADF;
        if (!(file->f_mode & FMODE_CAN_WRITE))
                return -EINVAL;
        if (unlikely(!access_ok(VERIFY_READ, buf, count)))
                return -EFAULT;
        ret = rw_verify_area(WRITE, file, pos, count);
        if (!ret) {
                if (count > MAX_RW_COUNT)
                        count =  MAX_RW_COUNT;
                file_start_write(file);
                ret = __vfs_write(file, buf, count, pos);
                if (ret > 0) {
                        fsnotify_modify(file);
                        add_wchar(current, ret);
                }
                inc_syscw(current);
                file_end_write(file);
        }
        return ret;
}

__vfs_write函数的调用如下,函数中会使用file_operations中实现的函数,先判断是否有.write函数,
如果没有则判断是有.write_iter函数,如果有则调用new_sync_write函数。
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos)
{
        if (file->f_op->write)
                return file->f_op->write(file, p, count, pos);
        else if (file->f_op->write_iter
                return new_sync_write(file, p, count, pos);
        else
               return -EINVAL;
}new_sync_write()函数中,会初始化kiocb,并调用函数call_write_iter()call_write_iter()函数会调用file->f_op->write_iter,不同文件系统有不同对应的函数,数据结构体如上篇读中多列。
Ext4为函数ext4_write_iter(),xfs文件系统为xfs_file_write_iter(),无文件系统默认的操作为blkdev_write_iter()。
根据不同的文件系统出现分支。

在ext4_write_iter()函数中,会调用函数__generic_file_write_iter(),该函数会将数据写到文件中。
该函数中判断IOCB_DIRECT,如果是直接写,最后需要调用filemap_write_and_wait_range()函数将page cache中的页刷入到磁盘,并无效化映射的页。
如果不是IOCB_DIRECT,则直接调用generic_perform_write()函数。

generic_perform_write()该函数是ext4文件系统和裸设备写操作的核心,
在generic_perform_write()函数中,会循环的调用iov_iter_copy_from_user_atomic函数,将数据从用户层复制到内核。
其中内核接收用户层数据的时候,使用了结构体iov_iter,代码如下:
struct iov_iter {
        int type;//迭代器类型
        size_t iov_offset;// 第一个iovec中,第一个字节的偏移
        size_t count;
        union {
                const struct iovec *iov;
                const struct kvec *kvec;
                const struct bio_vec *bvec;
                struct pipe_inode_info *pipe;
        };
        union {
                unsigned long nr_segs;
                struct {
                        int idx;
                        int start_idx;
                };
        };
};

iov_iter结构体其实是iovec的迭代器,iovec描述了在物理内存或虚拟内存中分散的缓存buffer。
通过iov_iter迭代器可以一次进行数据传输的处理非常高效。
struct iovec
{
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
不过在执行iov_iter_copy_from_user_atomic()函数执行会调用a_ops->write_begin来将数据读入到缓存中,
执行完毕后需要将页标记为脏,因为并没有直接刷入到磁盘,这是和直接IO存在差异的地方。

最后结束后,需要调用函数generic_write_sync,如果是IOCB_DSYNC需要调用函数vfs_fsync_range来同步写。

XFS文件系统写

  • xfs文件系统与ext4和裸设备存在较大差异,其核心函数是iomap_file_buffered_write。该函数引入的一个参数是操作函数结构体iomap_ops如下,指定了两个函数:
const struct iomap_ops xfs_iomap_ops = {
        .iomap_begin            = xfs_file_iomap_begin,
        .iomap_end              = xfs_file_iomap_end,
};
这两个函数类似在ext4文件系统中的a_ops->write_begin和a_ops->write_end。
xfs_file_iomap_begin()会根据IS_DAX(inode),如果不是直接IO,则直接调用函数xfs_file_iomap_begin_delay(),
然后通过函数iomap_write_actor()(调用iov_iter_copy_from_user_atomic())将数据从用户态复制到内核态。

最后刷IO是在file结构体对象释放时候,调用file_operations()中指定的.release函数, 
Ext4文件系统对应的release函数是ext4_release(),
xfs文件系统对应的release函数是xfs_file_release(), 
通用块对应的release函数是blkdev_close()。

Release函数会触发调用aops->write_pages,最后都会调用submit_bio函数。 
这样不会每次io都提交一个请求给块设备,在可扩展性方面得到了较大的提升。