08-块设备驱动.md 21 KB


title: Linux块设备驱动 tags:

  • 嵌入式Linux
  • 驱动开发
  • 块设备
  • 存储设备 created: 2026-09-17 updated: 2026-09-17 pdf_ref: "74_第六十八章 Linux 块设备驱动实验.pdf" ---

Linux块设备驱动

1. 块设备概述

1.1 块设备 vs 字符设备

块设备是针对存储设备的,比如SD卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。块设备驱动其实就是这些存储设备的驱动,块设备驱动相比字符设备驱动的主要区别如下:

特性 块设备 字符设备
访问单位 块(512字节为单位) 字节
数据传输 需要缓冲区 不需要缓冲
访问方式 随机访问 顺序访问
I/O调度 需要I/O调度器优化 无需调度
生命周期 有擦写次数限制 无限制

1.2 块设备应用场景

块设备主要用于数据的持久化存储,典型应用场景包括:

  • 系统启动:内核镜像、根文件系统存储
  • 用户数据:文档、图片、视频等文件存储
  • 数据库:需要频繁随机读写的场景
  • 嵌入式存储:eMMC、SD卡、NOR/NAND Flash

1.3 块设备驱动框架

块设备驱动是Linux三大驱动类型之一,其复杂度远高于字符设备。块设备驱动需要处理:

  1. 请求队列管理
  2. I/O调度优化
  3. 缓冲区管理
  4. 分区处理

2. Linux块设备架构

2.1 块设备架构图

graph TB
    subgraph "用户空间"
        APP[应用程序]
        VFS[虚拟文件系统VFS]
    end

    subgraph "内核空间"
        BIO[BIO层<br/>块I/O请求]
        QUEUE[请求队列<br/>request_queue]
        SCHEDULER[I/O调度器<br/>电梯算法]
        DRIVER[块设备驱动]
    end

    subgraph "硬件层"
        DISK[块设备<br/>HDD/SSD/eMMC]
    end

    APP --> VFS
    VFS --> BIO
    BIO --> QUEUE
    QUEUE --> SCHEDULER
    SCHEDULER --> DRIVER
    DRIVER --> DISK

2.2 块设备请求队列

内核将对块设备的读写都发送到请求队列 request_queue 中。request_queue 里是大量的 request(请求结构体),而 request 又包含了 biobio 保存了读写相关数据。

请求队列的工作流程:

  1. 上层应用发起I/O请求
  2. 请求被构造为 bio 结构
  3. bio 被合并或创建新的 request
  4. I/O调度器对请求进行排序优化
  5. 驱动程序依次处理请求

2.3 核心数据结构

2.3.1 block_device 结构体

Linux内核使用 block_device 来表示块设备,定义在 include/linux/fs.h 中:

struct block_device {
    dev_t bd_dev;                    /* 设备号 */
    int bd_openers;                 /* 打开次数 */
    struct inode *bd_inode;          /* inode指针 */
    struct super_block *bd_super;    /* 超级块 */
    struct mutex bd_mutex;           /* open/close互斥锁 */
    struct list_head bd_inodes;      /* inode链表 */
    void *bd_claiming;
    void *bd_holder;
    int bd_holders;
    bool bd_write_holder;
    struct block_device *bd_contains;
    unsigned bd_block_size;          /* 块大小 */
    struct hd_struct *bd_part;       /* 分区信息 */
    unsigned bd_part_count;
    int bd_invalidated;
    struct gendisk *bd_disk;         /* 通用磁盘结构 */
    struct request_queue *bd_queue;  /* 请求队列 */
    struct list_head bd_list;
    unsigned long bd_private;
};

重点关注 bd_disk 成员变量,此成员变量为 gendisk 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象。

2.3.2 gendisk 结构体

Linux内核使用 gendisk 来描述一个磁盘设备,定义在 include/linux/genhd.h 中:

struct gendisk {
    int major;                      /* 主设备号 */
    int first_minor;                /* 第一个次设备号 */
    int minors;                     /* 次设备号数量,即分区数 */

    char disk_name[DISK_NAME_LEN];  /* 磁盘名字 */
    char *(*devnode)(struct gendisk *gd, umode_t *mode);

    struct disk_part_tbl __rcu *part_tbl;  /* 分区表 */
    struct hd_struct part0;          /* 第一个分区 */

