针对块设备驱动将分为两部分介绍,第一部分是注册块设备,即将块设备成功添加到内核;第二部分是介绍如何读写块设备,因为没有实际块设备,这里选择使用内存来模拟块设备。
目录
一、认识块设备
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");