1. ext2 文件系统
1-1 宏观认识
所有的准备⼯作都已经做完,是时候认识下文件系统了。我们想要在硬盘上储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的⽬的就是组织和管理硬盘中的文件。在Linux 系统中,最常⻅的是 ext2 系列的文件系统。其早期版本为 ext2,后来⼜发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2 进⾏了增强,但是其核⼼设计并没有发⽣变化,我们仍是以较⽼的 ext2 作为演⽰对象。
ext2文件系统将整个分区划分成若⼲个同样⼤⼩的块组 (Block Group),如下图所⽰。只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘文件。

上图中启动块(Boot Block/Sector)的⼤⼩是确定的,为1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。
1-2 Block Group
ext2文件系统会根据分区的⼤⼩划分为数个Block Group。⽽每个Block Group都有着相同的结构组成。政府管理各区的例⼦
1-3 块组内部构成
1-3-1 超级块(Super Block)
存放文件系统本⾝的结构信息,描述整个分区的文件系统信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。 为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证文件系统的super block信息在这种情况下也能正常访问。所以⼀个文件系统的super block会在多个block group中进⾏备份,这些super block区域的数据保持⼀致。
1-3-2 GDT(Group Descriptor Table)
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组 的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
bash
// 磁盘级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];
};
1-3-3 块位图(Block Bitmap)
• Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
1-3-4 inode位图(Inode Bitmap)
• 每个bit表⽰⼀个inode是否空闲可用。
1-3-5 i节点表(Inode Table)
• 存放文件属性 如 文件⼤⼩,所有者,最近修改时间等
• 当前分组所有Inode属性的集合
• inode编号以分区为单位,整体划分,不可跨分区
1-3-6 Data Block
数据区:存放文件内容,也就是⼀个⼀个的Block。根据不同的文件类型有以下⼏种情况:
• 对于普通文件,文件的数据存储在数据块中。
• 对于⽬录,该⽬录下的所有文件名和⽬录名存储在所在⽬录的数据块中,除了文件名外,ls -l命令看到的其它信息保存在该文件的inode中。

• Block 号按照分区划分,不可跨分区
1-4 inode和datablock映射(弱化)
• inode内部存在
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ , EXT2_N_BLOCKS
=15,就是用来进⾏inode和block映射的
• 这样文件=内容+属性,就都能找到了。

📌 思考:
• 请解释:知道inode号的情况下,在指定分区,请解释:对文件进⾏增、删、查、改是在做什么?
💡 结论:• 分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SuperBlock、GDT、Block Bitmap、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之间的对应关系将文件名和文件的内容及属性连接起来。
1-5 ⽬录与文件名
问题:
• 我们访问文件,都是用的文件名,没用过inode号啊?
• ⽬录是文件吗?如何理解?
答案:
• ⽬录也是文件,但是磁盘上没有⽬录的概念,只有文件属性+文件内容的概念。
• ⽬录的属性不用多说,内容保存的是:文件名和Inode号的映射关系
cpp
#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;
}
bash
whb@bite:~/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
whb@bite:~/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进⽽对
文件进⾏访问。
1-6 路径解析
问题:打开当前⼯作⽬录文件,查看当前⼯作⽬录文件的内容?当前⼯作⽬录不也是文件吗?我们访问当前⼯作⽬录不也是只知道当前⼯作⽬录的文件名吗?要访问它,不也得知道当前⼯作⽬录的inode吗?
答案1:所以也要打开:当前⼯作⽬录的上级⽬录,额....,上级⽬录不也是⽬录吗??不还是上⾯的问题吗?
答案2:所以类似"递归",需要把路径中所有的⽬录全部解析,出⼝是"/"根⽬录。
最终答案3:⽽实际上,任何文件,都有路径,访问⽬标文件,⽐如:
/home/whb/code/test/test/test.c都要从根⽬录开始,依次打开每⼀个⽬录,根据⽬录名,依次访问每个⽬录下指定的⽬录,直到访问到test.c。这个过程叫做Linux路径解析。
💡 注意:• 所以,我们知道了:访问文件必须要有⽬录+文件名=路径的原因
• 根⽬录固定文件名,inode号,⽆需查找,系统开机之后就必须知道
可是路径谁提供?
• 你访问文件,都是指令/⼯具访问,本质是进程访问,进程有CWD!进程提供路径。
CWD(Current Working Directory,当前工作目录)是每个进程都有的一个属性,表示该进程"当前所在"的目录位置。
• 你open文件,提供了路径
可是最开始的路径从哪⾥来?
• 所以Linux为什么要有根⽬录, 根⽬录下为什么要有那么多缺省⽬录?
• 你为什么要有家⽬录,你⾃⼰可以新建⽬录?
• 上⾯所有⾏为:本质就是在磁盘文件系统中,新建⽬录文件。⽽你新建的任何文件,都在你或者系统指定的⽬录下新建,这不就是天然就有路径了嘛!
• 系统+用⼾共同构建Linux路径结构.
3-7 路径缓存
问题1:Linux磁盘中,存在真正的⽬录吗?
答案:不存在,只有文件。只保存文件属性+文件内容
问题2:访问任何文件,都要从/⽬录开始进⾏路径解析?
答案:原则上是,但是这样太慢,所以Linux会缓存历史路径结构
问题3: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结构,缓存新路径

1-8 挂载分区
我们已经能够根据inode号在指定分区找文件了,也已经能根据⽬录文件内容,找指定的inode了,在指定的分区内,我们可以为所欲为了。可是:
问题:inode不是不能跨分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪⼀个分区??? 不同分区可能有相同的inode,所以不知道该inode在哪个分区。
1-8-2 ⼀个结论
• 分区写⼊文件系统,⽆法直接使用,需要和指定的⽬录关联,进⾏挂载才能使用。
• 所以,可以根据访问⽬标文件的"路径前缀"准确判断我在哪⼀个分区。(理解到这个层⾯)
操作系统通过以下机制知道 inode 在哪个分区:
超级块定位:每个分区有唯一的超级块描述 inode 表位置
设备号映射:inode 通过 super_block→s_dev 关联到物理设备
挂载表管理:VFS 维护挂载点与设备的映射关系
块组计算:通过 inode 编号计算出在哪个块组的 inode 表中
路径解析:目录遍历过程中记录每个文件所在的设备
3-9 文件系统总结
• 下⾯用⼏张图总结,⼀张是画的,其他都是在⽹上找的.主要想从不同⻆度说明.




2. 软硬连接
2-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的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode263466 的硬连接数为2。
• 我们在删除文件时⼲了两件事情:1.在⽬录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。
2-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

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