系统性学习Linux-第六讲-Ext文件系统
- [1. 理解硬件](#1. 理解硬件)
-
- [1-1 磁盘、服务器、机柜、机房](#1-1 磁盘、服务器、机柜、机房)
- [1-2 磁盘物理结构](#1-2 磁盘物理结构)
- [1-3 磁盘的存储结构](#1-3 磁盘的存储结构)
- [1-4 磁盘的逻辑结构](#1-4 磁盘的逻辑结构)
-
- [1-4-1 理解过程](#1-4-1 理解过程)
- [1-4-2 真实过程](#1-4-2 真实过程)
- [1-5 CHS && LBA地址](#1-5 CHS && LBA地址)
- [2. 引入⽂件系统](#2. 引入⽂件系统)
-
- [2-1 引入 "块" 概念](#2-1 引入 "块" 概念)
- [2-2 引入 "分区" 概念](#2-2 引入 "分区" 概念)
- [2.3 引入 "inode" 概念](#2.3 引入 "inode" 概念)
- [3. ext2 文件系统](#3. ext2 文件系统)
-
- [3-1 宏观认识](#3-1 宏观认识)
- [3-2 Block Group](#3-2 Block Group)
- [3-3 块组内部构成](#3-3 块组内部构成)
-
- [3-3-1 超级块(Super Block)](#3-3-1 超级块(Super Block))
- [3-3-2 GDT(Group Descriptor Table)](#3-3-2 GDT(Group Descriptor Table))
- [3-3-3 块位图(Block Bitmap)](#3-3-3 块位图(Block Bitmap))
- [3-3-4 inode位图(Inode Bitmap)](#3-3-4 inode位图(Inode Bitmap))
- [3-3-5 i 节点表(Inode Table)](#3-3-5 i 节点表(Inode Table))
- [3-3-6 Data Block](#3-3-6 Data Block)
- [3-4 inode 和 datablock映射(弱化)](#3-4 inode 和 datablock映射(弱化))
- [3-5 目录与文件名](#3-5 目录与文件名)
- [3-6 路径解析](#3-6 路径解析)
- [3-7 路径缓存](#3-7 路径缓存)
- [3-8 挂载分区](#3-8 挂载分区)
-
- [3-8-1 一个实验:](#3-8-1 一个实验:)
- [3-8-2 一个结论](#3-8-2 一个结论)
- [3-9 文件系统总结](#3-9 文件系统总结)
- [4. 软硬连接](#4. 软硬连接)
-
- [4-1 硬连接](#4-1 硬连接)
- [4-2 软链接](#4-2 软链接)
- [4-3 软硬连接对比](#4-3 软硬连接对比)
- [4-4 软硬连接的用途:](#4-4 软硬连接的用途:)
本节重点:
-
理解磁盘物理结构
-
掌握 CHS 和 LBA 地址
-
掌握 Ext 系列文件系统原理
-
理解分区,格式化,路径解析,挂载等过程和操作
-
理解软硬连接使用和用途
1. 理解硬件
作者认为,从软件层开始学习固然尚可,但我们如果要深层理解软件层为何这样设计,那么终归要从硬件入手。
所以对硬件进行了解总归是没有坏处的。
1-1 磁盘、服务器、机柜、机房
-
机械磁盘是计算机中唯一的一个机械设备
-
磁盘 --- 外设
-
慢
-
容量大,价格便宜
1. 机械硬盘

2. 服务器、机柜、机房

1-2 磁盘物理结构

1-3 磁盘的存储结构
磁盘往往是由很多个盘片组成的,在盘片之上还存在磁道。

为了去组织管理这些存储空间的区域呢,磁盘内部对一张盘片的空间进行了分区。
划分成了一个个的扇区。
扇区:是磁盘存储数据的基本单位,512字节,块设备。

当磁盘想要读取文件,或者写入文件时,磁头就会去寻找对应的扇区,进行读写操作。



在这里要和读者们强调一个概念:有些读者可能会下意识认为,磁盘是以水平的盘片为单位,当这个水平盘片上的空间,
全部被利用完,才会对下一个盘片进行写操作,实际上不是这样的,我们看待磁盘要像看待洋葱或者卷纸一样,
它是外层垂直方向的扇区读写完成完后,往内层去,也就是一条过圆心半径,指向圆心的线段,为什么会这样呢?
因为磁盘、磁头均为为多层结构,一层磁盘一个磁头,所有磁头在同一时间内读写每层盘片的扇区是一致的,
所以如果以水平盘片为读写单位,那么多层的结构就形同虚设。(文章后面有更加详细的讲解,可以先跳过)
如何定位⼀个扇区呢?
-
可以先定位磁头(header)
-
确定磁头要访问哪⼀个柱⾯(磁道)(cylinder)
-
定位⼀个扇区(sector)
-
CHS地址定位
文件 = 内容+属性 都是数据,无非就是占据那几个扇区的问题!能定位⼀个扇区了,能不能定位多个扇区呢?

-
扇区是从磁盘读出和写入信息的最小单位,通常大小为 512 字节。
-
磁头(head)数:每个盘片⼀般有上下两面,分别对应1个磁头,共2个磁头
-
磁道(track)数:磁道是从盘片外圈往内圈编号0磁道,1磁道...,靠近主轴的同心圆用于停靠磁头,不存储数据
-
柱面(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数
-
扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
-
圆盘(platter)数:就是盘片的数量
-
磁盘容量=磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数
-
细节:传动臂上的磁头是共进退的(这点比较重要,后面会说明)
柱面(cylinder),磁头(head),扇区(sector),显然可以定位数据了,这就是数据定位(寻址)方式之一,CHS寻址方式。
❗️:CHS 寻址
对早期的磁盘非常有效,知道用哪个磁头,读取哪个柱面上的第几扇区就可以读到数据了。但是CHS模式支持的硬盘容量有限,因为系统用 8bit 来存储磁头地址,用 10bit 来存储柱面地址,用 6bit 来存储扇区地址,而⼀个扇区共有 512Byte ,这样使用 CHS 寻址一块硬盘最大容量为 256 * 1024 * 63 * 512B = 8064 MB(1MB = 1048576B)(若按1MB=1000000B来算就是8.4GB)
1-4 磁盘的逻辑结构
1-4-1 理解过程

磁带上面可以存储数据,我们可以把磁带 "拉直" ,形成线性结构

那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带,
那么磁盘的逻辑存储结构我们也可以类似于:

这样每一个扇区,就有了一个线性地址(其实就是数组下标),这种地址叫做 LBA

1-4-2 真实过程
一个细节:传动臂上的磁头是共进退的

柱面是一个逻辑上的概念,其实就是每一面上,相同半径的磁道逻辑上构成柱面。
所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘整体是由 "柱面" 卷起来的。

所以,磁盘的真实情况是:
磁道:某一盘面的某一个磁道展开:

即:一维数组
柱面:整个磁盘所有盘面的同一个磁道,即柱面展开:

也就类似于把圆柱最外层的一面展开的效果

-
柱面上的每个磁道,扇区个数是一样的
-
其数据结构与二位数组非常相似
整盘:

整个磁盘不就是多张二维的扇区数组表( 三维数组?)
所有,寻址⼀个扇区:先找到哪⼀个柱⾯(Cylinder) ,在确定柱⾯内哪⼀个磁道(其实就是磁头位置,Head),
在确定扇区(Sector),所以就有了 CHS ,我们之前学过 C/C++ 的数组,在我们看来,其实全部都是一维数组:

所以,每⼀个扇区都有⼀个下标,我们叫做 LBA(Logical Block Address) 地址,其实就是线性地址。
所以怎么计算得到这个 LBA 地址呢?(后面会详细说明,核心为LBA地址转成CHS地址,CHS如何转换成为LBA地址)

但是对与操作系统来说,OS内部并不需要关心 LBA 地址如何转成 CHS 地址,CHS 如何转换成为 LBA 地址。
以上操作磁盘会自行完成来做!通过固件(硬件电路,伺服系统)
1-5 CHS && LBA地址
CHS 转成 LBA :
-
磁头数*每磁道扇区数 = 单个柱面的扇区总数
-
LBA = 柱面、号C * 单个柱⾯的扇区总数 + 磁头号H * 每磁道扇区数 + 扇区号 S - 1
-
即:LBA = 柱⾯号C * (磁头数*每磁道扇区数) + 磁头号 H * 每磁道扇区数 + 扇区号 S - 1
-
扇区号通常是从 1 开始的,而在LBA中,地址是从 0 开始的
-
柱面和磁道都是从 0 开始编号的
-
总柱面,磁道个数,扇区总数等信息,在磁盘内部会⾃动维护,上层开机的时候,会获取到这些参数。
LBA转成CHS:
-
柱面号C = LBA // (磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数】
-
磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数
-
扇区号S = (LBA % 每磁道扇区数) + 1
-
"//": 表⽰除取整
所以:从此往后,在磁盘使用者看来,根本就不关心 CHS 地址,而是直接使用 LBA 地址,磁盘内部内部转换。
从现在开始,磁盘就是一个 元素为扇区 的一维数组,数组的下标就是每一个扇区的 LBA 地址。OS 使用磁盘,
就可以用一个数字访问磁盘扇区了。
2. 引入⽂件系统
2-1 引入 "块" 概念
其实硬盘是典型的 "块" 设备,操作系统读取硬盘数据的时候,其实是不会一个个扇区地读取,这样效率太低,
而是一次性连续读取多个扇区,即一次性读取一个 "块"(block)。硬盘的每个分区是被划分为一个个的 "块" 。
一个 "块" 的大小是由格式化的时候确定的,并且不可以更改,最常见的是 4 KB,即连续八个扇区组成一个 "块"。
"块"是文件存取的最小单位。

注意:
-
磁盘就是一个三维数组,我们把它看待成为一个 "一维数组" ,数组下标就是 LBA ,每个元素都是扇区
-
每个扇区都有 LBA ,那么 8 个扇区一个块,每一个块的地址我们也能算出来。
-
知道 LBA:块号 = LBA / 8
-
知道块号:LAB = 块号 * 8 + n . ( n 是块内第几个扇区)

2-2 引入 "分区" 概念
其实磁盘是可以被分成多个分区(partition)的,以 Windows 观点来看,你可能会有⼀块磁盘并且将它分区成 C , D , E 盘。
那个 C , D , E 就是分区。分区从实质上说就是对硬盘的一种格式化。但是 Linux 的设备都是以文件形式存在,
那是怎么分区的呢?柱面是分区的最小单位,我们可以利用参考柱面号码的方式来进行分区,
其本质就是设置每个区的起始柱面和结束柱面号码。 此时我们可以将硬盘上的柱面(分区)进行平铺,
将其想象成⼀个大的平面,如下图所示:

❗️注意:
柱面大小一致,扇区个位一致,那么其实只要知道每个分区的起始和结束柱面号,知道每一个柱面多少个扇区,那么该分区多大,其实和解释 LBA 是多少也就清楚了。

2.3 引入 "inode" 概念
之前我们说过 文件 = 数据 + 属性 ,我们使用 ls - l 的时候看到的除了看到文件名,还能看到文件元数据(属性)。
bash
[root@localhost linux]# ls -l
总⽤量 12
-rwxr-xr-x. 1 root root 7438 "9⽉ 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9⽉ 13 14:56" test.c
每行包含 7 列:
-
模式
-
硬链接数
-
文件所有者
-
组
-
大小
-
最后修改时间
-
文件名
ls - l 读取存储在磁盘上的文件信息,然后显示出来

其实这个信息除了通过这种方式来读取,还有⼀个 stat 命令能够看到更多信息
cpp
[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通⽂件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
到这我们要思考一个问题,文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息(属性信息),
比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做 inode ,中文译名为"索引节点"。

每一个文件都有对应的 inode ,里面包含了与该文件有关的一些信息。
为了能解释清楚 inode ,我们需要是深入了解一下文件系统。
❗️注意:
- Linux下文件的存储是属性和内容分离存储的
- Linux下,保存文件属性的集合叫做 inode ,一个文件,一个 inode ,inode 内有一个唯一的标识符,叫做 inode 号
所以⼀个文件的属性 inode 长什么样子呢?
cpp
/*
* Structure of an inode on the disk
*/
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 */
union
{
struct
{
__le32 l_i_reserved1;
} linux1;
struct
{
__le32 h_i_translator;
} hurd1;
struct
{
__le32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT2_N_BLOCKS]; /* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl; /* File ACL */
__le32 i_dir_acl; /* Directory ACL */
__le32 i_faddr; /* Fragment address */
union
{
struct
{
__u8 l_i_frag; /* Fragment number */
__u8 l_i_fsize; /* Fragment size */
__u16 i_pad1;
__le16 l_i_uid_high; /* these 2 fields */
__le16 l_i_gid_high; /* were reserved2[0] */
__u32 l_i_reserved2;
} linux2;
struct
{
__u8 h_i_frag; /* Fragment number */
__u8 h_i_fsize; /* Fragment size */
__le16 h_i_mode_high;
__le16 h_i_uid_high;
__le16 h_i_gid_high;
__le32 h_i_author;
} hurd2;
struct
{
__u8 m_i_frag; /* Fragment number */
__u8 m_i_fsize; /* Fragment size */
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* OS dependent 2 */
};
/*
* 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)
备注:EXT2_N_BLOCKS = 15
❗️再次注意:
- 文件名属性并未纳入到 inode 数据结构内部
- inode 的大小⼀般是 128 字节或者 256 ,我们后面统一 128 字节
- 任何文件的内容大小可以不同,但是属性大小一定是相同的
到目前为止,相信大家还有两个问题:
-
我们已经知道硬盘是典型的 "块" 设备,操作系统读取硬盘数据的时候,读取的基本单位是 "块"。"块" 又是硬盘的每个分区下的结构,难道 "块" 是随意的在分区上排布的吗?那要怎么找到"块"呢?
-
还有就是上面提到的存储文件属性的 inode ,又是如何放置的呢?文件系统就是为了组织管理这些的!!
3. ext2 文件系统
3-1 宏观认识
所有的准备⼯作都已经做完,是时候认识下⽂件系统了。我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的
文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。
在 Linux 系统中,最常见的是 ext2 系列的文件系统。其早期版本为 ext2 ,后来又发展出 ext3 和 ext4。
ext3 和 ext4 虽然对 ext2 进行了增强,但是其核⼼设计并没有发生变化,我们仍是以较老的 ext2 作为演示对象。
ext2 文件系统将整个分区划分成若干个同样大小的块组 (Block Group),如下图所示。
只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件。

上图中启动块(Boot Block/Sector)的大小是确定的,为 1KB ,由 PC 标准规定,用来存储磁盘分区信息和启动信息,
任何文件系统都不能修改启动块。启动块之后才是 ext2 文件系统的开始。
3-2 Block Group
ext2 文件系统会根据分区的大小划分为数个 Block Group。而每个 Block Group 都有着相同的结构组成。
就像政府管理各区的的治理模式一样。
3-3 块组内部构成
3-3-1 超级块(Super Block)
存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck 和 inode的总量,
未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,
最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block的信息被破坏,可以说整个⽂件系统结构就被破坏了
超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。
为了保证⽂件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信息,
在这种情况下也能正常访问。所以⼀个文件系统的 super block 会在多个 block group 中进行备份,
这些 super block 区域的数据保持⼀致。
cpp
/*
* Structure of the 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 */
__le32 s_log_block_size; /* Block size */
__le32 s_log_frag_size; /* Fragment size */
__le32 s_blocks_per_group; /* # Blocks per group */
__le32 s_frags_per_group; /* # Fragments per group */
__le32 s_inodes_per_group; /* # Inodes per group */
__le32 s_mtime; /* Mount time */
__le32 s_wtime; /* Write time */
__le16 s_mnt_count; /* Mount count */
__le16 s_max_mnt_count; /* Maximal mount count */
__le16 s_magic; /* Magic signature */
__le16 s_state; /* File system state */
__le16 s_errors; /* Behaviour when detecting errors */
__le16 s_minor_rev_level; /* minor revision level */
__le32 s_lastcheck; /* time of last check */
__le32 s_checkinterval; /* max. time between checks */
__le32 s_creator_os; /* OS */
__le32 s_rev_level; /* Revision level */
__le16 s_def_resuid; /* Default uid for reserved blocks */
__le16 s_def_resgid; /* Default gid for reserved blocks */
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
/*
* These fields are for EXT2_DYNAMIC_REV superblocks only.
*
* Note: the difference between the compatible feature set and
* the incompatible feature set is that if there is a bit set
* in the incompatible feature set that the kernel doesn't
* know about, it should refuse to mount the filesystem.
*
* e2fsck's requirements are more strict; if it doesn't know
* about a feature in either the compatible or incompatible
* feature set, it must abort and not try to meddle with
* things it doesn't understand...
*/
__le32 s_first_ino; /* First non-reserved inode */
__le16 s_inode_size; /* size of inode structure */
__le16 s_block_group_nr; /* block group # of this superblock */
__le32 s_feature_compat; /* compatible feature set */
__le32 s_feature_incompat; /* incompatible feature set */
__le32 s_feature_ro_compat; /* readonly-compatible feature set */
__u8 s_uuid[16]; /* 128-bit uuid for volume */
char s_volume_name[16]; /* volume name */
char s_last_mounted[64]; /* directory where last mounted */
__le32 s_algorithm_usage_bitmap; /* For compression */
/*
* Performance hints. Directory preallocation should only
* happen if the EXT2_COMPAT_PREALLOC flag is on.
*/
__u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/
__u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
__u16 s_padding1;
/*
* Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
*/
__u8 s_journal_uuid[16]; /* uuid of journal superblock */
__u32 s_journal_inum; /* inode number of journal file */
__u32 s_journal_dev; /* device number of journal file */
__u32 s_last_orphan; /* start of list of inodes to delete */
__u32 s_hash_seed[4]; /* HTREE hash seed */
__u8 s_def_hash_version; /* Default hash version to use */
__u8 s_reserved_char_pad;
__u16 s_reserved_word_pad;
__le32 s_default_mount_opts;
__le32 s_first_meta_bg; /* First metablock block group */
__u32 s_reserved[190]; /* Padding to the end of the block */
};
3-3-2 GDT(Group Descriptor Table)
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。
每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是 inode Table,从哪里开始是 DataBlocks ,
空闲的 inode 和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。
cpp
// 磁盘级blockgroup的数据结构
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__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];
};
3-3-3 块位图(Block Bitmap)
- Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用
3-3-4 inode位图(Inode Bitmap)
- 每个 bit 表示⼀个 inode 是否空闲可用。
3-3-5 i 节点表(Inode Table)
-
存放文件属性 如 文件大小,所有者,最近修改时间等
-
当前分组所有 Inode 属性的集合
-
inode 编号以分区为单位,整体划分,不可跨分区
3-3-6 Data Block
数据区:存放文件内容,也就是一个一个的 Block 。根据不同的文件类型有以下几种情况:
-
对于普通文件,文件的数据存储在数据块中。
-
对于目录,该目录下的所有文件名和目录名存储在所在目录的数据块中,除了⽂件名外,ls -l命令看到的其它信息保存在该⽂件的inode中。
-
Block 号按照分区划分,不可跨分区
3-4 inode 和 datablock映射(弱化)
-
inode 内部存在 __le32 i_block[EXT2_N_BLOCKS]; /* Pointers to blocks */ ,EXT2_N_BLOCKS =15,就是用来进行 inode 和block 映射的
-
这样文件 = 内容 + 属性,就都能找到了。

cpp
/*
* Structure of an inode on the disk
*/
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 */
union
{
struct
{
__le32 l_i_reserved1;
} linux1;
struct
{
__le32 h_i_translator;
} hurd1;
struct
{
__le32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT2_N_BLOCKS]; /* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl; /* File ACL */
__le32 i_dir_acl; /* Directory ACL */
__le32 i_faddr; /* Fragment address */
union
{
struct
{
__u8 l_i_frag; /* Fragment number */
__u8 l_i_fsize; /* Fragment size */
__u16 i_pad1;
__le16 l_i_uid_high; /* these 2 fields */
__le16 l_i_gid_high; /* were reserved2[0] */
__u32 l_i_reserved2;
} linux2;
struct
{
__u8 h_i_frag; /* Fragment number */
__u8 h_i_fsize; /* Fragment size */
__le16 h_i_mode_high;
__le16 h_i_uid_high;
__le16 h_i_gid_high;
__le32 h_i_author;
} hurd2;
struct
{
__u8 m_i_frag; /* Fragment number */
__u8 m_i_fsize; /* Fragment size */
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* OS dependent 2 */
};
#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 的⼤⼩通常是 128 字节 或 256 字节
❗️ 思考:
请解释:知道 inode 号的情况下,在指定分区,请解释:对文件进行增、删、查、改是在做什么?
✅️结论:分区之后的格式化操作,就是对分区进行分组,在每个分组中写入 SB 、GDT 、BlockBitmap 、Inode Bitmap 等管理信息,这些管理信息统称: 文件系统
只要知道文件的 inode 号,就能在指定分区中确定是哪一个分组,进而在哪一个分组确定是哪⼀个 inode
拿到 inode 文件属性和内容就全部都有了
下面,通过 touch 一个新文件来看看如何工作。
bash
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc
为了说明问题,我们将上图简化:

创建⼀个新文件主要有以下 4 个操作:
-
存储属性:内核先找到⼀个空闲的i节点(这⾥是263466)。内核把⽂件信息记录到其中。
-
存储数据:该文件需要存储在三个磁盘块,内核找到了三个空闲块:300 , 500 , 800。将内核缓冲区的第⼀块数据复制到 300 ,下⼀块复制到 500 ,以此类推。
-
记录分配情况:文件内容按顺序 300 , 500 , 800 存放。内核在 inode 上的磁盘分布区记录了上述块列表。
-
添加文件名到目录:新的文件名 "abc" 。linux 如何在当前的目录中记录这个文件?内核将入口哦(263466,abc)添加到目录文件。文件名和 inode 之间的对应关系将文件名和文件的内容及属性连接起来。
3-5 目录与文件名
问题:
- 我们访问文件,都是用的文件名,没用过 inode 号啊?
- 目录是文件吗?如何理解?
答案:
- 目录也是文件,但是磁盘上没有目录的概念,只有文件属性 + 文件内容的概念。
- ⽬录的属性不用多说,内容保存的是:文件名和 Inode 号的映射关系
验证代码:
cpp
// readdir.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
exit(EXIT_FAILURE);
}
DIR *dir = opendir(argv[1]); // 系统调⽤,⾃⾏查阅
if (!dir)
{
perror("opendir");
exit(EXIT_FAILURE);
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL)
{ // 系统调⽤,⾃⾏查阅
// Skip the "." and ".." directory entries
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
{
continue;
}
printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
}
closedir(dir);
return 0;
}
chengkl4@ubuntu:~/code/test/test$ ./readdir /
Filename: mnt, Inode: 1048577
Filename: tmp, Inode: 1179650
Filename: sys, Inode: 917506
Filename: libx32, Inode: 17
Filename: srv, Inode: 786434
Filename: lib64, Inode: 16
Filename: sbin, Inode: 18
Filename: dev, Inode: 131073
Filename: swapfile, Inode: 12
Filename: run, Inode: 1048578
Filename: log.txt, Inode: 20
Filename: proc, Inode: 1179649
Filename: lost+found, Inode: 11
Filename: etc, Inode: 262145
Filename: lib, Inode: 14
Filename: opt, Inode: 917505
Filename: usr, Inode: 1179651
Filename: lib32, Inode: 15
Filename: boot, Inode: 655361
Filename: var, Inode: 655365
Filename: product-service-1.0-SNAPSHOT.jar, Inode: 19
Filename: bin, Inode: 13
Filename: media, Inode: 393217
Filename: home, Inode: 786433
Filename: root, Inode: 655362
whb@bite:~/code/test/test$ ls -li /
total 1014436
13 lrwxrwxrwx 1 root root 7 Sep 14 2020 bin -> usr/bin
655361 drwxr-xr-x 3 root root 4096 May 6 14:34 boot
2 drwxr-xr-x 17 root root 3880 Jul 17 10:39 dev
262145 drwxr-xr-x 98 root root 4096 Oct 27 14:56 etc
786433 drwxr-xr-x 6 root root 4096 Sep 4 14:56 home
14 lrwxrwxrwx 1 root root 7 Sep 14 2020 lib -> usr/lib
15 lrwxrwxrwx 1 root root 9 Sep 14 2020 lib32 -> usr/lib32
16 lrwxrwxrwx 1 root root 9 Sep 14 2020 lib64 -> usr/lib64
17 lrwxrwxrwx 1 root root 10 Sep 14 2020 libx32 ->
usr/libx32
20 -rw-r--r-- 1 root root 461 Jul 16 15:51 log.txt
11 drwx------ 2 root root 16384 Sep 14 2020 lost+found
393217 drwxr-xr-x 4 root root 4096 Sep 14 2020 media
1048577 drwxr-xr-x 3 root root 4096 Oct 17 17:47 mnt
917505 drwxr-xr-x 3 root root 4096 May 6 14:37 opt
1 dr-xr-xr-x 169 root root 0 Jul 17 10:26 proc
19 -rw-r--r-- 1 root root 45457485 Feb 3 2024 product-service-
1.0-SNAPSHOT.jar
655362 drwx------ 13 xjh xjh 4096 Oct 27 09:36 root
2 drwxr-xr-x 25 root root 780 Oct 28 20:36 run
18 lrwxrwxrwx 1 root root 8 Sep 14 2020 sbin -> usr/sbin
786434 drwxr-xr-x 2 root root 4096 Apr 23 2020 srv
12 -rw------- 1 root root 993249280 Sep 14 2020 swapfile
1 dr-xr-xr-x 13 root root 0 Jul 17 18:26 sys
1179650 drwxrwxrwt 20 root root 4096 Oct 28 20:39 tmp
1179651 drwxr-xr-x 14 root root 4096 Nov 10 2023 usr
655365 drwxr-xr-x 12 root root 4096 Jun 16 16:40 var
-
所以,访问文件,必须打开当前目录,根据文件名,获得对应的 inode 号,然后进行文件访问
-
所以,访问文件必须要知道当前工作目录,本质是必须能打开当前工作目录文件,查看目录文件的内容!
bash
chengkl4@ubuntu:~/code/test/test$ pwd
/home/whb/code/test/test
whb@bite:~/code/test/test$ ls -li
total 24
1596260 -rw-rw-r-- 1 whb whb 814 Oct 28 20:32 test.c
#⽐如:要访问test.c, 就必须打开test(当前⼯作⽬录),然后才能获取test.c对应的inode进⽽对⽂件进⾏访问。
3-6 路径解析
问题:
- 打开当前工作目录文件,查看当前工作目录文件的内容?当前工作目录不也是文件吗?我们访问当前工作目录不也是只知道当前工作目录的文件名吗?要访问它,不也得知道当前工作目录的inode吗?
答案1:
- 所以也要打开:当前工作目录的上级目录,额...,上级目录不也是目录吗??不还是上面的问题吗?
答案2:
- 所以类似"递归",需要把路径中所有的目录全部解析,出⼝是"/"根⽬录。
最终答案3:
- 而实际上,任何文件,都有路径,访问目标文件,比如 /home/chengkl4/code/test/test/test.c都要从根目录开始,依次打开每⼀个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到 test.c 。这个过程叫做 Linux 路径解析。
💡 注意:
- 所以,我们知道了:访问文件必须要有目录 + 文件名 = 路径的原因
- 根目录固定文件名,inode 号,无需查找,系统开机之后就必须知道
可是路径谁提供?
-
你访问文件,都是指令/工具访问,本质是进程访问,进程有 CWD !进程提供路径。
-
你 open 文件,提供了路径
可是最开始的路径从哪里来?
-
所以 Linux 为什么要有根目录, 根目录下为什么要有那么多缺省⽬录?
-
你为什么要有家目录,你自己可以新建目录?
-
上面所有行为:本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系统指定的目录下新建,这不就是天然就有路径了嘛!
-
系统 + 用户共同构建 Linux 路径结构
3-7 路径缓存
问题1:
- Linux 磁盘中,存在真正的目录吗?
答案:
- 不存在,只有文件。只保存文件属性 + 文件内容
问题2:
- 访问任何文件,都要从根目录开始进行路径解析?
答案:
- 原则上是,但是这样太慢,所以 Linux 会缓存历史路径结构
问题2:
- Linux目录的概念,怎么产生的?
答案:
- 打开的文件是目录的话,由 OS 自己在内存中进行路径维护
Linux 中,在内核中维护树状路径结构的内核结构体叫做: struct dentry
cpp
struct dentry
{
atomic_t d_count;
unsigned int d_flags; /* protected by d_lock */
spinlock_t d_lock; /* per dentry lock */
struct inode *d_inode; /* 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; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct list_head d_lru; /* LRU list */
/*
* d_child and d_rcu can share memory
*/
union
{
struct list_head d_child; /* child of parent list */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
#ifdef CONFIG_PROFILING
struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
int d_mounted;
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
注意:
-
每个文件其实都要有对应的 dentry 结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构
-
整个树形节点也同时会隶属于 LRU(Least Recently Used,最近最少使用) 结构中,进行节点淘汰
-
整个树形节点也同时会隶属于 Hash ,方便快速查找
-
更重要的是,这个树形结构,整体构成了 Linux 的路径缓存结构,打开访问任何⽂件,都在先在这棵树下根据路径进行查找,找到就返回属性 inode 和内容,没找到就从磁盘加载路径,添加 dentry 结构,缓存新路径

3-8 挂载分区
我们已经能够根据 inode 号在指定分区找文件了,也已经能根据目录文件内容,找指定的 inode 了,在指定的分区内,
我们可以为所欲为了。可是:问题:inode 不是不能跨分区吗?Linux 不是可以有多个分区吗?
我怎么知道我在哪⼀个分区???
3-8-1 一个实验:
bash
$ dd if=/dev/zero of=./disk.img bs=1M count=5 #制作⼀个⼤的磁盘块,就当做⼀个分区
$ mkfs.ext4 disk.img # 格式化写⼊⽂件系统
$ mkdir /mnt/mydisk # 建⽴空⽬录
$ df -h # 查看可以使⽤的分区
Filesystem Size Used Avail Use% Mounted on
udev 956M 0 956M 0% /dev
tmpfs 198M 724K 197M 1% /run
/dev/vda1 50G 20G 28G 42% /
tmpfs 986M 0 986M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 986M 0 986M 0% /sys/fs/cgroup
tmpfs 198M 0 198M 0% /run/user/0
tmpfs 198M 0 198M 0% /run/user/1002
$ sudo mount -t ext4 ./disk.img /mnt/mydisk/ # 将分区挂载到指定的⽬录
$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 956M 0 956M 0% /dev
tmpfs 198M 724K 197M 1% /run
/dev/vda1 50G 20G 28G 42% /
tmpfs 986M 0 986M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 986M 0 986M 0% /sys/fs/cgroup
tmpfs 198M 0 198M 0% /run/user/0
tmpfs 198M 0 198M 0% /run/user/1002
/dev/loop0 4.9M 24K 4.5M 1% /mnt/mydisk
$ sudo umount /mnt/mydisk # 卸载分区
chengkl4@Ubuntu:/mnt$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 956M 0 956M 0% /dev
tmpfs 198M 724K 197M 1% /run
/dev/vda1 50G 20G 28G 42% /
tmpfs 986M 0 986M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 986M 0 986M 0% /sys/fs/cgroup
tmpfs 198M 0 198M 0% /run/user/0
tmpfs 198M 0 198M 0% /run/user/1002
📌 注意:
/dev/loop0 在Linux系统中代表第⼀个循环设备(loop device)。循环设备,也被称为回环设备或者 loopback 设备,是一种伪设备(pseudo-device),它允许将文件作为块设备(block device)来使用。这种机制使得可以将文件(比如 ISO 镜像文件)挂载(mount)为文件系统,就像它们是物理硬盘分区或者外部存储设备⼀样。
bash
chengkl4@ubuntu:/mnt$ ls /dev/loop* -l
brw-rw---- 1 root disk 7, 0 Oct 17 18:24 /dev/loop0
brw-rw---- 1 root disk 7, 1 Jul 17 10:26 /dev/loop1
brw-rw---- 1 root disk 7, 2 Jul 17 10:26 /dev/loop2
brw-rw---- 1 root disk 7, 3 Jul 17 10:26 /dev/loop3
brw-rw---- 1 root disk 7, 4 Jul 17 10:26 /dev/loop4
brw-rw---- 1 root disk 7, 5 Jul 17 10:26 /dev/loop5
brw-rw---- 1 root disk 7, 6 Jul 17 10:26 /dev/loop6
brw-rw---- 1 root disk 7, 7 Jul 17 10:26 /dev/loop7
crw-rw---- 1 root disk 10, 237 Jul 17 10:26 /dev/loop-control
3-8-2 一个结论
-
分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。
-
所以,可以根据访问目标文件的"路径前缀"准确判断我在哪⼀个分区。(理解到这个层面)
3-9 文件系统总结
- 下面用几张图总结,一张是画的,其他都是在网上找的.主要想从不同角度说明。




4. 软硬连接
4-1 硬连接
我们看到,真正找到磁盘上⽂件的并不是⽂件名,⽽是inode。其实在linux中可以让多个⽂件名对应于同⼀个inode。
bash
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -li abc def
263466 abc
263466 def
- abc 和 def 的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。
- 我们在删除文件时干了两件事情:
-
在目录中将对应的记录删除,
-
将硬连接数 -1 ,如果为 0 ,则将对应的磁盘释放。
-
4-2 软链接
硬链接是通过 inode 引用另外一个文件,软链接是通过名字引用另外⼀个文件,
但实际上,新的文件和被引用的文件的 inode 不同,应用常见上可以想象成⼀个快捷方式。在 shell 中的做法
bash
[root@localhost linux]# ln -s abc.s abc
[root@localhost linux]# ls -li
263563 -rw-r--r--. 2 root root 0 9⽉ 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9⽉ 15 17:53 abc.s -> abc
263563 -rw-r--r--. 2 root root 0 9⽉ 15 17:45 def

下⾯解释⼀下⽂件的三个时间:
-
Access 最后访问时间
-
Modify 文件内容最后修改时间
-
Change 属性最后修改时间
4-3 软硬连接对比
-
软连接是独立文件
-
硬链接只是文件名和目标文件 inode 的映射关系
4-4 软硬连接的用途:
硬链接
-
.和... 就是硬链接
-
文件备份
软连接
- 类似快捷方式