文章目录
- 1. 引入
- 2. ext2 文件系统
- 2.1 宏观认识
- 2.2 Block Group
- Data Blocks
- Block Bitmap
- inode
- inode Table
- inode Bitmap
- inode和datablock的映射
- GDT(Group Descriptor Table)块组描述符表
- Super Block(超级块)
- 2.3 磁盘格式化
- 补充
- 2.4 目录文件
- 2.5 路径解析
- 2.6 路径缓存
- 2.7 挂载分区
1. 引入
上一篇文章我们提到过:
扇区(sector):是磁盘硬件读写的最小单位。传统 HDD(机械硬盘) 通常是 512 字节,现在很多磁盘是 4K 物理扇区。
块(block):是操作系统/文件系统访问磁盘的基本单位,大小由文件系统决定,常见是 4KB。比如 ext4 的块大小通常为 4K。
磁盘也叫块设备。
引入"块"的概念:
其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个”块”(block)。
硬盘的每个分区是被划分为一个个的”块”。一个”块”的大小是由格式化的时候确定的,并且不可以更改,最常见的是4KB,即连续八个扇区组成一个 ”块”。”块”是文件存取的最小单位。
引入分区的概念:
磁盘可以抽象为一个三维数组,三维数组底层本质也是一个"一维数组",数组下标就是LBA,每个元素都是扇区
• 每个扇区都有LBA,那么8个扇区一个块,每一个块的地址我们也能算出来。
• 知道LBA:块号 = LBA/8
• 知道块号:LBA=块号*8 + n. (n是块内第几个扇区)
所以,站在文件系统的角度,就可以将磁盘看成是一个块数组——block array[n];
假设现在有一个磁盘,800GB
这个容量是比较大的,但是如果直接这样使用的话,其实是不太好管理的。就好比我们的国家,中国地域辽阔,为了方便管理,所以就划分成了一个个的省,省下面又分成市->县->乡镇->村
所以,磁盘这里也是类似的道理,一整块磁盘太大了,所以可以对磁盘进行一个分区!
比如将上面的磁盘分成这样四个区
然后我们来简单补充理解一下:OS是怎么把数据写给磁盘的?
一句话总结:
操作系统通过设备驱动程序,读写设备控制器的控制、状态和数据寄存器来访问外设;同时还会结合 DMA、中断等机制完成高效数据传输。
通俗点讲:操作系统主要通过驱动程序读写外设的寄存器来指挥外设,但搬大量数据时通常还会让 DMA 帮忙、靠中断通知。
2. ext2 文件系统
所有的准备工作都已经做完,是时候认识下文件系统了。我们想要在磁盘上存储文件,必须先把磁盘格式化为某种格式的文件系统,才能存储文件。
文件系统的目的就是组织和管理磁盘中的文件。在Linux 系统中,最常见的是 ext2 系列的文件系统。
其早期版本为 ext2,后来又发展出 ext3 和 ext4。
ext3 和 ext4 虽然对 ext2 进行了增强,但是其核心设计并没有发生变化,我们这里就以 ext2 作为演示对象。
2.1 宏观认识
物理磁盘可以被抽象成这样的扇区数组
块(block)是操作系统/文件系统访问磁盘的基本单位,大小由文件系统决定,常见是 4KB。比如 ext4 的块大小通常为 4K。
磁盘也叫块设备,一个块包含多个扇区
一整块磁盘太大了,所以可以对磁盘进行一个分区!(国->省)
只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件。
所有分区的信息,一般可以保存到第一个扇区中:
但是一个分区可能也有几百GB,也并不小。
所以就可以在分区里面继续划分(省->市)
ext2文件系统将整个分区划分成若干个同样大小的块组 (Block Group)
那现在问题就转换为如何管理一个小组了(分治思想)
上图中启动块(Boot Block/Sector)的大小是确定的,为1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。
那下面我们就来研究一下一个块组:
2.2 Block Group
ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
文件=内容+属性
内容和属性都是数据,都要被存储。
Linux中,文件的内容和属性是分开存储的。
块(block)是操作系统/文件系统访问磁盘的基本单位,大小由文件系统决定,常见是 4KB。比如 ext4 的块大小通常为 4K。磁盘也叫块设备
Data Blocks
Data Blocks 数据块真正存放文件内容(目录也是一种文件,它的数据块里存放“文件名 → inode 号”的目录项,后面介绍)
我们图是这样画的,但其实Data Blocks 占据了绝大部分的空间。
Data Blocks中的每一个数据块都有一个编号(从0开始)
比如说一个分组的Data Blocks中有这么多的数据块,这些数据块可能共同存放了多个文件的内容数据。
并且某一时刻,可能有的数据块被占用了,有的还未被使用,那如何辨别呢?
这就是下面我们要介绍的
Block Bitmap
Block Bitmap 块位图:
用一个二进制位表示本块组中一个数据块是否已分配。每一个比特位的位置就代表了不同的数据块,该比特位的值是1 表示已用,0 表示空闲。分配数据块时先查它。
下一个问题:文件的内容存放在数据块中,并且我们说Linux中,文件的内容和属性是分开存储的。那么文件的属性(元信息)存到哪里呢?
inode
inode 是 Unix/Linux 文件系统(如 ext2/ext3/ext4)里的核心概念,本质是一个结构体,全称 index node,索引节点。
inode中就几乎包含了上面列出的所有的文件属性,但是不包含文件名。
一个 inode 通常保存:
inode中有一个唯一的标识符,叫做inode号,也唯一标识了该inode对应的文件
我们可以来看一下ext2文件系统中inode的实现
(因为inode结构体的大小是固定的,当然属性的内容可以不一样)
inode Table
inode Table 是 ext2/ext3/ext4 文件系统中真正存放 inode 记录 的磁盘区域。你可以把它理解成:一个按 inode 号索引的固定大小数组,每个元素是一条磁盘 inode 记录。
基本结构
每个块组都有自己的 inode Table。
整个文件系统的 inode 总数在格式化时就确定了,不能动态增加。
每个 inode 记录大小固定:
ext2/ext3 默认 128 字节
ext4 默认 256 字节
inode Table 由连续的数据块组成,内部按固定大小切成一个个槽位。
每个槽位对应一个 inode 号。
所以 inode Table 不是“动态插入”的表,而是一块预先划分好的连续磁盘空间。
所以创建一个文件,初始时内容是空的,但也要先创建对应的inode,放入inode table中。
那如何知道inode table哪些位置是空闲的?哪些位置已被占用呢?
inode Bitmap
同样用位图表示本块组中每个 inode 是否已分配。创建文件时先在这里找空闲 inode。
总结一下:
inode Bitmap:用一个二进制位标记 inode Table 中每个 inode 是否已分配。
inode Table:实际存放 inode 记录。
创建文件时,先在 inode Bitmap 找空闲位,得到 inode 号,然后写 inode Table 对应槽位。
删除文件时,释放 inode 号,把 inode Bitmap 对应位清零,并清空或标记 inode Table 中的记录。
所以:
inode Bitmap 管分配状态,inode Table 存实际内容。
上面我们说:inode中有一个唯一的标识符,叫做inode号,也唯一标识了该inode对应的文件
ls后加个-i 选项就可以查看文件的inode号
那现在我们知道文件内容是存到data blocks中的,文件属性是存到inode table中的。现在两者是分离的。
那查找一个文件的时候,通过inode号找到inode就可以找到文件的属性,那如何找到文件内容呢?
inode和datablock的映射
ext2 inode 结构体中:
有一个数组,用来存放数据块的块号。这些块号指向真正存放文件内容的数据块。
这样就能进一步找到文件的内容了。
小文件用直接块,大文件逐级使用间接块
GDT(Group Descriptor Table)块组描述符表
每个块组对应一个描述符,记录该块组的块位图、inode 位图、inode 表的位置,以及空闲块数、空闲 inode 数、目录数等。所有描述符合起来就是 GDT。
Super Block(超级块)
存放文件系统本身的结构信息,描述整个分区的文件系统信息。
记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。
Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
那么有个问题:既然super block描述的是整个分区的文件系统信息,那为什么存在一个块组里面?
超级块在多个块组的开头都有一份拷贝。
为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作,就必须保证文件系统的super block信
息在这种情况下也能正常访问。
所以一个文件系统的super block会在多个block group中进行备份,这些super block区域的数据保持一致。
2.3 磁盘格式化
磁盘格式化,简单说就是:
在磁盘或分区上建立文件系统,让操作系统能够按文件、目录的方式存取数据。
它不是把磁盘物理上“擦干净”,而是写入一套管理规则和元数据。
作用:在分区内部创建文件系统
磁盘格式化会做这些事:
- 确定文件系统参数:
块大小
inode 数量、inode 大小
每块组多少块、多少 inode - 划分块组:
把分区分成多个块组 - 写入元数据:
超级块 Super Block
块组描述符表 GDT
块位图 Block Bitmap
inode 位图 inode Bitmap
inode 表 inode Table - 建立根目录:
创建 / 目录的 inode 和目录项
写入文件系统标识:
UUID、卷标、特性标志、挂载状态等
所以,格式化就是创建/重新初始化 超级块、GDT、位图、inode 表这些结构的过程。
补充
在同一个分区(同一个文件系统)内,块号和 inode 号都是全局唯一的。
但要注意“全局”的范围:仅限当前文件系统/分区内部。不同分区之间可以重复。
那么问题来了,我现在拿到一个inode号,怎么知道是在哪个分区呢?
“inode号”本身只是一个文件系统内部的局部标识,必须和“设备号”组合,才能在整个系统中唯一定位一个文件。
一个“块设备”对应一个设备号;而“分区”通常会被内核注册为一个独立的块设备,因此也拥有自己的设备号。
每个 inode 在内核中都有一个指针 i_sb,直接指向它所属文件系统的超级块(superblock)。超级块是文件系统的“户口本”,记录了该文件系统的全部元信息,其中就包括它所在的设备号(s_dev)。因此,内核只需从 inode 出发,通过 i_sb 就能知道它属于哪个文件系统、位于哪个设备上。
2.4 目录文件
上面我们还提到文件名是不在inode中的,那文件名在哪呢,我们平时直接使用的都是文件名,而不是什么inode号?
目录也是文件=目录的内容+目录的属性
目录同样也有这些属性,当然也是存到inode中。
内容当然也要存到data blocks中。
那目录的内容是什么呢?
目录也是一种文件,它的数据块里存放“文件名 → inode 号”的目录项
我们可以来验证一下,这里我已经写好了一份代码:
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<dirent.h>#include<sys/types.h>#include<unistd.h>intmain(intargc,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);}structdirent*entry;while((entry=readdir(dir))!=NULL)// 系统调用,自行查阅{// Skip the "." and ".." directory entriesif(strcmp(entry->d_name,".")==0||strcmp(entry->d_name,"..")==0){continue;}printf("Filename: %s, Inode: %lu\n",entry->d_name,(unsignedlong)entry->d_ino);}closedir(dir);return0;}这段代码是一个遍历指定目录、列出其中所有文件(不含子目录内容)及其 inode 号的小工具。功能上类似 ls -i <目录> 的简化版。
来运行看一下:
所以
文件名不在inode中保存,而是在该文件所在的目录中保存。
所以,我们平时用的都是文件名,如何通过文件名找到文件的内容和属性呢?
🆗,目录里面不是存了文件名 → inode 号的映射嘛,所以通过文件名就可以找到文件的inode号,那进而就可以在inode table中找到文件的inode,那文件的属性和内容不就都可以拿到了嘛。
所以,同一个目录下,不能存在同名文件,因为文件名是键,要通过文件名去找inode号。
所以,创建一个新文件主要有以下4个操作:
存储属性
内核先找到一个空闲的i节点。内核把文件信息记录到其中。存储数据
假设该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。(当然如果只是 touch abc 创建一个空文件,通常不会立即分配数据块,而是写入数据时再分配)记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?
内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
现在我们就在原理上明白:
之前我们讲权限的时候,为什么在一个目录下创建文件,需要w权限了
查看目录的内容,需要r权限
2.5 路径解析
所以:
访问一个文件时,要在其所在目录的数据块中查找“文件名 → inode 号”的目录项,最终得到目标文件的 inode 号,再通过 inode 访问文件数据。
比如这里我们要访问cloud.cpp,那就需要读取目录文件server的内容
可是,server也是一个文件啊!
那要访问server得内容,不就需要能够读取它得所在目录——cloud得内容嘛!
同样,cloud也是一个文件,那就…
就需要这样逐级往上,直到根目录。
根目录是确定的,我们看到它的inode号是2
所以,我们得出一个结论:
访问文件时,内核要通过路径名逐级解析目录,在每个目录的数据块中查找“文件名 → inode 号”的目录项,最终得到目标文件的 inode 号,再通过 inode 访问文件数据。
这就是为什么我们之前访问文件,或者open打开一个文件,必须要用文件路径;为什么进程的PCB中,要有CWD。
但是,这样的话,是不是每访问一个文件,都要根据路径进行逐级解析,这样,不慢吗?
2.6 路径缓存
问题1:Linux磁盘中,存在真正的目录吗?
答案:磁盘上没有“真正的目录”,只有文件。
目录是一种特殊文件,它的数据块里存放目录项。
问题2:访问任何文件,都要逐级进行路径解析?
答案:原则上是,但是这样太慢,所以Linux会缓存历史路径结构
问题2:Linux目录的概念,怎么产生的?
答案:目录是文件系统定义的一种特殊文件,用于组织文件和子目录。它在磁盘上有 inode 和数据块,数据块中存放目录项。
Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry
每个文件其实都要有对应的dentry结构,包括普通文件。
file结构体我们已经很熟悉了,我们看到它里面有个成员变量——struct dentry *f_dentry;
这样所有被打开的文件,就可以在内存中形成一个树形结构。
• 整个树形节点也同时会隶属于LRU(Least Recently Used,最近最少使用)结构中,内存紧张时进行节点淘汰
• 整个树形节点也同时会隶属于Hash,方便快速查找
•更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都在先在这棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry结构,缓存新路径
那这样的话:
做路径解析的时候,第一次通常是慢的(需要读磁盘、建缓存)。第二次、第 n 次的时候,只要 dentry 缓存没有被 LRU 淘汰,路径解析会优先从 dentry 哈希表和树结构中进行,速度极快。
这完美展示了 Linux VFS 的设计哲学:
用一棵树(dentry tree)描述层级,用哈希表(d_hash)加速查找,用 LRU 链表(d_lru)管理生命周期,用 d_inode 连接文件本体的 inode。
这就是 Linux 能够高效处理海量文件路径的底层原因!
2.7 挂载分区
分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。
“分区写入文件系统”:
mkfs(Make File System:它的作用正是在一个裸分区上,建立起文件系统所需的结构(如超级块、inode 表、根目录等),让操作系统能够在这个分区上管理和存取文件。这一步操作通常也被称为格式化。) 后
分区里有了超级块、inode 表、根目录,形成一棵自包含的目录树。
“无法直接使用”:
这棵树不在系统单根目录树里,没有路径能到达它。
“需要和指定的目录关联”:
使用mount命令选一个系统树里的目录作为挂载点(如 /home)。
“进行挂载才能使用”:
mount 建立 vfsmount(vfsmount 是 Linux VFS 中用来描述一次挂载行为的核心结构体。你可以把它理解为一个“连接器”或“挂载记录”,它负责将一个文件系统的根目录与系统目录树中的一个挂载点关联起来),路径解析经过挂载点时切换到该文件系统,文件才可通过路径访问。
一图总结