Linux系统笔记(七)文件系统

426 阅读13分钟

一、文件系统

文件系统的功能规划

  1. 文件系统要有严格的组织形式,使得文件能够以块为单位进行存储。

  2. 文件系统中也要有索引区,用来方便查找一个文件分成的多个块都存放在了什么位置。

  3. 如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有缓存层。

  4. 文件应该用文件夹的形式组织起来,方便管理和查询。

    在文件系统中,每个文件都有一个名字,这样我们访问一个文件,希望通过它的名字就可以找到。文件名就是一个普通的文本。当然文件名会经常冲突,不同用户取相同的名字的情况还是会经常出现的。要想把很多的文件有序地组织起来,我们就需要把它们成为目录或者文件夹。这样,一个文件夹里可以包含文件夹,也可以包含文件,这样就形成了一种树形结构。而我们可以将不同的用户放在不同的用户目录下,就可以一定程度上避免了命名的冲突问题。

    有了目录结构,定位一个文件的时候,我们还会分绝对路径(Absolute Path)和相对路径(Relative Path)。所谓绝对路径,就是从根目录开始一直到当前的文件,例如“/ 根目录/ 用户 A 目录 / 目录 1/ 文件 2”就是一个绝对路径。而通过 cd 命令可以改变当前路径,例如“cd / 根目录 / 用户 A 目录”,就是将用户 A 目录设置为当前目录,而刚才那个文件的相对路径就变成了“./ 目录 1/ 文件 2”。

  5. Linux 内核要在自己的内存里面维护一套数据结构,来保存哪些文件被哪些进程打开和使用。

文件系统相关命令行

格式化

使用 Windows 的时候,咱们常格式化的格式为NTFS(New Technology FileSystem)。在 Linux 下面,常用的是 ext3 或者 ext4。

格式化后的硬盘,需要挂在到某个目录下面,才能作为普通的文件系统进行访问。

mount /dev/vdc1 / 根目录 / 用户 A 目录 / 目录 1

Linux 里面一切都是文件,那从哪里看出是什么文件呢?要从 ls -l 的结果的第一位标识位看出来。

- 表示普通文件;
d 表示文件夹;
c 表示字符设备文件,这在设备那一节讲解;
b 表示块设备文件,这也在设备那一节讲解;
s 表示套接字 socket 文件,这在网络那一节讲解;
l 表示符号链接,也即软链接,就是通过名字指向另外一个文件,例如下面的代码,instance 这个文件就是指向了 /var/lib/cloud/instances 这个文件。

在文件系统上,需要维护文件的严格的格式,要通过 mkfs.ext4 命令来格式化为严格的格式。
每一个硬盘上保存的文件都要有一个索引,来维护这个文件上的数据块都保存在哪里。文件通过文件夹组织起来,可以方便用户使用。
为了能够更快读取文件,内存里会分配一块空间作为缓存,让一些数据块放在缓存里面。
在内核中,要有一整套的数据结构来表示打开的文件。
在用户态,每个打开的文件都有一个文件描述符,可以通过各种文件相关的系统调用,操作这个文件描述符。

二、硬盘文件系统

inode 与块的存储

硬盘分成相同大小的单元,我们称为块(Block)。一块的大小是扇区大小的整数倍,默认是4K。在格式化的时候,这个值是可以设定的。一大块硬盘被分成了一个个小的块,用来存放文件的数据部分。这样一来,如果我们像存放一个文件,就不用给他分配一块连续的空间了。我们可以分散成一个个小块进行存放。这样就灵活得多,也比较容易添加、删除和插入数据。

但是这也带来一个新的问题,那就是文件的数据存放得太散,找起来就比较困难。有什么办法解决呢?我们是不是可以像图书馆那样,也设立一个索引区域,用来维护“某个文件分成几块、每一块在哪里“等等这些基本信息?文件还有元数据部分,例如名字、权限等,这就需要一个结构inode来存放。

什么是 inode 呢?inode 的“i”是 index 的意思,其实就是“索引”,类似图书馆的索引区域。既然如此,每个文件都会对应一个 inode;一个文件夹就是一个文件,也对应一个 inode。 inode 里面有哪些信息:

