磁盘相关结构

什么是文件系统:

  • 我们看一下百度百科对文件系统的介绍

文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构;即在存储设备上组织文件的方法。操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。文件系统由三部分组成:文件系统的接口,对对象操纵和管理的软件集合,对象及属性。从系统角度来看,文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。

  • 可以看出文件系统是一个对磁盘中的文件进行管理和储存,并对文件进行保护和检索的系统. 在Linux系统中,一切皆文件,所以系统中一定会存在大量的文件,打开的和未打开的, 我们都知道操作系统会将要使用的文件加载到内存中,那么剩下的文件是存在于磁盘里的,这其中就存在几个问题

1.这个文件是怎么被找到的?
2.怎么保证它是我们想打开的文件呢?

  • 这两个问题就是我们这篇文章的核心了

磁盘结构

  • 首先我们需要知道电脑关机后数据是存放在磁盘上的,所以文件的数据也是存放在磁盘里的,所以把一个文件加载到内存里是分为两步的
  1. 在磁盘上找到文件
  2. 把文件加载到内存

磁盘面, 磁道, 磁头, 柱面, 扇区

  • 这就不得不涉及到一些硬件, 这里简单讲解一下, 有兴趣的读者可以自己深入了解一下
  • 参考下图,可以把每个磁盘面(可以把它理解为光盘, 不过它上面的磁道存储了信息)理解为多个同一圆心不同半径的圆, 多个磁盘面中相同半径的圆就构成了柱面, 磁盘面上的这些小方块就是扇区,相邻两个同心圆中间的间距就是磁道, 和机械臂杆相连的在每个磁盘面上下各有一个的就是磁头, 他负责读取数据
    在这里插入图片描述
  • 对磁盘有了一些简单的了解之后我们来思考一下磁头是怎么读取数据的, 首先确定要访问的柱面(cylinder), 接着定位磁头(header), 最后定位扇区(sector), 这就是CHS地址定位