    const struct block_device_operations *fops;  /* 操作集 */
    struct request_queue *queue;     /* 请求队列 */
    void *private_data;              /* 私有数据 */

    int flags;
    struct device *driverfs_dev;
    struct kobject *slave_dir;
};

关键成员说明:

  • major:主设备号
  • first_minor:起始次设备号
  • minors:次设备号数量,即磁盘分区数量
  • fops:块设备操作集,类似字符设备的 file_operations
  • queue:请求队列,所有针对该磁盘的请求都放到此队列中

2.3.3 bio 结构体

bio 是块设备I/O传输的基本单元,定义在 include/linux/blk_types.h 中:

struct bio {
    struct bio *bi_next;            /* 请求队列中的下一个bio */
    struct block_device *bi_bdev;   /* 指向块设备 */
    unsigned long bi_flags;         /* bio状态等信息 */
    unsigned long bi_rw;            /* I/O操作,读或写 */
    struct bvec_iter bi_iter;       /* I/O操作信息 */
    unsigned int bi_phys_segments;
    unsigned int bi_seg_front_size;
    unsigned int bi_seg_back_size;
    atomic_t bi_remaining;
    bio_end_io_t *bi_end_io;        /* bio完成回调 */
    void *bi_private;               /* 私有数据 */

    unsigned short bi_vcnt;          /* bio_vec列表中元素数量 */
    unsigned short bi_max_vecs;      /* bio_vec列表最大长度 */
    atomic_t bi_cnt;                 /* 引用计数 */
    struct bio_vec *bi_io_vec;       /* bio_vec列表 */
    struct bio_set *bi_pool;
    struct bio_vec bi_inline_vecs[0];
};

2.3.4 bvec_iter 结构体

描述要操作的设备扇区等信息:

struct bvec_iter {
    sector_t bi_sector;             /* I/O请求的设备起始扇区(512字节) */
    unsigned int bi_size;           /* 剩余的I/O数量 */
    unsigned int bi_idx;            /* bvec_vec中当前索引 */
    unsigned int bi_bvec_done;      /* 当前bvec已经处理完成的字节数 */
};

2.3.5 bio_vec 结构体

描述内存中的数据页信息:

struct bio_vec {
    struct page *bv_page;           /* 页 */
    unsigned int bv_len;            /* 长度 */
    unsigned int bv_offset;         /* 偏移 */
};

bio_vec 就是 "page, offset, len" 组合,page指定了所在的物理页,offset表示所在页的偏移地址,len就是数据长度。

3. 块设备驱动API

3.1 块设备注册与注销

3.1.1 register_blkdev

注册块设备,申请设备号:

int register_blkdev(unsigned int major, const char *name)

参数说明:

  • major:主设备号。若为0则由系统自动分配
  • name:块设备名字

返回值:

  • 成功:返回注册成功的主设备号
  • 失败:返回负值

3.1.2 unregister_blkdev

注销块设备:

void unregister_blkdev(unsigned int major, const char *name)

3.2 gendisk操作API

3.2.1 alloc_disk

申请gendisk:

struct gendisk *alloc_disk(int minors)

参数说明:

  • minors:次设备号数量,即分区数量

返回值:成功返回申请到的gendisk,失败返回NULL

3.2.2 del_gendisk

删除gendisk:

void del_gendisk(struct gendisk *gp)

3.2.3 add_disk

将gendisk添加到内核:

void add_disk(struct gendisk *disk)

3.2.4 set_capacity

设置磁盘容量(单位为扇区,每扇区512字节):

void set_capacity(struct gendisk *disk, sector_t size)

示例:设置2MB的磁盘容量

set_capacity(gendisk, (2 * 1024 * 1024) / 512);  // 4096个扇区

3.3 请求队列API

3.3.1 blk_init_queue

分配并初始化请求队列(用于机械硬盘等需要I/O调度的设备):

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

参数说明:

  • rfn:请求处理函数指针
  • lock:自旋锁指针

3.3.2 blk_cleanup_queue

删除请求队列:

void blk_cleanup_queue(struct request_queue *q)

3.3.3 blk_alloc_queue

分配无I/O调度的请求队列(用于eMMC、SD卡等非机械设备):

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

3.3.4 blk_queue_make_request

