Skip to content

Latest commit

 

History

History
executable file
·
138 lines (71 loc) · 13 KB

专栏:块设备文件与文件系统的关系.md

File metadata and controls

executable file
·
138 lines (71 loc) · 13 KB

专栏:块设备文件与文件系统的关系

块设备文件与文件系统之间的关系是什么

               

Linux操作系统秉承“一切皆文件”的设计思想,将所有的设备页看作文件来进行处理。目前的Linux版本中,当内核发现一个块设备时,用户空间会利用udev这一数据结构以及相关的操作来为块设备创建其所需的块设备文件。

关于块设备文件,可以从两方面来进行理解。从块设备文件的外部表现来看,它是属于某个外部文件系统上的一个文件。通常Linux内核将其存放在/dev目录下,用户像对常规文件一样来对其进行访问。从块设备文件的内部实现来看,它可以看作是一种特殊文件系统的所属文件,同时该块设备文件的文件逻辑编号与块设备逻辑编号一一对应。

那如何对常规文件与块设备文件进行区分?当类似于/dev目录下的挂载在宿主系统中的块设备文件,主要通过文件所对应的inode结构中i_mode字段来进行判别,另外在inode结构中i_zone[0]字段中存储了所对应的块设备的编号。

而为了对块设备文件进行便捷的组织与管理,Linux内核创建了bdev文件系统,该文件系统的目的是为了建立块设备文件在外部表现与内部实现之间的关联性。bdev文件系统是一个“伪”文件系统,它只被内核使用,而无需挂载到全局的文件系统树上。

块设备文件除了与常规文件类似的在根文件系统上存在inode之外,其在bdev文件系统上也存在对应的inode。两个inode之间通过块设备编号相关联,需要注意的是,前者的inode称之为次inode,而后者称之为主inode。

Linux中,存在对设备抽象之后的目录,即/dev,也就是前边所描述的宿主文件系统下的块设备文件。该目录由指向系统中硬件的特殊文件组成。所以程序员可以便捷的对硬件进行访问,而不需要使用一些特殊的接口函数。


这些文件其所对应的inode所具有的特征有以下几点:

1. 文件模式为块设备文件

2. 文件内容为块设备编号,保存在inode当中

3. 文件长度为0


虽然bdev文件系统是一个“伪”文件系统,不会挂载在全局文件系统树中,且只存在于内存中。但是Linux内核仍然根据文件系统的数据结构来对其进行创建。

对于每个块设备,在bdev文件系统中都有一个indoe,同时磁盘和分区也会有属于自己的inode。Linux内核利用blokc_inode数据结构表示块设备的inode,其中包含了两个字段,分别是struct block_device,即块设备描述符。另一个是struct inode,即inode描述符。

但是Linux系统为了能够对整体的inode进行统一的管理,因此在宿主系统中创建了与bdev文件系统中相对应的inode。如下图所示,展示了主inode与次inode之间的关系。

由上述的结构图中,可以知道,当对某个文件进行相关的文件操作时,可以该文件的file结构体中获取该文件所属的文件系统类型,并根据文件路径找到对应的dentry以及inode等,随后利用所对应的具体的数据结构中的相关操作来进行实际的文件操作。这是宿主文件系统中的通用文件操作的形式,而对于块设备文件,其间接的通过宿主文件系统获取到bdev文件系统中实际的block_inode,从而进行后续的文件操作。

比如对于所谓的宿主文件系统中打开文件的操作,利用Linux中通用的do _ sys _ open即可,而对块设备文件,则根据宿主文件系统与bdev文件系统之间的关系,最后由do _ sys _ open定位到blkdev_open()函数。

针对该函数进行分析,可以知道当打开某一块设备文件时,该函数所需的参数为该文件的inode以及file两种结构体。该函数首先利用file结构体对文件进行相关的判断与设置,随后利用inode节点获取到所对应的block_device结构体,同时利用该结构体将实际的inode中的地址空间映射复制到file结构体中。从上边的代码中可以看到block _ device的获取是通过bd _ acquire函数来完成的。


需要注意的是,这两个函数中的inode参数均为次inode。经过上述的分析,可以知道,块设备文件虽然与常规文件不同,但Linux仍采用了文件系统的形式来进行管理。Linux系统默认的将块设备作为文件来处理,因此在全局文件系统树进行挂载,并创建块设备文件系统中的dentry,inode等数据结构。而又为了将块设备文件与常规文件进行区分,Linux系统又创建了只位于内存中bdev文件系统,该文件系统中创建了块设备具体的super_block、dentry和inode,block_inode等数据结构。而前者的文件系统与后者的文件系统的关联主要依赖于前者的inode与后者的block_inode。这样一来,便可以形成一个统一的文件系统管理形式。

所以文件系统的编程模式基本上如下所述:

  • 定义超级块结构
  • 定义inode结构,需要注意的是,针对每一个具体的文件系统的inode,其中必须内嵌一个VFS inode结构
  • 实现各种类型文件的inode操作表以及毁掉方法
  • 实现各种类型文件的file操作表
  • 实现各种类型文件的address space操作表
  • 实现超级块操作表
  • 实现dentry操作表
  • 模块加载和卸载方法

以上便是块设备文件与文件系统之间的关系,以及构建文件系统的基本模式。

              