以数组形式理解磁盘结构

  • 如果是这样进行查找磁盘上的文件, 肯定是不利于计算机查找的, 但是你想象一下, 每个柱面的平面图是不是都是一个平面圆? 这样我们把它展开, 在加上扇区的存在, 这不就变成了一个数组吗?
    在这里插入图片描述
  • 可以把磁道-扇区看作是一个二维数组, 由于存在多个柱面, 又可以把柱面(磁道–扇区)看做一个二维数组, 学过C语言我们知道计算机内部实际上没有什么二维数组的概念,都是以一维数组的形式存储的, 按照这个**柱面-(磁道-扇区)**数组的遍历顺序起始也可以得到CHS, 先确定柱面, 然后确定磁头位置, 最后遍历扇区.
  • 既然我们把磁盘中扇区的位置转化成了数组的形式, 那么访问它一定是需要下标(LBA,(Logical Block Address)的, 那么怎么确定这个下标呢, 对操作系统来说, 只需要使用LBA即可, 这就涉及到了CHSLBA的转换了,.

CHS 和 LBA 的转换

  • CHSLBA:
    • 磁头数*每磁道扇区数=单个柱⾯的扇区总数
    • LBA=柱⾯号C * 单个柱⾯的扇区总数 + 磁头号H * 每磁道扇区数 + 扇区号S - 1
  • LBACHS(//表示整除):
    • 柱⾯号C=LBA//(磁头数*每磁道扇区数)
    • 磁头号H=(LBA%(磁头数*每磁道扇区数))//每磁道扇区数
    • 扇区号S=(LBA%每磁道扇区数)+1

文件系统

文件

  • 硬盘是典型的"块"设备, OS在读取数据的时候, 并不会一个一个扇区的读, 而是一次连续读取多个扇区(一个扇区为512字节), 即一次读取多个"块". 硬盘的每个分区是被划分为一个个的"块", 一个"块"的大小是由格式化的时候确定的, 一个"块"的大小最常见的为4kb, 即8个扇区的大小, "块"是文件存取的最小单位, 可以使用stat命令查看
stat test.txt
  File: test.txt
  Size: 265883          Blocks: 520        IO Block: 4096   regular file
Device: 8,32    Inode: 12888       Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/      wk)   Gid: ( 1000/      wk)
Access: 2025-11-04 08:27:45.312731603 +0800
Modify: 2025-11-04 08:27:43.472993418 +0800
Change: 2025-11-04 08:27:43.482993598 +0800
 Birth: 2025-11-04 08:27:43.472993418 +0800
  • 文件的本质: 文件 = 内容 + 属性, 在Linux中内容和属性时分开存储的, 使用ls -al时可以看到文件的各种属性和文件名
ls -al *.txt
-rw-r--r-- 1 wk wk 265883 Nov  4 08:27 test.txt
-rw-r--r-- 1 wk wk      6 Nov  4 08:29 test1.txt
  • 每行的内容分别为属性, 硬链接数, 文件所有者, 组, 文件大小, 最后修改时间, 文件名
  • 那么我们就要思考一个问题了, 既然数据是存储在块中的, 那么属性存储在哪里呢? 在ext2文件系统中, 存储文件属性的区域叫做inode, 每一个文件都有自己的inode, 为了更好的理解inode, 接下来我们深入了解一下etx2文件系统
  • 可以使用ls命令的-i参数来查看一个文件的inode
ls -il test.txt
12888 -rw-r--r-- 1 wk wk 265883 Nov  4 08:27 test.txt
  • 在Linux2.6.11内核代码中, inode是一个结构体, 里面包含了各种字段, 其中i_block[EXT2_N_BLOCKS]是指向数据块的指针
struct ext2_inode {
	/**
	 * 文件类型和访问权限。
	 *		0:未知文件
	 *		1:普通文件
	 *		2:目录
	 *		3:字符设备
	 *		4:块设备
	 *		5:命名管道
	 *		6:套接字
	 *		7:符号链接
	 */
	__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 */
};
  • ext2文件系统将整个分区划分成若⼲个同样⼤⼩的块组(就是下图中的Block Group, 其他属性我们稍后一起介绍),如下图所⽰. 只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘⽂件。
  • 先讲一下怎么通过inode编号定位文件位置, 现在有一个inode编号, 先计算其所属块组, 然后在通过计算inode table中找到对应的inode, 然后进行属性的读取, 同时也获得了数据块指针, 然后在Data Blocks中读取即可
  • 你可能会奇怪, 我们平时访问文件的时候都是使用文件名的呀, 并没有使用inode编号, 而且上面的这个结构体中并没有关于文件名的字段, 这是为什么呢?

ext2文件系统中, 文件名和属性是分开存储的,因为要inode结构体的大小一般是固定的,一般为128字节,文件名一般为字符串, 且字符串的大小不固定, 存储在inode结构体中会导致结构体大小浮动

  • 这就又引出来一个问题了–那文件名存储在哪里呢?这就要谈谈目录了

在Linux中,一切皆文件, 目录也是一个文件,既然它是文件, 也就有和下图一样的属性了, 那一个目录的数据不就是各种文件吗? 所以读取了一个目录的data blocks后, 目录的属性保存的就是文件名和inode的映射关系, 就可以通过inode和文件名的映射关系加载我们想要打开的文件的内容了,所以文件名可以说是存储在目录的data blocks里了

  • 所以Linux中根本不存在目录, 只有文件
    在这里插入图片描述

一个块组的各个属性

  • 通过上图我们可以看到一个块组内包含了Super Block(超级块), GDT(块组描述符表), Block Bitmap(块位图), inode Bitmap(inode位图), inode Table(inode表), Data Blocks(数据块), 接下来我们逐一解释每个属性是什么以及它们有什么作用
  • inode Table: 存储一个文件的各种属性,
  • inode Bitmap: 标记inode是否空闲可用
  • Block Bitmap: 记录哪个数据块(Data Blocks)是否被占用(删除文件的时候只需要把占用的位置为1即可, 所以删除文件一般很快)
  • GDT: 块组描述符表,描述块组属性信息,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组 的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
  • Super Block(存在于个别组中,且每个组的信息都是一样的,一个出问题剩下的可以拷贝进行恢复):文件系统信息。存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolckinode的总量,未使⽤的blockinode的数量,⼀个blockinode的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block的信息被破坏,可以说整个⽂件系统结构就被破坏了。
  • Data Blocks: 存储文件内容的位置

了解了上述属性后,我们来拆解一下创建文件的步骤

  1. 首先通过inode bitmap找到一个空闲的节点
  2. 通过block bitmap找到空闲的块
  3. data blocks中存储数据
  4. 把文件名和inode的映射关系添加到当前根目录的data blocks

路径缓存

  • 通过以上介绍我们知道了访问一个文件实际上是在访问目录(实际上还是在访问文件), 通过不断的访问目录最终得到了我们想要的数据. 数据是存储在磁盘上的, 每一次访问数据实际上是在做磁盘IO, 要知道磁盘的速度可是很慢的, 而且定位的话是要从根目录开始的, 这样效率太低了, 解决这个问题也很简单, OS每次把用户访问的路径缓存起来不就行了吗? 这样每次直接去加载文件内容就可以了, 不需要在去加载每个目录的data blocks了, 使用一个多叉树即可维护, 在Linux中维护这个树状结构的内核结构体叫做struct dentry
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 16-byte range, with 16-byte alignment.
	 */
	struct dentry *d_parent;	/* parent directory */
	struct qstr d_name;
	struct list_head d_lru;		/* LRU list */
	struct list_head d_child;	/* child of parent list */
	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 */
 	struct rcu_head d_rcu;
	struct dcookie_struct *d_cookie; /* cookie, if any */
	struct hlist_node d_hash;	/* lookup hash list */	
	int d_mounted;
	unsigned char d_iname[DNAME_INLINE_LEN_MIN];	/* small names */
};

  • 读者也可以自行测试, 在系统刚打开的时候使用下面这个命令, 多测试几次, 除了第一次比较慢之外, 其他时候应该是秒出的
