1. 块设备驱动概述:块设备与字符设备的区别、块设备驱动在内核中的位置、块设备驱动开发环境准备

1.1 块设备与字符设备的本质区别

在 Linux 设备驱动模型中,I/O 设备被划分为两大基本类型:块设备(Block Device)和字符设备(Character Device)。理解两者的差异是学习块设备驱动的第一道门槛。

对比维度 块设备(Block Device) 字符设备(Character Device)
数据访问单位 固定大小的块(通常为 512 字节、4KB 等) 字节流,无固定大小约束
随机访问能力 支持,可通过 lseek 定位到任意块 通常不支持随机访问(串行流)
I/O 调度 内核 I/O 调度器合并/排序请求 无调度层,直接传递
缓存策略 通过 Page Cache 缓存数据 通常无内核缓存(raw 模式)
设备节点类型 b(如 /dev/sda) c(如 /dev/ttyS0)
典型设备 硬盘、SSD、SD 卡、U 盘、NVMe 串口、鼠标、键盘、音频设备
驱动接口核心 struct block_device_operations + bio struct file_operations
关键认知:块设备驱动不直接处理每个字节的读写,而是通过内核的 块 I/O 层(Block I/O Layer) 接收 struct bio 请求,由 I/O 调度器优化后下发到硬件。字符设备驱动则直接面对应用程序的 read()/write() 系统调用。

1.2 块设备驱动在内核中的位置

块设备驱动并非孤立存在,它处于 Linux 存储栈的中间层,向上对接虚拟文件系统(VFS),向下控制硬件控制器。下图展示了块设备驱动在内核 I/O 路径中的精确位置:

Linux 块设备驱动在内核 I/O 栈中的位置 Linux 块设备 I/O 栈架构 用户空间应用程序 虚拟文件系统(VFS) 映射层(Page Cache / mmap) 通用块层(Generic Block Layer) bio 构造 / 分区处理 / 请求合并 I/O 调度层(IO Scheduler) CFQ / Deadline / NOOP / BFQ / Kyber 块设备驱动(Block Device Driver) request_fn / bio 处理 / DMA 传输 物理存储设备(eMMC / NVMe / SATA) 本课程核心关注层 I/O 调度器 + 块设备驱动
学习路线提示:本课程将重点覆盖上图中 通用块层、I/O 调度层 和 块设备驱动 三个层次。理解 bio 结构体如何穿越这些层级,是掌握块设备驱动的核心。

1.3 块设备驱动开发环境准备

开发块设备驱动需要一套完整的交叉编译与调试环境。以下是推荐的开发环境配置清单:

1.3.1 硬件与主机环境

  • 主机系统:Ubuntu 22.04 LTS / Debian 12(推荐)或 Fedora 38+
  • 目标平台:ARM64(树莓派 4/5、QEMU virt)或 x86_64(QEMU 模拟)
  • 内存要求:主机至少 8GB RAM,推荐 16GB
  • 磁盘空间:至少 50GB 空闲空间(用于内核源码编译)

1.3.2 软件工具链安装

# 安装基础编译工具
sudo apt update
sudo apt install -y build-essential flex bison libncurses-dev \
    libssl-dev libelf-dev bc git wget cpio

# 安装 QEMU 模拟器(用于测试块设备驱动)
sudo apt install -y qemu-system-arm qemu-system-x86 qemu-utils

# 安装调试工具
sudo apt install -y gdb-multiarch strace trace-cmd

1.3.3 内核源码获取与配置

# 下载长期支持(LTS)内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.y.tar.xz
tar -xf linux-6.6.y.tar.xz
cd linux-6.6.y

# 生成默认配置(以 ARM64 为例)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

# 启用块设备调试选项(关键!)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
重要:在 menuconfig 中务必开启以下选项,否则无法跟踪块设备 I/O 路径:
CONFIG_BLK_DEV_IO_TRACE=y
CONFIG_DEBUG_BLK_CGROUP=y
CONFIG_DEBUG_FS=y
CONFIG_DYNAMIC_DEBUG=y

1.3.4 块设备驱动开发核心头文件

块设备驱动开发需要熟悉以下内核头文件,它们定义了驱动与内核交互的核心数据结构:

头文件路径 核心内容
linux/blkdev.h struct request_queue、struct gendisk、struct block_device_operations
linux/bio.h struct bio、struct bio_vec、bio 操作 API
linux/blk-mq.h 多队列块设备层(blk-mq)接口
linux/elevator.h I/O 调度器接口与注册
linux/genhd.h 磁盘分区与 struct hd_struct

1.3.5 验证环境:第一个块设备驱动编译测试

编写一个最简单的块设备驱动框架,验证编译环境是否就绪:

// minimal_blk.c — 最小块设备驱动骨架
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

static struct gendisk *gd;
static struct request_queue *queue;
static DEFINE_SPINLOCK(lock);

static const struct block_device_operations blk_ops = {
    .owner = THIS_MODULE,
};

static int __init minimal_init(void)
{
    // 1. 分配请求队列
    queue = blk_alloc_queue(NUMA_NO_NODE);
    if (!queue)
        return -ENOMEM;

    // 2. 分配 gendisk
    gd = alloc_disk(1);  // 1 个分区
    if (!gd) {
        blk_cleanup_queue(queue);
        return -ENOMEM;
    }

    // 3. 初始化 gendisk
    gd->major = 0;  // 动态分配主设备号
    gd->first_minor = 0;
    gd->fops = &blk_ops;
    gd->queue = queue;
    snprintf(gd->disk_name, 32, "minimal_blk");

    // 4. 添加磁盘
    add_disk(gd);
    return 0;
}

static void __exit minimal_exit(void)
{
    del_gendisk(gd);
    put_disk(gd);
    blk_cleanup_queue(queue);
}

module_init(minimal_init);
module_exit(minimal_exit);
MODULE_LICENSE("GPL");
编译测试:将上述代码放入内核源码 drivers/block/ 目录,添加 Makefile 条目后执行 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules。若编译通过,说明开发环境已就绪。

1.4 本章小结

  • 块设备以固定大小块为单位进行随机访问,通过 I/O 调度器 优化请求顺序。
  • 块设备驱动位于 Linux 存储栈的中间层,上接通用块层,下接物理硬件。
  • 开发环境需要配置内核源码、交叉编译工具链、QEMU 模拟器,并开启块设备调试选项。
  • 最小块设备驱动骨架包含 request_queue、gendisk、block_device_operations 三个核心组件。