块I/O子系统所需的关键数据结构

由Linux文件系统的结构可知,文件系统最后是通过块I/O子系统来完成对物理磁盘的读写过程。

块I/O子系统主要负责对Linux系统中的块设备I/O请求进行处理,并利用相对应的块设备驱动来进行后续的处理。 Linux针对块I/O子系统,创建了相关的数据结构。分别有:gendisk,hd _ struct,block _ device,request _ queue,request,bio等。

Linux内核将磁盘类设备中为磁盘通用的信息提取出来,表示为通用磁盘描述符gendisk,磁盘类设备驱动负责分配、初始化通用磁盘描述符,将它添加到系统中。一个磁盘可以包含多个分区,每个分区都有相对于磁盘的分区编号。因此struct gendisk结构体中包含了针对分区进行组织的的字段struct disk _ part _ tbl,该字段指向了磁盘的分区表描述符的指针。利用该分区表指针可以找到具体的分区。

而Linux针对磁盘分区,使用了struct hd _ struct结构体来进行描述。在该结构体中,主要包含了两个关键的字段,一个是该分区在物理磁盘上的起始编号,即sector _ t start _ sect;另一个是该分区所占物理磁盘的长度(即扇区数),即sector _ t  nr _ sects。

实际上,上述的gendisk以及hd _ struct数据结构都是对某一个块设备的描述,gendisk是对块设备整体地一个描述,而hd _ struct是对块设备中每一个分区的描述,而分区可以看作一个单独的块设备,因此Linux系统创建了block _ device数据结构来对对该块设备的物理磁盘分区进行最终的描述。另外,需要注意的是该结构体中还包含了块设备所对应的inode、super _ block等数据结构信息。由此可见,block _ device数据结构在Linux中对块设备的组织起着重要的作用。如下图所示,展示了三者之间的关系。

Linux在注册块设备信息时,利用gendisk数据结构来完成的。首先是获取gendisk初始化的数据结构。

由上边的代码可以看到Linux系统为快磁盘赋值了块设备编号,以及inode_id,并为磁盘进行了分区初始化的操作。

当分配完成后,需要做的便是将该磁盘添加到系统中并进行注册。在Linux系统中,根目录下有/dev目录,其中存放的是块设备文件信息,无法直接读取其中的数据信息以及自身与硬件信息,需要将其挂载才能读取数据信息。为了能够读取块设备自身的相关硬件信息,Linux系统组织了/sysfs目录,其中根据各种设备的不同进行区分,从而保存了各种设备的相关硬件信息。

因此,块设备的注册,主要完成的任务便是在/sysfs目录中添加块设备的相关硬件信息。

这段代码中,首先利用gendisk结构体创建其所对应的device数据结构。随后利用device数据结构在/sysfs目录中创建其所对应的相关信息文件。同时还对该块设备的描述符进行获取。其中,需要注意的是,struct kobject数据结构,该数据结构是Linux系统为了便于管理设备而创建的驱动模型设备中的一种数据结构,在改代码中,可以看见其具有一定的作用。最后,这段代码的主要作用是完成如图所示的一个关联与注册。

以上便是块I/O子系统中所需要的关键的数据结构。根据这三个数据结构,Linux系统设计了 request _ queue、request、bio等数据结构,从而对 块设备的I/O请求进行相关的处理。

下图展示了块设备I/O请求所需要的数据结构之间的关联。

在块I/O子系统中,request_queue的直观理解是请求队列,但实际上它的精确含义要稍微解释一下。首先它根据上层的请求分为了两种队列,分别是派发队列与I/O调度队列。派发队列被组织成链表的形式,这是需要向块设备驱动提交的待处理请求链表,链表表头为queue_head域。而I/O调度队列则蕴涵在I/O调度器描述符(即elevator域)中,具体组织形式和调度方式取决于I/O调度算法的实现。

request代表着块设备驱动层请求,来自通用块层的请求,被提交到块设备驱动时,需要构造对应的块设备驱动层请求插入到块设备的请求队列中。块设备驱动层请求基于通用块层请求构造,但是和通用块层请求并非是一一对应的关系。

bio结构代表通用块层请求,是来自上层的请求。每个bio表示不同的访问上下文,源于不同应用,或者法子不同的线程,这也是不能直接修改bio来实现请求合并的原因。

另外,gendisk结构体中包含有指向request_queue队列首地址的指针字段。这样一来,块设备与I/O请求便整体关联了起来。即:

  • grndisk->queue = request_queue;

综上所述,可以知道,request_queue代表着块设备的请求队列,而reuqest代表着通用块层的请求,但是因为每种设备具有自己的驱动程序,因此利用bio数据结构来二次的构造实际的块设备驱动层请求。

文件系统层向块I/O层提交I/O请求时,主要利用的是submit_bio函数。

当块I/O子系统接收到上层的request请求后,块I/O子系统将开始对接收到的上层请求进行构造、排序或合并等操作。由上边的分析可以知道,每个request都与一个或多个bio向关联,因此下图展示了组织request时,bio的变化。

另外,Linux系统利用bio数据结构来对上述已组织的request描述符进行填充,及初始化。同时将该request添加到I/O调度器队列当中。其执行函数的实体为init_request_from_bio。

以上,便是对块I/O子系统的简要分析。