操作系统如何识别和控制硬盘

硬盘交给系统的是一段块地址

对操作系统来说,硬盘不是一堆文件,而是一长串按编号排好的块。每个块大小一样,从 0 开始编号。机械硬盘和 SSD 都一样,系统看到的是这些块,不是盘片或闪存颗粒。

系统向硬盘发起读写时,只给出两个信息:从第几个块开始,读或写多少个块。硬盘不需要知道这些块属于哪个文件。例如:

从第 100000 块开始,读 8 个块

100000 是起始块号,8 是长度。起始块号乘以块大小,就是字节偏移;8 乘以块大小,就是这次读写的长度。硬盘只认这个起始位置和长度。

文件系统要做的事,就是在这串块上建立结构:哪些块存文件名、目录、文件位置这些信息,哪些块存文件内容。

块大小不止一个

机械硬盘和 SSD 的块大小是多少?这个问题没有单一答案,“块”在不同层有不同含义:

单位 典型大小 谁在用
设备逻辑块 512 B 或 4096 B 主机寻址,LBA
设备物理块/扇区 4096 B(HDD 常见) 设备读写、纠错
NAND 页 4–16 KiB SSD 编程
NAND 擦除块 数百个页 SSD 擦除
文件系统块 XFS 默认 4 KiB 文件分配、索引
上层 chunk/分片 可配置 3FS/RustFS 等

主机看到的是逻辑块,大小由设备报告,不是“HDD 512 B、SSD 4 KiB”。常见三种 LBA 格式:

格式 逻辑扇区 物理扇区
512n 512 B 512 B
512e 512 B 4096 B
4Kn 4096 B 4096 B

HDD 常见 512e(逻辑 512 B、物理 4 KiB),也有 4Kn;SSD 常见逻辑 512 B 或 4 KiB,内部 NAND 页 4–16 KiB。文件系统块另算,XFS 默认 4 KiB。

512e 对外用 512 B 地址,底层按 4 KiB 物理扇区组织:写一个 512 B 的逻辑块,设备可能要读出整个 4 KiB、改其中一部分、再写回去,这就是读改写。4K 对齐的 4K 写一般不会触发。4Kn 直接按 4 KiB 寻址,没有这层转换,但老工具如果假定 512 B,可能会出错。

NAND 页和擦除块在 SSD 内部,主机看不到;FTL 负责把逻辑块映射到闪存页,垃圾回收和磨损均衡还会搬数据。Dell 512e/4Kn、SPDK SSD Internals

还要区分“改 LBA 格式”和“格式化文件系统”:mkfs.xfs 不改变设备逻辑块大小,只是在现有 LBA 空间里写文件系统结构;nvme format、sg_format 可以在支持的设备上切换 LBA 格式,但会重新初始化设备、清掉数据。

系统能看到硬盘的哪些信息

Linux 能读设备公开的容量、型号、逻辑/物理块大小、队列限制、discard 能力;smartctl 能查温度、寿命、错误记录。它们来自协议报告和驱动,不是系统直接看 NAND 或盘片。

lsblk -d -o NAME,MODEL,SIZE,TRAN,ROTA,LOG-SEC,PHY-SEC
cat /sys/block/sdb/queue/logical_block_size
blockdev --getss /dev/sdb
blockdev --getpbsz /dev/sdb
sudo smartctl -a /dev/sdb
sudo nvme smart-log /dev/nvme0

ROTA 只表示系统认为是不是旋转介质,是设备报告的提示;USB 硬盘盒、硬件 RAID、虚拟机会隐藏或改写这些报告。普通读写针对逻辑地址;flush 让设备落实缓存,discard 告诉设备某些范围不再需要。discard 不是安全擦除;要安全擦除,得看设备是否支持 ATA Secure Erase、NVMe Sanitize/Format 这类命令。sysfs-block、smartctl

系统看到的是设备报告,不是物理真相。

文件系统格式化:从块设备到文件与目录

  • 块设备不方便直接操作,所以在上层由文件系统管理

格式化建立了什么

格式化就是在这串块上创建文件系统:定块大小、容量布局、特性,写超级块、空间管理结构、根目录、日志。分区是定边界,格式化是建结构,挂载是接进目录树。

先把硬盘简化成一串固定大小的文件系统块(例如XFS 默认 4 KiB,其他文件系统的块大小可能不同)。一个 4 KiB 文件系统块,在 512 B LBA 上占 8 个连续逻辑扇区(起始 LBA 通常是 8 的倍数),在 4Kn 上占 1 个。文件系统按块管理;块设备层可以把相邻块合并成更大的 I/O;到驱动才变成“起始 LBA + 逻辑块数”的命令。

