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);#elseint 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;}
#endifreturn 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.测试②
非请求队列方式的驱动测试与上面的测试①没有什么区别,这里就不再赘述。