绑定"制造请求"函数:

void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)

3.4 请求处理API

函数 描述
blk_peek_request() 获取请求队列中下一个请求
blk_start_request() 开始处理请求
blk_fetch_request() 一次性获取并开启请求
blk_end_request() 请求中指定字节数据被处理完成
blk_end_request_all() 请求中所有数据全部处理完成
blk_end_request_cur() 当前请求中的chunk
__blk_end_request() 同blk_end_request,但需持有队列锁

4. 完整源码分析

4.1 使用请求队列的块设备驱动

本实例使用RAM模拟2MB的块设备,实现完整的块设备驱动框架。

4.1.1 头文件和宏定义

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/blkdev.h>
#include <linux/blk_types.h>

#define RAMDISK_SIZE    (2 * 1024 * 1024)  /* 容量大小为2MB */
#define RAMDISK_NAME    "ramdisk"           /* 名字 */
#define RAMDISK_MINOR   3                   /* 表示三个磁盘分区 */

4.1.2 设备结构体

/* ramdisk设备结构体 */
struct ramdisk_dev {
    int major;                      /* 主设备号 */
    unsigned char *ramdiskbuf;      /* ramdisk内存空间 */
    spinlock_t lock;                /* 自旋锁 */
    struct gendisk *gendisk;        /* gendisk */
    struct request_queue *queue;    /* 请求队列 */
};

struct ramdisk_dev ramdisk;  /* ramdisk设备 */

4.1.3 块设备操作集

/* 打开块设备 */
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
    printk("ramdisk open\r\n");
    return 0;
}

/* 释放块设备 */
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
    printk("ramdisk release\r\n");
}

/* 获取磁盘信息 */
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{
    geo->heads = 2;                                    /* 磁头 */
    geo->cylinders = 32;                               /* 柱面 */
    geo->sectors = RAMDISK_SIZE / (2 * 32 * 512);      /* 每磁道扇区数 */
    return 0;
}

/* 块设备操作函数 */
static struct block_device_operations ramdisk_fops = {
    .owner  = THIS_MODULE,
    .open   = ramdisk_open,
    .release = ramdisk_release,
    .getgeo = ramdisk_getgeo,
};

4.1.4 请求处理函数

/* 处理传输过程 */
static void ramdisk_transfer(struct request *req)
{
    unsigned long start = blk_rq_pos(req) << 9;  /* 扇区地址转字节地址 */
    unsigned long len = blk_rq_cur_bytes(req);   /* 数据长度 */

    /* bio中的数据缓冲区 */
    void *buffer = bio_data(req->bio);

    if (rq_data_dir(req) == READ)           /* 读数据 */
        memcpy(buffer, ramdisk.ramdiskbuf + start, len);
    else if (rq_data_dir(req) == WRITE)     /* 写数据 */
        memcpy(ramdisk.ramdiskbuf + start, buffer, len);
}

/* 请求处理函数 */
void ramdisk_request_fn(struct request_queue *q)
{
    int err = 0;
    struct request *req;

    /* 循环处理请求队列中的每个请求 */
    req = blk_fetch_request(q);
    while (req != NULL) {
        /* 针对请求做具体的传输处理 */
        ramdisk_transfer(req);

        /* 判断是否为最后一个请求 */
        if (!__blk_end_request_cur(req, err))
            req = blk_fetch_request(q);
    }
}

4.1.5 驱动入口和出口

/* 驱动入口函数 */
static int __init ramdisk_init(void)
{
    int ret = 0;

    /* 1、申请用于ramdisk内存 */
    ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
    if (ramdisk.ramdiskbuf == NULL) {
        ret = -EINVAL;
        goto ram_fail;
    }

    /* 2、初始化自旋锁 */
    spin_lock_init(&ramdisk.lock);

    /* 3、注册块设备 */
    ramdisk.major = register_blkdev(0, RAMDISK_NAME);
    if (ramdisk.major < 0) {
        goto register_blkdev_fail;
    }
    printk("ramdisk major = %d\r\n", ramdisk.major);

    /* 4、分配并初始化gendisk */
    ramdisk.gendisk = alloc_disk(RAMDISK_MINOR);
    if (!ramdisk.gendisk) {
        ret = -EINVAL;
        goto gendisk_alloc_fail;
    }

    /* 5、分配并初始化请求队列 */
    ramdisk.queue = blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
    if (!ramdisk.queue) {
        ret = -EINVAL;
        goto blk_init_fail;
    }

    /* 6、添加(注册)disk */
    ramdisk.gendisk->major = ramdisk.major;
    ramdisk.gendisk->first_minor = 0;
    ramdisk.gendisk->fops = &ramdisk_fops;
    ramdisk.gendisk->private_data = &ramdisk;
    ramdisk.gendisk->queue = ramdisk.queue;
    sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME);
    set_capacity(ramdisk.gendisk, RAMDISK_SIZE / 512);
    add_disk(ramdisk.gendisk);

    return 0;

