【Windows文件系统】从物理扇区到逻辑文件:搞懂磁盘分区、文件系统与数据库存储的底层链路
作者介绍:大家好,我是 CodeStats。一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程)范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。
写在前面:先问自己几个问题
在开始阅读之前,请你花十秒钟想一想下面这几个问题。如果你能全部回答上来,这篇文章你可以关掉了;如果其中有任何一个让你卡住了,那这篇文章就是为你写的:
-
一块崭新的硬盘,插上电脑之后,怎么就变成了C盘和D盘?分区到底改写了硬盘上的什么东西?
-
你双击一个文件,从鼠标点击到屏幕上弹出内容,中间经过了哪些步骤?文件系统在这里扮演了什么角色?
-
操作系统自己也是存在硬盘上的文件,那系统启动时,是谁来加载操作系统本身?这难道不是一个循环依赖的困境?
-
装系统的时候可以对C盘随便分区格式化,为什么系统跑起来之后,Windows却死活不让我动C盘?D盘为什么就可以?
-
Linux为什么只有一个“/”根目录,没有C盘D盘?Windows设计成盘符模式是落后还是另有原因?
-
MySQL的一张表,理论上能存多大?到底是MySQL自己说了算,还是底层文件系统说了算?
如果你带着这些疑问读完全文,你会发现:所有这些问题的答案,最终都指向同一套底层逻辑。把这套逻辑搞懂了,你就真正理解了计算机存储的“道”。
1. 磁盘的原始状态:LBA(逻辑块地址)
一块全新的硬盘从工厂出来的时候,就是一堆连续编号的空白扇区。每个扇区都有一个编号,从0开始依次递增:0,1,2,3……直到N。这个编号叫做 LBA(Logical Block Addressing,逻辑块寻址)。
硬盘只认LBA编号,根本不认识什么C盘D盘。你跟硬盘说“把LBA 1234567的数据读给我”,它知道怎么做;你跟它说“把C盘根目录下的test.txt读给我”,它听不懂——因为C盘和文件名都是操作系统强加给硬盘的逻辑概念。
思考: LBA是硬盘固件提供的一层“抽象”。它把机械硬盘复杂的柱面/磁头/扇区三维坐标,简化成了一个从0开始的一维整数序列。操作系统从此不需要关心磁头怎么摆动、盘片怎么旋转,只需要发一个LBA编号就够了。这层抽象让操作系统与硬件实现了解耦——无论底层是机械盘还是固态盘,操作系统看到的都是一样的LBA接口。
2. 分区表:在硬盘上“画格子”
要让一块空白硬盘变成C盘、D盘,就需要在硬盘开头的特定LBA位置写入一张分区表(Partition Table)。
分区表就是一张极其简单的表格:
| 分区 | 起始LBA | 结束LBA |
|---|---|---|
| 分区1 | 2048 | 204800000 |
| 分区2 | 204800001 | 409600000 |
操作系统启动后,先去读这张分区表,然后才知道:“哦,LBA 2048到204800000这片区域,就是你们平时说的C盘。”
思考:为什么起始LBA是2048而不是0? 因为LBA 0~2047这段空间被预留用于存放MBR、GPT头部、引导代码等元数据。这是现代分区工具的对齐策略,目的是保证分区起始位置与物理扇区(4K)对齐,避免读写性能损失。
2.1 MBR与GPT的对比
分区表有两种主流格式:
MBR(Master Boot Record):存储在LBA 0,占用512字节,最多4个主分区,单分区最大2.2TB。它是DOS时代的遗产,无冗余备份。
GPT(GUID Partition Table):存储在LBA 1及以后,支持128个以上分区,单分区几乎无上限,带CRC校验且在硬盘末尾有备份分区表,是UEFI时代的标配。
MBR的核心数据结构(C语言描述):
c
// MBR分区表项结构(16字节)
struct MBRPartitionEntry {
uint8_t status; // 0x80 = 活动分区(可引导)
uint8_t chs_start[3]; // 起始柱面/磁头/扇区
uint8_t type; // 分区类型(0x07=NTFS, 0x0C=FAT32, 0x83=Linux等)
uint8_t chs_end[3]; // 结束柱面/磁头/扇区
uint32_t lba_start; // 起始LBA地址(小端序)
uint32_t sector_count; // 分区总扇区数
};
// MBR整体结构(512字节)
struct MBR {
uint8_t boot_code[440]; // 引导代码
uint32_t disk_signature; // 磁盘签名
uint16_t reserved; // 保留(0x0000)
MBRPartitionEntry partitions[4]; // 4个分区表项,每个16字节
uint16_t signature; // 0xAA55(MBR有效标志)
};
思考:为什么会有“扩展分区+逻辑分区”这种补丁方案? 因为MBR设计于1983年,没人想到硬盘会发展到今天的大小;而GPT设计于2000年前后,一步到位。架构设计的历史包袱,往往比技术本身更值得琢磨。
3. MFT(主文件表):文件系统的“总索引”
分区只是画了格子,要往格子里存文件,还需要格式化。格式化的本质是:在分区对应的LBA范围内,写入一套文件系统的元数据结构。以Windows最常用的NTFS为例,格式化会在分区的特定位置写入一份 MFT(Master File Table,主文件表)。
MFT可以理解为NTFS文件系统的总账本:
-
硬盘上的每一个文件(包括文件夹),在MFT里都至少对应一条记录,每条记录1KB。
-
每条记录存储了该文件的所有元数据:文件名、大小、时间戳、权限等。
-
最关键的是,每条记录里存储了该文件的数据存放在哪些LBA地址上——这个字段叫做 Run List(簇流列表)。
MFT记录的核心结构(简化版):
c
// NTFS MFT记录头(部分字段)
struct MFTRecordHeader {
uint32_t signature; // "FILE" 或 "BAAD"/"HOLE"等
uint16_t fixup_offset; // 修复数组偏移
uint16_t fixup_count; // 修复数组条目数
uint64_t log_sequence_number; // 日志序列号(用于事务恢复)
uint16_t sequence_number; // 序列号(用于检测记录复用)
uint16_t hard_link_count; // 硬链接计数
uint16_t attribute_offset; // 第一个属性的偏移
uint16_t flags; // 0x0001=正在使用, 0x0002=目录
uint32_t real_size; // MFT记录实际大小
uint32_t allocated_size; // MFT记录分配大小
uint64_t base_record; // 基础记录(用于扩展属性)
uint16_t next_attribute_id; // 下一个属性ID
};
// Run List(簇流列表)的存储格式
// 压缩存储:[长度][起始簇号] 交替出现,每个字段使用变长整数编码
// 示例:读取时解析为 (长度, 起始LBA) 对序列
3.1 文件的物理存储方式
假设一个文件内容被存放在不连续的三个位置,Run List记录为:LBA 100~105、LBA 500~503、LBA 200~202。读取时操作系统依次读取三段数据,然后在内存中拼接成完整文件。
思考:为什么NTFS要设计成“物理不连续、逻辑连续”? 因为磁盘碎片不可避免。如果强制要求每个文件连续存储,磁盘会很快出现“每个空闲区域都不够大”的外部碎片问题。Run List的设计用牺牲一点读取速度换取极高的存储空间利用率——这也解释了为什么机械硬盘需要定期做“磁盘碎片整理”。
3.2 小文件的特殊处理
如果文件小于约900字节,NTFS直接把文件内容存储在MFT记录本身的剩余空间里,不额外分配LBA区域。这叫 “常驻属性” 。这就是为什么几字节的文本文件在NTFS分区上显示占用“0字节”——因为它根本没占用额外的数据簇,全塞在MFT里了。
思考:为什么阈值是900字节而不是1000? 因为MFT记录共1KB(1024字节),扣除记录头、文件名、时间戳等固定开销,剩余空间约900字节——这个值是算出来的,不是拍脑袋定的。
4. 读写文件的完整流程:从双击到弹出内容
4.1 写入文件的步骤(新建test.txt,输入“Hello World”)
-
NTFS驱动扫描
$Bitmap(位图文件),找到空闲LBA区域。 -
系统把“Hello World”写入空闲LBA扇区。
-
系统在MFT里新建记录,填写文件名、大小、时间、Run List。
-
系统在目录索引表里加一行:
"test.txt" → MFT记录号XXX。
4.2 读取文件的步骤(双击test.txt)
-
系统去目录索引表查找test.txt,获得MFT记录号。
-
系统去MFT区域找到该记录,读出Run List:数据在LBA 56789~56790。
-
系统向硬盘发送命令:“请读取LBA 56789~56790的数据。”
-
硬盘返回数据,系统在屏幕上显示“Hello World”。
text
┌─────────┐ 路径查找 ┌─────────┐ MFT记录号 ┌─────────┐ Run List ┌─────────┐
│ 双击文件 │ ──────────► │目录索引表│ ────────────► │MFT记录 │ ───────────► │LBA地址 │
└─────────┘ └─────────┘ └─────────┘ └────┬────┘
│
▼
┌─────────┐
│ 硬盘读取 │
└─────────┘
4.3 删除文件时发生了什么?
系统在MFT里把该记录标记为“未使用”,在$Bitmap里把占用的LBA从“1”改为“0”——并没有擦除数据本身。物理扇区上的0和1依然静静躺着,直到被新文件覆盖。这就是刚删除的文件可以恢复的原因;而“安全擦除”软件的原理就是在删除后立刻用随机数据反复覆盖这些LBA区域。
思考: “删除”和“覆盖”之间的时间差,就是数据恢复软件的生存空间。
5. 操作系统如何加载自己?——引导与自举
操作系统内核(如Windows的ntoskrnl.exe)本身是一个文件,存放在C盘的NTFS分区里。要读取它,必须要有NTFS驱动(ntfs.sys);但ntfs.sys本身也是一个文件,也存放在C盘。这就形成了一个 “循环依赖”困境。
破解之道是引入引导加载程序(Bootloader),分三步完成“自举”:
第一步:BIOS/UEFI固件。通电后最先运行的是主板固件,它只懂LBA地址,去硬盘特定位置(UEFI下是ESP分区,传统BIOS下是MBR区域)读取一段极小的引导代码。
第二步:引导管理器(Windows Boot Manager)。固件把控制权交给bootmgr,它内置了一个极精简的只读NTFS驱动,足够解析MFT,找到C:\Windows\System32下的ntoskrnl.exe和ntfs.sys,将其预读到内存。
第三步:操作系统内核加载。内存里有了内核和驱动后,控制权交给操作系统,内核安装完整的ntfs.sys驱动,至此才拥有完整的NTFS读写能力。
5.1 ESP分区的作用
UEFI模式下,硬盘上还有一个FAT32格式的ESP分区(约100MB)。UEFI固件原生支持FAT32,所以不需要内置NTFS驱动就能直接读取.efi引导文件。这相当于在硬件和操作系统之间插入了更精简的一层,绕开了NTFS依赖。
思考:为什么ESP分区必须用FAT32? 因为UEFI规范强制要求所有主板固件实现FAT驱动,而NTFS是微软私有文件系统,其他厂商没有义务支持——这本质上是“标准制定权”的问题。
6. 安装系统时如何进行分区和格式化?
理解了第5章,这个问题就很清晰了:当你用Windows安装U盘启动电脑时,U盘会把一个叫Windows PE(预安装环境) 的微型操作系统加载到内存中。这个PE完全运行在内存条里,不依赖硬盘上的任何文件系统,它自带了硬盘驱动和分区工具。
安装时的具体步骤:
-
Windows PE向硬盘的LBA 0(MBR)或LBA 1(GPT头部)写入分区表数据。
-
用户选中C盘区域点击“格式化”——PE向该区域开头写入一套全新的空MFT。
-
PE将U盘或光盘中的Windows系统文件解压到C盘,每写入一个文件就在MFT里创建一条记录。
-
PE在ESP分区或MBR区域写入引导代码,告诉电脑下次启动时去C盘找
bootmgr。
思考:为什么安装程序能往硬盘写数据而普通软件不行? 因为Windows PE运行在系统级权限,通过BIOS中断或UEFI运行时服务直接操作硬盘I/O端口,绕过了操作系统的文件系统权限控制。权限的本质是:谁能向LBA扇区写入数据。
7. 为什么运行时C盘不能动,D盘却可以?
根本原因:是否正在被使用。
C盘正在被操作系统本身使用——Windows内核、驱动程序、系统文件(包括MFT)都在C盘上,正在被持续读取和写入。运行时修改C盘的分区结构或文件系统结构,就像一边开飞机一边拆机翼,系统会立刻崩溃。
而D盘(如果没有运行中的程序)是“可以卸载的”,系统可以像弹出U盘一样暂时卸载卷,修改分区表或MFT后再重新加载。
7.1 Windows磁盘管理的具体限制
-
允许:对C盘执行“压缩卷”(缩容),只改变分区的结束LBA位置,不破坏已有数据,但需有足够空闲空间。
-
不允许:对C盘执行“扩展卷”,因为C盘右侧通常有D盘挡着;也不允许任何需要移动数据的操作。
7.2 第三方工具是怎么做到的?
傲梅分区助手等工具之所以能“无损调整C盘大小”,靠的是 “重启后执行” :软件在注册表的BootExecute项中写入任务脚本,重启后、Windows内核加载前,软件的内核驱动接管磁盘,此时C盘完全没有被占用,驱动直接修改分区表和MFT结构,包括移动物理数据。任务完成后再次重启,Windows正常启动,发现C盘变大了。
思考: 这也是调整C盘大小必须重启的原因——要在操作系统“不在场”的时候动手。即使对D盘操作,如果有进程在读写D盘文件,操作系统同样会拒绝卸载卷。“正在使用”是操作系统的红线,不分C盘D盘。
8. Linux为什么只有“/”目录,没有C盘D盘?
两种设计哲学的根本差异:
-
Windows的设计:先有物理分区,后有路径。每个物理分区直接当成一个“根”——C盘是一个根,D盘是另一个根,物理和逻辑死死绑定。
-
Linux的设计:先有逻辑目录树,再把分区挂上去。只有一个根目录
/。所有分区都被“挂载(Mount)”到目录树的某个文件夹下,例如:/dev/sda1挂载到/,/dev/sda2挂载到/home,/dev/sdb1挂载到/var。
一句话: Windows下分区是主角,目录是分区的名字;Linux下目录树是主角,分区只是挂在这棵树上的果实。
8.1 Linux设计的经典优势
当/home分区磁盘满了,要换一块2TB新硬盘时——Linux下插上新硬盘挂载到/mnt/newdisk,拷完数据后修改/etc/fstab把新硬盘的挂载点从/mnt/newdisk改成/home,重启完成。所有用户访问的路径依然是/home/xxx,路径完全不变,但底层的物理硬盘已经换了。
Linux的设计让服务器即使换了无数块硬盘,整个系统的目录结构永远保持一致——这是Linux统治服务器领域的核心根基之一。
8.2 补充:NTFS也支持挂载
实际上,NTFS也支持将分区挂载到空文件夹(Windows里叫“装入空白NTFS文件夹”)。你可以把D盘挂载到C:\Mount\DDrive下,然后通过路径访问。
但为什么大家不用?因为Windows的盘符模式已根深蒂固30年,所有软件和用户习惯都建立在盘符之上——历史惯性的力量,使更好的方案也难以替换。
思考: 盘符模式的优势是什么?简单、直观、对普通用户极其友好。“C盘就是系统盘,D盘就是放资料的地方”——每个用户都能听懂。而Linux的挂载点模式对非技术用户的理解成本极高。技术的先进性不等于易用性,这是产品设计中永远需要权衡的矛盾。
9. MySQL单表大小限制的真相
MySQL的数据最终以普通文件形式存放在硬盘上。以InnoDB引擎为例,在innodb_file_per_table = ON(默认开启)的情况下,每张表对应一个独立的.ibd文件——这张表的所有数据和索引全部存放在这个文件里。
MySQL本身没有限制单表最大记录数。一张表能存多少行,取决于每行数据的大小,以及底层文件系统对单个文件大小的限制。
| 文件系统 | 单文件大小限制 | 备注 |
|---|---|---|
| FAT32 | 4GB | MySQL官方禁止用于生产环境 |
| NTFS | 64GB ~ 16TB | 现代Windows默认 |
| EXT4 | 16TB ~ 1EB | 当前主流Linux文件系统 |
| XFS | 8EB | 大型服务器常用 |
9.1 实际结论
使用独立表空间的InnoDB表,单表最大容量 = min(InnoDB内部64TB,文件系统单文件上限) 。在Windows+NTFS下约16TB,Linux+EXT4约16TB,Linux+XFS可达8EB(理论上)。
一个经典的“坑”:如果MySQL数据目录放在FAT32格式的U盘上,单表最大只能4GB——生产环境完全不可接受。这也是MySQL官方文档明确要求生产环境必须使用NTFS或EXT4/XFS、严禁使用FAT的原因。
9.2 如何突破单表大小限制?
-
共享表空间(
innodb_file_per_table = OFF):所有表数据存储在共享文件中,但共享表空间本身也有大小限制。 -
分区表(Partitioning):把一个逻辑大表按规则拆分成多个物理子表(每个子表一个独立
.ibd文件),只要每个子文件不超限,整张表就能突破天花板。 -
分库分表:应用层将数据路由到不同数据库实例或不同表,是互联网大厂处理TB级数据的常用手段。
10. 全文总结:整条链路串起来
现在我们把整条链路从头到尾串一遍:
| 层级 | 核心概念 | 做了什么? |
|---|---|---|
| 物理层 | LBA | 硬盘出厂时把整个盘片编号成从0开始的一维整数序列。硬盘只认LBA,不认C盘D盘。 |
| 分区层 | 分区表(MBR/GPT) | 操作系统在LBA 0或LBA 1写入分区表,划定“LBA 0~100万是分区1,100万~200万是分区2”。C盘D盘的边界由此而来。 |
| 文件系统层 | MFT | 格式化时在分区开头写入MFT——每个文件对应一条记录,记录里存了文件名、大小、Run List(文件数据存放在哪些LBA上)。 |
| 文件读写层 | 目录索引+MFT+LBA | 读文件:路径→目录索引→MFT记录→Run List→LBA→硬盘读取。写文件:分配空闲LBA→写入数据→更新MFT→更新目录索引。 |
| 系统启动层 | Bootloader | 破解“没有NTFS驱动就读不了NTFS文件”的循环依赖。BIOS→bootmgr(内置微型NTFS驱动)→加载内核和驱动→操作系统完全接管。 |
| 系统运行层 | 卷锁定+重启执行 | 运行时C盘不可动(正在使用),D盘可动(可卸载)。第三方工具改C盘靠“重启后在操作系统不在场时执行”。 |
| 跨系统对比 | Windows盘符 vs Linux挂载点 | Windows:物理分区是主角。Linux:逻辑目录树是主角。各有优劣和历史渊源。 |
| 应用层 | MySQL+文件系统 | MySQL以.ibd文件存储数据。单表上限 = min(InnoDB内部64TB,底层文件系统单文件上限)。 |
最后送你三句话
盘符只是人类给LBA编号取的别名,分区表只是一张告诉操作系统“哪片LBA叫什么名字”的表格,MFT只是一张告诉操作系统“哪个文件存在哪个LBA上”的索引。 理解了这三层抽象,所有存储相关的问题都迎刃而解。
技术设计没有“好”与“坏”,只有“适应当下”与“背负历史”。Windows的盘符模式背负着30年的兼容性包袱,Linux的挂载点模式受益于Unix的“一切皆文件”哲学。读懂一个设计,先读懂它的历史。
从物理扇区到逻辑文件,中间隔了四层抽象(分区表→文件系统→目录索引→文件路径)。每一层抽象都解决了一个问题,同时也带来了新的限制。搞懂每一层做了什么、没做什么,才是真正的“底层思维”。
💡 如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多朋友看到!也欢迎在评论区留下你的思考或疑问,我们一起探讨。
👍 点赞 | ⭐ 收藏 | 💬 评论 | 🔄 分享
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)