find . -name *.cpp

挂载分区

  • 首先inode是不可以跨分区的, 但是Linux存在多个分区, 我怎么知道我位于哪个分区呢?在Linux中可以使用df .来判断当前处于哪个分区, df -h可以查看分区情况
  • 我们来做个实验
 dd if=/dev/zero of=./disk.img bs=1M count=5  
 mkfs.ext4 disk.img
 df -h
 sudo mount -t ext4 ./disk.img /mnt/mydisk/ 
 df -h
  • 对比前后输出会发现多了一行
  • 未挂载前

Filesystem      Size  Used Avail Use% Mounted on
none            3.9G     0  3.9G   0% /usr/lib/modules/5.15.153.1-microsoft-standard-WSL2
none            3.9G  4.0K  3.9G   1% /mnt/wsl
drivers         476G  137G  340G  29% /usr/lib/wsl/drivers
/dev/sdc       1007G   19G  938G   2% /
none            3.9G   80K  3.9G   1% /mnt/wslg
none            3.9G     0  3.9G   0% /usr/lib/wsl/lib
rootfs          3.9G  2.2M  3.9G   1% /init
none            3.9G  516K  3.9G   1% /run
none            3.9G     0  3.9G   0% /run/lock
none            3.9G     0  3.9G   0% /run/shm
none            3.9G   76K  3.9G   1% /mnt/wslg/versions.txt
none            3.9G   76K  3.9G   1% /mnt/wslg/doc
C:\             476G  137G  340G  29% /mnt/c
D:\             477G  325G  153G  68% /mnt/d
tmpfs           788M   28K  788M   1% /run/user/1000
  • 挂载后
Filesystem      Size  Used Avail Use% Mounted on
none            3.9G     0  3.9G   0% /usr/lib/modules/5.15.153.1-microsoft-standard-WSL2
none            3.9G  4.0K  3.9G   1% /mnt/wsl
drivers         476G  137G  340G  29% /usr/lib/wsl/drivers
/dev/sdc       1007G   19G  938G   2% /
none            3.9G   80K  3.9G   1% /mnt/wslg
none            3.9G     0  3.9G   0% /usr/lib/wsl/lib
rootfs          3.9G  2.2M  3.9G   1% /init
none            3.9G  516K  3.9G   1% /run
none            3.9G     0  3.9G   0% /run/lock
none            3.9G     0  3.9G   0% /run/shm
none            3.9G   76K  3.9G   1% /mnt/wslg/versions.txt
none            3.9G   76K  3.9G   1% /mnt/wslg/doc
C:\             476G  137G  340G  29% /mnt/c
D:\             477G  325G  153G  68% /mnt/d
tmpfs           788M   28K  788M   1% /run/user/1000
/dev/loop0      4.7M   24K  4.4M   1% /mnt/mydisk
  • 挂载后是可以访问的
 wk ~ ls /mnt/mydisk/
lost+found
 wk ~ sudo touch /mnt/mydisk/1.txt
 wk ~ ls /mnt/mydisk/1.txt
