虚拟文件系统

在Linux中,一切对象皆文件,为了用户层面能够以比较一致的接口访问文件,提供了VFS,VFS简单来说就是一个转换功能,将上层命令,根据具体对象转发到对应的驱动去。

虚拟文件系统是典型的面向对象方法设计出来的一个东西,包括四个重要类型:

superblock
用于表示一个挂载的文件系统,相当于文件系统总信息
inode
表示一个真正的文件
dentry
表示一个目录项,请不要和文件系统中的目录混淆,它只不过是路径中的一个节点而已
file
表示进程打开的文件,很显然多个进程可以打开同一个文件,所以可以有很多个file指向同一个inode

由于C语言没有将方法和成员绑定的功能,所以对应的有四个函数指针集: super_operations、inode_operations、dentry_operations和file_operations。比方用户比较常用的read/write调用,最终都会进入到file_operations提供的read/write。

对于虚拟文件系统层来说,我个人认为它提供了两个核心功能,一个是转发功能,其实就是面向对象概念中的多态。一个是缓存功能,缓存功能极大的提高了对文件的查询速度。缓存的核心数据结构就是dentry,查询文件就是通过dentry来找到具体的文件。

dentry有三个状态:

used
此时dentry关联到一个inode,并且inode正被人访问
unused
此时dentry关联到一个inode,但是inode没人访问,所以dentry有被回收的可能
negative
此时dentry没有关联到inode,可能的原因是inode被删除,路径名不正确等等。要注意的是,negative目录项很有存在价值,因为当你试图多次去打开一个不存在的文件时,系统能快速的确定文件不存在。

为了能够极大的提高文件搜索速度,内核做了一个dentry cache,简称dcache。 dcache核心包括如下三个部分:

  1. 一个used dentry链表
  2. 一个LRU dentry链表,该链表包括unused和negative dentry
  3. 一个hash table和hash function用于将路径映射到dentry

哈希表就是dentry_hashtable,每一个元素其实还是一个链表,该链表保存了具有相同hash值的目录项。VFS提供了一个通用hash算法,具体的文件系统也可以自己设计一套更优秀的hash算法。函数d_lookup()就是利用哈希算法来查找目录项。

除了上面提到的几个数据结构,还有几个数据结构,这里一并介绍一下:

file_system_type
这是一个用于管理内核中所有文件系统的数据结构,每个文件系统的设计者都需要注册一个这样的数据结构
files_struct
这个数据结构保存一个进程打开的所有文件和文件描述符信息
fs_struct
保存进程当前目录和根目录

块设备层

文件系统底层实际上是调用块设备层提供的接口,块设备就是由固定大小的块组成的数据,一般都是512B,称之为扇区。在概念上,一个块可以有多个扇区,扇区的大小固定为512B。

在Linux中用buffer_head来描述一个buffer,该数据结构的目的是映射磁盘上的块到内存中。在Linux早期,该数据结构还有一个功能,就是IO传输单元,当然这样存在两个问题,致使后来内核做了较大的修改。

  1. 没有page好用,描述小于一个page的buffer是效率很低。
  2. 数据结构过大,只能描述一个buffer,要用多buffer同时传输,就需要多个buffer_head,很耗内存。

内核中用来作为IO传输的容器是bio,包括要传输的数据段,各buffer之间不需要在内存中连续。比较高明的地方是,内核允许buffers由多个块组成,也就是即便一个块中包含多个位置的数据,也能够进行IO操作,有一个术语叫scatter-gather IO。 bio存在的目的就是表述正在传输的IO操作,它本身是一个容器,每一个传输单元用bio_vec表示。

struct bio_vec {
        struct page *bv_page;
        unsigned int bv_len;
        unsigned int bv_offset;
};

也就是说每一个IO请求由一个bio表示,每个IO请求包含一个或多个传输块,每个传输块由bio_vec表示。

对块设备的操作请求都被放到一个请求队列当中,即request_queue,实际是一个request双向链表。例如文件系统将用户请求做成一个请求加入到请求队列中去,只有请求队列不空,块设备驱动就会从请求队列抓取请求进行处理。在数据上存在如下关系:一个request可以包含多个bio,因为一个请求操作多个连续设备块,但是设备块连续并不意味着内存块也会连续。一个bio可以描述多个段。

