Linux IO子系统和文件系统读写流程

图片 1

转自:https://www.ibm.com/developerworks/cn/linux/l-cn-read/

IBM developworks中,〈read系统调用剖析〉阐述就很清楚。

赵 健博 (zhaojianbo@ncic.ac.cn), 硕士, 中国科学院计算技术研究所

图片 2

图4 read 系统调用在 ext2 层中处理时函数调用关系

图片 3

由图 4 可知,该层入口函数 generic_file_read 调用函数 __generic_file_aio_read ,后者判断本次读请求的访问方式,如果是直接 io (filp->f_flags 被设置了 O_DIRECT 标志,即不经过 cache)的方式,则调用 generic_file_direct_IO 函数;如果是 page cache 的方式,则调用 do_generic_file_read 函数。函数 do_generic_file_read 仅仅是一个包装函数,它又调用 do_generic_mapping_read 函数。

在讲解 do_generic_mapping_read 函数都作了哪些工作之前,我们再来看一下文件在内存中的缓存区域是被怎么组织起来的。

read系统调用的处理分为用户空间和内核空间处理两部分。其中,用户空间处理只是通过0x80中断陷入内核,接着调用其中断服务例程,即sys_read以进入内核处理流程。

在 IBM Bluemix 云平台上开发并部署您的下一个应用。

图片 4

清单2 sys_read 函数的代码
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
 struct file *file;
 ssize_t ret = -EBADF;
 int fput_needed;

 file = fget_light(fd, &fput_needed);
 if (file) {
 loff_t pos = file_pos_read(file);
 ret = vfs_read(file, buf, count, &pos);
 file_pos_write(file, pos);
 fput_light(file, fput_needed);
 }

 return ret;
}

代码解析:

  • fget_light() :根据 fd 指定的索引,从当前进程描述符中取出相应的 file 对象(见图3)。
  • 如果没找到指定的 file 对象,则返回错误
  • 如果找到了指定的 file 对象:
  • 调用 file_pos_read() 函数取出此次读写文件的当前位置。
  • 调用 vfs_read() 执行文件读取操作,而这个函数最终调用 file->f_op.read() 指向的函数,代码如下:

if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);

  • 调用 file_pos_write() 更新文件的当前读写位置。
  • 调用 fput_light() 更新文件的引用计数。
  • 最后返回读取数据的字节数。

到此,虚拟文件系统层所做的处理就完成了,控制权交给了 ext2 文件系统层。

在解析 ext2 文件系统层的操作之前,先让我们看一下 file 对象中 read 指针来源。

我们在Linux上总是要保存数据,数据要么保存在文件系统里(如ext3),要么就保存在裸设备里。我们在使用这些数据的时候都是通过文件这个抽象来访问的,操作系统会把我们需要的数据提交给我们,而我们则无需和块设备打交道。

准备:

注:所有清单中代码均来自 linux2.6.11 内核原代码

读数据之前,必须先打开文件。处理 open 系统调用的内核函数为 sys_open 。 
所以我们先来看一下该函数都作了哪些事。清单1显示了 sys_open 的代码(省略了部分内容,以后的程序清单同样方式处理)

我们所有分析的,是基于2.6.32及其后的内核.

前提条件:

对于具体的一次 read 调用,内核中可能遇到的处理情况很多。这里举例其中的一种情况:

  • 要读取的文件已经存在
  • 文件经过 page cache
  • 要读的是普通文件
  • 磁盘上文件系统为 ext2 文件系统,有关 ext2 文件系统的相关内容,参见参考资料

I/O子系统是个层次很深的系统,数据请求从用户空间最终到达磁盘,经过了复杂的数据流动。

