Linux:文件系统
- 磁盘
- 物理结构
- 存储结构
- 逻辑结构
- 文件系统
- inode
- 分区 & 分组
- 分区管理
- inode Bitmap & inode Table
- Block Bitmap & Data Blocks
- GDT & Super Block & Boot Block
- 重新理解目录
- 软硬链接
- 软链接
- 硬链接
- 软硬链接的使用场景
磁盘
计算机需要存储数据,主要有两种存储数据的硬件,内存和硬盘,其中内存需要通电,具有较高的数据交换效率,适合用于存储计算时产生的临时数据。对于长期保存的数据,一方面来说我们无法保证计算机永远通电,另一方面内存的价格太高了,因此计算机需要硬盘这种无需通电,价格低廉的存储介质来存储数据。
存储在硬盘中的数据,一般称为文件
。本博客讲解在硬盘中,数据是如何被组织管理的,也就是文件系统
。
硬盘主要有磁盘
和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和组ID -
i_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
:存储当前分组所有文件的inode
inode 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
即可。
还要注意的是:不能给目录创建硬连接,只能给目录创建软链接。
软硬链接的使用场景
软链接和硬链接各有适用的场景:
软链接:
- 在不同文件系统或分区之间创建快捷方式
- 链接到可能会被移动或删除的文件或目录
- 创建指向目录的快捷方式
硬链接:
- 为同一个文件创建多个名称
- 提高文件访问效率,因为硬链接直接指向文件数据块
- 备份或归档文件时保留文件的硬链接关系