Linux:文件系统
磁盘
计算机需要存储数据,主要有两种存储数据的硬件,内存和硬盘,其中内存需要通电,具有较高的数据交换效率,适合用于存储计算时产生的临时数据。对于长期保存的数据,一方面来说我们无法保证计算机永远通电,另一方面内存的价格太高了,因此计算机需要硬盘这种无需通电,价格低廉的存储介质来存储数据。
存储在硬盘中的数据,一般称为文件。本博客讲解在硬盘中,数据是如何被组织管理的,也就是文件系统。
硬盘主要有磁盘和SSD,而当代的个人计算机已经不使用磁盘了,而是使用SSD:

上图中,左侧是磁盘,右侧是SSD。那么为什么我们还要讲磁盘,而不是SSD呢?
首先,磁盘的文件系统与SSD是十分相似的,磁盘文件系统的设计考虑了存储介质的通用性,因此可以很好地适用于SSD,不过可能会有针对SSD的特殊优化,但是不会影响文件系统的大框架。
其次,我们个人计算机使用的磁盘叫做消费级磁盘,虽然个人计算机已经不再使用磁盘了,但是由于低廉的价格,服务器主机使用的依然是磁盘,这种磁盘叫做企业级磁盘。
Linux作为一个适用于服务器的操作系统,通过磁盘来理解文件系统,毫无疑问是合适的。
物理结构
先来看看磁盘的物理结构,了解磁盘底层是如何运作的。

对于企业级磁盘来说,一个磁盘会有多个盘面,也就是上图中的样子。
磁盘的盘面很像我们平常见到的光盘,光盘一般是一面图案,一面用于读取。而磁盘的盘面两面都是可以读写的。上图中,磁头就是用来对磁盘进行读写的工具,马达会带着盘面高速旋转,此时磁头再来回摆动,就可以通过磁头访问到整个盘面。由于盘的两面都可读写,因此一个盘的正反两面都是需要一个磁头的。
磁盘之所以叫做磁盘,就是因为其通过磁性来存储数据。计算机数据是由众多0和1二进制组成的,而磁铁分为NS两级,可以想象磁盘中有无数个小磁铁,通过改变这个小磁铁的NS极,来改变这个位置表达的01。
存储结构
简单了解磁盘的物理结构后,再看看磁盘的存储结构:

磁盘的每个盘面,都会被为多个等宽的同心圆环,这样一个圆环叫做磁道/柱面。而一个磁道又会被划分为很多个小扇形,每个扇形叫做一个扇区。
扇区是磁盘IO的基本单位,大小一般为固定的
512 byte
有人可能就会有疑问:既然每个扇区的宽度都是相同的,那么内圈的扇区面积小,外圈扇区面积大,为什么扇区的大小都为512 byte?
磁盘在生产的时候,厂家会调整不同区域的密度,内圈区域密度大,外圈区域密度小,最后可以保证每个扇区的大小都是相同的。
计算机通过磁盘访问数据时,通过CSH定位法:
- 决定访问哪一个
磁道(Cylinder)- 决定访问哪一个
磁头(Head),也就是决定哪一个盘面- 决定访问哪一个
扇区(Sector)
只需要确定CHS这三者,就可以确定一个磁盘的扇区,进行写入或读取。
逻辑结构
在操作系统眼中,磁盘不是一个环形结构,而是一个逻辑上的结构来访问。
在播音机中,常会见到这样的磁带:

磁带有两个磁带盘,如右图,左侧的磁带被拉直后,经过读写头,再卷入右侧的磁带盘。
这个过程中,环形的磁带被拉直,形成一个线性结构,那么我们是否可以在逻辑上把环形的磁盘拉直为一个线性结构?