参考资料

  • 查看文章“使用 Linux 系统调用的内核命令”, 了解系统调用的基本原理以及如何实现自己的系统调用的方法。
  • 查看文章“Linux 文件系统剖析”,了解 linux 文件系统的相关内容。
  • 参看文章“Ext2 文件系统的硬盘布局”和书籍“ understanding the linux kernel(3rd edition)”第18章的内容,了解 ext2 文件系统的相关内容。
  • 查看书籍“understanding the linux kernel(3rd edition)”第 14、15 章内容,了解 IO 调度算法、page cache 的技术等内容。

然后,VFS将控制权交给了ext2文件系统。(ext2在此作为示例,进行解析)

相关的内核数据结构:

  • Dentry : 联系了文件名和文件的 i 节点
  • inode : 文件 i 节点,保存文件标识、权限和内容等信息
  • file : 保存文件的相关信息和各种操作文件的函数指针集合
  • file_operations :操作文件的函数接口集合
  • address_space :描述文件的 page cache 结构以及相关信息,并包含有操作 page cache 的函数指针集合
  • address_space_operations :操作 page cache 的函数接口集合
  • bio : IO 请求的描述

vfs_read会调用与具体文件相关的read函数执行读取操作,file->f_op.read。

图6 read 系统调用在内核中所经历的函数调用层次

图片 5

对于read系统调用在内核的处理,如上图所述,经过了VFS、具体文件系统,如ext2、页高速缓冲存层、通用块层、IO调度层、设备驱动层、和设备层。其中,VFS主要是用来屏蔽下层具体文件系统操作的差异,对上提供一个统一接口,正是因为有了这个层次,所以可以把设备抽象成文件。具体文件系统,则定义了自己的块大小、操作集合等。引入cache层的目的,是为了提高IO效率。它缓存了磁盘上的部分数据,当请求到达时,如果在cache中存在该数据且是最新的,则直接将其传递给用户程序,免除了对底层磁盘的操作。通用块层的主要工作是,接收上层发出的磁盘请求,并最终发出IO请求(BIO)。IO调度层则试图根据设置好的调度算法对通用块层的bio请求合并和排序,回调驱动层提供的请求处理函数,以处理具体的IO请求。驱动层的驱动程序对应具体的物理设备,它从上层取出IO请求,并根据该IO请求中指定的信息,通过向具体块设备的设备控制器发送命令的方式,来操纵设备传输数据。设备层都是具体的物理设备。

清单1 sys_open 函数代码
asmlinkage long sys_open(const char __user * filename, int flags, int mode)
{
 ……
 fd = get_unused_fd();
 if (fd >= 0) {
 struct file *f = filp_open(tmp, flags, mode);
 fd_install(fd, f);
 }
 ……
 return fd;
 ……
}

代码解释:

  • get_unuesed_fd() :取回一个未被使用的文件描述符(每次都会选取最小的未被使用的文件描述符)。
  • filp_open() :调用 open_namei() 函数取出和该文件相关的 dentry 和 inode (因为前提指明了文件已经存在,所以 dentry 和 inode 能够查找到,不用创建),然后调用 dentry_open() 函数创建新的 file 对象,并用 dentry 和 inode 中的信息初始化 file 对象(文件当前的读写位置在 file 对象中保存)。注意到 dentry_open() 中有一条语句:

f->f_op = fops_get(inode->i_fop);

这个赋值语句把和具体文件系统相关的,操作文件的函数指针集合赋给了 file 对象的 f _op 变量(这个指针集合是保存在 inode 对象中的),在接下来的 sys_read 函数中将会调用 file->f_op 中的成员 read 。

  • fd_install() :以文件描述符为索引,关联当前进程描述符和上述的 file 对象,为之后的 read 和 write 等操作作准备。
  • 函数最后返回该文件描述符。

图3显示了 sys_open 函数返回后, file 对象和当前进程描述符之间的关联关系,以及 file 对象中操作文件的函数指针集合的来源(inode 对象中的成员 i_fop)。

VFS层:

清单6 mpage_readpage 函数的代码
int mpage_readpage(struct page *page, get_block_t get_block)
{
    struct bio *bio = NULL;
    sector_t last_block_in_bio = 0;

    bio = do_mpage_readpage(bio, page, 1,
                         &last_block_in_bio, get_block);
    if (bio)
        mpage_bio_submit(READ, bio);
    return 0;
}

