磁盘,为系统提供了最基本的持久化存储;
在格式化硬盘时,硬盘被分成三个区域:
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