【Linux 内核专栏 13】快IO层

本篇定位 11 篇写的字符设备是"按字节流",块设备是"按块/扇区随机访问"(磁盘/eMMC/SSD)。本篇讲清块设备怎么工作:bio 请求 → request_queue → IO 调度器合并排序 → 块设备驱动处理。读完能理解 dd if=/dev/sda 走的路径、能看懂 IO 调度器选型、能调 IO 性能。
块设备是磁盘类设备的抽象,你裸机直接读写扇区,Linux 多了"IO 调度"

裸机读写 eMMC:发命令读扇区号。Linux 块层在中间加了 IO 调度器:合并相邻请求、排序减少寻道、缓存预读。这是裸机没有的优化层(因为单任务不需要)。理解块层 = 理解 Linux 磁盘性能为什么强。


目录

  • [一、块设备 vs 字符设备](#一、块设备 vs 字符设备)
  • [二、块 IO 的核心数据结构](#二、块 IO 的核心数据结构)
    • [2.1 bio(块 IO 请求)](#2.1 bio(块 IO 请求))
    • [2.2 request(调度后的请求)](#2.2 request(调度后的请求))
    • [2.3 request_queue(请求队列)](#2.3 request_queue(请求队列))
    • [2.4 三者关系](#2.4 三者关系)
  • [三、IO 调度器(Elevator)](#三、IO 调度器(Elevator))
    • [3.1 为什么需要 IO 调度](#3.1 为什么需要 IO 调度)
    • [3.2 常见调度器](#3.2 常见调度器)
    • [3.3 合并的类型](#3.3 合并的类型)
    • [3.4 看调度器](#3.4 看调度器)
    • [3.5 对照裸机](#3.5 对照裸机)
  • [四、blk-mq(多队列块 IO)⭐](#四、blk-mq(多队列块 IO)⭐)
    • [4.1 为什么 blk-mq](#4.1 为什么 blk-mq)
    • [4.2 blk-mq 结构](#4.2 blk-mq 结构)
    • [4.3 blk-mq 驱动接口](#4.3 blk-mq 驱动接口)
  • 五、块设备驱动骨架
    • [5.1 简化结构](#5.1 简化结构)
    • [5.2 gendisk(磁盘对象)](#5.2 gendisk(磁盘对象))
    • [5.3 block_device_operations](#5.3 block_device_operations)
  • [六、page cache 与块层衔接(04/12 篇)](#六、page cache 与块层衔接(04/12 篇))
    • [6.1 读文件路径](#6.1 读文件路径)
    • [6.2 写文件路径](#6.2 写文件路径)
    • [6.3 直接 IO(O_DIRECT)](#6.3 直接 IO(O_DIRECT))
  • 七、分区
    • [7.1 分区是什么](#7.1 分区是什么)
    • [7.2 Linux 分区](#7.2 Linux 分区)
  • [八、IO 性能监控](#八、IO 性能监控)
    • [8.1 iostat](#8.1 iostat)
    • [8.2 其他工具](#8.2 其他工具)
    • [8.3 关键指标](#8.3 关键指标)
    • [8.4 性能调优](#8.4 性能调优)
  • 九、对照总表
  • 十、本篇小结
  • 速查表

一、块设备 vs 字符设备

字符设备 块设备
访问单位 字节 扇区(512B)/块(常 4KB)
寻址 顺序流 随机(扇区号)
缓存 驱动自管(或不缓存) page cache(04 篇)
接口 file_operations(read/write) bio/request_queue
例子 UART/SPI/I2C 磁盘/eMMC/SSD/ramdisk
FS 无(裸设备) 挂文件系统
  • 块设备上挂文件系统(ext4/FAT)
  • 字符设备是裸接口(用户直接操作)

二、块 IO 的核心数据结构

2.1 bio(块 IO 请求)

c 复制代码
struct bio {
    struct block_device *bi_bdev;       // 目标块设备
    sector_t bi_sector;                  // 起始扇区
    unsigned int bi_size;                // 总字节数
    unsigned short bi_vcnt;              // bio_vec 数量
    unsigned short bi_idx;               // 当前 bio_vec 索引
    struct bio_vec *bi_io_vec;           // 段数组(每段一页)
    unsigned int bi_opf;                 // 操作 + 标志(READ/WRITE/FLUSH/FUA)
    bio_end_io_t *bi_end_io;             // 完成回调
    void *bi_private;
    ...
};

struct bio_vec {
    struct page *bv_page;       // 数据页(page cache 的)
    unsigned int bv_len;        // 段长度
    unsigned int bv_offset;     // 页内偏移
};
  • bio = 一次块 IO 请求
  • 含目标扇区、操作(读/写)、数据段(bio_vec 数组,可散页)
  • 一个 bio 可跨多页(bio_vec 数组)

2.2 request(调度后的请求)

c 复制代码
struct request {
    struct request_queue *q;            // 所属队列
    struct bio *bio;                     // 关联的 bio(可能多个 bio 合并)
    struct list_head queuelist;          // 挂队列
    sector_t __sector;                   // 起始扇区
    unsigned int __data_len;             // 字节数
    ...
};
  • request = 调度后的请求(bio 经调度器合并/排序后成 request)
  • 一个 request 可含多个合并的 bio

2.3 request_queue(请求队列)

c 复制代码
struct request_queue {
    struct request *last_merge;          // 最后合并的(快速合并)
    struct elevator_queue *elevator;     // IO 调度器
    struct blk_mq_ctx *queue_ctx;        // 软件队列(blk-mq)
    ...
    make_request_fn *make_request_fn;    // bio → request
    request_fn_proc *request_fn;          // 处理队列(老接口)
    ...
};
  • 每个块设备一个 request_queue
  • 管理请求:接收 bio → 调度 → 交给驱动

2.4 三者关系

复制代码
bio (用户/FS 发的 IO)
  → request_queue
  → IO 调度器(合并/排序)
  → request (调度后)
  → 块设备驱动处理
  → 完成,调 bio_end_io

bio → request → 驱动 是块层主链

裸机直接发扇区命令。Linux:bio(上层发)→ 调度器合并成 request → 驱动处理。调度器是关键优化:合并相邻 IO(少命令)、排序(减少寻道)。HDD 时代极大提速,SSD 时代仍减命令开销。


三、IO 调度器(Elevator)

3.1 为什么需要 IO 调度

  • 上层发来的 bio 可能乱序、零散
  • 直接发磁盘:磁头乱跳(HDD 慢),命令多(SSD 开销)
  • 调度器:① 合并相邻扇区请求;② 按扇区排序;③ 公平性

3.2 常见调度器

调度器 特点 适用
mq-deadline 按 deadline 排序,防饥饿 通用,默认
BFQ 按进程公平分配带宽 桌面/交互
Kyber 简单,适应 NVMe 高速 SSD
none 不调度,直接发 NVMe(硬件自调度)

3.3 合并的类型

  • 前合并(front merge):新 bio 在某 request 前,合并成更大
  • 后合并(back merge):新 bio 在某 request 后,合并
  • 合并后一个 request 处理更大范围,减少命令数

3.4 看调度器

bash 复制代码
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none   # 当前用 mq-deadline

# 改(运行时)
echo bfq > /sys/block/sda/queue/scheduler

3.5 对照裸机

裸机 Linux 块层
IO 顺序 来一个发一个 调度器合并排序
寻道优化 无 有(HDD 关键)
公平性 无 有(BFQ)

嵌入式视角:嵌入式 eMMC 选调度器

嵌入式 eMMC/SD:常 mq-deadline 或 none(eMMC 随机访问好,调度收益小)。NVMe:kyber/none。HDD:mq-deadline(寻道优化)。BSP 按 storage 选。


四、blk-mq(多队列块 IO)⭐

4.1 为什么 blk-mq

  • 老块层单队列,多核竞争锁,高速设备(SSD/NVMe)瓶颈
  • blk-mq:每 CPU 一个软件队列 + 硬件队列,并行处理

4.2 blk-mq 结构

复制代码
软件队列(每 CPU 一个)
  → 合并/排序
  → 硬件队列(可能多个,NVMe 每核一个)
  → 驱动处理
  • 软件队列无锁(每核独立)
  • 硬件队列并行发命令
  • NVMe 性能飞跃靠 blk-mq

4.3 blk-mq 驱动接口

块设备驱动实现 blk_mq_ops:

c 复制代码
static const struct blk_mq_ops my_mq_ops = {
    .queue_rq = my_queue_rq,      // 处理 request
    .init_hctx = my_init_hctx,
    ...
};

五、块设备驱动骨架

5.1 简化结构

c 复制代码
struct my_blkdev {
    struct gendisk *disk;          // 磁盘对象
    struct request_queue *queue;   // 请求队列
    spinlock_t lock;
    void *data;                    // 模拟存储
    ...
};

// 处理请求(blk-mq)
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
                                const struct blk_mq_queue_data *bd) {
    struct request *rq = bd->rq;
    struct my_blkdev *dev = rq->q->queuedata;

    blk_mq_start_request(rq);

    // 遍历 bio
    struct bio *bio = rq->bio;
    while (bio) {
        my_transfer(dev, bio);   // 读/写数据
        bio = bio->bi_next;
    }

    blk_mq_end_request(rq, BLK_STS_OK);
    return BLK_STS_OK;
}

static int my_probe(...) {
    dev = devm_kzalloc(...);

    // 块设备注册
    dev->disk = blk_mq_alloc_disk(&my_tag_set, dev);
    dev->queue = dev->disk->queue;

    dev->disk->major = my_major;
    dev->disk->first_minor = 0;
    dev->disk->fops = &my_fops;
    dev->disk->private_data = dev;
    strcpy(dev->disk->disk_name, "mybd");

    // 设置容量(扇区数)
    set_capacity(dev->disk, MY_SIZE_SECTORS);

    add_disk(dev->disk);   // 注册磁盘
    return 0;
}

5.2 gendisk(磁盘对象)

c 复制代码
struct gendisk {
    int major;                     // 主设备号
    int first_minor;
    int minors;                    // 次设备号数量(分区数)
    char disk_name[DISK_NAME_LEN]; // 名字(sda)
    const struct block_device_operations *fops;  // 操作
    struct request_queue *queue;
    sector_t capacity;             // 容量(扇区)
    ...
};
  • gendisk = 一个块设备(磁盘)
  • add_disk 注册后,/dev/sda 出现

5.3 block_device_operations

c 复制代码
struct block_device_operations {
    int (*open)(struct block_device *, fmode_t);
    void (*release)(...);
    int (*ioctl)(...);
    int (*getgeo)(...);            // 几何信息(HDD)
    ...
};
  • 和字符设备的 file_operations 类似但不同
  • 块设备主要数据流不经 fops(走 request_queue),fops 只管 open/ioctl

块驱动不直接处理 read/write

你字符驱动 read/write 在 file_operations。块驱动不这样------read/write 经 page cache → bio → request_queue → 调度 → 驱动的 queue_rq。块驱动的 fops 只管 open/ioctl/getgeo。这是块层和字符层的大区别。


六、page cache 与块层衔接(04/12 篇)

6.1 读文件路径

复制代码
read(fd, buf, n)  (12 篇 VFS)
  → ext4_read
  → page cache(04 篇)
    命中 → 拷用户
    不命中 → 发 bio(READ)→ 块层 → 调度 → 驱动读盘 → 填 page cache → 拷用户

6.2 写文件路径

复制代码
write(fd, buf, n)
  → ext4_write
  → 写 page cache(标记 dirty)
  → 返回
  → writeback 线程:发 bio(WRITE)→ 块层 → 驱动写盘

6.3 直接 IO(O_DIRECT)

  • 绕过 page cache
  • 用户 buffer ↔ 块层直接(bio)

七、分区

7.1 分区是什么

  • 一个块设备(磁盘)划分多个分区
  • 每个分区像独立块设备(/dev/sda1, /dev/sda2)
  • 分区表:MBR / GPT

7.2 Linux 分区

bash 复制代码
ls /dev/sda*       # sda, sda1, sda2
fdisk -l /dev/sda  # 看分区
  • 主块设备 gendisk,次设备号区分分区
  • 每分区可挂不同 FS

八、IO 性能监控

8.1 iostat

bash 复制代码
iostat -x 1
# Device  rrqm/s  wrqm/s  r/s  w/s  rkB/s  wkB/s  avgrq-sz  avgqu-sz  await  %util
# sda       0.0     0.0   10   20   40     80     4.0       0.5       15.0   45.0
  • r/s w/s:每秒读写次数(IOPS)
  • rkB/s wkB/s:每秒读写带宽
  • await:平均 IO 延迟(ms)
  • %util:利用率(100% 满载)

8.2 其他工具

工具 作用
iostat IO 统计
iotop 按进程看 IO
blktrace 块层事件跟踪(16 篇)
fio IO 性能压测
dd 简单测速

8.3 关键指标

指标 含义 关注
IOPS 每秒 IO 次数 随机小 IO(SSD 关键)
带宽 MB/s 顺序大 IO
延迟 ms/μs 实时性
队列深度 未完成 IO 数 并发

8.4 性能调优

  • 调度器选型(HDD:deadline / SSD:none)
  • 预读(readahead):blockdev --setra
  • IO 大小对齐(块大小倍数)
  • O_DIRECT 绕 cache(数据库)
  • blk-mq 多队列(SSD/NVMe)

九、对照总表

概念 裸机/FreeRTOS Linux 块层
扇区访问 直接发命令 bio → 调度 → request → 驱动
IO 合并 无 调度器合并
寻道优化 无 调度器排序
缓存 无 page cache
调度 无 mq-deadline/BFQ/none
多队列 无 blk-mq
分区 自己管 gendisk + 分区
监控 无 iostat/iotop

十、本篇小结

  • 块设备:按扇区/块随机访问(磁盘/eMMC/SSD),接口是 bio/request_queue(非 file_operations)
  • bio :块 IO 请求(扇区+操作+数据段);request :调度后合并的;request_queue:每设备一个队列
  • IO 调度器:合并相邻 + 排序,选型 mq-deadline/BFQ/kyber/none;HDD 选 deadline(寻道),SSD/NVMe 选 none/kyber
  • blk-mq:多队列,每 CPU 软队列 + 硬件队列,NVMe 性能根基
  • 块驱动:blk_mq_alloc_disk + queue_rq 处理 request;gendisk 代表磁盘;fops 只管 open/ioctl(数据走队列)
  • 读路径:read → VFS → page cache → bio → 块层 → 驱动;写:write → page cache dirty → writeback → bio → 块层
  • 分区:主块设备 + 次设备号区分
  • 监控:iostat(IOPS/带宽/延迟/利用率)/iotop/blktrace/fio
  • 你裸机直接发扇区命令,Linux 块层加了调度器/缓存/多队列优化

速查表

想干啥 用什么
看调度器 cat /sys/block/sdX/queue/scheduler
改调度器 echo bfq > /sys/block/sdX/queue/scheduler
看 IO 统计 iostat -x 1
按进程看 IO iotop
跟踪块事件 blktrace / blkparse
压测 IO fio
简单测速 dd if=/dev/zero of=test bs=1M count=100
看分区 fdisk -l / ls /dev/sdX*
设预读 blockdev --setra N /dev/sdX
看队列深度 cat /sys/block/sdX/queue/nr_requests

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。


相关推荐
嵌入式分享1 小时前
嵌入式分享#63:RK3588 怎么选?全系列选型指南
linux
Pointer Pursuit1 小时前
Linux调试器 ——gdb/cgdb
linux·运维·服务器
IanSkunk1 小时前
从检查设备到训练方案:眼科与视光中心设备联动的数据通路、字段口径与校验规则
服务器·数据库·负载均衡
chenlance1 小时前
PADS灌铜、覆铜过程和技巧小汇总
linux·服务器·网络
xixiaoyunya1 小时前
服务器自动备份怎么做:十几分钟搭建一套稳定可监控的方案
运维·服务器
HideF2 小时前
【无标题】
linux
高山有多高2 小时前
【Linux笔记】NAT与内网穿透
linux
xiangw@GZ2 小时前
纽扣电池型号
嵌入式硬件
迷途之人不知返2 小时前
自主实现一个简单的shell命令行
linux