linux storage stack 学习

td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
层级 模块/组件 说明 数据流向 (箭头)
1️⃣ 应用层 应用程序 (read/write/mmap) 用户发出文件操作或内存请求 → VFS、malloc、mmap
2️⃣ 虚拟文件系统 VFS VFS 虚拟文件系统接口,对应不同 FS 驱动 → 各类文件系统
3️⃣ 文件系统层 Block-based FS Network FS Pseudo FS Special-purpose FS Raw Flash FS 真实实现的数据存储文件系统 → 页缓存 → Block Layer 或 Fuse/userspace
4️⃣ 页缓存 Page Cache page cache 缓存文件页,减少磁盘访问 ←→ 文件系统 / Block Layer
5️⃣ 块层 Block Layer block layer blk_mq 多队列块 I/O 层,带调度器 → request-based drivers / device-mapper / BIO drivers
6️⃣ I/O 调度器 kyber, bfq, mq_deadline I/O 排序与优化 → 请求块驱动
7️⃣ 设备映射层 Device Mapper dm-crypt, dm-raid, dm-thin, ... 提供加密、镜像、缓存、RAID 等块设备功能 → block layer / request-based drivers
8️⃣ 请求型驱动 Request-based drivers dm-multipath, /dev/loop*, virtio-blk, mmcblk*, rbd, nbd 处理封装好的 I/O 请求 → SCSI 层或直接设备
9️⃣ BIO驱动层 BIO-based drivers nvme, pmem, null_blk, brd 更原始的块设备驱动,绕过调度 → PCIe/NVMe/持久内存等
🔟 SCSI 层 scsi_mid, scsi_transport_fc/sas, 低层驱动 (mpt3sas, ahci, vmware_pvscsi) 连接各类 SCSI/SATA/SAS 设备 → 硬件 HBA
🔟 网络文件系统路径 fuse, nfs, cifs, ceph, iscsi 文件系统通过网络协议访问远程设备 → userspace → network → 远程 block
🔟 MTD 闪存路径 mtd, ubi, mtdblock, ubifs/jffs2 支持原始 NAND/NOR flash → SPI-NAND/NOR 硬件
🔟 NVMe 路径 nvme, nvme-pci, nvme-rdma, nvme-tcp 高速块设备接口或远程存储 → PCIe 总线或网络
🔟 物理设备 HDD/SSD, NVMe, SD卡, SAS, RAID, Raw NAND, Persistent memory, N64 cartridge 最终数据读写落盘 ← 来自所有路径的终点
Application
  ↓
VFS
  ↓
ext4
  ↓
Page Cache
  ↓
Block Layer + blk_mq
  ↓
NVMe 驱动(nvme-pci)
  ↓
PCIe 总线
  ↓
NVMe SSD 物理设备
Application
  ↓
VFS
  ↓
FUSE (userspace)
  ↓
SSHFS
  ↓
Network
  ↓
远程块设备 (可能是 NFS, iSCSI, etc.)
Application
  ↓
VFS
  ↓
ext4
  ↓
Device Mapper (dm-crypt → dm-linear → dm-raid)
  ↓
Block Layer
  ↓
SCSI Layer
  ↓
mpt3sas 驱动
  ↓
RAID 控制器
  ↓
SATA 硬盘

其中,页缓存(PageCache)是操作系统对文件的缓存,用来减少对磁盘的 I/O 操作,以页为单位的,内容就是磁盘上的物理块,页缓存能帮助程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于 OS 使用 PageCache 机制对读写访问操作进行了性能优化。

页缓存读取策略:当进程发起一个读操作 (比如,进程发起一个 read() 系统调用),它首先会检查需要的数据是否在页缓存中:

  • 如果在,则放弃访问磁盘,而直接从页缓存中读取。

  • 如果不在,则内核调度块 I/O 操作从磁盘去读取数据,并读入紧随其后的少数几个页面(不少于一个页面,通常是三个页面),然后将数据放入页缓存中。