这样我们就可以把整个磁盘抽象为一个线性结构,此时操作系统对磁盘的管理,就变成了对数组的增删查改!
但是由于扇区的大小为512 byte,如果操作系统只能每次按照512 byte为单位进行写入,效率太低了。所以操作系统一般以4 kb为单位进行写入,其中4 kb = 8 * 512 byte,即每次按照八个扇区为一个基本单位进行写入。哪怕用户只写入1 byte数据,操作系统也会在磁盘中开出4 kb大小的空间。

操作系统会在系统层面把八个扇区合成一个数据块,并对每个数据块进行重新编址,比如上图中,1 - 8号扇区,就被合成了1号数据块。这个数据块的地址,叫做LBA(Logical Block Address)地址,当操作系统要对磁盘写入时,先把LBA地址转化为线性地址,也就是一个个扇区的地址,然后再把线性地址转化为CHS地址,之后磁盘就可以根据CHS地址来访问到指定扇区了。
文件系统
简单了解了磁盘的硬件结构后,再来看看操作系统是如何在全局上管理这个磁盘的。
inode
所有被存储在磁盘的数据,都是以文件的形式,那么每个文件是如何被管理的呢?
文件可以被分为两个部分:
文件 = 内容 + 属性 {\color{Red} \mathbf{文件 = 内容 + 属性 } } 文件=内容+属性
在Linux中,文件的内容和属性是分开管理的,因为文件的内容大小是不确定的,而每个文件的具有属性都是相同的,只是属性值不同。Linux把文件的属性放在结构体struct inode中管理。
Linux 2.6.10内核的struct inode部分源码如下:
struct inode {
struct hlist_node i_hash;
struct list_head i_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
umode_t i_mode;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
//......
};
由于inode的成员太多,我这里只截取了一小部分,大约是总量的四分之一。所有文件的属性都被这样的inode管理。
以下是inode中一些重要的成员及其含义:
i_ino:inode的编号,这个编号是文件系统中唯一标识一个inode的数字i_mode:文件的访问权限和文件类型(常规文件、目录、设备文件等)i_uid和i_gid:文件所有者的用户ID和组IDi_size:文件大小(以字节为单位)i_atime、i_mtime和i_ctime:最后访问时间、最后修改时间和最后状态改变时间i_blocks:文件占用的磁盘块数i_links_count:文件的硬链接数
这些inode成员记录了文件的基本属性、访问权限、所有权、位置信息等,是文件系统管理文件的关键数据结构。不同的文件系统可能会有一些细微的差异,但基本结构和含义是相同的。
以上重要成员中,你也许对6 7有疑问,这三者会在博客后续讲解。
第一条i_ino用于在一个文件系统中标识一个唯一的inode,操作系统对文件写入时,就是通过i_ino来查找文件的,我们一般称其为inode编号。
可以通过ls的-i选项查看文件的inode编号:

其中左侧第一栏就是文件对应的inode编号了。
分区 & 分组
现在的计算机已经不怎么会缺少存储空间了,因为硬盘的价格普遍不贵,很多计算机的容量都是以TB为单位了,服务器中的存储空间就更大了。操作系统不可能一次性对以TB为单位的磁盘进行管理,于是就把磁盘分为很多个区域,然后分别管理每一个区域,这就是磁盘分区。
提到分区,想必第一个想到的就是各自Windows主机中的C盘和D盘了,以下为我的个人主机,大小约为1TB:

C盘和D盘并不是两个硬盘,而是一个硬盘的两个分区。也就是说是我的Windows中,把1TB分为了两个分区来管理,当然你也可以自己再划分更多的分区出来。
而对于一个分区,操作系统又会把它划分为很多个分组:

现在操作系统只需要按照相同的方式来管理一个个相同的分组,就可以管理好整个硬盘了。
分区管理
现在开始,我们只讨论一个分区内部如何管理的了,因为每个分区的管理是相同的,只需要理解一个分区如何管理,就知道所有分区如何管理,最后就知道整个磁盘如何管理的了。
操作系统通过文件系统来管理一个分区,每个分区都会有一套独立的文件系统,现在主流文件系统是Ext3文件系统,本博客后续讲解的是Ext2文件系统,两者没有太大差别。
值得注意是,一个硬盘可以有多个分区,不同分区是可以搭载不同的文件系统的。
基于Ext2文件系统,每个分区都有如下结构:

