0%

Linux file read

本文基于linux kernel 3.19.8版本梳理一下读文件的过程,从vfs开始,到发送请求给block层结束。

普通模式 (1)

这里的普通是指没有设置O_DIRECT(也没设置O_SYNC,但它只对write起作用,read肯定是同步的,所以不提)。

vfs_read()

这个函数接收4个参数:

  • struct file *file:读取的目标文件;
  • char __user *buf:用户态buffer的地址;
  • size_t count:用户态buffer的大小;
  • loff_t *pos:目标文件中读取的起始位置;

首先做一些检查,包括:

  • 读权限检查,FMODE_READFMODE_CAN_READ
  • 用户态指针检查,即检查应用程序提供的buffer是否合法。注意access_ok(VERIFY_WRITE, buf, count)里面的VERIFY_WRITE是从buffer的角度出发的,读完数据要往buffer里写嘛。
  • 调用rw_verify_area检查:1. 参数检查,*pos是否小于0,*pos+count是否小于0;2. 是否有mandatory lock冲突;3. security permission;

若这些检查都通过,则调用__vfs_read(),把自己的4个参数传过去。

__vfs_read()

这个函数从vfs_read()接收那4个参数,直接传入filesystem相关的函数file->f_op->read。这是一个函数指针,对于ext4和xfs来说它都指向new_sync_read

new_sync_read()

到现在,我们手上还是那4个参数(只是参数名略不同):

  • struct file *filp;
  • char __user *buf;
  • size_t len;
  • loff_t *ppos;

这个函数用这4个参数,初始化3个结构体:

  • struct iovec iov = { .iov_base = buf, .iov_len = len };:即用户态buffer;
  • struct iov_iter iter;:指向用户态buffer的迭代器,用于fill用户态buffer;
  • struct kiocb kiocb;:这个结构体对于同步read和libaio有不同用法,我们当前讨论的是同步read,主要用到里面的这5个字段:ki_ctx=NULL; ki_filp=filpki_obj.tsk=current; ki_pos=*ppos; ki_nbytes=len;其中ki_ctx=NULL表明这是一个同步read而不是libaio;

初始化完成之后,再次调用filesystem相关的函数filp->f_op->read_iter,传入两个参数:&kiocb&iter,前者代表从哪里读;后者代表读到的输入放到哪里。filp->f_op->read_iter对于ext4指向的是generic_file_read_iter;对于xfs指向的是xfs_file_read_iterxfs_file_read_iter做一些处理之后也最终调用generic_file_read_iter,所以我们看这个函数。

generic_file_read_iter()

如前所述,这个函数两个参数是struct kiocb *iocbstruct iov_iter *iter。它从iocb中取出:

  • struct file *file = iocb->ki_filp;
  • loff_t *ppos = &iocb->ki_pos;

如果O_DIRECT没有设置(正是我们讨论的case),直接调用do_generic_file_read,传入file, ppos, iterretval(值为0)。

do_generic_file_read()

这个函数就比较复杂了。它从前面generic_file_read_iter()得到的参数是:

  • struct file *filp:目标文件;
  • loff_t *ppos:目标文件中读取的起始位置;
  • struct iov_iter *iter:指向用户态buffer;
  • ssize_t written:值为0;

首先是初始化一些变量:

  • struct address_space *mapping = filp->f_mapping;mapping里面的page_tree就是pagache;
  • struct inode *inode = mapping->host;mapping的owner inode。1. 对于普通文件来说,就是filp->f_dentry->d_inode;2. 对于block device file来说,就是bdev special filesystem里面的inode;filp就是/dev/sdc这样的文件,所属文件系统是devtmpfs;这个文件只是bdev special filesystem里面的文件的”指针”;bdev special filesystem没有mount,所以不可见。”指针”是这样实现的:devtmpfs文件系统中的inode有两个字段:i_bdevi_rdev;其中i_bdev是一个指向struct block_device的指针,而struct block_device嵌套在struct bdev_inode之内(可以通过struct block_device的地址得到外面struct bdev_inode的地址)。struct bdev_inode是什么呢?它就是bdev special filesystem内的inode。所以说,可以认为devtmpfs文件系统中的inode的i_bdev指向bdev special filesystem内的inode。当这个i_bdev为NULL的时候,就用到i_rdev,它是主设备号和从设备号,可以通过它去打开struct block_device,然后把返回的地址存入i_bdev。见blkdev_open()函数。所以说,devtmpfs文件系统中的文件是bdev special filesystem里面的文件的”指针”或”快捷方式”。这里拿到的inode就是bdev special filesystem里的inode,它是pagacache的owner,也是服务读请求的主体。
  • index:把文件看做是一个连续的page(通常是4KiB)数组,index就是read请求的第1字节所在的page号,也就是这个page在pagecache中的位置;
  • last_index:read请求的最后1字节所在page的后面一个page的号(注意不是最后1字节所在的那个page的号),也就是那个page在pagecache中的位置。这样形成一个前闭后开的区间[index, last_index)
  • offset:read请求的第1字节在它所在的page内的偏移。虽然内核是按page从底层读数据的,但返回给用户必须精确到字节,offset就是用来标记从第1个page的哪个位置开始往iter(指向用户态buffer)拷贝数据。当read请求区间跨page时,对于后续的page,offset被更新为0,表示从page的开头开始拷贝。
  • 还有read ahead相关的prev_indexprev_offset,暂且不看它们。

然后就开始一个大循环。假如read ahead完全没有起到作用,一次循环从block层读取一个page并拷贝到iter(用户态buffer),即page-by-page的读;否则,read ahead一下获取了很多page,一次循环只是拷贝一个page到iter(用户态buffer)。值得一提的是,read ahead完全起不到作用的情况极少发生,即使用户通过posix_fadvise设置了POSIX_FADV_RANDOM,比较新的内核也会走force_page_cache_readahead流程,详见read ahead

首先还是先看一下page-by-page的读吧,虽然它极少发生。

  • 调用find_get_page(mapping, index)在pagecache中找index指向的page;
  • 没有找到,从而调用page_cache_sync_readahead(对于read ahead被完全禁止的情况,ra->ra_pages=0,所以这个函数什么都不做直接返回),然后再调用find_get_page
  • 当然还是找不到,从而goto no_cached_page;在这里分配一个内存page,添加到pagacache中,然后goto readpage;
  • readpage处,调用mapping->a_ops->readpage;它是一个文件系统相关的函数指针,对于ext4来说,指向ext4_readpageext4_readpage再调用mpage_readpage,构造一个bio,发到block层。返回之后,goto page_ok;
  • page_ok处,把page的数据拷贝到iter(用户态buffer)
  • continue进行下一轮循环;
写的不错,有赏!