Linux 文件系统从磁盘到应用的完整路径

9次阅读

ext4通过页缓存优先响应read(),未命中时由inode/extent树定位逻辑块,经bio提交至块层,最终由NVMe驱动转为Submission Queue命令;O_DIRECT跳过页缓存但需内存对齐,路径解析依赖dentry/inode缓存。

Linux 文件系统从磁盘到应用的完整路径

ext4 文件系统如何把 read() 调用变成磁盘读取

linux 应用调用 read() 时,不会直接发命令给硬盘。它走的是内核抽象层:先查页缓存(page cache),命中就直接返回;没命中才触发底层 I/O。ext4 作为文件系统驱动,负责把“读文件第 1024 字节”翻译成“读物理块设备上某几个 block”,而这些 block 的位置由 ext4 的 inode、间接块、extent 树共同决定。

关键点在于:ext4 不管理扇区地址,只管逻辑块号(logical block number);真正映射到磁盘 LBA 是由块设备层(如 sd 驱动)和 SCSI/ATA 协议完成的。

  • ext4 的 inode 中记录文件大小、权限、以及指向数据块的指针(直接块、间接块或 extent)
  • 小文件常用直接块;大文件倾向用 extent(连续块范围),减少元数据开销
  • read() 触发 generic_file_read_iter()ext4_readpage()mpage_readpages() → 最终提交 bio 到块层

bio 到 NVMe SSD 的实际写入路径

当 ext4 准备好要读的逻辑块列表后,会构造一个或多个 bio 结构体,交给通用块层(blk-mq)。这时还没碰硬件——bio 会被排队、合并、限速、加密(如果启用了 dm-crypt),再经由设备队列送到驱动。

对 NVMe 设备来说,最终调用的是 nvme_submit_cmd(),把命令放进 Submission Queue,由 SSD 控制器自己拉取执行。注意:NVMe 协议绕过了传统 ide/SCSI 的中间层,所以延迟更低,但调试时也更难抓到“中间状态”。

  • 同一块数据可能被 page cachebuffer cache(已弱化)、SSD 内部 DRAM 缓存NAND 闪存的 page buffer 多次缓存
  • hdparm -I /dev/nvme0n1 可查控制器是否启用写缓存(Write Cache),这直接影响 fsync() 是否真落盘
  • 使用 blktrace + btt 可跟踪一个 read() 在块层各阶段耗时,定位卡点在调度器、驱动还是设备

为什么 open("/path/to/file", O_DIRECT) 会跳过页缓存

O_DIRECT 意味着应用告诉内核:“别管我的内存是不是对齐、别用你的 page cache,我来负责缓冲,你直通块层。” 这要求用户空间缓冲区地址和长度都按 logical_block_size 对齐(通常是 512B 或 4K),否则 read() 返回 -EINVAL

跳过页缓存看似更快,实则风险不少:没有缓存复用、每次都是真实 I/O、且容易因对齐失败静默降级为普通读(取决于内核版本和文件系统)。

  • ext4 在挂载时若指定 dioread_nolock,可避免 O_DIRECT 读时对 inode 加锁,提升并发性能
  • io_uring 配合 O_DIRECT 是当前高性能 I/O 的主流组合,但需检查内核是否开启 CONFIG_IO_URING
  • 不要在数据库日志写入等强一致性场景盲目禁用页缓存——page cache 提供了统一的脏页回写和 fsync 语义

用户态程序看到的路径名怎么变成磁盘地址

路径解析不是一次性动作。每次 open()stat() 都要从根目录开始 walk:/proc/1234/fd/0 → 找到 dentry → 关联 inode → 确认文件类型和权限 → 最终拿到 file 结构体。这个过程大量依赖 dentry cache 和 inode cache,否则每 open 一次都要读磁盘上的目录块。

ext4 的目录项(Struct ext4_dir_entry_2)是变长结构,按 hash 分桶(htree 目录),所以大目录下 ls 快,但 find . -name "*.log" 仍要遍历所有叶子块。

  • ls -l 显示的 inode 号来自 ext4 的 inode table,不是磁盘物理地址;同一个 inode 号在不同挂载点可能指向不同内容(bind mount)
  • /proc/mounts 中的 uuidfstype 决定了内核用哪个文件系统驱动处理该设备
  • 硬链接共用一个 inode,软链接则是独立 inode 存放路径字符串——这意味着 readlink() 不触发数据块读取,只读 inode 自身

真正复杂的不是路径有多长,而是每一层缓存策略、锁粒度、异步时机都不一样。比如 page cache 回写由 writeback 内核线程控制,而 ext4 journal 提交又依赖 jbd2 线程,两者节奏错开就可能造成意外延迟。这类细节不出问题时没人注意,一出就是疑难杂症。

text=ZqhQzanResources