2. Linux存储子系统架构:VFS层、块设备层、设备驱动层、物理设备层,I/O调度器与请求队列
各位同学,今天我们来聊聊Linux存储子系统的整体架构。说实话,我刚入行那会儿,面对这一堆层次也是有点懵的。但后来我发现,只要抓住一条主线——数据从用户空间到物理磁盘的完整路径,一切就清晰了。
Linux存储子系统,说白了就是一套分层模型。它把复杂的存储操作拆成了五个关键层次:VFS层、块设备层、I/O调度层、设备驱动层、物理设备层。每一层各司其职,又紧密配合。
核心观点:理解这五层架构,是做好存储驱动移植和分区管理的基础。你想想看,如果连数据怎么走都不清楚,出了问题怎么定位?
2.1 VFS层——一切皆文件的基石
VFS,全称Virtual File System,虚拟文件系统。它是Linux「一切皆文件」哲学的落地实现。不管底层是ext4、FAT32还是NFS,用户程序只需要用open()、read()、write()这些标准接口就行。
我个人习惯把VFS理解成一个适配器模式。它定义了一套通用的文件操作接口,然后让具体的文件系统去实现。这样上层应用就不用关心底层是什么存储介质了。
实战经验:我在移植一个嵌入式板子时,发现应用程序读写文件特别慢。排查了半天,最后发现是VFS层的dentry缓存没配置好。调整了dentry_cache大小后,性能直接翻倍。嗯,VFS层的缓存策略,真的不能忽视。
2.2 块设备层——数据的中转站
VFS把文件操作转成对块设备的访问。块设备层就是负责管理这些块设备的核心模块。
这里有个关键概念——bio结构体。它是块设备层的基本I/O单元。每次读写请求,都会被封装成一个或多个bio。bio里记录了要读写的扇区号、数据缓冲区、完成回调函数等信息。
// bio结构体核心字段(简化版)
struct bio {
sector_t bi_sector; // 起始扇区号
struct bio_vec *bi_io_vec; // 数据段向量
unsigned int bi_vcnt; // 向量数量
bio_end_io_t *bi_end_io; // I/O完成回调
struct block_device *bi_bdev; // 目标块设备
};
你想想看,如果没有块设备层,每个文件系统都要直接跟硬件驱动打交道,那代码得多乱?块设备层把这种复杂性封装起来了。
2.3 I/O调度层——请求队列的指挥官
这是很多工程师容易忽略的一层。I/O调度器,说白了就是管理请求队列的。它决定先处理哪个请求、后处理哪个请求。
Linux内核提供了几种调度器:
| 调度器 | 特点 | 适用场景 |
|---|---|---|
| CFQ(完全公平队列) | 按进程分配时间片,公平性好 | 通用桌面/服务器 |
| Deadline(截止时间调度器) | 按请求截止时间排序,减少饿死 | 数据库、实时系统 |
| NOOP(先进先出) | 简单合并,不做重排序 | SSD、NAND Flash |
| MQ-DEADLINE(多队列版) | 多队列并行,高性能 | NVMe、多核系统 |
避坑指南:我曾经在一个NAND Flash项目上用了CFQ调度器,结果随机读写性能惨不忍睹。后来换成NOOP,性能提升了30%。为什么?因为Flash没有机械寻道时间,不需要重排序。选错调度器,性能直接打折扣。
2.4 设备驱动层——硬件与内核的桥梁
设备驱动层,就是具体操作硬件的那部分代码。它负责:
- 注册块设备到内核
- 实现
request_fn或make_request_fn回调 - 处理中断和DMA传输
- 管理硬件寄存器
我记得第一次写块设备驱动时,最头疼的就是请求队列的处理。驱动从请求队列里取出请求,然后转换成硬件命令。这个过程稍有不慎,就会导致数据错乱。
// 块设备驱动的核心回调(伪代码)
static void mydev_request(struct request_queue *q)
{
struct request *req;
while ((req = blk_fetch_request(q)) != NULL) {
// 获取请求的扇区号和方向
unsigned long sector = blk_rq_pos(req);
int rw = rq_data_dir(req); // READ 或 WRITE
// 获取数据缓冲区
struct bio_vec *bvec;
struct req_iterator iter;
rq_for_each_segment(bvec, req, iter) {
void *buffer = page_address(bvec->bv_page) + bvec->bv_offset;
// 调用硬件接口读写
if (rw == READ)
mydev_read_sector(sector, buffer, bvec->bv_len);
else
mydev_write_sector(sector, buffer, bvec->bv_len);
}
// 通知请求完成
__blk_end_request_all(req, 0);
}
}
2.5 物理设备层——最终的战场
最底层就是物理设备了。在嵌入式领域,常见的有:
- eMMC:嵌入式多媒体卡,手机、平板常用
- NAND Flash:裸闪存,需要FTL层管理
- SD卡:可移动存储
- SPI Flash:小容量,常用于Bootloader
每种设备的特性都不一样。比如NAND Flash有坏块问题,eMMC内部自带FTL和磨损均衡。做驱动移植时,必须清楚这些差异。
核心要点:物理设备层不是简单的「读写寄存器」。它涉及时序、电压、ECC校验、坏块管理等一系列硬件细节。我见过太多工程师只关注上层代码,结果硬件时序没调对,数据读出来全是错的。
2.6 数据流全景——从应用到磁盘
好了,我们把五层串起来,看看一次write()调用到底经历了什么:
- 用户程序调用
write(fd, buf, len) - VFS层根据fd找到对应的file和dentry,调用具体文件系统的write方法
- 文件系统将文件偏移转换为块号,构造bio结构体,提交到块设备层
- 块设备层将bio封装成request,放入请求队列
- I/O调度器对请求队列进行排序和合并
- 设备驱动从队列取出请求,通过DMA或PIO方式写入硬件
- 物理设备完成数据写入,触发中断
- 中断处理函数回调上层,逐层通知完成
整个过程看起来复杂,但每一层都有明确的职责。做驱动移植时,我建议你重点关注设备驱动层和I/O调度层。这两层是性能瓶颈的高发区。
我的习惯:每次移植新驱动,我都会先在/sys/block/<device>/queue/scheduler里看看当前用的什么调度器。然后跑一遍fio测试,对比不同调度器的性能。这个习惯帮我避免了好几次线上事故。
好了,这一章的内容就到这里。Linux存储子系统的五层架构,是后续所有章节的基础。下一章我们会深入VFS层的核心数据结构,看看dentry、inode和file之间到底是怎么关联的。
公众号:蓝海资料掘金营,微信deep3321