针对块设备驱动将分为两部分介绍,第一部分是注册块设备,即将块设备成功添加到内核;第二部分是介绍如何读写块设备,因为没有实际块设备,这里选择使用内存来模拟块设备。
目录
一、认识块设备
1、什么是块设备
2、块设备类型
二、模拟设备创建
三、注册块设备
1、申请主设备号
2、申请gendisk
3、初始化请求队列
4、初始化 gendisk
5、添加到内核
四、补充:分配内存
五、完整代码(待完善)
一、认识块设备
1、什么是块设备
块设备针对的是存储设备,如SD卡、EMMC、机械硬盘,与字符设备相比,块设备的读写是以块为单位,通过使用缓冲区来暂时保存数据,等满足一定条件再一次写入到块设备。这么做可以减少对块设备的擦除次数,降低块设备的读写频率,提高使用寿命。
2、块设备类型
Linux 内核提供了数据类型 block_device 来表示块设备,其中 gendisk 保存了块设备的属性信息及可执行的操作,如块设备号、设备容量、块设备分区等;bd_queue 缓存来自文件系统的读写请求,块设备会对这些请求逐个处理
struct block_device { /* ... */ struct gendisk * bd_disk; struct request_queue * bd_queue; /* ... */ }; struct gendisk { int major; /* major number of driver */ int first_minor; int minors; /* maximum number of minors, =1 for * disks that can't be partitioned. */ char disk_name[DISK_NAME_LEN]; /* name of major driver */ /* ... */ const struct block_device_operations *fops; struct request_queue *queue; void *private_data; }
讯享网
注意:block_device 和 gendisk 都包含 request_queue 类型的成员, 实际上他们指向的是同一个请求队列,因此,在初始化时,初始化其中一个即可,一般初始化gendisk中的请求队列。
二、模拟设备创建
现在我们打算通过内存来模拟实现一个块设备,与上面介绍的流程相差无几,相当于将交互对象由块设备替换成了内存。

讯享网#define blkdev_NAME "blkdev" // 块设备名称 #define DISK_MINOR 1 // 模拟块设备的分区数 #define DISK_SIZE 3 * 1024 * 1024 // 模拟块设备的大小(3MB),单位:字节 struct blk_dev { uint8_t* diskbuf; // 一小块内存,用于模拟块设备 int major; // 块设备主设备号 spinlock_t lock; // 自旋锁 struct gendisk* gendisk; // 块设备(保存了块设备相关信息) struct request_queue* queue; // 请求队列 }; static struct blk_dev blkdev;
三、注册块设备
第1~3步都是在为第4步初始化gendisk做准备。

