作者介绍:大家好,我是 CodeStats。一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程)范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。

写在前面:先问自己几个问题

在开始阅读之前,请你花十秒钟想一想下面这几个问题。如果你能全部回答上来,这篇文章你可以关掉了;如果其中有任何一个让你卡住了,那这篇文章就是为你写的:

  1. 一块崭新的硬盘,插上电脑之后,怎么就变成了C盘和D盘?分区到底改写了硬盘上的什么东西?

  2. 你双击一个文件,从鼠标点击到屏幕上弹出内容,中间经过了哪些步骤?文件系统在这里扮演了什么角色?

  3. 操作系统自己也是存在硬盘上的文件,那系统启动时,是谁来加载操作系统本身?这难道不是一个循环依赖的困境?

  4. 装系统的时候可以对C盘随便分区格式化,为什么系统跑起来之后,Windows却死活不让我动C盘?D盘为什么就可以?

  5. Linux为什么只有一个“/”根目录,没有C盘D盘?Windows设计成盘符模式是落后还是另有原因?

  6. 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”)

  1. NTFS驱动扫描$Bitmap(位图文件),找到空闲LBA区域。

  2. 系统把“Hello World”写入空闲LBA扇区。

  3. 系统在MFT里新建记录,填写文件名、大小、时间、Run List。

  4. 系统在目录索引表里加一行:"test.txt" → MFT记录号XXX

4.2 读取文件的步骤(双击test.txt)

  1. 系统去目录索引表查找test.txt,获得MFT记录号。

  2. 系统去MFT区域找到该记录,读出Run List:数据在LBA 56789~56790。

  3. 系统向硬盘发送命令:“请读取LBA 56789~56790的数据。”

  4. 硬盘返回数据,系统在屏幕上显示“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.exentfs.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完全运行在内存条里,不依赖硬盘上的任何文件系统,它自带了硬盘驱动和分区工具。

安装时的具体步骤:

  1. Windows PE向硬盘的LBA 0(MBR)或LBA 1(GPT头部)写入分区表数据。

  2. 用户选中C盘区域点击“格式化”——PE向该区域开头写入一套全新的空MFT。

  3. PE将U盘或光盘中的Windows系统文件解压到C盘,每写入一个文件就在MFT里创建一条记录。

  4. 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的“一切皆文件”哲学。读懂一个设计,先读懂它的历史。

从物理扇区到逻辑文件,中间隔了四层抽象(分区表→文件系统→目录索引→文件路径)。每一层抽象都解决了一个问题,同时也带来了新的限制。搞懂每一层做了什么、没做什么,才是真正的“底层思维”。

💡 如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多朋友看到!也欢迎在评论区留下你的思考或疑问,我们一起探讨。

👍 点赞 | ⭐ 收藏 | 💬 评论 | 🔄 分享

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