/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap block */
__le32 bg_inode_table; /* Inodes table block */
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
inode Bitmap & inode Table
一个分组中,会有大量的文件,每个文件都要有自己的inode来存储自己的信息,一个分组的所有文件的inode结构体,都存储在该分组的inode Table中。

当新创建文件的时候,要给这个文件分配一个inode,那么一个分组要如何给一个文件分配inode呢?总不可能是生成一个随机数,然后直接分配这个inode编号对应的inode给文件吧?
因此每个分组还会维护一张位图inodeBitmap,其用inode编号对应的位来标记一个inode的状态。通过查找位图,从而快速判断一个inode是否已经被分配过了。
inode描述一个文件属性的结构体,包含了大量文件的属性。Ext2也有自己的inode,其基于inode增加了某些其它信息,用于帮助管理。
Linux 2.6.10内核的struct ext2_inode部分源码如下:
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation 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; /* Blocks count */
__le32 i_flags; /* File flags */
//...
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
//...
};
我们可以从中看到非常多熟悉的成员,比如i_mode,i_uid等等,但是我特意在最后写出来了一个i_block成员,这是一个极为重要的成员,其与硬盘空间分配有关,接下来我们了解一下空间是如何分配的。
Block Bitmap & Data Blocks
刚才提到,文件 = 内容 + 属性,我们已经了解了文件的属性被inode管理,而inode被分组中的inode Table管理。那么文件系统如何管理内容的呢?这就是Block Bitmap与Data Blocks的任务了。

分组是用来存储文件的,自然要存储文件的内容,而文件的内容要放到数据块中,而所有的数据块,都由Data Blocks管理。
与
inode Bitmap相似的,Block Bitmap也是一个位图,用于标识哪些数据块被使用了,哪些没被使用,从而快速给文件分配数据块。
那么Data Blocks是如何分配数据的?
我们在上一个小节末尾留了一个成员i_block,其存在于inode中:
struct ext2_inode {
//...
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
//...
};
Linux源码对其的注释是/* Pointers to blocks */,也就是指向的数据块。即inode中的i_block成员,用于存储该文件使用了哪些数据块。
i_block是一个数组,元素的类型是__le32,本质上是一个32位的int类型,元素的个数是EXT2_N_BLOCKS,这个值等于15。
在源码中,EXT2_N_BLOCKS定义如下:
/*
* Constants relative to the data blocks
*/
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
最后值就是12 + 1 + 1 + 1 = 15,为什么要周转这么久,一个一个加上去?
了解了i_block是如何指向数据块后,我们就可以解决这个问题了:
i_block的数组下标分为四个区域:[0, 11],[12],[13],[14],
前十二个元素
[0, 11],它们直接指向存储文件内容的数据块:

哪些数据块存储了文件内容,这些元素就存储对应数据块的编号。
下标为
[12]的元素,指向一级间接块

当文件使用的数据块超过了12个,就会启用下标为[12]的元素,其指向一级间接块,一级间接块中存储了其他数据块的编号,被一级间接块指向的数据块,才是真正存放文件内容的数据块。
上图中,绿色的数据块是存放文件内容的数据块,蓝色数据块是一级间接块。
下标为
[13]的元素,指向二级间接块

当使用的数据块数量超过一级间接块的范围,就会启用下标为[13]的元素,其指向二级间接块,二级间接块指向一级间接块,被一级间接块指向的数据块,才是真正存放文件内容的数据块。上图中,灰色的为二级间接块。
下标为
[14]的元素,指向三级间接块

