「Linux」- 文件系统(学习笔记)

磁盘,为系统提供了最基本的持久化存储;

在格式化硬盘时,硬盘被分成三个区域:
1)superblock(超级块):
1)inode table(索引节点区):存放所有 inode;
2)数据区:存放文件数据;

文件系统,则在磁盘的基础上,提供了一个用来管理文件的树状结构。

super_block

简单说,VFS 负责提供架构,而具体文件系统必须按照 VFS 的架构去实现 —— super_block(超级块)是对该观点的体现。

解释说明

VFS super_block,对应着具体文件系统的控制块结构(超级块),就是说每个具体的文件系统都要实现超级块。VFS super_block 通过读取具体文件系统的控制块接口,来填充其内容,即 VFS super_block 是具体文件系统的控制块的内存抽象(内存表示)。

VFS super_block,用以记录当前文件系统整体的状态。例如:inode 和逻辑块的使用情况;super_block 操作函数;当前文件系统的特有信息;

struct super_block {}

1)该结构包含重要当前文件系统的全局信息:

struct super_block {
        unsigned long             s_blocksize;                  // 文件系统块大小
        unsigned char             s_blocksize_bits;
        ...                                                     // 省略超级块的链表、设备号等代码
        unsigned long long        s_maxbytes;                   // 最大文件的大小

        struct file_system_type   *s_type;                      // 指向 struct file_system_type 指针
        struct super_operations   *s_op;                                   

        unsigned long             s_magic;                      // 每个文件系统都由的魔数数字
        struct dentry             *s_root;                      // 指向文件系统的 root dentry 指针

        struct list_head          s_inodes;                     // 链表头,指向文件系统的所有 inode 对象,用于遍历所有 inode 对象
        struct list_head          s_dirty;                      // 指向所有 dirty 的 inode 对象

        struct block_device       *s_bdev;                      // 指针,指向文件存在的块设备
        void                      *s_fs_info;                   // Filesystem private info
};

2)在 struct super_block 中,成员 struct super_operations *s_op 包含很多重要的函数指针。例如,成员函数 read_inode 提供读取 inode 信息的功能,每个文件系统都要提供该函数的实现。在 ext2 中,提供 ext2_read_inode 函数。

作用分析

每个文件系统都具有 super_block 结构,每个 super_block 都要链接到一个 super_blocks 双向链表。
文件系统的每个文件在打开时,都需要分配一个 inode 结构,这些 inode 结构又都要链接到超级块。

super_block 与 inode 关系如下:

dentry, directory entry

在计算机中,文件、目录以树形结构保存,所以应该有某种数据结构的能够体现这种树形结构 —— dentry(目录项)体现出文件系统的这种结构。

解释说明

每个目录(或文件)都是个 VFS dentry 结构,多个 dentry 结构层层关联(父子相连),就构成文件系统的树形目录结构。

为了加快对 VFS dentry 查找,内核使用 hash 来缓存 dentry,称为 dentry cache;当进行 dentry 查找时,将先在 dentry cache 中进行。

补充说明:在 VFS 中,文件也是目录(目录也是文件),即文件与目录都为 dentry 结构。

struct dentry {}

1)该结构用来记录 文件的名字、索引节点指针、与其他目录项的关联关系 等等信息:

struct dentry {
    ...                                                                         // 省略 dentry 锁、标志等代码
    
    struct inode *d_inode;                                                      // 指向一个 inode 结构
                                                                                // 该 inode 和 dentry 共同描述单个普通文件(或目录);
                                                                                // Linux 文件系统为每个文件(或目录)都分配这两个数据结构;
                                                                                // Where the name belongs to - NULL is negative
    /*
     * The next three fields are touched by __d_lookup.  Place them here
     * so they all fit in a cache line.
     */
    struct hlist_node d_hash;                                                   // 哈希链表,链接到 dentry cache
    struct dentry *d_parent;                                                    // 指针,指向父目录 dentry 结构
    struct qstr d_name;                                                         // 我们所数值的目录名(或文件名)

    /*
     * d_child and d_rcu can share memory
     */
    union {
            struct list_head d_child;                                           // 是 dentry 自身的链表头,需要链接到父 dentry 的 d_subdir 成员;
                                                                                // 当移动目录时,需要链接到新父 dentry 的 d_subdir 成员;
            struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs;                                                 // 子项(文件/目录)的链表头,所有子项都要链接到该链表

    struct dentry_operations *d_op;
    struct super_block *d_sb;                                                   /* The root of the dentry tree */
    int d_mounted;                                                              // 如果非零,表示该 dentry 为挂载点
};

与 inode 不同,dentry 是由内核维护的内存数据结构,所以通常也被叫做目录项缓存。

作用分析

例如,加入目录结构:

/ usr / wj
/ usr / nk 
/ home / mnt / cj

mnt 为挂载点,其下包含 cj 文件

则 dentry 关系如下:

每个 dentry 都链接到 父 dentry,以形成文件系统的树形结构:
1)<usr>.dentry.d_child 与 <home>.dentry.d_child 都指向 </>.dentry.d_subdirs;
2)<wj>.dentry.d_child 与 <nk>.dentry.d_child 都指向 <usr>.dentry.d_sudirs;