一、Page Cache读策略

  1. 用户进程调用 read(fd) 用户请求从打开的文件中读取数据,系统调用进入内核态。

  2. 内核根据文件描述符 fd 查找对应的 file 结构 file 结构中包含文件的当前偏移和 inode 指针。

  3. file 结构获取 inode 指针 inode 是文件的核心结构,记录了文件大小、类型和磁盘数据块映射关系。

  4. 检查文件描述符是否有效 内核确认该文件是否处于合法、打开状态。

  5. 计算读取的数据偏移和长度 基于当前文件偏移和用户指定的读取长度,决定本次读操作的数据范围。

  6. 通过 inode 执行块映射(bmap) 把文件逻辑偏移转换成物理磁盘块号,用于后续数据定位。

  7. 查找页缓存(Page Cache)是否已经缓存对应页

    1. 使用 find_get_page() 在页缓存中查找数据页。

    2. 如果命中:直接将页面数据拷贝到用户缓冲区,read() 完成。

    3. 如果未命中:进入下一阶段,调用文件系统的 readpage()readpages()

  8. 进入底层 I/O 流程,检查缓冲区缓存(Buffer Cache) 文件系统会根据页内偏移定位对应的磁盘块,查看块缓存是否已经存在。

  9. 查询对应块的 buffer_head 缓存结构

    1. 如果存在:检查其状态。

      • uptodate(数据是最新的):将其数据复制到页缓存页;

      • 再把页缓存页的数据复制到用户缓冲区。

    2. 如果不存在或 uptodate 为假:需要发起磁盘 I/O。

  10. 触发磁盘读操作:分层检查设备缓存(Disk Cache)

1.  如果磁盘设备缓存命中(例如 SSD 或硬盘的内部缓存):快速返回数据;
    
2.  如果未命中:发起物理磁盘读操作,进行寻道、读盘等。
  1. 读取的数据会先被填入缓冲区缓存(buffer_head)中 然后再更新页缓存页,并最终复制到用户缓冲区。

  2. 用户缓冲区填充完毕后,返回实际读取的字节数给用户进程

+-----------------------+
| 用户进程调用 read(fd)  |
+-----------+-----------+
            |
            v
+------------------------------+
| 内核根据 fd 查找对应的 file  |
| 结构,获得文件的 inode 指针  |
+-----------+------------------+
            |
            v
+------------------------------+
| 内核检查文件描述符 fd 是否有效 |
+-----------+------------------+
            |
            v
+------------------------------+
| 计算文件偏移 offset 和长度    |
+-----------+------------------+
            |
            v
+------------------------------+
| 通过 inode 进行块映射          |
| - 根据文件偏移定位磁盘块号    |
| - 调用块映射函数(bmap)       |
+-----------+------------------+
            |
            v
+------------------------------+
| 从页缓存(Page Cache)查找页  |
| - find_get_page()             |
+-----------+------------------+
            |
    +-------+-------+
    |               |
    | 页缓存命中     | 页缓存未命中
    |               |
    v               v
+-------------+  +----------------------------------------+
| 拷贝页面数据 |  | 触发文件系统 readpage()/readpages()     |
| 到用户缓冲区 |  +-----------+----------------------------+
+-------------+              |
            |                v
            |       +------------------------------------+
            |       | 检查缓冲区缓存(Buffer Cache)       |
            |       +-----------+------------------------+
            |                   |
            |         +---------+----------+
            |         |                    |
            |   查询对应块的 buffer_head  |  块缓存不存在(未命中)
            |         |                    |
            |    +----+----+          +----+----+
            |    | 是否存在 |          |  发起磁盘I/O请求   |
            |    +----+----+          |(submit_bio()/blk_execute_rq())|
            |         |               +---------+----------+
            |     是  |                         |
            |         v                         v
            |  +--------------+        +------------------------+
            |  | buffer_head  |        | 磁盘设备缓存(Disk Cache)|
            |  | 状态检查     |        +-----------+------------+
            |  +------+-------+                    |
            |         |                            |
            |  +------+-------+            +-------+---------+
            |  | 是否块数据   |            | 设备缓存命中     |
            |  | uptodate?   |            |                 |
            |  +------+-------+            |                 |
            |    是   | 否                 |                 |
            |         v                   v                 v
            | +-----------------+   +--------------+  +-----------------+
            | | 返回缓存数据    |   | 发起磁盘I/O   |  | 快速返回数据     |
            | | 到页缓存页      |   | 读入块数据    |  | 从设备缓存      |
            | +-----------------+   +--------------+  +-----------------+
            |         |                   |                  |
            |         v                   v                  v
            | +--------------------+   +-----------------------------+
            | | 拷贝数据到用户缓冲区 |  | 将磁盘读入数据填充缓冲区缓存 |
            | +--------------------+   +-----------------------------+
            |         |                   |
            +---------+-------------------+
                      |
                      v
+----------------------------+
| 返回读取的字节数给用户       |
+----------------------------+

二、Page Cache写策略