该函数首先调用函数 do_mpage_readpage 函数创建了一个 bio 请求,该请求指明了要读取的数据块所在磁盘的位置、数据块的数量以及拷贝该数据的目标位置——缓存区中 page 的信息。然后调用 mpage_bio_submit 函数处理请求。 mpage_bio_submit 函数则调用 submit_bio 函数处理该请求,后者最终将请求传递给函数 generic_make_request ,并由 generic_make_request 函数将请求提交给通用块层处理。

到此为止, page cache 层的处理结束。

它根据文件fd指定的索引,从当前进程描述符中取出相应的file对象,并调用vfs_read执行文件读取操作。

图1 read 系统调用在核心空间中的处理层次

图片 6

  • 虚拟文件系统层的作用:屏蔽下层具体文件系统操作的差异,为上层的操作提供一个统一的接口。正是因为有了这个层次,所以可以把设备抽象成文件,使得操作设备就像操作文件一样简单。
  • 在具体的文件系统层中,不同的文件系统(例如 ext2 和 NTFS)具体的操作过程也是不同的。每种文件系统定义了自己的操作集合。关于文件系统的更多内容,请参见参考资料。
  • 引入 cache 层的目的是为了提高 linux 操作系统对磁盘访问的性能。 Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,如果在 cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能。
  • 通用块层的主要工作是:接收上层发出的磁盘请求,并最终发出 IO 请求。该层隐藏了底层硬件块设备的特性,为块设备提供了一个通用的抽象视图。
  • IO 调度层的功能:接收通用块层发出的 IO 请求,缓存请求并试图合并相邻的请求(如果这两个请求的数据在磁盘上是相邻的)。并根据设置好的调度算法,回调驱动层提供的请求处理函数,以处理具体的 IO 请求。
  • 驱动层中的驱动程序对应具体的物理块设备。它从上层中取出 IO 请求,并根据该 IO 请求中指定的信息,通过向具体块设备的设备控制器发送命令的方式,来操纵设备传输数据。
  • 设备层中都是具体的物理设备。定义了操作具体设备的规范。

内核函数sys_read是read系统调用在该层的入口点。

开始您的试用

从下图,我们可以清除的看到:

IO 调度层的处理

对 make_request_fn 函数的调用可以认为是 IO 调度层的入口,该函数用于向请求队列中添加请求。该函数是在创建请求队列时指定的,代码如下(blk_init_queue 函数中):

q->request_fn   = rfn;
blk_queue_make_request(q, __make_request);

函数 blk_queue_make_request 将函数 __make_request 的地址赋予了请求队列 q 的 make_request_fn 成员,那么, __make_request 函数才是 IO 调度层的真实入口。

__make_request 函数的主要工作为:

  1. 检测请求队列是否为空,若是,延缓驱动程序处理当前请求(其目的是想积累更多的请求,这样就有机会对相邻的请求进行合并,从而提高处理的性能),并跳到3,否则跳到2
  2. 试图将当前请求同请求队列中现有的请求合并,如果合并成功,则函数返回,否则跳到3
  3. 该请求是一个新请求,创建新的请求描述符,并初始化相应的域,并将该请求描述符加入到请求队列中,函数返回

将请求放入到请求队列中后,何时被处理就由 IO 调度器的调度算法决定了(有关 IO 调度器的算法内容请参见参考资料)。一旦该请求能够被处理,便调用请求队列中成员 request_fn 所指向的函数处理。这个成员的初始化也是在创建请求队列时设置的:

q->request_fn   = rfn;
blk_queue_make_request(q, __make_request);

第一行是将请求处理函数 rfn 指针赋给了请求队列的 request_fn 成员。而 rfn 则是在创建请求队列时通过参数传入的。

对请求处理函数 request_fn 的调用意味着 IO 调度层的处理结束了。