所有 dentry 都链接到 dentry_hashtable 数组(结构为 hash 链表):
1)这里的 dentry 是指内存中的 dentry 结构(文件被打开,那内存中将包含对应的 dentry 结构);
2)该 dentry 被链接到 dentry_hashtable 的某个 hash 链表头,后续直接从 hash 链表里查找,避免再次读盘(dentry cache)

通过 dentry.d_mounted 字段,暗示目录是否为挂载点:
1)被挂载的文件系统也是个 dentry 树,但并没有直接链接到当前 dentry 树;
2)每个文件系统都具有 vfsmount 结构,当其被挂载时,vfsmount 被链接到内核全局的 mount_hashtable 链表数组。
—- mount_hashtable 是个数组,每个成员都是 hash 链表;
—- 当发现目录是挂载点时,从 mount_hashtable 数组找到 hash 链表头,在对其进行遍历,找到文件所在文件系统的 vfsmount,然后目录的 dentry 被替换为新文件系统的根目录。

inode, index node

VFS dentry 仅表达出树形结构,并为包含目录(或文件)的元数据,例如大小、创建时间、访问权限等等 —— 这些信息包含在 inode(索引节点)结构中。

解释说明

VFS inode,记录文件的元信息,比如 文件大小、访问权限、修改日期、数据的位置、索引编号、对文件的读写函数等等。
inode 与文件对应,它跟文件内容一样,都会被持久化存储到磁盘中,同样占用磁盘空间。
真实的文件会具有多个 VFS dentry,因为指向文件的路径会有多个(例如我们熟知的 hard link(硬链接))。

inode 与 dentry 的关系:
1)inode 才是每个文件的唯一标志,而 dentry 负责维护文件系统的树状结构;
2)dentry 和 inode 的关系是多对一:即单个文件可存在于多个目录中(我们熟知的硬链接,就是同个文件存在于不同目录中)。

struct inode {}

struct inode {
	struct list_head        i_list;                     // 链接到 描述当前 inode 状态的 链表;
	                                                    // 每当创建 inode 时,i_list 都要链接到 inode_in_use 链表,表示该 inode 正在使用;
	struct list_head        i_sb_list;                  // 链接到 super_block 的 inode 链表(s_inodes);
	                                                    // 每当创建 inode 时,i_sb_list 也要链接到 s_inodes 链表;
	struct list_head        i_dentry;                   // 文件能够被多个 dentry 引用,这些 dentry 都要链接该链表头;
	
	unsigned long           i_ino;                      // 索引编号,inode 的号码;
	atomic_t                i_count;                    // inode 的引用计数,即被引用多少次;
	loff_t                  i_size;                     // 以字节为单位的文件长度;
	unsigned int            i_blkbits;                  // 文件块的位数;
	
	struct inode_operations             *i_op;
	
	const struct file_operations        *i_fop;         // 文件的读写函数、异步 IO 函数,都包含在该结构中
	                                                    // 每个具体的文件系统,都要提供各自的文件操作函数
	                                                    // former ->i_op->default_file_ops
	
	struct address_space                *i_mapping;     // 该结构用于缓存文件内容
	                                                    // 对文件的读写操作,首先要在 i_mapping 中进行
	                                                    // 然后等待合适的机会,再从缓存写入硬盘;
	
	struct block_device                 *i_bdev;        // 指向块设备的指针,即文件所在的文件系统所绑定的块设备;
    ...                                                                         // 省略锁等代码
};

作用分析

内核提供一个 hash 链表数组,inode_hasttable,所有 inode 都要链接到其中的某个 hash 链表。与 dentry_hasttable 类似。

Linux 的 字符设备、块设备、普通文件、目录 等等都是文件,都具备 inode 结构,inode.i_mode 指示不同的类型:
1)i_mode=S_IFBLK,块设备
2)i_mode=S_IFCHR,字符设备
3)i_mode=S_IFDIR,目录
4)i_mode=S_IFIFO,FIFO

inode.i_mapping 用于缓存文件的数据内容,其使用 redix 结构来保存文件数据。

file & file object

在文件系统中,并不存在“文件”这样的数据结构 —— 文件是我们对数据集的表述,属于抽象概念。

解释说明

file,是指硬盘中的文件;
file object,代表进程文件交互的关系。当进程想打开文件时,内核就会在内存中创建与 file 对应的 file object;

在不同进程中,同个文件具有不同的文件对象。
内核会给每个打开的 file 创建与之对应的 file object 实例,同时返回文件号码;

struct file {}

struct file {
        struct dentry                       *f_dentry;           // 指向文件对应的 dentry 结构
        struct vfsmount                     *f_vfsmnt;           // 指向文件所属文件系统的 vfsmount 对象;
        const struct file_operations        *f_op;               
        
