STM32MP157驱动开发——Linux块设备驱动
- 一、简介
- 二、驱动开发
- 1.使用请求队列的方式
- 2.测试①
- 3.不使用请求队列的方式
- 4.测试②
参考文章:【正点原子】I.MX6U嵌入式Linux驱动开发——Linux 块设备驱动
一、简介
之前学习的都是关于字符设备的驱动,包括 platform 子系统、I2C总线等,本质上都是对字符设备驱动的一层封装。这节就学习第二种驱动模式——块设备驱动。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统。这一节使用开发板板载 RAM 模拟一个块设备,学习块设备驱动框架的使用。
块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。块设备与字符设备的区别如下:
1.块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。而字符设备是以字节为单位进行数据传输的,不需要缓冲。
2.块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。而字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。
块设备驱动框架的常用属性及使用流程就参考原子哥教程中的讲解,这里就不多赘述。
二、驱动开发
本节尝试使用开发板上的 RAM 模拟一段块设备,也就是 ramdisk,然后编写块设备驱动。
1.使用请求队列的方式
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/hdreg.h>
#include <linux/kdev_t.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blk-mq.h>
#include <linux/buffer_head.h>
#include <linux/bio.h>
#define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为2MB */
#define RAMDISK_NAME "ramdisk" /* 名字 */
#define RADMISK_MINOR 3 /* 表示有三个磁盘分区!不是次设备号为3!*/
/* ramdisk设备结构体 */
struct ramdisk_dev{
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk内存空间,用于模拟块设备 */
struct gendisk *gendisk; /* gendisk */
struct request_queue *queue; /* 请求队列 */
struct blk_mq_tag_set tag_set; /* blk_mq_tag_set */
spinlock_t lock; /* 自旋锁 */
};
struct ramdisk_dev *ramdisk = NULL; /* ramdisk设备指针 */
/*
* @description : 处理传输过程
* @param-req : 请求
* @return : 0,成功;其它表示失败
*/
static int ramdisk_transfer(struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9; /* blk_rq_pos获取到的是扇区地址,左移9位转换为字节地址 */
unsigned long len = blk_rq_cur_bytes(req); /* 大小 */
/* bio中的数据缓冲区
* 读:从磁盘读取到的数据存放到buffer中
* 写:buffer保存这要写入磁盘的数据
*/
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);
return 0;
}
/*
* @description : 开始处理传输数据的队列
* @hctx : 硬件相关的队列结构体
* @bd : 数据相关的结构体
* @return : 0,成功;其它值为失败
*/
static blk_status_t _queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data* bd)
{
struct request *req = bd->rq; /* 通过bd获取到request队列*/
struct ramdisk_dev *dev = req->rq_disk->private_data;
int ret;
blk_mq_start_request(req); /* 开启处理队列 */
spin_lock(&dev->lock);
ret = ramdisk_transfer(req); /* 处理数据 */
blk_mq_end_request(req, ret); /* 结束处理队列 */
spin_unlock(&dev->lock);
return BLK_STS_OK;
}
/*
* 队列操作函数
*/
static struct blk_mq_ops mq_ops = {
.queue_rq = _queue_rq,
};
/*
* @description : 打开块设备
* @param - dev : 块设备
* @param - mode : 打开模式
* @return : 0 成功;其他 失败
*/
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}
/*
* @description : 释放块设备
* @param - disk : gendisk
* @param - mode : 模式
* @return : 0 成功;其他 失败
*/
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release\r\n");
}
/*
* @description : 获取磁盘信息
* @param - dev : 块设备
* @param - geo : 模式
* @return : 0 成功;其他 失败
*/
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,
};
/*
* @description : 初始化队列相关操作
* @set : blk_mq_tag_set对象
* @return : request_queue的地址
*/
static struct request_queue * create_req_queue(struct blk_mq_tag_set *set)
{
struct request_queue *q;
#if 0
/*
*这里是使用了blk_mq_init_sq_queue 函数
*进行初始化的。
*/
q = blk_mq_init_sq_queue(set, &mq_ops, 2, BLK_MQ_F_SHOULD_MERGE);
#else
int ret;
memset(set, 0, sizeof(*set));
set->ops = &mq_ops; //操作函数
set->nr_hw_queues = 2; //硬件队列
set->queue_depth = 2; //队列深度
set->numa_node = NUMA_NO_NODE;//numa节点
set->flags = BLK_MQ_F_SHOULD_MERGE; //标记在bio下发时需要合并
ret = blk_mq_alloc_tag_set(set); //使用函数进行再次初始化
if (ret) {
printk(KERN_WARNING "sblkdev: unable to allocate tag set\n");
return ERR_PTR(ret);
}
q = blk_mq_init_queue(set); //分配请求队列
if(IS_ERR(q)) {
blk_mq_free_tag_set(set);
return q;
}
#endif
return q;
}
/*
* @description : 创建块设备,为应用层提供接口。
* @set : ramdisk_dev对象
* @return : 0,表示成功;其它值为失败
*/
static int create_req_gendisk(struct ramdisk_dev *set)
{
struct ramdisk_dev *dev = set;
/* 1、分配并初始化 gendisk */
dev->gendisk = alloc_disk(RADMISK_MINOR);
if(dev == NULL)
return -ENOMEM;
/* 2、添加(注册)disk */
dev->gendisk->major = ramdisk->major; /* 主设备号 */
dev->gendisk->first_minor = 0; /* 起始次设备号 */
dev->gendisk->fops = &ramdisk_fops; /* 操作函数 */
dev->gendisk->private_data = set; /* 私有数据 */
dev->gendisk->queue = dev->queue; /* 请求队列 */
sprintf(dev->gendisk->disk_name, RAMDISK_NAME); /* 名字 */
set_capacity(dev->gendisk, RAMDISK_SIZE/512); /* 设备容量(单位为扇区)*/
add_disk(dev->gendisk);
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init ramdisk_init(void)
{
int ret = 0;
struct ramdisk_dev * dev;
printk("ramdisk init\r\n");
/* 1、申请内存 */
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if(dev == NULL) {
return -ENOMEM;
}
dev->ramdiskbuf = kmalloc(RAMDISK_SIZE, GFP_KERNEL);
if(dev->ramdiskbuf == NULL) {
printk(KERN_WARNING "dev->ramdiskbuf: vmalloc failure.\n");
return -ENOMEM;
}
ramdisk = dev;
/* 2、初始化自旋锁 */
spin_lock_init(&dev->lock);
/* 3、注册块设备 */
dev->major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
if(dev->major < 0) {
goto register_blkdev_fail;
}
/* 4、创建多队列 */
dev->queue = create_req_queue(&dev->tag_set);
if(dev->queue == NULL) {
goto create_queue_fail;
}
/* 5、创建块设备 */
ret = create_req_gendisk(dev);
if(ret < 0)
goto create_gendisk_fail;
return 0;
create_gendisk_fail:
blk_cleanup_queue(dev->queue);
blk_mq_free_tag_set(&dev->tag_set);
create_queue_fail:
unregister_blkdev(dev->major, RAMDISK_NAME);
register_blkdev_fail:
kfree(dev->ramdiskbuf);
kfree(dev);
return -ENOMEM;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ramdisk_exit(void)
{
printk("ramdisk exit\r\n");
/* 释放gendisk */
del_gendisk(ramdisk->gendisk);
put_disk(ramdisk->gendisk);
/* 清除请求队列 */
blk_cleanup_queue(ramdisk->queue);
/* 释放blk_mq_tag_set */
blk_mq_free_tag_set(&ramdisk->tag_set);
/* 注销块设备 */
unregister_blkdev(ramdisk->major, RAMDISK_NAME);
/* 释放内存 */
kfree(ramdisk->ramdiskbuf);
kfree(ramdisk);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
①首先是宏定义部分,RAMDISK_SIZE 就是模拟块设备的大小,这里设置为 2MB。RAMDISK_NAME 为本实验名字,RADMISK_MINOR 是本实验此设备号数量。注意:非次设备号。
设备号数量决定了本块设备的磁盘分区数量。
②通过 ramdisk 的设备结构体定义一个全局变量 ramdisk_dev 类型的指针。
③先看一下驱动模块的加载和卸载,即ramdisk_init()和ramdisk_exit()。
- 在ramdisk_init()中先为自定义的ramdisk结构体申请空间,使用kmalloc申请2MB大小空间。然后初始化一个自旋锁,用于在队列操作的时候做保护。接着使用 register_blkdev 函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里让内核自动分配一个主设备号,因此 register_blkdev 函数的第一个参数为 0。create_req_queue 函数用于创建一个多队列,这一部分主要用于操作块设备。create_req_gendisk 函数用于创建一个块设备和提供一些接口给应用层调用。
- ramdisk_exit 函数在卸载块设备驱动模块的时候使用,需要将前面注册的对象进行卸载、实例化的对象要进行释放。
④create_req_queue 函数用于创建一个多队列,首先设置多队列的重要参数,比如一些操作函数、队列深度、硬件队列个数和标志位等。然后设置 blk_mq_tag_set 的 ops 成员变量,这就是块设备的队列操作集,由开发人员实现。再使用 blk_mq_alloc_tag_set 函数进行再次初始化 blk_mq_tag_set 对象,最后根据此对象分配请求队列。
也可以使用 blk_mq_init_sq_queue 函数一步到位,第一个参数为 blk_mq_tag_set 对象、第二个参数为操作函数集合、第三个参数为硬件队列个数,第四个参数为标志位。
⑤使用 create_req_gendisk 函数进行初始化块设备。先使用 alloc_disk 分配一个 gendisk,然后初始化申请到的 gendisk 对象,重点是设置 geddisk 的 fops 成员变量。再使用 set_capacity 函数设置本块设备容量大小。注意:这里的大小是扇区数,不是字节数,一个扇区是 512 字节。
gendisk 初始化完成以后就可以使用 add_disk 函数将 gendisk 添加到内核中,也就是向内核添加一个磁盘设备。
⑥gendisk 的 fops 操作集。就是块设备的操作集 block_device_operations,本节仅实现了 open、release 和 getgeo,其中 open 和 release 函数都是空函数,重点是 getgeo 函数。此函数用来获取磁盘信息,保存在参数 geo 中。
⑦blk_mq_tag_set 的 ops 操作集,也就是请求处理函数集合。使用 blk_mq_start_request 函数开启多队列处理,blk_mq_end_request 函数去结束多队列处理。ramdisk_transfer 数据处理函数,使用 ramdisk_transfer 数据处理函数,使用 bio_data 函数获取请求中的 bio 保存的数据。rq_data_dio 函数判断当前是读还是写,如果是写的话就将 bio 中的数据拷贝到 ramdisk 指定地址(扇区),如果是读的话就从 ramdisk 中的指定打字(扇区)读取数据放到 bio 中。
总结:主要是两个重要的结构体:blk_mq_tag_set 和 gendisk。可以把 blk_mq_tag_set 看作真正的 IO 读写操作(ops 操作集就是 IO 操作),有了底层操作之后,还需要 gendisk 结构体为上层提供接口调用(fops 就是实现上层调用的操作)。
2.测试①
驱动编写完成后,就可以编译出.ko文件进行挂载测试。
另外,还需要在 buildroot 中的 busybox 使能 mkfs.vfat 命令。在 buildroot 源码目录下,使用sudo make busybox-menuconfig
命令打开 busybox 配置界面,选中以下选项:
然后使用以下命令,编译出新的根文件系统:
sudo make busybox #编译新的busybox
sudo make #打包出新的buildroot
然后就可以使用新的根文件系统进行启动。
驱动挂载成功后,可以使用fdisk -l
命令查看磁盘信息。其中就包括 2MB 的ramdisk设备。
然后使用以下命令进行格式化,然后挂载,就可以操作这块磁盘了。
mkfs.vfat /dev/ramdisk #格式化磁盘为 vfat 格式
mkdir /mnt/ram_disk -P #创建 ramdisk 挂载目录
mount /dev/ramdisk /mnt/ram_disk #挂载 ramdisk
3.不使用请求队列的方式
请求队列会用到 I/O 调度器,适合机械硬盘这种存储设备。对于 EMMC、SD、ramdisk 这样没有机械结构的存储设备,可以直接访问任意一个扇区,因此可以不需要 I/O 调度器,也就不需要请求队列了。
参考 linux 内核的 drivers/block/zram/zram_drv.c,把 blk_mq_tag_set 相关的都删除掉,然后修改 create_req_queue 函数即可,在此函数里使用 create_req_queue 函数设置“制造请求”函数。
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/hdreg.h>
#include <linux/kdev_t.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blk-mq.h>
#include <linux/buffer_head.h>
#include <linux/bio.h>
#define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为2MB */
#define RAMDISK_NAME "ramdisk" /* 名字 */
#define RADMISK_MINOR 3 /* 表示有三个磁盘分区!不是次设备号为3!*/
/* ramdisk设备结构体 */
struct ramdisk_dev{
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk内存空间,用于模拟块设备 */
struct gendisk *gendisk; /* gendisk */
struct request_queue *queue; /* 请求队列 */
spinlock_t lock; /* 自旋锁 */
};
struct ramdisk_dev *ramdisk = NULL; /* ramdisk设备指针 */
/*
* @description : 打开块设备
* @param - dev : 块设备
* @param - mode : 打开模式
* @return : 0 成功;其他 失败
*/
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}
/*
* @description : 释放块设备
* @param - disk : gendisk
* @param - mode : 模式
* @return : 0 成功;其他 失败
*/
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release\r\n");
}
/*
* @description : 获取磁盘信息
* @param - dev : 块设备
* @param - geo : 模式
* @return : 0 成功;其他 失败
*/
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,
};
/*
* @description : “制造请求”函数
* @param-q : 请求队列
* @return : 无
*/
static blk_qc_t 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;
struct ramdisk_dev *dev = q->queuedata;
offset = (bio->bi_iter.bi_sector) << 9; /* 获取要操作的设备的偏移地址 */
spin_lock(&dev->lock);
/* 处理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, dev->ramdiskbuf + offset, len);
else if(bio_data_dir(bio) == WRITE) /* 写数据 */
memcpy(dev->ramdiskbuf + offset, ptr, len);
offset += len;
}
spin_unlock(&dev->lock);
bio_endio(bio);
return BLK_QC_T_NONE;
}
/*
* @description : 初始化队列相关操作
* @set : blk_mq_tag_set对象
* @return : request_queue的地址
*/
static struct request_queue * create_req_queue(struct ramdisk_dev *set)
{
struct request_queue *q;
q = blk_alloc_queue(GFP_KERNEL);
blk_queue_make_request(q, ramdisk_make_request_fn);
q->queuedata = set;
return q;
}
/*
* @description : 创建块设备,为应用层提供接口。
* @set : ramdisk_dev对象
* @return : 0,表示成功;其它值为失败
*/
static int create_req_gendisk(struct ramdisk_dev *set)
{
struct ramdisk_dev *dev = set;
/* 1、分配并初始化 gendisk */
dev->gendisk = alloc_disk(RADMISK_MINOR);
if(dev == NULL)
return -ENOMEM;
/* 2、添加(注册)disk */
dev->gendisk->major = ramdisk->major; /* 主设备号 */
dev->gendisk->first_minor = 0; /* 起始次设备号 */
dev->gendisk->fops = &ramdisk_fops; /* 操作函数 */
dev->gendisk->private_data = set; /* 私有数据 */
dev->gendisk->queue = dev->queue; /* 请求队列 */
sprintf(dev->gendisk->disk_name, RAMDISK_NAME); /* 名字 */
set_capacity(dev->gendisk, RAMDISK_SIZE/512); /* 设备容量(单位为扇区)*/
add_disk(dev->gendisk);
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init ramdisk_init(void)
{
int ret = 0;
struct ramdisk_dev * dev;
printk("ramdisk init\r\n");
/* 1、申请内存 */
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if(dev == NULL) {
return -ENOMEM;
}
dev->ramdiskbuf = kmalloc(RAMDISK_SIZE, GFP_KERNEL);
if(dev->ramdiskbuf == NULL) {
printk(KERN_WARNING "dev->ramdiskbuf: vmalloc failure.\n");
return -ENOMEM;
}
ramdisk = dev;
/* 2、初始化自旋锁 */
spin_lock_init(&dev->lock);
/* 3、注册块设备 */
dev->major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
if(dev->major < 0) {
goto register_blkdev_fail;
}
/* 4、创建多队列 */
dev->queue = create_req_queue(dev);
if(dev->queue == NULL) {
goto create_queue_fail;
}
/* 5、创建块设备 */
ret = create_req_gendisk(dev);
if(ret < 0)
goto create_gendisk_fail;
return 0;
create_gendisk_fail:
blk_cleanup_queue(dev->queue);
create_queue_fail:
unregister_blkdev(dev->major, RAMDISK_NAME);
register_blkdev_fail:
kfree(dev->ramdiskbuf);
kfree(dev);
return -ENOMEM;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ramdisk_exit(void)
{
printk("ramdisk exit\r\n");
/* 释放gendisk */
del_gendisk(ramdisk->gendisk);
put_disk(ramdisk->gendisk);
/* 清除请求队列 */
blk_cleanup_queue(ramdisk->queue);
/* 注销块设备 */
unregister_blkdev(ramdisk->major, RAMDISK_NAME);
/* 释放内存 */
kfree(ramdisk->ramdiskbuf);
kfree(ramdisk);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
重点为 create_req_queue 函数,使用 blk_alloc_queue 和 blk_queue_make_request 这两个函数取代了上一种方式中 blk_mq_tag_set 结构体相关的操作。blk_alloc_queue 函数用来申请一个请求队列,blk_queue_make_request 函数设置“制造请求”函数,这里设置的制造请求函数为 ramdisk_make_request_fn,由开发人员实现。
ramdisk_make_request_fn 函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个 bio 参数里面,所以 ramdisk_make_request_fn 函数里面是全部是对 bio 的操作。
4.测试②
非请求队列方式的驱动测试与上面的测试①没有什么区别,这里就不再赘述。