对设驱开发人员或与此相关的设计人员,特别是IO很密集,我们就需要搞清楚IO具体是如何动作的,免得滥用IO和导致设计问题。(http://blog.yufeng.info/archives/751)

Read 系统调用在核心空间中处理的层次模型

图1显示了 read 系统调用在核心空间中所要经历的层次模型。从图中看出:对于磁盘的一次读请求,首先经过虚拟文件系统层(vfs layer),其次是具体的文件系统层(例如 ext2),接下来是 cache 层(page cache 层)、通用块层(generic block layer)、IO 调度层(I/O scheduler layer)、块设备驱动层(block device driver layer),最后是物理块设备层(block device layer)

Read 系统调用在核心空间中的处理过程

0x80 中断处理程序接管执行后,先检察其系统调用号,然后根据系统调用号查找系统调用表,并从系统调用表中得到处理 read 系统调用的内核函数 sys_read ,最后传递参数并运行 sys_read 函数。至此,内核真正开始处理 read 系统调用(sys_read 是 read 系统调用的内核入口)。

在讲解 read 系统调用在核心空间中的处理部分中,首先介绍了内核处理磁盘请求的层次模型,然后再按该层次模型从上到下的顺序依次介绍磁盘读请求在各层的处理过程。

Readpage 函数的由来

address_space 对象是嵌入在 inode 对象之中的,那么不难想象: address_space 对象成员 a_ops 的初始化工作将会在初始化 inode 对象时进行。如清单3中后半部所显示。

if (test_opt(inode->i_sb, NOBH))
 inode->i_mapping->a_ops = &ext2_nobh_aops;
else
    inode->i_mapping->a_ops = &ext2_aops;

可以知道 address_space 对象的成员 a_ops 指向变量 ext2_aops 或者变量 ext2_nobh_aops 。这两个变量的初始化如清单5所示。

清单5 变量 ext2_aops 和变量 ext2_nobh_aops 的初始化
struct address_space_operations ext2_aops = {
    .readpage          =ext2_readpage,
    .readpages         = ext2_readpages,
    .writepage         = ext2_writepage,
    .sync_page         = block_sync_page,
    .prepare_write     = ext2_prepare_write,
    .commit_write       = generic_commit_write,
    .bmap                 = ext2_bmap,
    .direct_IO           = ext2_direct_IO,
    .writepages          = ext2_writepages,
};

struct address_space_operations ext2_nobh_aops = {
    .readpage= ext2_readpage,
    .readpages           = ext2_readpages,
    .writepage           = ext2_writepage,
    .sync_page           = block_sync_page,
    .prepare_write      = ext2_nobh_prepare_write,
    .commit_write       = nobh_commit_write,
    .bmap                 = ext2_bmap,
    .direct_IO           = ext2_direct_IO,
    .writepages          = ext2_writepages,
};

从上述代码中可以看出,不论是哪个变量,其中的 readpage 成员都指向函数 ext2_readpage 。所以可以断定:函数 do_generic_mapping_read 最终调用 ext2_readpage 函数处理读数据请求。

到此为止, ext2 文件系统层的工作结束。

图3 file 对象和当前进程描述符之间的关系

图片 7

到此为止,所有的准备工作已经全部结束了,下面开始介绍 read 系统调用在图1所示的各个层次中的处理过程。

大部分程序员可能会有这样的疑问:当在程序中调用库函数 read 时,这个请求是经过哪些处理最终到达磁盘的呢,数据又是怎么被拷贝到用户缓存区的呢?本文介绍了从 read 系统调用发出到结束处理的全过程。该过程包括两个部分:用户空间的处理、核心空间的处理。用户空间处理部分是系统调用从用户态切到核心态的过程。核心空间处理部分则是 read 系统调用在 linux 内核中处理的整个过程。

本文由美洲杯赌球发布于计算机教程,转载请注明出处:Linux IO子系统和文件系统读写流程

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。