块设备事件

在内核空间对磁盘进行检测是在linux-2.6.38的时候才引入,主要是增加一个工作队列对所有磁盘进行轮询,如果磁盘状态发生改变就会通知用户。

disk_event

struct disk_events {
    struct list_head    node;
    struct gendisk      *disk;
    spinlock_t          lock;
    struct mutex        block_mutex;
    int                 block;
    unsigned int        pending;
    unsigned int        clearing;

    long                poll_msecs;
    struct delayed_work dwork;
};
node
每个磁盘的disk_events会被添加到一个全局的链表中,这个链表的名字就叫disk_events,和这个数据结构的名字相同,定义也在同一个文件。 node用于将disk_events结构体插入到链表disk_events中。受disk_events_mutex保护。
disk
代表一个分区
lock
保护工作队列、clearing、pending、dwork(queue)等。
block_mutex
用于防止在锁定的时候多次cancel轮询工作,因为cancel的时候要休眠,所以不能使用lock同步。插入工作的时候不会休眠,所以用的lock来同步。
block
用于指示锁定深度,只要block大于0就不会启动轮询工作
pending
已经发出去的事件
clearing
正在清理的事件
dwork
即disk_events_workfn

分配和添加磁盘事件都是在add_disk()这个重量级函数中调用的,都是用于初始化,只不过分配的时间比较早,此时初始化工作还没完成,不能立即开启轮询。所以将其分成两步来实现:

  • 分配磁盘事件
    • 分配存储,设置block深度为1,也就是说初始状态是不会轮询的
    • 设置poll_msecs为-1
  • 添加磁盘事件
    • 创建sysfs文件
    • 添加事件到全局链表
    • 解锁事件,此时开始轮询

最后的释放过程类似:

  • 删除磁盘事件
    • 锁定事件
    • 从全局链表中删除当前磁盘的事件
    • 删除sysfs文件
  • 释放磁盘事件
    • 释放存储空间

影响轮询间隔的有两个参数,一个是poll_msecs,这个是在每个磁盘内部拥有的,如果这个值小于0,就视为无效,此时使用全局的disk_events_dfl_poll_msecs。有一个函数disk_events_poll_jiffies()用于获取轮询间隔。

在每个磁盘的sysfs文件系统目录下面都有一个叫events_poll_msecs的文件,该文件用与显示和设定间隔。除了这个文件,还有一个全局间隔设定文件,位于如下路径:

/sys/module/block/parameters/events_dfl_poll_msecs

我们一般不会去单独设定每个磁盘的轮询间隔,而是统一设定所有磁盘的轮询间隔。

  • 锁定磁盘事件
    • 增加锁定深度,如果原来是开启的就关闭
  • 解锁磁盘事件
    • 减少锁定深度,减少到0的时候才真正开启轮询

轮询工作有两种调用方式,一种是以函数方式执行,一种是以工作队列方式执行。

  • 检查磁盘事件
    • 调用fops->check_events(),获得events。在block_device_operations中有两个和磁盘检测有关的函数指针,其中的media_changed()已经被标识为待废除,将用check_events()替代。
    • 将获得的events去掉pending部分,并将新的events加入到pending
    • 如果未锁定,再度调度dwork
    • 如果接收到磁盘事件就填充环境变量,并发出uevent
  • 刷新磁盘事件刷新通过传递一个mask给clearing来实现,所以clearing就存储了要清除的事件。检查事件的时候要对clearing进行处理。如果当前在锁定状态,就仅将要刷新的值加入到clearing而已。
  • 清除磁盘事件
    • 由参数提供一个mask,指定要清除的事件。
    • 阻塞事件,确保不会因为并发操作造成混乱
    • 提取clearing,调用检测函数,传递clearing给检测函数。提取的clearing是event->clearing和mask的并集,提取之后event->clearing将被清除,注意这里是调用函数而不是启动工作队列,因为这里必须要顺序进行。
    • 解锁磁盘事件
    • 返回pending事件。 pending是event->pending和mask的交集,提取之后event->pending将清除mask部分。