文章目录
磁盘
计算机需要存储数据,主要依靠两种硬件设备:内存和硬盘。内存是一种需要通电才能保持数据的存储设备,具有较高的数据交换效率,适合存储计算过程中产生的临时数据。而对于长期保存的数据,由于无法保证计算机长期通电且内存价格较高,硬盘成为了一种必不可少的存储介质。硬盘不仅不需要持续通电,而且价格相对低廉,适合存储长期数据。
存储在硬盘中的数据通常被称为文件。本篇博客将介绍硬盘中的数据是如何组织和管理的,也就是文件系统。
如今,大部分个人计算机不再使用传统的磁盘(HDD
),而是使用固态硬盘(SSD
)。然而,我们为什么仍然要探讨磁盘,而不是SSD
呢?
首先,磁盘的文件系统与SSD非常相似。磁盘文件系统的设计考虑了存储介质的通用性,因此可以很好地适用于SSD。尽管可能会有一些针对SSD的特殊优化,但这些优化不会影响文件系统的整体架构。
其次,我们个人计算机使用的是消费级硬盘
,而服务器主机通常使用的是企业级硬盘
,尽管个人计算机已不再普遍使用磁盘,但由于价格低廉,企业级磁盘依然在服务器中得到广泛应用。
由于Linux作为一款适用于服务器的操作系统,它通过磁盘来理解文件系统,因此,讲解磁盘文件系统对于理解Linux的工作原理是非常重要的。
磁盘的物理结构
在了解文件系统之前,我们先来看看磁盘的物理结构,了解磁盘底层是如何运作的。
对于企业级硬盘
来说,一个硬盘可能包含多个盘面,这就像图中所示。
磁盘的盘面类似于我们日常见到的光盘,不同的是,磁盘的两面都可以进行读写。上图中的磁头用于对磁盘进行数据读写,硬盘的马达带动盘面高速旋转,磁头则在盘面上来回摆动,从而实现对盘面各个位置的数据访问。由于盘的两面都可读写,因此每个盘面正反两侧都需要一个磁头。
磁盘之所以被称为“磁盘”,是因为它通过磁性原理来存储数据。计算机中的数据以二进制0和1的形式存在,而磁铁具有南北极性。通过改变磁盘中无数个小磁铁的南北极方向,磁盘可以将这些位置转化为二进制数据。
磁盘的存储结构
了解了磁盘的物理结构后,我们再来看看磁盘的存储结构。
每个盘面会被划分为多个等宽的同心圆环,每个圆环称为一个磁道(或柱面)。磁道又被划分为许多个扇形区域,这些扇形区域称为“扇区”。
扇区是磁盘进行输入输出(IO)的最小单位,每个扇区的大小通常为512字节。
有人可能会疑问,既然每个扇区的宽度相同,那么内圈的扇区面积较小,外圈的扇区面积较大,为什么它们的大小仍然是512字节?
实际上,磁盘制造商在生产磁盘时,会调整不同区域的存储密度。内圈的区域存储密度较大,外圈的区域存储密度较小,因此最终可以保证每个扇区的大小是统一的。
计算机通过磁盘访问数据时,会使用CSH
(Cylinder-Head-Sector)定位法:
- 确定访问哪个磁道(Cylinder)。
- 确定访问哪个磁头(Head),即选择哪个盘面。
- 确定访问哪个扇区(Sector)。
通过确定这三者(CHS),就可以精确定位到磁盘中的某个扇区,从而进行读写操作。
磁盘的逻辑结构
在操作系统的视角下,磁盘并不是一个环形结构,而是一个线性结构。可以将磁盘的环形结构想象成一条被拉直的磁带。
类似于磁带的工作原理,我们可以将磁盘的物理结构转化为一个线性结构,这样操作系统就能够像操作一个数组一样,对磁盘中的数据进行增删查改。
不过,由于磁盘的最小单位为512字节,如果操作系统每次按照512字节为单位进行读写,效率会非常低。因此,操作系统一般以4KB为单位进行写入操作。4KB相当于8个512字节的扇区。因此,即便用户只写入1字节数据,操作系统也会分配4KB的空间来进行存储。
操作系统会将这8个扇区合成一个数据块,每个数据块都有一个新的地址,这个地址称为LBA
(Logical Block Address
,逻辑块地址
)。当操作系统需要对磁盘进行写入时,它会将LBA地址转换为线性地址,即扇区地址,再通过CHS
地址定位到物理磁盘上的目标扇区,从而进行读写操作。
通过对磁盘物理结构和逻辑结构的理解,我们可以更深入地了解文件系统是如何管理和访问磁盘上存储的数据的。
文件系统
了解了磁盘的硬件结构后,接下来我们来看操作系统如何在全局上管理磁盘。
inode
所有存储在磁盘上的数据都是以文件的形式存在,那么每个文件是如何被管理的呢?
文件可以分为两个部分:
文件 = 内容 + 属性
在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
成员记录了文件的基本属性、访问权限、所有权和位置信息,是文件系统管理文件的关键数据结构。虽然不同的文件系统可能有一些细微差别,但基本结构和含义是相同的。
关于i_atime
、i_mtime
和i_ctime
,这三者将在后续博客中讲解。
i_ino
字段用于唯一标识一个文件的inode
,操作系统在对文件进行读写时,会通过i_ino
查找文件,通常我们称之为inode编号
。
可以通过ls -i
命令查看文件的inode
编号:
ls -i filename
其中,输出的第一栏即为文件对应的inode
编号。
分区 & 分组
如今,计算机的存储空间通常非常充裕,硬盘的价格也很低,许多计算机的硬盘容量已经达到TB级别,服务器的存储空间更是庞大。操作系统无法一次性管理如此巨大的存储空间,因此它将硬盘分为多个区域,分别管理这些区域,这就是硬盘的分区。
提到分区,很多人首先会想到Windows中的C盘
和D盘
。以下是我个人主机的分区信息,硬盘总容量大约为512GB:
C盘
和D盘
并不是两个独立的硬盘,而是同一个硬盘的两个分区。换句话说,Windows将512GB的硬盘划分为两个分区来管理。当然,用户也可以根据需要创建更多的分区。
对于一个分区,操作系统又会将其划分为多个分组
。这样,操作系统就能通过管理每个分组来管理整个硬盘。
分区管理
现在我们只讨论一个分区内部如何管理的,因为每个分区的管理方式是相同的。只要理解了一个分区如何管理,我们就能知道如何管理整个硬盘。
操作系统通过文件系统来管理一个分区,每个分区都有一套独立的文件系统。目前主流的文件系统是Ext3
,而本博客后续会讲解的是Ext2
文件系统,两者的差异并不大。
需要注意的是,一个硬盘可以有多个分区,而不同的分区可以使用不同的文件系统。
以Ext2
文件系统为例,每个分区的结构如下:
inode Bitmap & inode Table
每个分组中包含大量的文件,每个文件都有一个inode
来存储自己的信息。一个分组的所有文件的inode
结构体都存储在该分组的inode Table
中。
当创建新文件时,需要为文件分配一个inode
,但一个分组如何分配inode
呢?显然,不能随机生成一个inode
编号并直接分配。为此,每个分组会维护一个inode位图
(inode Bitmap
),它用来标记inode
的使用状态。通过查找位图,操作系统可以快速判断某个inode
是否已经被分配。
inode
描述文件的属性,包含大量的文件信息。Ext2
文件系统在inode
的基础上增加了一些额外的信息,用于帮助管理。
以下是Linux 2.6.10内核中struct ext2_inode
的部分源码:
struct ext2_inode {
__le16 i_mode; /* 文件模式 */
__le16 i_uid; /* 所有者UID的低16位 */
__le32 i_size; /* 文件大小(字节) */
__le32 i_atime; /* 访问时间 */
__le32 i_ctime; /* 创建时间 */
__le32 i_mtime; /* 修改时间 */
__le32 i_dtime; /* 删除时间 */
__le16 i_gid; /* 组ID的低16位 */
__le16 i_links_count; /* 链接数 */
__le32 i_blocks; /* 占用的磁盘块数 */
__le32 i_flags; /* 文件标志 */
//...
__le32 i_block[EXT2_N_BLOCKS]; /* 指向数据块的指针 */
};
我们可以看到许多熟悉的成员,例如i_mode
、i_uid
等。不过,值得注意的是,i_block
是一个非常重要的成员,它与硬盘空间的分配密切相关。接下来,我们来了解文件内容的存储是如何管理的。
Block Bitmap & Data Blocks
前面提到,文件由内容 + 属性
构成,文件的属性由inode
管理,而文件的内容则由Data Blocks
管理。
Block Bitmap
是一个位图,用来标识哪些数据块被使用,哪些数据块未被使用,从而帮助操作系统快速为文件分配数据块。
Data Blocks
则是用来存储文件的内容的,每个数据块都由Data Blocks
进行管理。
在前面的inode
中,我们看到了i_block
成员,它指向文件的数据块。具体来说,i_block
是一个包含指向数据块的指针的数组。
struct ext2_inode {
//...
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
//...
};
Linux源码中定义了EXT2_N_BLOCKS
,表示最多可以有15个指向数据块的指针:
#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)
这段代码定义了inode
中的i_block
最多可以有15个指针,其中:
-
[0, 11]
:直接指向存储文件内容的数据块。
-
[12]
:指向一级间接块,存储其他数据块的指针。当文件使用的数据块超过了12个,就会启用下标为[12]的元素,其指向一级间接块,一级间接块中存储了其他数据块的编号,被一级间接块指向的数据块,才是真正存放文件内容的数据块。下图中,绿色的数据块是存放文件内容的数据块,蓝色数据块是一级间接块。
[13]
:指向二级间接块,存储一级间接块的指针。
[14]
:指向三级间接块,存储二级间接块的指针。
通过这种间接块的设计,文件系统可以管理文件使用的所有数据块,从而高效地存储和访问文件内容。
GDT & Super Block & Boot Block
现在我们已经了解了文件是如何存储的,inode Table
管理文件的inode
,而Data Blocks
管理文件的内容。那么,整个文件系统是如何被管理的呢?这就与Super Block
、Group Descriptor Table (GDT)
和Boot Block
有关了。
GDT
(块组描述符表)用于描述每个分组的宏观信息。Super Block
存储整个文件系统的结构信息,是文件系统的核心数据结构。Boot Block
用于帮助计算机加载操作系统。
- 在 Linux 2.6.10 内核中,
ext2_group_desc
结构体如下所示:
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* 块位图块 */
__le32 bg_inode_bitmap; /* 索引节点位图块 */
__le32 bg_inode_table; /* 索引节点表的首块 */
__le16 bg_free_blocks_count;/* 空闲块数 */
__le16 bg_free_inodes_count;/* 空闲索引节点数 */
__le16 bg_used_dirs_count; /* 分配给目录的节点数 */
__le16 bg_pad;
__le32 bg_reserved[3];
};
可以看到,GDT
(组描述符表)用于描述各个区域的起始位置,以及当前分组的总体状态。每个分组都有自己的 GDT
。
Super Block 结构体
Super Block
用于存放文件系统本身的结构信息。以下是 Linux 2.6.10 内核中 ext2_super_block
结构体的部分源码:
struct ext2_super_block {
__le32 s_inodes_count; /* 索引节点的数量 */
__le32 s_blocks_count; /* 块的数量 */
__le32 s_r_blocks_count; /* 保留块的数量 */
__le32 s_free_blocks_count; /* 空闲块的数量 */
__le32 s_free_inodes_count; /* 空闲索引节点的数量 */
__le32 s_first_data_block; /* 第一个数据块 */
//...
};
从上面的代码可以看出,Super Block
存储了整个文件系统的 inode
和数据块的数量等信息。每个分区只需要一个 Super Block
,但是为什么在分组内也会有呢?
并不是所有的分组都有 Super Block
,只有很小一部分分组拥有它。Super Block
是文件系统的核心数据结构,一旦损坏,整个文件系统将崩溃。因此,为了避免单点故障,文件系统会在多个分组中保留 Super Block
的备份。
当需要访问 Super Block
时,系统会到特定的分组去查找。如果某个分组的 Super Block
损坏,系统可以使用其他分组中的 Super Block
来恢复,从而增强文件系统的稳定性。
Boot Block
Boot Block
是用于计算机启动时,告知磁盘分区信息的区域。它帮助计算机加载操作系统等关键信息。
操作系统本质上是一种软件,计算机开机时需要加载操作系统,而操作系统被存储在硬盘上。Boot Block
存储了操作系统的位置,帮助计算机启动操作系统。
值得注意的是,Boot Block
只在整个硬盘中存在一个,并且位于硬盘的第一个分区头部。
文件系统结构总结
至此,我们已经了解了如何存储和管理文件的属性和内容。下面总结一下我们讲解的文件系统结构:
inode Table
:存储当前分组所有文件的inode
。inode Bitmap
:标识当前分组的inode
使用情况。Data Blocks
:管理当前分组的数据块。Block Bitmap
:标识当前分组的数据块使用情况。GDT
(Group Descriptor Table):宏观描述一个分组。Super Block
:描述一个分区,是文件系统的核心结构。Boot Block
:描述整个硬盘的分区情况,帮助计算机加载操作系统。
重新理解目录
有了上述的知识后,我们可以重新理解目录这一结构。目录的本质也是文件,但目录是如何存储其内部的文件呢?
目录内部存储的是 文件名 -> inode编号
的映射关系。
这里有一个重要的知识点:文件名
不是文件的属性,也不存储在 inode
中!
文件名存在于目录文件中,而不是文件本身。我们通过目录来访问文件名,本质上是通过目录文件查找文件名对应的 inode
编号,然后再访问文件本身。
例如,当我们访问路径 /usr/bin/ls
的文件时,过程如下:
- 先在根目录中找到
usr
对应的inode
编号。 - 然后在
usr
目录中找到bin
对应的inode
编号。 - 接着在
bin
目录中找到ls
对应的inode
编号。 - 最终访问
ls
文件。
软链接与硬链接
在 Linux 中,文件和目录有两种特殊的链接方式,分别称为 软链接
(Soft Link)和 硬链接
(Hard Link)。这两种链接方式都用于创建文件或目录的快捷方式,但它们在实现原理和使用场景上有显著差异。
软链接
软链接(也称为符号链接)是一种特殊类型的文件,它包含了另一个文件或目录的路径信息。
创建软链接的命令如下:
ln -s 源文件/目录 软链接名称
其中,-s
表示软链接(symbolic link)。
假设当前目录下有一个 soft.txt
文件:
现在为其建立一个软链接,名为 link-soft.txt
:
ln -s soft.txt link-soft.txt
运行 ls
命令时,可以看到 link-soft.txt -> soft.txt
,即 link-soft.txt
是 soft.txt
的链接。
从 ls
输出中可以看出,软链接和原文件的 inode
不同,说明它们是两个不同的文件,只是软链接文件内部存储了目标文件的路径。
软链接的主要特点
- 文件类型:软链接是一个特殊的文件类型,在文件列表中以
l
开头表示。 - 链接目标:软链接存储的是链接目标的路径信息,而不是实际的文件内容。
- 独立性:软链接是独立于链接目标的,即使链接目标被删除或移动,软链接仍然存在,只是无法访问实际的文件或目录。
- 跨文件系统:软链接可以跨越不同的文件系统,链接到不同分区或网络共享中的文件或目录。
硬链接
硬链接是另一种创建文件快捷方式的方法,与软链接有所不同。
创建硬链接的命令如下:
ln 源文件/目录 硬链接名称
示例
假设当前目录下有一个 hard.txt
文件:
现在为其建立一个硬链接,名为 link-hard.txt
:
ln hard.txt link-hard.txt
运行 ls
命令时,可以看到 link-hard.txt
与 hard.txt
共享相同的 inode
,这意味着它们指向同一个文件数据块。
硬链接的本质是,在目录内部多增加了一个文件名与 inode
的映射关系。
从 ls
输出中可以看到,硬链接的 inode
与原文件的 inode
相同,因此它们指向的是相同的文件数据块。
注意,在输出中,hard.txt
的链接数为 2,这表示有两个文件名指向同一个 inode
,这个值叫做 硬链接数
。
硬链接的主要特点
- 文件类型:硬链接在文件列表中与普通文件没有区别。
- 链接目标:硬链接指向实际文件的数据块,而不是文件路径。
- 独立性:硬链接依赖于链接目标,如果链接目标被删除,硬链接也将失效。
- 跨文件系统:硬链接只能在同一个文件系统内创建,不能跨越不同的文件系统。
删除软链接和硬链接
如果要删除一个软链接或硬链接,可以使用 unlink
命令:
unlink 链接名称
需要注意的是,不能给目录创建硬链接,只能为目录创建软链接。
软硬链接的使用场景
软链接和硬链接各有其适用的场景:
软链接的使用场景:
- 跨文件系统或分区创建快捷方式:软链接可以链接到不同分区或网络共享中的文件或目录。
- 链接到可能会被移动或删除的文件或目录:即使目标文件被移动,软链接仍然存在,只是无法访问。
- 创建指向目录的快捷方式:可以使用软链接来方便地指向目录。
硬链接的使用场景:
- 为同一个文件创建多个名称:硬链接允许为同一个文件创建多个文件名,所有文件名指向同一个
inode
。 - 提高文件访问效率:硬链接直接指向文件数据块,因此对文件数据的访问更加高效。
- 备份或归档时保留文件的硬链接关系:在备份或归档过程中,保留硬链接关系可以确保文件的完整性和一致性。
硬链接的应用
在 Linux 中,硬链接
被广泛应用于路径转换等场景。关于硬链接的一些特性,我们常见的问题包括:
为什么创建普通目录的硬链接数是 2?而普通文件的硬链接数是 1?
普通文件的硬链接数是 1
这个问题比较容易理解,因为在目录中,每个文件名都与一个 inode
相关联。普通文件的硬链接数为 1,意味着在当前目录中,只有一个文件名与该文件的 inode
对应。
普通目录的硬链接数是 2
对于目录而言,硬链接数是 2 的原因稍微复杂一点。除了当前目录下包含了目录本身(例如 dir
和 dir
的 inode
),目录下还有一个特殊的目录项:“.
”。这个点(.
)表示当前路径,指向目录本身的 inode
。所以,目录的硬链接数是 2。这个点是 dir
的别名,它非常重要,因为我们经常使用它来表示当前路径。如果没有这个别名,访问路径时将不再是 ./xxx/...
,而是需要完全写出路径,例如 dir/xxx/...
,不方便操作。
- 在目录中创建新目录
接下来,我们在 dir
下创建一个名为 otherdir
的子目录:
- 此时
dir
的硬链接数变为 3,因为除了它本身的目录项外,dir
目录还包含了子目录other
中的".."
,即父目录的别名,它指向dir
的inode
。 other
的硬链接数为 2,表示other
目录包含了两个链接:一个是它自己的名字,另一个是指向dir
的".."
。- 注意:Linux中不能为目录建立硬链接,否则可能会形成
路径环绕
。