1、申请主设备号
无论是字符设备还是块设备,都有一个设备号,下面为当前块设备申请主设备号。
相关API:
/ * @brief 注册块设备 * @param major 主设备号(0表示由OS自动分配设备号,1~255表示自定义主设备号) * @param name 块设备名称 * @return 成功返回主设备号,失败返回负值 */ int register_blkdev(unsigned int major, const char *name); / * @brief 注销块设备 * @param major 主设备号 * @param name 块设备名称 */ void unregister_blkdev(unsigned int major, const char *name);
实际应用:
讯享网/* 注册块设备 */ blkdev.major = register_blkdev(0, blkdev_NAME); if (blkdev.major < 0) { printk("block device register failed!\n"); return -1; }
2、申请gendisk
Linux内核为了方便保存块设备属性信息,提供了名为 gendisk 的数据类型。
相关API:
/ * @brief 申请gendisk * @param minors 申请的分区数(相当于在告诉内核块设备有多少个分区) * @return 成功返回gendisk的地址,失败返回NULL */ struct gendisk *alloc_disk(int minors); / * @brief 释放gendisk * @param gp 要删除的gendisk */ void del_gendisk(struct gendisk *gp);
实际应用:
讯享网blkdev.gendisk = alloc_disk(DISK_MINOR); if (blkdev.gendisk == NULL) { printk("gendisk allocate failed\n"); unregister_blkdev(blkdev.major, blkdev_NAME); return -1; }
3、初始化请求队列
请求队列是一种用于存储待处理的请求的数据结构,当用户发起I/O操作时,Linux内核可以有效管理这些请求,如优先级较高的请求优先处理,这样可以提高系统的性能和响应速度。
相关API如下
/ * @brief 初始化请求队列 * @param rfn 请求处理函数 * 函数指针:void (request_fn_proc) (struct request_queue *q) * @param lock 自旋锁 * @return 成功返回请求队列的地址,失败返回NULL */ request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); / * @brief 删除请求队列 * @param q 要删除的请求队列 */ void blk_cleanup_queue(struct request_queue *q);
- rfn: 当块设备收到一个IO请求时,请求处理函数会被触发来处理这个请求
- lock:自旋锁,需要自己从外部传递,请求队列作为临界资源,自然需要考虑互斥问题
实际应用:
讯享网spin_lock_init(&blkdev.lock); blkdev.queue = blk_init_queue(request_handle, &blkdev.lock); if (blkdev.queue == NULL) { printk("gendisk allocate failed\n"); del_gendisk(blkdev.gendisk); unregister_blkdev(blkdev.major, blkdev_NAME); return -1; }
请求处理函数:
// 请求处理函数 void request_handle(struct request_queue *q) { }
4、初始化 gendisk
接下来将对 gendisk 结构体中的一部分成员进行初始化,有些是必须要初始化的,如主设备号、块设备操作函数、设备名、请求队列等。
每一个块设备都有大小,要设置块设备大小需要用到 set_capacity,函数声明如下:
讯享网/ * @brief 设置块设备大小 * @param disk gendisk 指针 * @param size 块设备大小,单位: 扇区 */ void set_capacity(struct gendisk *disk, sector_t size);
初始化内容如下:
// 块设备操作函数 static struct block_device_operations blkdev_fops = { .owner = THIS_MODULE, }; blkdev.gendisk->major = blkdev.major; // 主设备号 blkdev.gendisk->first_minor = 0; // 起始次设备号 blkdev.gendisk->fops = &blkdev_fops; // 块设备操作函数 blkdev.gendisk->queue = blkdev.queue; // 块设备请求队列 strcpy(blkdev.gendisk->disk_name, blkdev_NAME); // 块设备名称 set_capacity(blkdev.gendisk, DISK_SIZE/512); // 告诉内核块设备大小,单位:扇区
5、添加到内核
最终将初始化好的 gendisk 添加到内核,相当于在告诉内核当前块设备的相关信息,因为内核无法主动得知块设备信息。
相关API:
讯享网/ * @brief 将gendisk添加到内核 * @param disk gendisk 指针 */ void add_disk(struct gendisk *disk);
实际应用:
add_disk(blkdev.gendisk);
四、补充:分配内存
前面只是设置了块设备的大小为3MB,但实际上并没有分配这块内存,因为到下一步读写块设备才会需要操作内存,所以申请一块3MB的内存在当前阶段非必须。
分配内存可以使用kmalloc 或者 kzalloc,效果一样,区别在于kzalloc在分配的同时会先清空内存,对应的使用 kfree 来释放内存。
讯享网/ * @param size 分配的内存大小,单位: 字节 * @param flags 内存分配的标志,用于指定内存分配的行为(如内存对齐、内存映射类型等) * @return 返回一个指向分配的内存块的指针,如果分配失败则返回NULL */ void* kzalloc(size_t size, gfp_t flags); / * @param ptr 内存块的地址 */ void kfree(const void *ptr);
| flags可选项 | 解析 |
GFP_KERNEL |
表示内存分配应该在可抢占的内核上下文中进行,并且可以使用内核的页面置换机制(最常用的标志,用于普通的内核操作) |
GFP_ATOMIC |
表示内存分配应该在不可抢占的内核上下文中进行,且不可被页面置换,以避免死锁或其他错误。这通常用于中断处理程序或其他不能延迟等待内存分配的上下文。 |
GFP_DMA |
表示内存分配应该返回可以通过内存DMA访问的内存块。这用于需要进行DMA传输的设备驱动程序。 |
GFP_DMA32 |
类似于GFP_DMA,但是指定返回的内存应该位于32位物理地址空间内,以满足32位DMA的限制。如果系统支持大于4GB的物理地址空间,则该标志将被忽略。 |
GFP_HIGHUSER |
表示内存分配应该返回用户态进程可以访问的内存块。这用于在内核中为用户态进程分配内存。 |
GFP_NOFS |
表示内存分配应该在文件系统内部的上下文中进行,且不能导致内核执行文件系统操作(如页面缓存)。这通常用于文件系统代码中,以避免死锁或其他错误。 |
GFP_NOWAIT |
表示在无法立即获得所需的内存时,kmalloc或vmalloc函数应立即返回NULL,而不是等待内存可用再分配。 |
GFP_ZERO |
表示分配的内存块应该在分配后清零。这个标志可以用于kmalloc和kzalloc函数。 |
实际使用:
blkdev.diskbuf = kzalloc(DISK_SIZE, GFP_KERNEL); if (blkdev.diskbuf == NULL) { return -1; }
五、完整代码(待完善)
将驱动代码编译成模块,加入到内核后,我们输入 fdisk -l 可以看到当前系统中已经注册的块设备,包括我们刚才注册的块设备。

