3. 块设备驱动框架搭建:模块初始化与退出、分配与注册gendisk、实现简单的open/release函数
本章是块设备驱动开发的骨架搭建阶段。我们将从零开始,构建一个最小化的块设备驱动框架,重点掌握 模块生命周期管理、gendisk 结构体的分配与注册,以及 块设备操作接口中的 open/release 回调。理解这些基础组件,是后续实现真实硬件(如 SD 卡)驱动的必要前提。
核心目标: 完成一个可加载/卸载的内核模块,该模块在加载时创建一个虚拟块设备(如 /dev/vdX),支持打开和关闭操作,但不涉及实际数据传输。
3.1 块设备驱动核心数据结构
在 Linux 块设备层,驱动与内核交互的核心是 gendisk 和 block_device_operations 两个结构体。下表列出了它们的核心字段与职责:
| 结构体 | 关键字段 | 说明 |
|---|---|---|
struct gendisk |
major, first_minor, minors |
设备号范围:主设备号、起始次设备号、次设备号数量 |
disk_name |
设备名称,如 "vdblk" | |
fops |
指向 block_device_operations 的指针 |
|
queue |
请求队列,用于 I/O 调度 | |
private_data |
驱动私有数据指针 | |
struct block_device_operations |
open |
设备打开回调,可做权限检查或状态初始化 |
release |
设备关闭回调,可做资源清理 | |
owner |
通常设为 THIS_MODULE,防止模块被卸载 |
设计原则: 在框架阶段,
open 和 release 函数可以只返回 0(成功),但必须存在。内核在访问块设备时会默认调用这两个回调,缺失会导致内核崩溃。
3.2 模块初始化与退出流程
一个完整的块设备驱动模块,其生命周期包含以下步骤。我们使用流程图来直观展示:
3.3 分配与注册 gendisk
gendisk 是块设备在内核中的核心抽象。分配和注册过程必须遵循严格的顺序:
- 分配主设备号: 使用
register_blkdev(major, name),若 major 传 0 则内核动态分配。 - 分配 gendisk:
alloc_disk(minors),minors 表示支持的次设备号数量(通常为 1 或 16)。 - 初始化 gendisk 字段: 设置
major、first_minor、disk_name、fops、queue等。 - 注册 gendisk:
add_disk(disk),此调用会使设备节点在 /dev 下出现。
常见错误: 在
add_disk 之后,内核可能立即调用 open 回调。因此,所有必要的初始化(如队列、私有数据)必须在 add_disk 之前完成,否则会导致空指针访问。
3.4 实现简单的 open / release 函数
在框架阶段,open 和 release 函数的主要作用是验证设备状态和引用计数管理。以下是一个最小实现:
static int vd_open(struct block_device *bdev, fmode_t mode)
{
struct gendisk *disk = bdev->bd_disk;
struct vd_dev *dev = disk->private_data;
pr_info("vd_open: device %s opened, mode=0x%x\n", disk->disk_name, mode);
// 检查设备是否已卸载(可选)
if (!dev) {
pr_err("vd_open: device not ready\n");
return -ENXIO;
}
// 增加模块引用计数,防止模块被卸载
try_module_get(THIS_MODULE);
return 0;
}
static void vd_release(struct gendisk *disk, fmode_t mode)
{
pr_info("vd_release: device %s released\n", disk->disk_name);
// 减少模块引用计数
module_put(THIS_MODULE);
}
关键点说明:
- 参数差异:
open接收struct block_device *,而release接收struct gendisk *,这是内核版本演进的结果,需注意适配。 - 引用计数: 使用
try_module_get/module_put确保模块在设备打开期间不会被卸载。 - 错误返回: 如果设备处于异常状态,应返回负的错误码(如
-ENXIO)。
3.5 完整框架代码示例
将上述组件整合,得到一个最小化的块设备驱动框架:
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
#define VD_MAJOR 0 // 动态分配
#define VD_MINORS 1
#define VD_NAME "vdblk"
static int vd_major;
static struct gendisk *vd_disk;
static struct request_queue *vd_queue;
static int vd_open(struct block_device *bdev, fmode_t mode)
{
pr_info("vd: open called\n");
try_module_get(THIS_MODULE);
return 0;
}
static void vd_release(struct gendisk *disk, fmode_t mode)
{
pr_info("vd: release called\n");
module_put(THIS_MODULE);
}
static struct block_device_operations vd_fops = {
.owner = THIS_MODULE,
.open = vd_open,
.release = vd_release,
};
static int __init vd_init(void)
{
int ret;
// 1. 注册主设备号
vd_major = register_blkdev(VD_MAJOR, VD_NAME);
if (vd_major < 0) {
pr_err("vd: failed to register blkdev\n");
return vd_major;
}
// 2. 分配请求队列(无真实硬件,使用虚拟队列)
vd_queue = blk_alloc_queue(GFP_KERNEL);
if (!vd_queue) {
pr_err("vd: failed to alloc queue\n");
unregister_blkdev(vd_major, VD_NAME);
return -ENOMEM;
}
// 3. 分配 gendisk
vd_disk = alloc_disk(VD_MINORS);
if (!vd_disk) {
pr_err("vd: failed to alloc disk\n");
blk_cleanup_queue(vd_queue);
unregister_blkdev(vd_major, VD_NAME);
return -ENOMEM;
}
// 4. 初始化 gendisk
vd_disk->major = vd_major;
vd_disk->first_minor = 0;
vd_disk->fops = &vd_fops;
vd_disk->queue = vd_queue;
vd_disk->private_data = NULL; // 后续可指向设备结构体
snprintf(vd_disk->disk_name, 32, VD_NAME);
// 5. 注册 gendisk
add_disk(vd_disk);
pr_info("vd: initialized, major=%d\n", vd_major);
return 0;
}
static void __exit vd_exit(void)
{
del_gendisk(vd_disk);
put_disk(vd_disk);
blk_cleanup_queue(vd_queue);
unregister_blkdev(vd_major, VD_NAME);
pr_info("vd: exited\n");
}
module_init(vd_init);
module_exit(vd_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Course");
MODULE_DESCRIPTION("Minimal block device driver framework");
3.6 验证与调试
编译并加载模块后,可以通过以下步骤验证框架是否正常工作:
- 检查设备节点:
ls -l /dev/vdblk应显示块设备文件。 - 查看内核日志:
dmesg | tail应看到 "vd: initialized" 和 open/release 的打印。 - 测试打开/关闭:
sudo fdisk -l /dev/vdblk会触发 open 和 release 回调。 - 卸载模块:
sudo rmmod vdblk,确认设备节点消失且日志显示 "vd: exited"。
调试技巧: 如果
add_disk 后设备节点未出现,检查 /proc/devices 中是否注册了主设备号。如果模块卸载时系统崩溃,通常是因为 del_gendisk 调用顺序错误或引用计数未正确管理。
3.7 本章小结
通过本章的学习,我们完成了块设备驱动框架的搭建:
- 掌握了
gendisk和block_device_operations的核心字段与作用。 - 理解了模块初始化与退出的标准流程,以及各步骤的依赖关系。
- 实现了最小化的
open和release函数,并理解了引用计数管理的重要性。 - 提供了一个可直接编译运行的完整框架代码,为后续章节添加 I/O 能力奠定了基础。
下一阶段,我们将为这个框架添加请求处理函数,使其能够真正处理读写请求,并实现数据传输。