格式化后,一部分文件系统块被文件系统拿去做元数据,剩下的块放文件数据或保持空闲。每一格是一个块,行首标出这一行的起始块号:

        0   1   2   3   4   5   6   7
      ┌───┬───┬───┬───┬───┬───┬───┬───┐
  0–7 │ M │ M │ M │ M │ A │ A │ A │ B │
      ├───┼───┼───┼───┼───┼───┼───┼───┤
 8–15 │ B │ . │ A │ . │ . │ . │ . │ . │
      ├───┼───┼───┼───┼───┼───┼───┼───┤
16–23 │ . │ . │ . │ . │ C │ C │ C │ C │
      ├───┼───┼───┼───┼───┼───┼───┼───┤
24–31 │ . │ . │ . │ . │ . │ . │ . │ . │
      ├───┼───┼───┼───┼───┼───┼───┼───┤
32–39 │ . │ . │ . │ . │ . │ . │ . │ . │
      ├───┼───┼───┼───┼───┼───┼───┼───┤
40–47 │ . │ . │ . │ . │ . │ . │ . │ . │
      ├───┼───┼───┼───┼───┼───┼───┼───┤
48–55 │ . │ . │ . │ . │ . │ . │ . │ . │
      ├───┼───┼───┼───┼───┼───┼───┼───┤
56–63 │ . │ . │ . │ . │ . │ . │ . │ . │
      └───┴───┴───┴───┴───┴───┴───┴───┘

M 元数据:超级块、inode/目录、空间分配结构、日志
A 文件 A 的数据:4–6、10 号块
B 文件 B 的数据:7–8 号块
C 文件 C 的数据:20–23 号块
. 空闲块

这是简化示意,不按真实比例,也不代表文件系统把元数据全放在开头。

常见文件系统结构

文件系统 创建入口 主要结构
ext4 mkfs.ext4 超级块、块组、inode 表、位图、日志
XFS mkfs.xfs AG、B+ 树、根目录、日志
Btrfs mkfs.btrfs 超级块、元数据树、逻辑到设备映射
ZFS zpool create + zfs create pool/vdev、label、dataset

ZFS 没有 mkfs.zfs:先 zpool create 建池,再 zfs create 建 dataset(数据集);池和文件系统是两层。

ext4 在文件系统起点偏移 1024 B 写主超级块,后面是块组描述符、块位图、inode 位图、inode 表和数据块;inode 总数在 mkfs 时就定了。XFS 把空间划成多个 AG,每个 AG 以超级块开头,后面是 AGF(空闲空间管理头)、AGI(inode 管理头)、AGFL(空闲列表),再往后是该 AG 的空闲空间 B+ 树和 inode B+ 树;整个文件系统默认还有一块内部日志,inode 按需分配。Btrfs 在设备偏移 64 KiB 写超级块,另有镜像;超级块里带系统 chunk 数组,并指向根树、chunk 树和日志树,其他树由根树管理。ZFS 则在 vdev 上写 label(池标签)和 uberblock(根指针),由它们描述 pool 的拓扑和当前事务组。ext4 超级块、Btrfs 磁盘格式

超级块是入口,记录块大小、容量、UUID、根目录位置、状态。它不固定在盘的最开头:

文件系统 超级块位置
ext4 文件系统起点偏移 1024 B,部分块组有副本
XFS 每个 AG 以超级块开头,AG 0 的是主超级块
Btrfs 设备/分区起点偏移 64 KiB,另有镜像
ZFS 无单一超级块;靠 vdev label 和 uberblock

这里的“起点”是文件系统起点,不是整盘绝对偏移。分区从 1 MiB 开始,ext4 的超级块就在 1 MiB + 1024 B。

XFS 的 AG 0 从文件系统偏移 0 开始,主超级块占第一个扇区。512 B 扇区、4 KiB 文件系统块时,超级块、AGF、AGI、AGFL 都挤在 AG 的第一个 4 KiB 块里,不是各占一个块。AG 数量由 mkfs 按设备规模规划,也可以用 -d agcount= 或 agsize= 指定;每个 AG 最小 16 MiB,最大略小于 1 TiB。XFS 分配组、mkfs.xfs

所以,“XFS 从 block 0 开始使用吗”要分开看:AG 0 的元数据确实从偏移 0 开始,第一个文件系统块被超级块等结构占用;但普通文件的逻辑块 0 仍然是正常可用的,两者不是同一个地址空间。

