linux storage stack 学习
| 层级 | 模块/组件 | 说明 | 数据流向 (箭头) |
| 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)
-
用户程序调用
read(fd, buf, count)系统调用,传入文件描述符fd、缓冲区和长度; -
内核接收到系统调用,开始处理。
| 名称 | 所在空间 | 作用 |
| buf | 用户空间 | 存放读取的数据,供用户程序使用 |
| page cache | 内核空间 | 缓存文件数据,提高读写效率 |
| buffer cache | 内核空间 | 缓存磁盘块数据(旧机制) |
2. 内核解析文件描述符
-
通过
fd在当前进程的files_struct中查找对应的struct file *; -
进而从
file->f_inode获取对应的struct inode *,它代表这个打开文件在内核中的元数据对象。
3. 检查文件描述符是否合法
-
检查
fd是否在合法范围; -
是否已经被打开;
-
是否具有读权限(检查
file->f_mode & FMODE_READ); -
如果不合法,直接返回
-EBADF或-EACCES等错误码。
4. 计算读取的文件偏移量与长度
-
读取起始位置为
offset = file->f_pos(文件当前位置); -
加上用户请求的长度
length = count,得出本次读取的目标范围; -
这些信息决定接下来要访问哪些页。
5. 查找页缓存映射信息
-
利用
inode->i_mapping获取该文件的address_space; -
每个文件都有一个页缓存地址空间,管理该文件的数据页;
-
将偏移量转换为页索引
index = offset / PAGE_SIZE。
每个
inode都有一个成员i_mapping****,它是一个指向struct address_space的指针。这个结构叫做“地址空间”,在这里指的是文件在页缓存中的表示。简单的理解
inode表示一个文件;
i_mapping表示这个文件在内存中缓存的数据;
6. 查找页缓存页
-
通过
find_get_page(mapping, index)查询页缓存(Page Cache)中是否已有对应的数据页; -
这一步在 radix tree / xarray 结构中查找是否存在该页。
-
页缓存中的页是以页号(index)为键,保存在一棵树结构中(如 radix tree 或 xarray)中。
-
文件偏移是从 0 开始;
-
每页大小是 4KB(PAGE_SIZE);
-
那么偏移 0 属于页号 0,偏移 4096 属于页号 1,依此类推。
-
7. 命中 / 未命中分支
命中:
-
说明页缓存中已经有对应页,并且是
uptodate的; -
内核直接将该页中的数据通过
copy_to_user()拷贝到用户提供的缓冲区。
未命中:
-
页缓存中没有目标页,或不完整;
-
内核会调用底层文件系统的
readpage()、readpages()或更现代的iomap_readpage(); -
这些函数会负责从磁盘读取数据并填充页缓存页。
8. 填充 Page Cache 页:
-
底层函数构造
bio结构(块 I/O 请求); -
提交给块层,可能进一步调度到 I/O scheduler;
-
通过
submit_bio()或iomap_dio_rw()发起实际的磁盘读取请求; -
读入的数据会写入目标的
struct page。
9. 页缓存页填充完成
-
读取完成后,内核设置该页的标志位:
SetPageUptodate(); -
表示此页缓存页的数据是最新的、可靠的。
10. 数据拷贝到用户空间
-
内核再次将这页的数据从
struct page拷贝到用户空间的缓冲区; -
通常用
copy_to_user()实现; -
如果请求跨越多个页,会重复上述过程。
11. 返回读取结果
-
读取完成后,内核更新
file->f_pos(文件偏移); -
返回成功读取的字节数给用户进程。
+-----------------------+
| 用户进程调用 read(fd) |
+-----------+-----------+
|
v
+------------------------------+
| 由 fd 查找 struct file |
| 获取 struct inode |
+-----------+------------------+
|
v
+------------------------------+
| 检查文件描述符是否合法 |
+-----------+------------------+
|
v
+------------------------------+
| 计算文件偏移 offset 和长度 |
+-----------+------------------+
|
v
+------------------------------+
| 使用 inode->i_mapping |
| - 映射页缓存 address_space |
| - 计算页号 index |
+-----------+------------------+
|
v
+------------------------------+
| 查找页缓存页: |
| - find_get_page(mapping, index) |
+-----------+------------------+
|
+------+------+
| |
| 命中 | 未命中
| |
v v
+-------------+ +--------------------------------+
| copy_to_user | | 调用文件系统 ->readpage 或 |
| 拷贝页到用户 | | iomap_readpage() |
+-------------+ +----------------+---------------+
|
v
+-----------------------------+
| 填充页缓存页的 struct page |
| - 通过 bio 发起磁盘读取 |
| - submit_bio()/iomap_dio_rw() |
+-----------------------------+
|
v
+-----------------------------+
| 页缓存页填充完成 |
| 设置 PG_uptodate |
+-----------------------------+
|
v
+---------------------+
| 将页拷贝到用户空间 |
+---------------------+
|
v
+-------------------------------+
| 返回读取的字节数给用户进程 |
+-------------------------------+
二、Page Cache写策略
用户写入的数据不会立即同步到磁盘,而是优先写入内存中的 Page Cache,稍后再由内核异步刷新到磁盘。整个流程如下:
-
用户调用
write(fd,buf,count)系统调用- 用户进程请求写入某个文件的指定位置。参数
fd是文件描述符,buf是用户态缓冲区,count是写入字节数。
- 用户进程请求写入某个文件的指定位置。参数
-
内核解析文件描述符,获取 inode 和 file 结构
-
内核通过 fd 查找对应的
struct file。 -
通过
file->f_inode获取文件对应的struct inode,包含文件元数据及地址空间信息。
-
-
内核根据 offset 计算写入位置
根据当前文件偏移量(或由 pwrite() 显式提供的 offset)
-
一般从
file->f_pos(文件当前位置)读取偏移,或 pwrite 显式提供。 -
计算写入起始页号 index = offset / PAGE_SIZE。
-
计算页内偏移 offset_in_page = offset % PAGE_SIZE。
-
计算写入结束页,确定总共涉及多少页。
-
定位 Page Cache 中的页面
-
通过
inode->i_mapping获取该文件的address_space结构。 -
使用
find_or_create_page(mapping, index)查找或创建对应页缓存页。 -
锁定页(例如
lock_page()),防止并发修改。
-
-
拷贝数据到页缓存页(内核空间)
-
使用
copy_from_user(page_address(page) + offset_in_page, buf, bytes_to_copy) -
将用户缓冲区中的数据复制到内核内存页缓存中对应偏移位置。
-
支持分多页复制,循环处理。
-
-
标记该页为 dirty page
-
调用
set_page_dirty(page),通知内核该页内容已被修改。 -
更新
address_space脏页计数,准备写回。
-
-
更新文件偏移和释放锁
-
更新
file->f_pos,移动文件当前位置。 -
解锁页缓存页(
unlock_page())。
-
-
write() 返回用户,写入完成(仅内存中)
-
内核仅保证数据已写入 Page Cache(内存),没有保证磁盘同步。
-
write() 返回写入字节数,通常很快完成。
-
-
后台异步写回机制启动(写回守护线程)
-
Linux 维护写回线程(
kworker),定期扫描脏页。 -
触发写回的条件包括:
-
脏页数超过阈值(如
dirty_ratio)。 -
脏页存在时间过长(
dirty_expire_centisecs)。 -
内存压力(内存紧张需要回收)。
-
应用调用同步接口(fsync、fdatasync)。
-
文件关闭时。
-
-
-
异步写回执行写磁盘操作
1. 写回线程调用文件系统接口:`writepage()` 或 `writepages()`,完成页写入。
2. 现代文件系统多采用 iomap/dio 机制完成块映射和 I/O 调度。
3. 写回时可能进行块分配和日志同步(如 journaling)。
4. 提交 BIO 请求(`submit_bio()`)到底层块设备。
- 数据写入设备缓存及磁盘
1. I/O 调度器将 BIO 发送至设备驱动。
2. 设备驱动写入硬盘或固态硬盘缓存。
3. 写完成后,设备返回完成信号。
- 内核清除脏页标记,页变 clean
1. 写完成后调用 `clear_page_dirty_for_io(page)` 或类似接口。
2. 页变为 clean,更新 `address_space` 脏页计数。
3. 该页数据与磁盘数据保持同步。
+--------------------------+
| 用户进程调用 write(fd) |
| 参数:buf, count, offset |
+------------+-------------+
|
v
+--------------------------+
| 内核根据 fd 查找 struct file |
| 及对应 inode, address_space |
+------------+-------------+
|
v
+--------------------------+
| 计算写入偏移 offset 和长度 |
+------------+-------------+
|
v
+--------------------------+
| 查找或分配页缓存页(page) |
| - find_get_page() 或 alloc_page()|
+------------+-------------+
|
v
+--------------------------+
| 锁定页面,防止并发修改 |
+------------+-------------+
|
v
+--------------------------+
| 将用户数据从 buf 拷贝到 |
| 页缓存页内存中 |
+------------+-------------+
|
v
+--------------------------+
| 设置页为脏页 (PageDirty) |
| 设置 PG_uptodate 标志 |
+------------+-------------+
|
v
+--------------------------+
| 判断写策略 |
| - 写直达(write-through) |
| - 同步写(O_SYNC) |
| - 写回(write-back) |
+------------+-------------+
|
+-------+-------+
| |
| 写直达/同步写 | 写回策略
| |
v v
+----------------+ +----------------------------+
| 立刻提交磁盘写请求 | 延迟写入:脏页等待定时器或内存压力触发 |
| submit_bio()/iomap_dio_rw() | |
+----------------+ +----------------------------+
| |
v v
+----------------+ +----------------------------+
| 磁盘写操作完成 | | 脏页保持脏状态,等待写盘 |
| 清除 PageDirty | | 定时器或回收机制调用写盘 |
+----------------+ +----------------------------+
| |
+------------+------------+
|
v
+-----------------------+
| 解锁页缓存页,允许访问 |
+-----------+-----------+
|
v
+-----------------------+
| 返回写入的字节数给用户 |
+-----------------------+

浙公网安备 33010602011771号