blk_init_fail:
    put_disk(ramdisk.gendisk);
gendisk_alloc_fail:
    unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
    kfree(ramdisk.ramdiskbuf);
ram_fail:
    return ret;
}

/* 驱动出口函数 */
static void __exit ramdisk_exit(void)
{
    del_gendisk(ramdisk.gendisk);
    put_disk(ramdisk.gendisk);
    blk_cleanup_queue(ramdisk.queue);
    unregister_blkdev(ramdisk.major, RAMDISK_NAME);
    kfree(ramdisk.ramdiskbuf);
}

module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alientek");

4.2 不使用请求队列的块设备驱动

对于eMMC、SD卡等非机械存储设备,可以不使用I/O调度器,直接处理bio。

4.2.1 驱动入口函数(差异部分)

static int __init ramdisk_init(void)
{
    /* ... 省略相同部分 ... */

    /* 5、分配请求队列(无I/O调度) */
    ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
    if (!ramdisk.queue) {
        ret = -EINVAL;
        goto blk_allo_fail;
    }

    /* 6、设置"制造请求"函数 */
    blk_queue_make_request(ramdisk.queue, ramdisk_make_request_fn);

    /* 7、添加(注册)disk - 与上一个实验相同 */
    /* ... */
}

4.2.2 制造请求函数

/* "制造请求"函数 */
void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
{
    int offset;
    struct bio_vec bvec;
    struct bvec_iter iter;
    unsigned long len = 0;

    offset = (bio->bi_iter.bi_sector) << 9;  /* 获取设备偏移地址 */

    /* 处理bio中的每个段 */
    bio_for_each_segment(bvec, bio, iter) {
        char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
        len = bvec.bv_len;

        if (bio_data_dir(bio) == READ)           /* 读数据 */
            memcpy(ptr, ramdisk.ramdiskbuf + offset, len);
        else if (bio_data_dir(bio) == WRITE)     /* 写数据 */
            memcpy(ramdisk.ramdiskbuf + offset, ptr, len);

        offset += len;
    }

    set_bit(BIO_UPTODATE, &bio->bi_flags);
    bio_endio(bio, 0);  /* 结束bio */
}

5. 实验验证

5.1 编译和加载驱动

# 编译驱动
make

# 复制到开发板
scp ramdisk.ko root@192.168.1.200:/lib/modules/4.1.15/

# 加载驱动
depmod                    # 首次加载需要
modprobe ramdisk.ko       # 加载驱动模块

5.2 查看块设备信息

fdisk -l                  # 查看所有磁盘信息
ls /dev/ramdisk           # 查看设备节点

5.3 格式化和挂载

# 格式化为vfat格式
mkfs.vfat /dev/ramdisk

# 挂载到/tmp目录
mount /dev/ramdisk /tmp

# 测试读写
cd /tmp
echo "Hello Block Device" > test.txt
cat test.txt

# 卸载
umount /tmp

5.4 卸载驱动

rmmod ramdisk             # 卸载驱动模块

6. 跨平台对比

6.1 IMX6ULL vs STM32 vs RK3568块设备差异

特性 IMX6ULL STM32MP1 RK3568
主存储接口 eMMC/SD eMMC/SD eMMC/SD/NVMe
eMMC控制器 USDHC SDMMC SDHCI
块大小 512字节 512字节 512字节
DMA支持
最大频率 200MHz 200MHz 200MHz
分区支持 支持 支持 支持

6.2 驱动差异分析

  • IMX6ULL:使用USDHC控制器,驱动位于 drivers/mmc/host/sdhci-esdhc-imx.c
  • STM32MP1:使用SDMMC控制器,驱动位于 drivers/mmc/host/stmmci.c
  • RK3568:使用SDHCI控制器,支持NVMe SSD

