2. Linux存储子系统架构:VFS层、块设备层、设备驱动层、物理设备层,I/O调度器与请求队列

各位同学,今天我们来聊聊Linux存储子系统的整体架构。说实话,我刚入行那会儿,面对这一堆层次也是有点懵的。但后来我发现,只要抓住一条主线——数据从用户空间到物理磁盘的完整路径,一切就清晰了。

Linux存储子系统,说白了就是一套分层模型。它把复杂的存储操作拆成了五个关键层次:VFS层、块设备层、I/O调度层、设备驱动层、物理设备层。每一层各司其职,又紧密配合。

核心观点:理解这五层架构,是做好存储驱动移植和分区管理的基础。你想想看,如果连数据怎么走都不清楚,出了问题怎么定位?

Linux 存储子系统五层架构 VFS 层(虚拟文件系统) open() / read() / write() — 统一文件操作接口 块设备层(Block Layer) 块设备抽象 / bio 结构体 / 请求合并 I/O 调度层(I/O Scheduler) CFQ / Deadline / NOOP — 请求队列与调度策略 设备驱动层(Device Driver) 块设备驱动 / 中断处理 / DMA 传输 物理设备层(Physical Device) eMMC / NAND Flash / SD 卡 / SATA 硬盘 用户态 内核态 内核态 内核态 硬件

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_fnmake_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()调用到底经历了什么:

  1. 用户程序调用write(fd, buf, len)
  2. VFS层根据fd找到对应的file和dentry,调用具体文件系统的write方法
  3. 文件系统将文件偏移转换为块号,构造bio结构体,提交到块设备层
  4. 块设备层将bio封装成request,放入请求队列
  5. I/O调度器对请求队列进行排序和合并
  6. 设备驱动从队列取出请求,通过DMA或PIO方式写入硬件
  7. 物理设备完成数据写入,触发中断
  8. 中断处理函数回调上层,逐层通知完成

整个过程看起来复杂,但每一层都有明确的职责。做驱动移植时,我建议你重点关注设备驱动层I/O调度层。这两层是性能瓶颈的高发区。

我的习惯:每次移植新驱动,我都会先在/sys/block/<device>/queue/scheduler里看看当前用的什么调度器。然后跑一遍fio测试,对比不同调度器的性能。这个习惯帮我避免了好几次线上事故。

好了,这一章的内容就到这里。Linux存储子系统的五层架构,是后续所有章节的基础。下一章我们会深入VFS层的核心数据结构,看看dentry、inode和file之间到底是怎么关联的。


公众号:蓝海资料掘金营,微信deep3321