/mnt/mydisk/1.txt
  • 接下来执行这条命令后,再次使用df -h和访问这个文件,看看是否还可以访问
 wk  ~   sudo umount /mnt/mydisk
 wk  ~  df -h
Filesystem      Size  Used Avail Use% Mounted on
none            3.9G     0  3.9G   0% /usr/lib/modules/5.15.153.1-microsoft-standard-WSL2
none            3.9G  4.0K  3.9G   1% /mnt/wsl
drivers         476G  137G  340G  29% /usr/lib/wsl/drivers
/dev/sdc       1007G   19G  938G   2% /
none            3.9G   80K  3.9G   1% /mnt/wslg
none            3.9G     0  3.9G   0% /usr/lib/wsl/lib
rootfs          3.9G  2.2M  3.9G   1% /init
none            3.9G  520K  3.9G   1% /run
none            3.9G     0  3.9G   0% /run/lock
none            3.9G     0  3.9G   0% /run/shm
none            3.9G   76K  3.9G   1% /mnt/wslg/versions.txt
none            3.9G   76K  3.9G   1% /mnt/wslg/doc
C:\             476G  137G  340G  29% /mnt/c
D:\             477G  325G  153G  68% /mnt/d
tmpfs           788M   28K  788M   1% /run/user/1000
 wk  ~  ls /mnt/mydisk/
 wk  ~  ls /mnt/mydisk/ -al
total 8
drwxr-xr-x 2 root root 4096 Nov 25 23:19 .
drwxr-xr-x 7 root root 4096 Nov 25 23:19 ..
  • 可以看到是一个空的目录, 之前我是创建了一个txt文件的, 所以如果想要写入文件系统, 需要和指定的目录挂载后才可以使用, 从df -h命令的输出可以看出, 我们可以通过这些前缀判断当前处于哪一个分区

软硬链接

软链接

  • 创建软链接的方法:
ln -s targetfile linkfile
  • 效果类似这样, 前面的属性l表示这是一个链接文件
    在这里插入图片描述
  • 可以看到原来的文件和链接文件的inode编号是不一样的,这也就表示这两个文件不是同一个文件, 可以这两个文件的关系理解为Windows的快捷方式, 访问的都是相同的内容
    在这里插入图片描述
  • 解除链接
unlink linkfile
或者直接rm linkfile也可以

硬链接

  • 直接使用ln targetfile linkfile即可, 通过下图中inode可以看出这是两个相同的文件, 所以硬链接实际上就是统一个文件的不同名字
    在这里插入图片描述
  • 可以看到建立硬链接后linkfiletargetfile的硬链接数都变成了2
    在这里插入图片描述
  • 这里我们新创建一个目录, 可以看到它的.的硬链接数是2, 除了.外还有父目录的一个指向
    在这里插入图片描述
  • 通过这张图也可以看出.是硬链接,他和上级目录查看的Code目录的inode是一样的
    在这里插入图片描述
  • 可以把...都看做是硬链接文件

ext3和ext4相对于ext2的扩展

这里只介绍一部分, 有兴趣的读者可以自己去看看

etx3的扩展

  • 在磁盘上开辟一个“日志区域”, 记录元数据(有时包括数据)的变更.
  • 好处:
    • 系统崩溃后,无需长时间fsck,只需重放日志
    • 极大提升文件系统一致性和恢复速度
  • 日志模式:
    • journal:数据+元数据都记日志(最安全,最慢)
    • ordered(默认):先写数据,再记元数据日志
    • writeback:只记元数据日志(最快,安全性稍低)

ext4的一些扩展

  • Extent代替间接块映射
    • ext2/3存在的问题: 大文件需多级间接块(12 direct + 1/2/3 级间接),查找效率低
    • 使用Extent: 连续块范围表示为[起始块, 长度], 减少元数据开销, 能提高大文件IO的性能
  • 延迟分配:
    • 写数据时并不立即分配磁盘块, 只有当数据真正刷盘时才分配
    • 好处 :
      • 更容易找到大块连续空间->减少碎片
      • 合并小写入->提升吞吐
  • 更大的文件系统和文件支持:
    • inode使用48-bit块寻址(ext2ext3使用32-bit)
    • 最大文件系统: 1EB(12024 * 1024 TB)
    • 最大单文件: 16TB

这篇文章就到这里了,如果觉得写的还不错的话,欢迎点赞加关注,如果有写的不对的地方,欢迎批评指正( •̀ ω •́ )✧

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