struct ext4_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */
	__le32	i_atime;	/* Access time */
	__le32	i_ctime;	/* Inode Change time */
	__le32	i_mtime;	/* Modification time */
	__le32	i_dtime;	/* Deletion Time */
	__le16	i_gid;		/* Low 16 bits of Group Id */
	__le16	i_links_count;	/* Links count */
	__le32	i_blocks_lo;	/* Blocks count */
	__le32	i_flags;	/* File flags */
......
	__le32	i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
	__le32	i_generation;	/* File version (for NFS) */
	__le32	i_file_acl_lo;	/* File ACL */
	__le32	i_size_high;
......
};

从这个数据结构中,我们可以看出,inode 里面有文件的读写权限 i_mode,属于哪个用户i_uid,哪个组 i_gid,大小是多少 i_size_io,占用多少个块 i_blocks_io。咱们讲 ls 命令行的时候,列出来的权限、用户、大小这些信息,就是从这里面取出来的。

另外,这里面还有几个与文件相关的时间。i_atime 是 access time,是最近一次访问文件的时间;i_ctime 是 change time,是最近一次更改 inode 的时间;i_mtime 是 modifytime,是最近一次更改文件的时间。

这里你需要注意区分几个地方。首先,访问了,不代表修改了,也可能只是打开看看,就会改变 access time。其次,修改 inode,有可能修改的是用户和权限,没有修改数据部分,就会改变 change time。只有数据也修改了,才改变 modify time。

刚才说的“某个文件分成几块、每一块在哪里”,这些在 inode 里面,应该保存在i_block 里面。

在 ext2 和 ext3 中,其中前 12 项直接保存了块的位置,也就是说,我们可以通过i_block[0-11],直接得到保存文件内容的块。 但是,如果一个文件比较大,12 块放不下。当我们用到 i_block[12] 的时候,就不能直接放数据块的位置了,要不然 i_block 很快就会用完了。这该怎么办呢?我们需要想个办法。我们可以让 i_block[12] 指向一个块,这个块里面不放数据块,而是放数据块的位置,这个块我们称为间接块。也就是说,我们在 i_block[12] 里面放间接块的位置,通过i_block[12] 找到间接块后,间接块里面放数据块的位置,通过间接块可以找到数据块。

如果文件再大一些,i_block[13] 会指向一个块,我们可以用二次间接块。二次间接块里面存放了间接块的位置,间接块里面存放了数据块的位置,数据块里面存放的是真正的数据。如果文件再大一些,i_block[14] 会指向三次间接块。原理和上面都是一样的,就像一层套一层的俄罗斯套娃,一层一层打开,才能拿到最中心的数据块。

如果你稍微有点经验,现在你应该能够意识到,这里面有一个非常显著的问题,对于大文件来讲,我们要多次读取硬盘才能找到相应的块,这样访问速度就会比较慢。为了解决这个问题,ext4 做了一定的改变。它引入了一个新的概念,叫作Extents

我们来解释一下 Extents。比方说,一个文件大小为 128M,如果使用 4k 大小的块进行存储,需要 32k 个块。如果按照 ext2 或者 ext3 那样散着放,数量太大了。但是 Extents 可以用于存放连续的块,也就是说,我们可以把 128M 放在一个 Extents 里面。这样的话,对大文件的读写性能提高了,文件碎片也减少了。Exents 如何来存储呢?它其实会保存成一棵树。 树有一个个的节点,有叶子节点,也有分支节点。每个节点都有一个头,ext4_extent_header 可以用来描述某个节点。

inode 位图和块位图

到这里,我们知道了,硬盘上肯定有一系列的 inode 和一系列的块排列起来。接下来的问题是,如果我要保存一个数据块,或者要保存一个 inode,我应该放在硬盘上的哪个位置呢?难道需要将所有的 inode 列表和块列表扫描一遍,找个空的地方随便放吗?

当然,这样效率太低了。所以在文件系统里面,我们专门弄了一个块来保存 inode 的位图。在这 4k 里面,每一位对应一个 inode。如果是 1,表示这个 inode 已经被用了;如果是 0,则表示没被用。同样,我们也弄了一个块保存 block 的位图。

文件系统的格式

数据块的位图是放在一个块里面的,共 4k。每位表示一个数据块,共可以表示 4∗1024∗8=2^15 个数据块。如果每个数据块也是按默认的 4K,最大可以表示空间为 2^15∗4∗1024=2^27 个 byte,也就是 128M。

目录的存储格式

