本篇定位 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 |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。