        ...                                                      // 省略部分代码
        
        loff_t                              f_pos;               // 进程对文件操作的位置
                                                                 // 例如,以读取前 10 字节,f_pos=11;
        struct fown_struct                  f_owner;
        unsigned int                        f_uid, f_gid;        // 文件的 User ID、Group ID
        
        struct file_ra_state                f_ra;                // 文件预读的设置。

        struct address_space                *f_mapping;          // 该结构封装文件的读写缓存页面
};

作用分析

每个进程都指向各自的文件描述符表 —— 该表(数组)包含进程打开的每个文件。

当进程打开文件时,内核会创建与之对应的问 file object,并将其指针包含到文件描述符表中。

在读写文件前,必须先打开文件 —— “打开”便是 file object 创建的过程。当创建 file object 中,才能对文件进行读写。

文件系统分析(代码角度)

对于用户来说,对文件系统的主要操作是 创建目录、创建文件、文件读写(打开文件)。

这些过程涉及 硬盘空间管理、硬盘读写,而读写要经过 文件系统 ⇒ 通用块设备层 ⇒ 硬盘驱动,过于复杂。

这是个简单的例子,用于理解文件系统的基本概念;

mount(挂载)

文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。我们平时使用 ls 命令看到的文件系统,实际上是 VFS,而不是 xfs、ext4 等等文件系统。

观测文件系统的性能

查看文件系统容量

# df /dev/sda1

# df -h /dev/sda1 // 加上 -h 选项,以获得更好的可读性

# df -i /dev/sda1  // 查看索引节点的使用情况

// 如果 inode 全部使用,则会出现“df 显示剩余空间充足,但是空间不足”的问题

// 索引节点的容量,(也就是 Inode 个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。

观察目录项和索引节点缓存

核使用 Slab 机制,管理目录项和索引节点的缓存。/proc/meminfo 只给出了 Slab 的整体大小,具体到每一种 Slab 缓存,还要查看 /proc/slabinfo 这个文件。

# cat /proc/slabinfo | grep -E '^#|dentry|inode'

// dentry 行表示目录项缓存
// inode_cache 行,表示 VFS 索引节点缓存
// 其余的则是各种文件系统的索引节点缓存

// /proc/slabinfo 的列比较多,具体含义你可以查询  man slabinfo

在实际性能分析中,我们更常使用 slabtop 来找到占用内存最多的缓存类型:

# slabtop
 Active / Total Objects (% used)    : 1149991 / 1422582 (80.8%)
 Active / Total Slabs (% used)      : 37781 / 37781 (100.0%)
 Active / Total Caches (% used)     : 110 / 141 (78.0%)
 Active / Total Size (% used)       : 278331.67K / 357378.34K (77.9%)
 Minimum / Average / Maximum Object : 0.01K / 0.25K / 23.19K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
193011 180385  93%    0.20K   4949       39     39592K vm_area_struct
191373 101342  52%    0.10K   4907       39     19628K buffer_head
146944 133042  90%    0.06K   2296       64      9184K anon_vma_chain
100352  92760  92%    0.03K    784      128      3136K kmalloc-32
 89880  60916  67%    0.19K   4280       21     17120K dentry
 89824  71806  79%    0.57K   3208       28     51328K radix_tree_node
 75532  67981  90%    0.09K   1642       46      6568K anon_vma
 61184  50791  83%    0.25K   1912       32     15296K filp

// 从这个结果可以看到,在系统中 radix_tree_node 占用最多的 Slab 缓存

实验探究:命令 find / -name file-name 对缓存的影响

使用 slabtop 可以看到缓存的变换:
1)清空缓存:echo 3 > /proc/sys/vm/drop_caches ; sync
2)运行 slabtop -s c 命令
3)执行 find / -name test 命令
4)观察 slabtop 输出

我们可以猜测到 ext4 proc dentry 会升高,如果使用网络文件系统,那么网络文件系统的缓存也会升高。

其他内容

文件数据读写的最小单位(逻辑块)
问题:磁盘读写的最小单位是扇区,只有 512B 大小。每次都读写这么小的单位,效率一定很低。
方案:文件系统又把连续的扇区组成逻辑块,然后每次都以逻辑块为最小单元,来管理数据。常见的逻辑块大小为 4KB(也就是由连续的 8 个扇区组成)。

文件系统格式化时的三个存储区域
存储在磁盘中的持久化数据,因此在格式化时需要处理:
1)超级块,存储整个文件系统的状态。
2)索引节点区,用来存储索引节点。
3)数据块区,则用来存储文件数据。

参考文献

23 | 基础篇:Linux 文件系统是怎么工作的?
32 | 答疑(四):阻塞、非阻塞 I/O 与同步、异步 I/O 的区别和联系
filesystems – What is a Superblock, Inode, Dentry and a File? – Unix & Linux Stack Exchange