磁盘文件初识

前面我们所谈论的都是被打开的文件,它们都是在内存当中,现在我们再来谈谈那些没有被打开的文件
没有被打开的文件就存储在磁盘上,放在磁盘当中的文件也是以目录结构组织的,呈树状,为绝对路径或者相对路径,我们在磁盘上查找文件即通过这样的方式,并且保存在磁盘上的文件最基本的诉求就是被我们所找到呀,这些都是文件系统的基本工作
理解磁盘,服务器,机柜,机房
(0)磁盘 是存储硬件 → 装在服务器 里 → 服务器放进机柜 → 大量机柜集中在机房。
(1)磁盘(最底层的数据载体)
主流类型
- 机械硬盘 (HDD):容量大、价格低、读写慢,适合冷数据、大容量存储。
- 固态硬盘 (SSD):速度快、抗震强,现在服务器主力,分 SATA SSD、NVMe SSD(高性能)。
(2)服务器(算力 + 存储的 "主机")
本质是高性能专用电脑,负责计算、运行程序、对外提供服务(网站、数据库、云服务等)。
内部组成:CPU(算力)、内存(临时运算)、磁盘(持久存储)、网卡、电源
(3)机柜(服务器的 "货架 / 机箱")
作用:集中摆放设备、节约空间、统一供电布线、物理防护。
(4)机房(所有设备的 "专属大楼 / 房间"
核心配套系统(缺一不可):供电,制冷,消防,网路,安防
理解磁盘
理解磁盘物理结构
一张图为例,简单明了

磁盘的读写原理
- 存储规则 :盘片表面有无数微小磁粒,磁极朝向不同 ,分别代表二进制的 0 和 1。
- 写入数据 磁头移动到目标位置,通过磁场,把 0、1 序列 "刻" 在盘片上。
- 读取数据 盘片高速旋转,通过将磁信号转化成电信号,还原出原本的 0 和 1 数据。
总结:用磁粒正反代表 0/1,盘片转动 + 磁头定位,完成读写
磁盘的存储结构


扇区
扇区是从磁盘读出和写入信息的最小单位,通常大小为 512 字节。
理解:只要访问这个区块里任意 1 字节,控制器就必须把整个扇区(512B)全部读进内存 ;修改时也必须先读整扇区、改内容、再**整扇区写回。**硬件无法单独读写单个字节,必须整扇区操作。
磁盘容量大小
磁头(head)数:每个盘片⼀般有上下两面,分别对应1个磁头,共2个磁头
磁道(track)数:磁道是从盘片外圈往内圈编号0磁道,1磁道...,靠近主轴的同心圆用于停靠磁 头,不存储数据
柱面数量 = 单个盘面的磁道数 每一个柱面对应所有盘面同半径的一圈磁道,有多少圈磁道,就有多少个柱面
扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
磁盘(platter)数:就是盘片的数量
磁盘容量=磁头数×磁道(柱面)数×每道扇区数×每扇区字节数
CHS 磁盘寻址(Cylinder-Heads-Sector,柱面 - 磁头 - 扇区)

三个参数含义
-
C 柱面 Cylinder 所有盘片同一半径的一组磁道,磁臂停在该半径就对应一个柱面;改变柱面需要移动磁臂(寻道,很慢)。
-
H 磁头 Head 对应盘片的读写面,一张盘片上下两面各 1 个磁头。同一柱面下,切换磁头不用移动磁臂,速度快。
-
S 扇区 Sector 一条磁道被切分的最小物理读写块,传统每个扇区 512 字节。同一磁头、柱面下,只需等盘片旋转到目标扇区。
CHS 寻址逻辑
定位一块数据的完整顺序:先柱面 → 再磁头 → 最后扇区
- 移动磁臂到目标柱面(C);
- 选中该柱面下对应的磁头(H);
- 等待盘片旋转,读到该磁道上的目标扇区(S)。
磁盘的逻辑结构
理解过程
磁带上面可以存储数据,我们可以把磁带"拉直",形成线性结构
那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带,那么磁盘的逻辑存储结构我们也可以类似于:
这样每⼀个扇区,就有了⼀个线性地址(其实就是数组下标),这种地址叫做 LBA
当然操作系统不需要把每一个lba都记录下来,每一个squ的大小是固定的,通过指针(地址)加减扇区大小操作就可以记录下每一个lba
这是逻辑抽象层面,在磁盘内部还是通过csh,它们两个可以相互转化,后面再说,简单理解磁盘看作黑盒子,通过lba就可以在磁盘内部找到对应扇区
真实过程
柱⾯是⼀个逻辑上的概念,其实就是每⼀⾯上,相同半径的磁道逻辑上构成柱⾯。
所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘整体是由"柱面"卷起来的。

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

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

柱面上的每个磁道,扇区个数是⼀样的
这不就是⼆维数组吗
整盘:

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

所以,每⼀个扇区都有⼀个下标,我们叫做 LBA(Logical Block Address) 地址,其实就是线性地址
OS只需要使用LBA就可以了!!LBA地址转成CHS地址,CHS又可以转换成为LBA地址
CHS和LBA地址转换:
单个柱面的扇区总数=磁道数*每磁道扇区数
(1)CHS->LBA
LBA=柱面号C*单个柱面的扇区总数+磁头号H*每磁道扇区数+扇区号S-1
磁头号是从0开始
扇区号通常是从1开始的,而在LBA中,地址是从0开始的(所以要减1)
eg:
我们要得到红色部分的lba,(柱面号)1*8(单个柱面的扇区总数)+(磁头号)1*4(每磁道扇区数)+2(扇区号)-1=13
(2)LBA->CHS
柱面号C=LBA//单个柱面的扇区总数
磁头号H=(LBA%单个柱面的扇区总数)//每磁道扇区数
扇区号S=(LBA%每磁道扇区数)+1
"//":表示除取整
eg:C=13//8=1 ,H=(13%8)//4=1 S=(13%4)+1=2
总结:
(1)从此往后,在磁盘使用者看来,根本就不关心CHS地址,而是直接使用LBA地址,磁盘内部自己转换。
(2)磁盘就是以扇区为元素的⼀维数组,数组的下标就是每⼀个扇区的LBA地址。OS使用磁盘,就可以用⼀个数字访问磁盘扇区了。
引入文件系统
在引入文件系统前我们先来引出两个概念
块的概念
引入"块"概念其实硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样 效率太低,而是⼀次性连续读取多个扇区,即⼀次性读取⼀个"块"(block)。
硬盘的每个分区是被划分为⼀个个的"块"。⼀个"块"的大小是由格式化的时候确定的,并且可以更改,最常见的是4KB,即连续八个扇区组成⼀个"块"。"块"是文件存取的最小单位
每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
• 知道LBA:块号=LBA/8
•知道块号:LAB=块号*8+n(n是块内第几个扇区)

"分区"概念
其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有⼀块磁盘并且将 它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备 都是以文件形式存在,那是怎么分区的呢?

例如我们现在要对磁盘800gb的空间进行管理,对应的块数实在是太大了不好管理,所以我们可以分成300gb,300gb,200gb三个区域来管理,由于这3个分区域管理方式一样,所以对800gb的管理就转换为对300gb的管理 ,但300gb还是太大了,所以可以继续把这段空间再进行划分为组,每一小组30gb**,所以只要把这30gb管理好就能把这300gb管理好**
所以整体上要把磁盘的空间管理好就转换为把这30gb的空间管理好
这种思想叫分治
上面已经帮我们引出了相关概念做好了准备,下面我们就要认识文件系统了
ext2文件系统
我们想要在硬盘上储文件,必须先把硬盘格式化为某种格式的文件系统 ,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。在 Linux系统中,最常见的是ext2系列的文件件系统。其早期版本为ext2,后来又发展出ext3和ext4。 ext3和ext4虽然对ext2进行了增强,但是其核心设计并没有发发变化,我们仍是以较老的ext2作为演示对象。

1.都是4kb
EXT2 文件系统所有磁盘存储空间统一划分为固定大小4KB的数据块(inode Table,Data Blocks......)一块 4KB 的数据块可以存放 32 个 inode( 4KB=4×1024=4096 Byte,EXT2 标准配置**单个 inode 结构体占用 128 Byte,**单块inode数量=4096/128=3
2.inode
inode(单个文件的元数据容器,结构体) 每一个文件 / 目录对应一个独立 inode( 每个文件对应一个 inode 号),存有:
- 文件元数据(属性)
- 文件类型、权限、硬链接数
- 属主 UID、属组 GID
- 大小、三个时间戳(访问 atime、修改 mtime、状态改变 ctime)
- 数据块指针 (核心)通过数据块号找到datablocks里面对应的数据块
- 唯一 inode 编号 分区内唯一标识,系统靠它定位文件
特别注意的是文件名不在inode内部

ls-li查看文件编号

3.inode Table
inode table 是整块硬盘分区上,所有 inode 的「数组 / 存储区域」想拿到文件信息,必须通过 inode 号定位、读出 inode table 里对应的 inode 结构体。
- inode:是存储单个文件的元数据
- inode table:存放所有 inode 的整张表(分区级结构)
4.Block Bitmap
Block Bitmap(块位图) 由二进制比特位组成,每 1bit 对应一个 Data Block:1=该数据块已占用,0=空闲可用;记录所有数据块的占用状态。
5.inode Bitmap
Inode Bitmap(inode 位图) 每 1bit 对应 Inode Table 里一条 inode 记录:1=该inode已分配给文件,0=inode空闲;记录 inode 条目是否被使用。
6.Data Blocks
Data Blocks 磁盘上真正存放文件的区域,由许多个大小4KB的Data Block组成,每一个数据块都有自己的编号
一个问题为什么下载一个文件慢,删除一个文件快?
(1)删除文件仅修改两张位图(根据inode 将两张位图的比特位从1改变为0即可),完全不操作 Data Blocks 里的真实文件数据,几乎无磁盘 IO,所以可以做到秒删
(2)下载文件需要完整分配资源、写入真实数据,存在大量磁盘 IO 操作
文件删除后要恢复原理
文件删除后node Bitmap 对应位 = 0、Block Bitmap 对应数据块位 = 0,系统判定这些资源空闲, 允许新文件覆盖(所以误删后尽量不要乱操作,免得覆盖了要恢复文件的内容);但是磁盘 Data Block 内的原始文件二进制数据完好无损,就能通过inode 的数据块指针找到内容
SuperBlock 超级块:管整个分区
每个块组 0 开头,其它块组大部分都有备份副本,防止损坏无法挂载(很重要必须得有备份)
记录整块分区的整体信息:
- 分区总块数、总 inode 数量、每个块组有多少块、每个块组多少 inode;
- 块大小(4K/1K)、inode 大小、日志开关、文件系统 UUID、挂载记录;
- 全局空闲统计:整个分区剩余多少空闲块、空闲 inode;
- 定义分区如何切割成一个个块组,告诉内核一共有多少个块组。
GDT 块组描述符表:管每一个块组
紧跟在超级块后面,同样在多个块组留有完整副本。
GDT 是一张数组表 :数组里每一项 = 块组描述符(Group Descriptor),一个块组对应一条描述符。
单个块组描述符存储内容主要有比如 哪里开始是inodeTable,从哪里开始是Data Blocks,即在磁盘块的位置和**当前块****(SB是整个分区)**组内剩余空闲数据块、空闲 inode 数量等等
对比一下:
- SuperBlock:大局,管整个分区有多少资源、怎么分块组;
- GDT:索引,管每个块组内部位图、inode 表在哪,每组剩多少空闲空间
磁盘分区的格式化
格式化就是依托 superblock(全局参数)、GDT(块组索引),初始化每个块组的 blockbitmap和inodebitmap、inode 表,在分区内搭建完整文件系统管理结构,让操作系统可以管理 inode 和数据块,实现文件的创建、读写、删除。
格式化和删除文件的核心区别
- 删除文件:仅修改两张的标记位,不动 inode 表原有记录、不覆盖数据块内容;
- 格式化分区:直接就重建整套 superblock、GDT、两张位图、清空 inode 表,原有文件的管理信息全部失效,数据极易被新文件覆盖,恢复难度大。
inode号和数据块号的编号特点
(1)同一分区,不同组
- inode 号、数据块号不是每个块组内部从 1 重新计数 ,而是整个分区统一连续编号。 例:块组 0 的 inode 耗尽后,块组 1 的 inode 接着块组 0 的最大 inode 号往下编;数据块同理,块组 0 所有块编完,块组 1 的块号延续递增。
- 好处:内核只需要一个数字(inode 号 / 块号),就能算出它属于哪个块组,再去 GDT 块组描述符表找到对应块组的位图、inode 表。
(2)不同分区
- 分区是文件系统的载体 ,每个分区是一套独立完整的 EXT 文件系统(独立 SuperBlock、GDT、全套位图、inode 表、数据块)。
- A 分区的 inode 号、块号只在 A 分区内部生效,放到 B 分区完全没有意义,两套文件系统的编号体系互不通用;
总结:
- 分区内:inode 号、块号 = 唯一;
- 跨分区:inode 号、块号可以重复(sda1 的 inode 100 和 sda2 的 inode 100 是完全无关的两个文件)。
操作系统对分区的管理
也是通过先描述在组织的方式,操作系统对分区的管理,是要把当前分区所对应的文件系统( 每个分区是一套独立完整的 EXT 文件系统),的文件管理信息加载到内存(链表组织管理),所有文件操作优先操作内存中的管理信息。(在内存操作)
怎么通过inode找到文件的属性和内容
inode 号 → 通过 SuperBlock、GDT 定位 inode table,读出 inode 结构体(获得文件属性)→ 读取 inode 内部数据块指针,按块号读取数据块,得到文件内容。
Linux下怎么看待目录?
目录和普通文件在磁盘的保存方式本质上是一样的,无非就是inode和数据内容,本质都是"普通文件"
目录的inode和数据内容存什么呢?
inode和文件的inode都是存相应的属性,而目录的内容存的是文件名和inode号的映射关系,所以文件名就存储当前文件所属的目录的数据内容里,即linux同一个目录下文件名是唯一的,不能有重复(inode和文件名一一对应)
从目录查找文件的步骤(路径和文件名)
eg:

先打开当前所在路径->读取对应的目录code的数据内容->得到code.c的文件名和inode编号->根据inode编号在磁盘找到对应文件(上面已经写过的步骤怎么通过inode在磁盘找文件)
但在读取目录的数据内容时我们也得拿到目录code的inode呀,那目录的inode怎么得到呢?答案就是相同的方法继续再往前找,一直找到根目录,所以原则上在linux下找任何文件都得从根目录下开始进行路径解析,查找到文件,而路径当然就是进程的cwd提供
比如ls code.c中ls内部就记录下来了自己当前进程的所在路径(路径),code.c就是我们自己提供的文件名(文件名),路径和文件名两者一拼接后,再进行路径解析即可
struct dentry
虽然原则上我们访问任何文件都是从根目录开始进行路径解析,可是这不就是相当于一直在磁盘上进行IO操作吗,效率很低。所以Linux会缓存历史路径结构,这样在查找时如果已经缓存命中了就不用再到磁盘上操作,直接在内存操作就可以了,只需要把缓存没有命中的从磁盘加载一下,形成缓存就可以了,省去了许多在磁盘上的IO操作
Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry

通过struct dentry查找文件
(1)dentry有缓存(全程不走磁盘)
- 已知父目录 dentry + 文件名 → 找到目标文件 dentry(直接通过hash表)
- 拿到目标 dentry → 获取文件属性、读取文件内容(dentry里面保存了inode,不用再去磁盘找)
(2)dentry没有缓存(磁盘查找->Ext2 磁盘查找流程)
就要访问目录内容拿着inode编号去磁盘查找,然后添加dentry 结构,缓存新路径
总结:查找文件,本质就是一层层遍历 dentry 缓存树;如果有缓存就不走磁盘直接返回dentry进而拿到inode,没有缓存就访问该文件所属目录的内容拿到inode编号去磁盘找到文件然后添加dentry 结构,缓存新路径。
在前面我们拿着inode去磁盘查找文件时,都是默认已经找到了属于哪一个分区,但实际上应该怎么找到分区?
挂载:我们的分区不能直接访问(进不去),必须要和一个特定的目录进行关联,这样通过进入这个目录,就相当于进入了这个分区
df-h就可以查看挂载情况

/dev/vda1就是挂载到根目录下
所以在查找属于哪一个分区的时候,就反向往前查找最近一个有路径挂载的分区
eg:在找code.c的inode属于哪一个分区时就往前找,如果知道了code挂载到了a分区,那么code.c就也在a分区

但其实一般是通过dentry和超级块的方式活得
dentry->d_inode:拿到目录项对应的 inodeinode->i_sb:inode 必然属于一个超级块(super_block)sb->s_dev:超级块对应块设备号(分区的设备号)- 用设备号就能唯一确定属于哪个分区
(一个文件系统(分区)对应一个 super_block,一个 inode 只属于一个 super_block)
inode和datablock的映射关系(弱化)
inode内部存在__le32 i_blockEXT2_N_BLOCKS;/* Pointers to blocks */ , EXT2_N_BLOCKS =15,就是用来进行inode和block映射的
只有前12个inode号直接指向数据块,而后3个都是间接指向。这样存储的数据内容才足够大

理解vfsmount和分区(找分区要不要vfsmount)
把文件 当成一个人:
- inode = 这个人本身(身份证、家住哪)
- dentry = 这个人的名字
- 分区 = 这个人真正住的房子(物理位置,永远不变)
- vfsmount = 你从哪个门看到他(前门 / 后门 / 侧门)
结论:
-
想知道【文件真正存在哪个分区】 看 inode ,不用看挂载。
-
想知道【你现在是从哪个挂载点看到这个文件】 才需要 vfsmount。
不管你把文件挂在哪、怎么看 ,它物理上只存在一个分区 。同一个文件,你可以从多个门看到它、
即一个文件可以被多次挂载到不同路径,挂载只是访问方式而已。
inode和fd(文件描述符)区别
inode(文件本体)
- 代表真正的文件
- 存在硬盘,唯一不变
- 记录:文件大小、权限、数据在哪、属于哪个分区
- 多个进程、多个 fd 可以共用同一个 inode
. fd(文件描述符)
- 只是一个整数:0、1、2、3、4...
- 是进程打开文件 时,内核给进程的临时值
- 只在当前进程里有效
- 关文件就消失
- 同一个文件可以被打开多次,产生多个不同 fd 但它们指向同一个 inode
总结表!!!!

软链接
ln -s 软链接指令

可以看到软链接的文件是一个单独的文件,因为它有自己独立的inode编号。(l开头表示是软链接文件)
有什么用?
如果我们的可执行程序不在当前路径,那要去运行它就得加上路径会很麻烦
eg:mv a.out bin/exe/excute过后运行起来就会很麻烦,但只要ln -s bin/exe/excute/a.out exe软链接形成一个新的文件exe就可以直接执行

所以软链接就相当于一种快捷方式,比如windows的桌面图标就是一种快捷方式,直接双击图标就可以运行可执行程序。
软链接的文件也肯定有文件的内容和属性,文件属性就和普通文件一样,而文件内容就指向目标文件的路径
删除软链接后的文件直接r或者unlink

硬链接
ln指令

可以看到code-hard和code.c的inode号一样,所以硬链接不是独立的一个文件。
硬链接的本质是创建一组新的文件名和目标文件inode号的映射关系
(eg:即code.hard这个新文件名和目标文件code.c的inode的映射关系)
而上面的2就是引用计数,即有几个文件名和inode映射引用计数就是几,现在code-hard这个文件名也指向code.c所以code.c的引用计数就是2了
硬链接有什么用呢?
我们不妨先看一段代码

所以现在看来硬链接可以帮我们对文件进行备份
再来看一个例子

为什么新建的目录和文件前者的引用计数是2而后者是1呢?

进入到code目录里面ll -ai我们发现有.和.. 并且.的inode号和code一样,所以就相当于有.和code两个文件名指向inode917508所以它的引用计数是2,并且也知道了为什么说.就是指的是当前文件
紧接着我们进入code里面再创建一个目录hello,发现code的引用计数又变为3了这是为什么呢?

答案很简单就是因为hello的..指向了code,所以引用计数就又得+1