讯享网#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/uaccess.h> #include <linux/fs.h> // 注册/注销块设备 #include <linux/genhd.h> // 申请/释放gendisk #include <linux/blkdev.h> // 申请/释放请求队列 #define blkdev_NAME "blkdev" // 块设备名称 #define DISK_MINOR 1 // 模拟块设备的分区数 #define DISK_SIZE 3 * 1024 * 1024 // 模拟块设备的大小,单位:字节 struct blk_dev { uint8_t* diskbuf; // 一小块内存,用于模拟块设备 int major; // 块设备主设备号 spinlock_t lock; // 自旋锁 struct gendisk* gendisk; // 块设备(保存了块设备相关信息) struct request_queue* queue; // 请求队列 }; static struct blk_dev blkdev; static struct block_device_operations blkdev_fops = { .owner = THIS_MODULE, }; void request_handle(struct request_queue *q) { } static int __init blkdriver_init(void) { /* 分配内存 */ blkdev.diskbuf = kzalloc(DISK_SIZE, GFP_KERNEL); if (blkdev.diskbuf == NULL) { return -1; } /* 注册块设备 */ blkdev.major = register_blkdev(0, blkdev_NAME); if (blkdev.major < 0) { printk("block device register failed!\n"); return -1; } /* 申请 gendisk */ blkdev.gendisk = alloc_disk(DISK_MINOR); if (blkdev.gendisk == NULL) { printk("gendisk allocate failed\n"); unregister_blkdev(blkdev.major, blkdev_NAME); return -1; } /* 申请请求队列 */ spin_lock_init(&blkdev.lock); blkdev.queue = blk_init_queue(request_handle, &blkdev.lock); if (blkdev.queue == NULL) { printk("gendisk allocate failed\n"); del_gendisk(blkdev.gendisk); unregister_blkdev(blkdev.major, blkdev_NAME); return -1; } /* 初始化 gendisk */ blkdev.gendisk->major = blkdev.major; // 主设备号 blkdev.gendisk->first_minor = 0; // 起始次设备号 blkdev.gendisk->fops = &blkdev_fops; // 块设备操作函数 blkdev.gendisk->queue = blkdev.queue; // 块设备请求队列 strcpy(blkdev.gendisk->disk_name, blkdev_NAME); // 块设备名称 set_capacity(blkdev.gendisk, DISK_SIZE/512); // 告诉内核块设备大小,单位:扇区 /* 添加到内核 */ add_disk(blkdev.gendisk); return 0; } static void __exit blkdriver_exit(void) { /* 释放内存 */ kfree(blkdev.diskbuf); /* 注销块设备 */ unregister_blkdev(blkdev.major, blkdev_NAME); /* 释放gendisk */ del_gendisk(blkdev.gendisk); /* 清除请求队列 */ blk_cleanup_queue(blkdev.queue); } module_init(blkdriver_init); module_exit(blkdriver_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("author_name");

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/27701.html