--- 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、机械硬盘、固态硬盘等。块设备驱动其实就是这些存储设备的驱动,块设备驱动相比字符设备驱动的主要区别如下: | 特性 | 块设备 | 字符设备 | | -------- | ------------------------------------- | ---------- | | 访问单位 | 块(VFS 基本传输单位,扇区为512字节) | 字节 | | 数据传输 | 需要缓冲区 | 不需要缓冲 | | 访问方式 | 随机访问 | 顺序访问 | | I/O调度 | 需要I/O调度器优化 | 无需调度 | | 生命周期 | 有擦写次数限制 | 无限制 | > 原书 68.1 指出:块设备以块为单位读写,使用缓冲区暂存数据、满足条件后再一次性写入物理设备,以减少擦除次数、提高寿命;字符设备不需要缓冲区,实时按字节访问。因此块设备寿命受 Flash 擦写次数限制。 ### 1.2 块设备应用场景 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十八章内容,为扩展知识。 块设备主要用于数据的持久化存储,典型应用场景包括: - **系统启动**:内核镜像、根文件系统存储 - **用户数据**:文档、图片、视频等文件存储 - **数据库**:需要频繁随机读写的场景 - **嵌入式存储**:eMMC、SD卡、NOR/NAND Flash ### 1.3 块设备驱动框架 块设备驱动是Linux三大驱动类型之一,其复杂度远高于字符设备。块设备驱动需要处理: 1. 请求队列管理 2. I/O调度优化 3. 缓冲区管理 4. 分区处理 ## 2. Linux块设备架构 ### 2.1 块设备架构图 ```mermaid graph TB subgraph "用户空间" APP[应用程序] VFS[虚拟文件系统VFS] end subgraph "内核空间" BIO[BIO层
块I/O请求] QUEUE[请求队列
request_queue] SCHEDULER[I/O调度器
电梯算法] DRIVER[块设备驱动] end subgraph "硬件层" DISK[块设备
HDD/SSD/eMMC] end APP --> VFS VFS --> BIO BIO --> QUEUE QUEUE --> SCHEDULER SCHEDULER --> DRIVER DRIVER --> DISK ``` ### 2.2 块设备请求队列 内核将对块设备的读写都发送到请求队列 `request_queue` 中。`request_queue` 里是大量的 `request`(请求结构体),而 `request` 又包含了 `bio`,`bio` 保存了读写相关数据。 请求队列的工作流程: 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` 中: ```c 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` 中: ```c 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` 中: ```c 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 结构体 描述要操作的设备扇区等信息: ```c 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 结构体 描述内存中的数据页信息: ```c 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 注册块设备,申请设备号: ```c int register_blkdev(unsigned int major, const char *name) ``` 参数说明: - `major`:主设备号 - `name`:块设备名字 返回值(原书 68.2.1): - 若 `major` 为 1~255,表示自定义主设备号:返回 0 表示注册成功,负值表示失败 - 若 `major` 为 0,表示由系统自动分配主设备号:返回值为系统分配的主设备号(1~255),负值表示失败 #### 3.1.2 unregister_blkdev 注销块设备: ```c void unregister_blkdev(unsigned int major, const char *name) ``` ### 3.2 gendisk操作API #### 3.2.1 alloc_disk 申请gendisk: ```c struct gendisk *alloc_disk(int minors) ``` 参数说明: - `minors`:次设备号数量,即分区数量 返回值:成功返回申请到的gendisk,失败返回NULL #### 3.2.2 del_gendisk 删除gendisk: ```c void del_gendisk(struct gendisk *gp) ``` #### 3.2.3 add_disk 将gendisk添加到内核: ```c void add_disk(struct gendisk *disk) ``` #### 3.2.4 set_capacity 设置磁盘容量(单位为扇区,每扇区512字节): ```c void set_capacity(struct gendisk *disk, sector_t size) ``` 示例:设置2MB的磁盘容量 ```c set_capacity(gendisk, (2 * 1024 * 1024) / 512); // 4096个扇区 ``` #### 3.2.5 引用计数 get_disk / put_disk 内核通过这两个函数调整 `gendisk` 的引用计数: ```c struct kobject *get_disk(struct gendisk *disk); /* 增加引用计数 */ void put_disk(struct gendisk *disk); /* 减少引用计数 */ ``` #### 3.2.6 块设备操作集 block_device_operations 和字符设备的 `file_operations` 类似,块设备操作集定义在 `include/linux/blkdev.h`(原书示例代码 68.2.3.1): ```c struct block_device_operations { int (*open)(struct block_device *, fmode_t); void (*release)(struct gendisk *, fmode_t); int (*rw_page)(struct block_device *, sector_t, struct page *, int rw); int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long); int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long); long (*direct_access)(struct block_device *, sector_t, void **, unsigned long *pfn, long size); unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing); int (*media_changed)(struct gendisk *); /* 已废弃,用 check_events 代替 */ void (*unlock_native_capacity)(struct gendisk *); int (*revalidate_disk)(struct gendisk *); int (*getgeo)(struct block_device *, struct hd_geometry *); void (*swap_slot_free_notify)(struct block_device *, unsigned long); struct module *owner; }; ``` 重点成员:`open`/`release`(打开/关闭块设备)、`ioctl`/`compat_ioctl`(I/O 控制,64 位系统上 32 位程序走 `compat_ioctl`)、`getgeo`(获取磁盘磁头/柱面/扇区信息)、`owner`(一般为 `THIS_MODULE`)。块设备操作集**没有** `read`/`write`,数据读写通过 `request_queue`/`request`/`bio` 完成。 `getgeo` 用到的 `hd_geometry` 结构(原书示例代码 68.3.1.4): ```c struct hd_geometry { unsigned char heads; /* 磁头 */ unsigned char sectors; /* 一个磁道上的扇区数量 */ unsigned short cylinders; /* 柱面 */ unsigned long start; }; ``` ### 3.3 请求队列API #### 3.3.1 blk_init_queue 分配并初始化请求队列(用于机械硬盘等需要I/O调度的设备): ```c request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock) ``` 参数说明: - `rfn`:请求处理函数指针 - `lock`:自旋锁指针 #### 3.3.2 blk_cleanup_queue 删除请求队列: ```c void blk_cleanup_queue(struct request_queue *q) ``` #### 3.3.3 blk_alloc_queue 分配无I/O调度的请求队列(用于eMMC、SD卡等非机械设备): ```c struct request_queue *blk_alloc_queue(gfp_t gfp_mask) ``` #### 3.3.4 blk_queue_make_request 绑定"制造请求"函数: ```c void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn) ``` ### 3.4 请求处理API | 函数 | 描述 | | ------------------------- | -------------------------------------- | | `blk_peek_request()` | 获取请求队列中下一个请求,无则返回NULL | | `blk_start_request()` | 开始处理指定请求 | | `blk_fetch_request()` | 一次性获取并开启请求(=peek+start) | | `blk_end_request()` | 请求中指定字节数据被处理完成 | | `blk_end_request_all()` | 请求中所有数据全部处理完成 | | `blk_end_request_cur()` | 完成当前请求中的 chunk | | `blk_end_request_err()` | 处理完请求,直到下一个错误产生 | | `__blk_end_request()` | 同 `blk_end_request`,但需持有队列锁 | | `__blk_end_request_all()` | 同 `blk_end_request_all`,需持有队列锁 | | `__blk_end_request_cur()` | 同 `blk_end_request_cur`,需持有队列锁 | | `__blk_end_request_err()` | 同 `blk_end_request_err`,需持有队列锁 | 关键函数原型(原书 68.2.4): ```c request *blk_peek_request(struct request_queue *q); void blk_start_request(struct request *req); struct request *blk_fetch_request(struct request_queue *q); /* peek + start */ ``` #### 3.4.1 遍历与辅助宏 ```c /* 遍历请求中的所有 bio(原书示例代码 68.2.4.5) */ #define __rq_for_each_bio(_bio, rq) \ if ((rq->bio)) \ for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next) /* 遍历 bio 中的所有段(原书示例代码 68.2.4.6) */ #define bio_for_each_segment(bvl, bio, iter) \ __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter) void bio_endio(struct bio *bio, int error); /* 通知 bio 处理结束,error=0 表示成功 */ ``` 请求处理中还会用到这些辅助函数: | 函数 | 说明 | | ---------------------------- | ------------------------------------------------- | | `blk_rq_pos(req)` | 获取请求要操作的扇区地址(左移9位转换为字节地址) | | `blk_rq_cur_bytes(req)` | 获取当前请求要处理的数据长度 | | `rq_data_dir(req)` | 判断请求方向(`READ`/`WRITE`) | | `bio_data(req->bio)` | 获取请求中 bio 的数据缓冲区 | | `bio_data_dir(bio)` | 判断 bio 方向(`READ`/`WRITE`) | | `page_address(bvec.bv_page)` | 获取 bio_vec 中页的虚拟地址 | ## 4. 完整源码分析 ### 4.1 使用请求队列的块设备驱动 本实例使用RAM模拟2MB的块设备,实现完整的块设备驱动框架。 #### 4.1.1 头文件和宏定义 ```c #include #include #include #include #include #include #include #include #include #include #include #include #define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为2MB */ #define RAMDISK_NAME "ramdisk" /* 名字 */ #define RAMDISK_MINOR 3 /* 表示三个磁盘分区!不是次设备号为3! */ ``` #### 4.1.2 设备结构体 ```c /* 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 块设备操作集 ```c /* 打开块设备 */ 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 请求处理函数 ```c /* 处理传输过程 */ 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 驱动入口和出口 ```c /* 驱动入口函数 */ 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 驱动入口函数(差异部分) ```c 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 制造请求函数 ```c /* "制造请求"函数 */ 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 编译和加载驱动 ```bash # 编译驱动,得到 ramdisk.ko make # 将 ramdisk.ko 拷贝到开发板根文件系统 rootfs/lib/modules/4.1.15 目录中 # 重启开发板后进入该目录: cd /lib/modules/4.1.15 # 加载驱动 depmod # 首次加载需要 modprobe ramdisk.ko # 加载驱动模块 ``` ### 5.2 查看块设备信息 ```bash fdisk -l # 查看所有磁盘信息,其中包括 ramdisk ls /dev/ramdisk # 查看设备节点 ``` 驱动加载成功后会在 `/dev/` 下生成名为 `ramdisk` 的设备,`fdisk -l` 会显示其大小为 2MB,同时提示 `/dev/ramdisk` 没有分区表(因为尚未格式化)。 ### 5.3 格式化和挂载 ```bash # 格式化为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 卸载驱动 ```bash rmmod ramdisk # 卸载驱动模块 ``` ## 6. 跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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. 面试精选 > ⚠️ **来源说明**:本节面试题不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识。 ### Q1: 块设备驱动和字符设备驱动有什么区别? **答**: 1. **访问单位**:块设备以块(通常512字节)为单位,字符设备以字节为单位 2. **缓冲机制**:块设备使用缓冲区暂存数据,字符设备实时传输 3. **访问方式**:块设备支持随机访问,字符设备是顺序访问 4. **I/O调度**:块设备需要I/O调度器优化读写顺序,字符设备不需要 5. **数据结构**:块设备使用 `gendisk` 和 `request_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_queue` → `request` → `bio` 使用 `request_queue` 的原因: 1. **I/O调度优化**:可以对多个bio进行合并和排序,减少寻道时间 2. **公平性保证**:防止某个进程饿死其他进程的I/O请求 3. **流控机制**:可以限制队列深度,避免过载 4. **机械硬盘优化**:对于机械硬盘,顺序读写比随机读写快很多 对于eMMC、SSD等无机械结构的设备,可以直接使用 `blk_queue_make_request` 绕过I/O调度。 ### Q4: 如何实现一个虚拟块设备驱动?需要实现哪些关键函数? **答**: 实现虚拟块设备驱动需要以下关键步骤: 1. **注册块设备**:`register_blkdev()` 申请设备号 2. **分配gendisk**:`alloc_disk()` 分配磁盘结构 3. **初始化请求队列**: - 机械设备:`blk_init_queue()` 绑定请求处理函数 - 非机械设备:`blk_alloc_queue()` + `blk_queue_make_request()` 4. **设置gendisk**:设置主设备号、操作集、请求队列、容量等 5. **添加磁盘**:`add_disk()` 注册到内核 6. **实现操作集**:`open`、`release`、`getgeo` 等 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卡驱动]] --- **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十八章 Linux 块设备驱动实验