6.3 块设备驱动通用性

所有平台的块设备驱动都遵循统一的Linux块设备框架,主要差异在于:

  1. 硬件控制器寄存器操作
  2. DMA传输配置
  3. 时钟和电源管理
  4. 平台特有的初始化流程

7. 面试精选

Q1: 块设备驱动和字符设备驱动有什么区别?

  1. 访问单位:块设备以块(通常512字节)为单位,字符设备以字节为单位
  2. 缓冲机制:块设备使用缓冲区暂存数据,字符设备实时传输
  3. 访问方式:块设备支持随机访问,字符设备是顺序访问
  4. I/O调度:块设备需要I/O调度器优化读写顺序,字符设备不需要
  5. 数据结构:块设备使用 gendiskrequest_queue,字符设备使用 cdev
  6. 操作集:块设备使用 block_device_operations,字符设备使用 file_operations

Q2: 什么是bio结构体?它在块设备I/O中起什么作用?

bio 是块设备I/O传输的基本单元,描述了要读写的起始扇区、数据长度、读写方向、页偏移等信息。

主要作用:

  1. 数据载体:保存实际要传输的数据地址和长度
  2. I/O请求描述:描述从设备的哪个位置读写多少数据
  3. 分散聚集I/O:通过 bio_vec 数组支持不连续内存的I/O操作
  4. 异步完成通知:通过 bi_end_io 回调函数通知I/O完成

bio 结构体是连接用户空间和块设备驱动的桥梁。

Q3: request_queue和bio是什么关系?为什么不直接使用bio?

  • request_queue:请求队列,包含多个 request
  • request:由一个或多个 bio 合并而成
  • bio:最小的I/O传输单元

关系:request_queuerequestbio

使用 request_queue 的原因:

  1. I/O调度优化:可以对多个bio进行合并和排序,减少寻道时间
  2. 公平性保证:防止某个进程饿死其他进程的I/O请求
  3. 流控机制:可以限制队列深度,避免过载
  4. 机械硬盘优化:对于机械硬盘,顺序读写比随机读写快很多

对于eMMC、SSD等无机械结构的设备,可以直接使用 blk_queue_make_request 绕过I/O调度。

Q4: 如何实现一个虚拟块设备驱动?需要实现哪些关键函数?

: 实现虚拟块设备驱动需要以下关键步骤:

  1. 注册块设备register_blkdev() 申请设备号
  2. 分配gendiskalloc_disk() 分配磁盘结构
  3. 初始化请求队列
    • 机械设备:blk_init_queue() 绑定请求处理函数
    • 非机械设备:blk_alloc_queue() + blk_queue_make_request()
  4. 设置gendisk:设置主设备号、操作集、请求队列、容量等
  5. 添加磁盘add_disk() 注册到内核
  6. 实现操作集openreleasegetgeo
  7. 实现数据传输
    • 请求队列模式:实现 request_fn_proc 处理函数
    • 制造请求模式:实现 make_request_fn 处理函数

Q5: 块设备的I/O调度算法有哪些?各有什么特点?

: Linux内核支持多种I/O调度算法:

  1. NOOP(No Operation)

    • 最简单的调度器,不进行任何排序
    • 适合SSD、eMMC等无机械结构的设备
    • 延迟最低
  2. CFQ(Complete Fair Queuing)

    • 完全公平队列,为每个进程维护独立队列
    • 保证每个进程获得公平的I/O带宽
    • 适合桌面系统和交互式应用
  3. Deadline

    • 为每个请求设置截止时间
    • 优先处理即将超时的请求
    • 避免请求饿死
    • 适合数据库等对延迟敏感的场景
  4. Anticipatory

    • 基于CFQ,增加预读优化
    • 预测接下来可能的读请求
    • 已被逐步淘汰

选择建议:

  • SSD/eMMC:使用NOOP或NONE
  • 桌面系统:使用CFQ
  • 服务器:使用Deadline或CFQ

相关知识

  • [[字符设备驱动]]
  • [[Linux设备模型]]
  • [[内存管理]]
  • [[DMA传输]]
  • [[eMMC驱动]]
  • [[SD卡驱动]]