用户写入的数据不会立即同步到磁盘,而是优先写入内存中的 Page Cache,稍后再由内核异步刷新到磁盘。整个流程如下:

  1. 用户调用 write() 系统调用

用户进程请求写入某个文件的指定位置。参数 fd 是文件描述符,buf 是用户态缓冲区,count 是写入字节数。

  1. 内核解析文件描述符,获取 inode 和 file 结构

内核通过 fd 查找对应的 file 结构,进而获取到该文件的 inode。这是文件在内核中的核心表示,包括元数据、块映射、文件系统接口等。

  1. 内核根据 offset 计算写入位置

根据当前文件偏移量(或由 pwrite() 显式提供的 offset),内核计算出目标位置,进而确定写入的数据将对应于哪个页缓存页(4KB 对齐的页)以及页内偏移。

  1. 定位 Page Cache 中的页面

    1. 内核检查页缓存中是否已有目标页:

      • 如果有,就复用;

      • 如果没有,就分配一个新的页缓存页,并挂接到该 inode 的 page cache 链表中(radix tree / xarray)。

  2. 拷贝数据到页缓存页(内核空间)

调用 copy_from_user() 将用户缓冲区的数据拷贝到页缓存页的正确偏移位置。此时,数据还在内存中,还未落盘

  1. 标记该页为 dirty page

内核将该页设置为 “dirty” 状态,表示该页数据已被用户修改、但尚未刷新到磁盘。

  1. 用户态 write() 调用立即返回

由于数据已经写入 Page Cache,并不需要等到磁盘操作完成,write() 通常会快速返回,告知用户进程“写入成功”。

此时写操作只是对内存的操作,性能非常高。

  1. 后台异步写回机制启动(writeback)

一段时间后,或者由于以下原因之一,内核将把 dirty pages 刷新到磁盘:

  • 脏页数量达到阈值(如 /proc/sys/vm/dirty_ratio);

  • 脏页存在时间过久(如 /proc/sys/vm/dirty_expire_centisecs);

  • 内存压力大(需要回收页框);

  • 应用主动调用 fsync()fdatasync()

  • 文件关闭时(close());

  • 系统调用 sync()

  1. 写回由后台线程执行

Linux 使用后台线程(如 flush-8:0、旧的 pdflush)扫描并处理 dirty pages:

  • 调用文件系统的 writepage()writepages()

  • 进行块设备映射,提交磁盘写入请求(通过 submit_bio());

  • 如果文件系统是延迟分配的(如 ext4),还需执行块分配。

  1. 数据写入磁盘或设备缓存

数据最终通过 I/O 调度器发送到底层磁盘驱动,写入磁盘或设备缓存(例如 SSD 控制器缓存)。

  1. 内核清除 dirty 标记

写入完成后,内核将该页的 dirty 标记清除,表示此页已和磁盘同步,变为“干净”页(clean page)。

+-----------------------+
| 用户进程调用 write(fd)|
+-----------+-----------+
            |
            v
+------------------------------+
| 内核查找 fd 对应 file/inode  |
+-----------+------------------+
            |
            v
+------------------------------+
| 计算写入 offset 和写入长度   |
+-----------+------------------+
            |
            v
+------------------------------+
| 定位页缓存页(Page Cache)   |
| - 查找是否已有页             |
| - 若无则分配新页             |
+-----------+------------------+
            |
            v
+------------------------------+
| 将用户数据拷贝到页缓存页中   |
| - 使用 copy_from_user()      |
| - 页标记为 dirty              |
+-----------+------------------+
            |
            v
+------------------------------+
| 用户 write() 返回成功        |
| (此时数据仍在内存中)        |
+-----------+------------------+
            |
            v
+------------------------------+
| 内核后台线程或显式 fsync()  |
| 检测到 dirty 页需要刷新      |
+-----------+------------------+
            |
            v
+------------------------------+
| 调用文件系统写回函数(如 ext4_writepage) |
+-----------+------------------+
            |
            v
+------------------------------+
| 生成磁盘写请求                |
| 调用 submit_bio() 或 iomap   |
+-----------+------------------+
            |
            v
+------------------------------+
| 数据写入磁盘或设备缓存        |
+-----------+------------------+
            |
            v
+------------------------------+
| 页缓存页清除 dirty 标记       |
+------------------------------+

https://segmentfault.com/a/1190000023615225#item-2-1

posted @ 2025-07-30 21:41  xyh0703  阅读(17)  评论(0)    收藏  举报