其实目录本身也是个文件,也有 inode。inode 里面也是指向一些块。和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。每一项都会保存这个目录的下一级的文件的文件名和对应的 inode,通过这个 inode,就能找到真正的文件。第一项是“.”,表示当前目录,第二项是“...”,表示上一级目录,接下来就是一项一项的文件名和 inode。

软链接和硬链接的存储格式

所谓的链接(Link),我们可以认为是文件的别名,而链接又可分为两种,硬链接与软链接。 如图所示,硬链接与原始文件共用一个 inode 的,但是 inode 是不跨文件系统的,每个文件系统都有自己的 inode 列表,因而硬链接是没有办法跨文件系统的。

而软链接不同,软链接相当于重新创建了一个文件。这个文件也有独立的 inode,只不过打开这个文件看里面内容的时候,内容指向另外的一个文件。这就很灵活了。我们可以跨文件系统,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。

无论是文件夹还是文件,都有一个 inode。inode 里面会指向数据块,对于文件夹的数据块,里面是一个表,是下一层的文件名和 inode 的对应关系,文件的数据块里面存放的才是真正的数据。

三、虚拟文件系统

Linux 可以支持多达数十种不同的文件系统。它们的实现各不相同,因此 Linux 内核向用户空间提供了虚拟文件系统这个统一的接口,来对文件系统进行操作。它提供了常见的文件系统对象模型,例如 inode、directory entry、mount 等,以及操作这些对象的方法,例如 inode operations、directory operations、file operations 等。

然后就是对接的是真正的文件系统,例如我们上节讲的 ext4 文件系统。为了读写 ext4 文件系统,要通过块设备 I/O 层,也即 BIO 层。这是文件系统层和块设备驱动的接口。为了加快块设备的读写效率,我们还有一个缓存层。最下层是块设备驱动程序。

挂载文件系统

想要操作文件系统,第一件事情就是挂载文件系统。内核是不是支持某种类型的文件系统,需要我们进行注册才能知道。例如,咱们上一节解析的 ext4 文件系统,就需要通过 register_filesystem 进行注册,传入的参数是ext4_fs_type,表示注册的是 ext4 类型的文件系统。

对于每一个进程,打开的文件都有一个文件描述符,在 files_struct 里面会有文件描述符数组。每个一个文件描述符是这个数组的下标,里面的内容指向一个 file 结构,表示打开的文件。这个结构里面有这个文件对应的 inode,最重要的是这个文件对应的操作file_operation。如果操作这个文件,就看这个 file_operation 里面的定义了。对于每一个打开的文件,都有一个 dentry 对应,虽然叫作 directory entry,但是不仅仅表示文件夹,也表示文件。它最重要的作用就是指向这个文件对应的 inode。如果说 file 结构是一个文件打开以后才创建的,dentry 是放在一个 dentry cache 里面的,文件关闭了,他依然存在,因而他可以更长期的维护内存中的文件的表示和硬盘上文件的表示之间的关系。inode 结构就表示硬盘上的 inode,包括块设备号等。几乎每一种结构都有自己对应的 operation 结构,里面都是一些方法,因而当后面遇到对于某种结构进行处理的时候,如果不容易找到相应的处理函数,就先找这个 operation 结构,就清楚了。

四、文件缓存

系统调用层和虚拟文件系统层

文件系统的读写,其实就是调用系统函数 read 和 write。根据是否使用内存做缓存,我们可以把文件的 I/O 操作分为两种类型。

  1. 缓存 I/O。大多数文件系统的默认 I/O 操作都是缓存 I/O。对于读操作来讲,操作系统会先检查,内核的缓冲区有没有需要的数据。如果已经缓存了,那就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。对于写操作来讲,操作系统会先将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说,写操作就已经完成。至于什么时候再写到磁盘中由操作系统决定,除非显式地调用了 sync 同步命令。缓存 I/O 读写的流程不一样。对于读,从块设备读取到缓存中,然后从缓存中拷贝到用户态。对于写,从用户态拷贝到缓存,设置缓存页为脏,然后启动一个线程写入块设备
  2. 直接 IO,就是应用程序直接访问磁盘数据,而不经过内核缓冲区,从而减少了在内核缓存和用户程序之间数据复制。直接 I/O 读写的流程是一样的,调用 ext4_direct_IO,再往下就调用块设备层了。