当使用的数据块数量超过二级间接块的范围,就会启用下标为[14]的元素,其指向三级间接块,三级间接块指向二级间接块。上图中,紫色的为三级间接块。
文件系统利用这样一个间接块的设计,用长度为15的数组,保存了文件使用的所有数据块。
现在我们再看到当时EXT2_N_BLOCKS的定义过程:
/*
* Constants relative to the data blocks
*/
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
#define EXT2_NDIR_BLOCKS 12- 这个宏定义了 Ext2 文件系统中 inode 中直接块的数量为 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS- 其中
IND表示:singly indirect block 一级间接块 - 这个宏定义了一级间接块的数组下标为 12
- 其中
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)- 其中
DIND表示:doubly indirect block 二级间接块 - 这个宏定义了二级间接块的数组下标为 13
- 其中
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)- 其中
TIND表示:triple indirect block 三级间接块 - 这个宏定义了三级间接块的数组下标为 14
- 其中
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)- 这个宏定义了 Ext2 文件系统中总共的数据块索引数为 15
- 包括 12 个直接块、1 个一级间接块、1 个二级间接块和 1 个三级间接块。
GDT & Super Block & Boot Block
现在我们已经讲清楚了一个文件是如何存储的,文件 = 内容 + 属性,属性被inode Table管理,而内容被Data Blocks管理。那么这个文件系统整体又是如何被管理的呢?这就与Super Block,Group Descriptor Table还有Boot Block有关了。

Group Descriptor Table叫做块组描述符表,简称GDT,用于宏观描述一个分组。
Linux 2.6.10内核的struct ext2_group_desc源码如下:
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap block */
__le32 bg_inode_table; /* Inodes table block */
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
我简单翻译一下注释:
bg_block_bitmap:组中块位图所在的块号
bg_inode_bitmap:组中索引节点位图所在块的块号
bg_inode_table:组中索引节点表的首块号
bg_free_blocks_count:组中空闲块数
bg_free_inodes_count:组中空闲索引节点数
bg_used_dirs_count:组中分配给目录的节点数
可以看到,GDT描述了各个区域的起始位置,以及当前分组的总体状态,每个分组都有自己的GDT。
Super Block用于存放文件系统本身的结构信息
Linux 2.6.10内核的struct ext2_super_block 部分源码如下:
struct ext2_super_block {
__le32 s_inodes_count; /* Inodes count */
__le32 s_blocks_count; /* Blocks count */
__le32 s_r_blocks_count; /* Reserved blocks count */
__le32 s_free_blocks_count; /* Free blocks count */
__le32 s_free_inodes_count; /* Free inodes count */
__le32 s_first_data_block; /* First Data Block */
//...
};
从上图中可以看出这个Super Block存储了整个文件系统的inode的数量,数据块的数量等等。这种对文件系统宏观描述的结构,因该来说每个分区只需要一个即可,为什么会放在分组里面呢?
并不是所有的分组都有Super Block,只有非常小一部分分组有。Super Block是代表一个文件系统的核心数据结构,一旦Super Block损毁,整个文件系统都会崩溃,因此文件系统不只把Super Block保留一份,而是保留多份放到不同分组中。
需要Super Block时到特定分组去访问,一旦某个分组的Super Block被损毁,用其它的Super Block进行拷贝,从而修复Super Block。这样可以极大提高文件系统的稳定性。
Boot Block用于计算机开机时告知磁盘的分区情况,帮助计算机加载操作系统等
操作系统本质也是一个软件,开机时计算机要加载操作系统,毫无疑问操作系统也要被存储在硬盘中,Boot Block就会存储操作系统的存储位置,从而帮助计算机启动操作系统。
Boot Block不仅仅是一个分区只有一个,而是整个硬盘中只有一个,放在整个硬盘的第一个分区头部。
现在总结一下刚才讲解的结构:

inode Table:存储当前分组所有文件的inodeinode Bitmap:标识当前分组的inode的使用情况Data Blocks:管理当前分组的数据块Block Bitmap:标识当前分组的数据块的使用情况GDT:宏观描述一个分组Super Block:描述一个分区,文件系统的核心Boot Block:描述整个硬盘的分区情况,帮助计算机加载操作系统
重新理解目录
有了以上知识后,我们再来重新理解一下目录这个结构。目录的本质也是文件,那么目录是如何存储其内部的文件的呢?
目录内部存储的是
文件名到inode编号的映射关系
这里有一个很重要的知识:文件名不属于文件的属性,也不存在于inode中!
文件名存在于目录中,而不存在于文件本身。我们通过目录来访问文件名,本质是去目录文件中,通过文件名找到对应的inode编号,然后再访问到文件。
比如说现在通过路径/usr/bin/ls来访问一个文件,其过程为:
- 先在根目录中找到文件名
usr对应的inode编号 - 访问文件
usr,在文件usr中找到文件名bin对应的inode编号 - 访问文件
bin,在文件bin中找到文件名ls对应的inode编号 - 访问文件
ls
软硬链接
在Linux中,文件和目录有两种特殊的链接方式,分别称为软链接(Soft Link)和硬链接(Hard Link)。这两种链接方式都是用于创建文件或目录的快捷方式,但它们在实现原理和使用场景上存在一些差异。
软链接
软链接也称为符号链接,它是一种特殊的文件,其中包含了另一个文件或目录的路径信息
创建软链接的命令如下:
ln -s 源文件/目录 软链接名称
此处的-s表示soft。
示例:
当前目录下有一个soft.txt:

现在为其建立一个软链接,名为link-soft.txt:

可以看到我们成功创建了一个软链接,使用ls时也指明了link-soft.txt -> soft.txt,即文件link-soft.txt是soft.txt的链接。
通过最左侧第一栏可知,软链接和原文件的inode不同,说明是不同的两个文件,只是软链接的文件内部存储的是目标文件的路径。
软链接的主要特点如下:
- 文件类型:软链接是一种特殊的文件类型,在文件列表中以
l开头表示 - 链接目标:软链接中存储的是链接目标的路径信息,而不是实际的文件内容
- 独立性:软链接是独立于链接目标的,即使链接目标被删除或移动,软链接仍然存在,只是无法访问实际的文件或目录
- 跨文件系统:软链接可以跨越不同的文件系统,链接到不同分区或网络共享中的文件或目录
硬链接
硬链接是另一种创建文件快捷方式的方法,它与软链接有一些不同之处。
创建硬链接的命令如下:
ln 源文件/目录 软链接名称
示例:
当前目录下有一个hard.txt:

现在为其建立一个硬链接,名为link-hard.txt:

可以看到我们成功创建了一个硬链接,值得注意的是:硬链接的inode与原文件的inode相同!
硬链接本质上,只是在目录内部,多增加了一个文件名与inode的映射关系。
从左往右数第三栏,你会发现soft.txt的值为1,而hard.txt的值为2,这个值叫做硬连接数,即有多少个相同的文件名指向这个inode。
硬链接的主要特点如下:
- 文件类型:硬链接在文件列表中看起来与普通文件没有区别
- 链接目标:硬链接指向实际文件的数据块,而不是文件路径
- 独立性:硬链接依赖于链接目标,如果链接目标被删除,硬链接也将失效
- 跨文件系统:硬链接只能在同一个文件系统内创建,不能跨越不同的文件系统
如果想要删除一个软连接或者硬连接,使用指令unlink即可。
还要注意的是:不能给目录创建硬连接,只能给目录创建软链接。
软硬链接的使用场景
软链接和硬链接各有适用的场景:
软链接:
- 在不同文件系统或分区之间创建快捷方式
- 链接到可能会被移动或删除的文件或目录
- 创建指向目录的快捷方式
硬链接:
- 为同一个文件创建多个名称
- 提高文件访问效率,因为硬链接直接指向文件数据块
- 备份或归档文件时保留文件的硬链接关系