欢迎光临散文网 会员登陆 & 注册

一文带你玩转Linux文件系统之-通用块处理层!

2022-05-12 15:55 作者:补给站Linux内核  | 我要投稿

概述

  • 由于不同块设备(如磁盘,机械硬盘等)有着不同的设备驱动程序,为了让文件系统有统一的读写块设备接口,Linux实现了一个 通用块层。如下图中的红色部分:


  • 通用块层 的引入为了提供一个统一的接口让文件系统实现者使用,而不用关心不同设备驱动程序的差异,这样实现出来的文件系统就能用于任何的块设备。

  • 通用块层 将对不同块设备的操作转换成对逻辑数据块的操作,也就是将不同的块设备都抽象成是一个数据块数组,而文件系统就是对这些数据块进行管理。如下图:

注意:不同的文件系统可能对逻辑数据块定义的大小不一样,比如 ext2文件系统 的逻辑数据块大小为 4KB。


  • 通过对设备进行抽象后,不管是磁盘还是机械硬盘,对于文件系统都可以使用相同的接口对逻辑数据块进行读写操作。

【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)    


通用块读写接口

  • 通用块层 提供了 ll_rw_block() 函数对逻辑块进行读写操作,ll_rw_block() 函数的原型如下:

  • 在分析 ll_rw_block() 函数前,我们先来介绍一下 buffer_head 这个结构,因为要理解 ll_rw_block() 函数必须先了解 buffer_head 结构。

  • struct buffer_head 结构代表一个要进行读或者写的数据块,其定义如下:

  • 为了让读者更加清晰,上面忽略了 buffer_head 结构的某些字段,上面比较重要的是 b_blocknr 字段和 b_size 字段, b_blocknr 字段指定了要读写的数据块号,而 b_size 字段指定了数据块的大小。还有就是 b_rdev 字段,其指定了数据块所属的设备。

  • 有了 buffer_head 结构,就可以对数据块进行读写操作。接下来,我们看看 ll_rw_block() 函数的实现:

  • 下面介绍一下 ll_rw_block() 函数各个参数的作用:

  1. rw:要进行的读或者写操作,一般可选的值为 READ、WRITE 或者 READA 等。

  2. nr:bhs 数组的大小。

  3. bhs:要进行读写操作的数据块数组。

  • ll_rw_block() 函数的实现比较简单,遍历 bhs 数组,并且对所有的 buffer_head 进行上锁和增加其计数器,然后调用 submit_bh() 函数把其提交到 IO调度层 进行I/O操作。

  • 我们接着看看 submit_bh() 函数的实现:

  • 数据块是 通用块层 的概念,而真实的块设备是以扇区作为读写单元的。所以在进行IO操作前,必须将数据块号转换成真正的扇区号,而代码 bh->b_blocknr * count 就是用于将数据块号转换成扇区号。转换成扇区号后,submit_bh() 函数接着调用 generic_make_request() 进行下一步的操作。

  • 我们接着分析 generic_make_request() 函数的实现:

  • 每一个块设备都有一个类型为 request_queue_t 的I/O请求队列,而 blk_get_queue() 函数用于获取块设备对应的I/O请求队列,然后调用I/O请求队列的 mark_request_fn() 方法把I/O请求添加到队列中。

  • 那么I/O请求队列的 mark_request_fn() 方法到底是什么呢?这个方法由块设备驱动提供,也可以通过调用 blk_init_queue() 函数设置为默认的 __make_request() 方法。我们主要分析默认的 __make_request() 方法:

  • __make_request() 函数首先通过调用 elevator->elevator_merge_fn() 方法尝试将当前I/O请求与其他正在排队的I/O请求进行合并,因为如果当前I/O请求与正在排队的I/O请求相邻,那么就可以合并为一个I/O请求,从而减少对设备I/O请求的次数。

  • 如果不能与排队的I/O请求进行合并,那么就调用 get_request() 函数申请一个I/O请求对象,然后初始化此对象各个字段,再通过调用 add_request() 函数把I/O请求对象添加到I/O请求队列中。add_request() 函数实现如下:

  • add_request() 函数的实现非常简单,首先调用 drive_stat_acct() 函数更新统计信息,然后调用 list_add() 函数把I/O请求添加到I/O请求队列中。

执行I/O请求

  • ll_rw_block() 函数只是把I/O请求添加到设备的I/O请求队列中,那么I/O请求队列中的I/O请求什么时候会执行呢?答案就是当调用 run_task_queue(&tq_disk) 函数时。

  • run_task_queue() 函数是 Linux 用于运行任务队列的入口,而 tq_disk 队列就是块设备I/O的任务队列。当执行 run_task_queue(&tq_disk) 函数时,便会处理 tq_disk 任务队列中的例程。

  • 当调用 ll_rw_block() 函数添加I/O请求时,会触发调用 generic_plug_device() 函数,而 generic_plug_device() 函数会把设备的I/O请求队列添加到 tq_disk 任务队列中, generic_plug_device() 函数实现如下:

通过 Linux 的任务队列机制,设备的I/O请求队列将会被执行。执行I/O请求主要是由块设备驱动完成,在块设备驱动程序初始化时可以通过调用 blk_init_queue() 函数指定处理I/O请求队列的方法。blk_init_queue() 函数原型如下:

参数 rfn 就是处理I/O请求队列的例程函数。


一文带你玩转Linux文件系统之-通用块处理层!的评论 (共 条)

分享到微博请遵守国家法律