文件、目录、空间的关系可以简化成:

目录项:文件名 → inode
inode:属性 + 数据映射
extent:文件逻辑区间 → 设备区间
分配结构:哪些块空闲

打开一张照片时,先按文件名找到 inode,再按 extent 找到数据。文件可以占多个不连续区间;extent 用一条记录描述一段连续映射,比逐块记录省空间。

格式化之后要挂载:mount /dev/sdb1 /data。mount 是命令,/etc/fstab 是配置,/data 是挂载点。挂载前 /data 属于原来的文件系统,挂载后原内容被遮住,卸载才回来。同一个文件系统可以挂到多个目录,同一路径可以叠加挂载,通常通过该路径访问的是最上层挂载,下层内容被遮住;mount --bind 则是把一个目录挂到另一个目录,不涉及新设备。mount 手册

一次读写经过哪些层

以一次 4 KiB 读为例,路径大致是:

应用 read(fd, buf, 4096)
   ↓
VFS:统一的文件接口
   ↓
文件系统的缓存读路径:查询页缓存
   ├─ 命中有效缓存:直接返回数据
   └─ 未命中:映射文件区间(文件偏移 → extent → 设备块)
          ↓
块层:bio → 合并/拆分 → blk-mq 队列
   ↓
驱动:NVMe/SCSI 命令(SATA 经 libATA)
   ↓
控制器 → 介质

VFS 是 Linux 的虚拟文件系统层,给应用提供统一的 open/read/write 接口;具体文件系统负责把文件偏移翻译成设备块。块层把请求合并、拆分,按队列发给驱动。blk-mq 是多队列块层,NVMe 通常每个 CPU 核有提交/完成队列。驱动把 bio(块层请求)变成设备命令:NVMe 驱动写提交队列、敲门铃;SCSI 驱动发 SCSI 命令块(CDB);对于 SATA 盘,libATA 将上层 SCSI 读写命令转换为 ATA 命令,交给设备执行。控制器完成后中断或轮询,驱动再唤醒等待的进程。VFS、blk-mq、libATA

驱动在主机侧,固件在设备侧。文件系统驱动关心“文件区间在哪”,设备驱动关心“怎么把 I/O 交给设备”。应用看到的是文件偏移,设备执行的是 LBA 读写,中间每一步都在做地址换算。

命中有效页缓存时,读取所需的数据可以直接从内存返回;普通缓存写通常先修改页缓存,再延迟回写。O_DIRECT 绕开页缓存,但仍然走块层和驱动,本身不提供与同步写入相同的持久化保证。普通 write 成功不代表数据已持久化;应用可以使用 fsync、fdatasync,或通过 O_SYNC、O_DSYNC 请求相应的同步写入语义,并检查操作是否成功。新增文件若要保证目录项也已持久化,还需要对父目录执行 fsync。这些机制决定了“write 返回成功”和“数据真的落盘”不是一回事。fsync 手册、open 手册

系统怎么判断硬盘中存在文件系统格式

设备型号和扇区大小不能告诉你里面有没有文件系统。mount /dev/sdb1 /data 不带 -t 时,mount 用 libblkid(块设备探测库)读已知位置的签名:

格式 签名 偏移
ext2/3/4 0xEF53(磁盘字节 53 EF) 0x438
XFS XFSB 0
Btrfs _BHRfS_M 0x10040
NTFS NTFS 引导扇区偏移 3
FAT32 FAT32 0x52

偏移基准是分区/LV 的起点,不是整盘。认出类型后,挂载请求交给对应驱动,驱动再检查超级块、特性、校验。探测认出来,不代表结构没坏;显式 -t xfs 能跳过猜测,但跳不过驱动验证。mount 手册

ZFS 是个特例:它没有单一超级块,也不靠 mount /dev/sdb1 这种方式挂载。zpool import 扫描 vdev label 和 uberblock,找到 pool 后再挂 dataset;mount -t zfs tank/data 用的是 dataset 名,不是块设备名。LVM 和 mdraid 也一样:blkid 先认出 LVM2_member 或 linux_raid_member,LVM 由 pvscan/vgchange 激活,mdraid 由 mdadm --assemble 组装,最后才是文件系统。

lsblk -f
sudo blkid -p /dev/sdb1
findmnt -T /data -o SOURCE,TARGET,FSTYPE,OPTIONS
file -s /dev/sdb1

所以,操作系统判断文件系统格式,不是“问硬盘”,而是按约定去读文件系统自己写在块里的签名和结构。硬盘只提供字节和地址;格式是软件层的事。