目录
在Linux的设备驱动架构中,块设备是与字符型设备不同类型的另一种设备,因此内核在支持块设备驱动程序时所使用的相关数据结构和I/O模型的设计等方面都与字符型设备驱动程序有所不同,Linux内核对块设备的支持要更为复杂。相对于字符型设备,块设备是针对存储设备的,比如SD卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
- 块设备以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
- 块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟再一次性将缓冲区中的数据写入块设备中。
使用缓冲区的目的是为了提高块设备寿命,数据先写入缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样能减少对块设备的擦除次数,从而提高块设备寿命。而字符设备是顺序的数据流设备,其按照字节进行读写访问,不需要缓冲区,对于字符设备的访问都是实时的。
根据块设备结构的不同其I/O算法也会不同,对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备就可以任意读写扇区(块设备物理存储单元)。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能,linux 里面针对不同类型的存储设备实现了不同的I/O调度算法。
Linux块设备子系统
Linux内核针对块设备驱动提供了相关层级组件,称为块子系统,如下图所示。图中,最上层是Linux内核的文件系统组件,主要是磁盘文件系统,块设备文件等。接下来是一个通用的块层,用来完成块设备的相关核心功能。在通用的块层之下是I/O调度器组件,主要用来对块设备请求队列中的请求进行调度,以最大程度优化硬件操作的性能(比如I/O调度器可能会对请求队列中的某些请求进行合并或者调整各请求间的顺序,以尽可能减少磁盘磁头移动的距离)。I/O调度器之下则是块设备驱动程序,它控制对应的硬件设备完成来自上层的I/O请求等操作。
块设备驱动
linux内核使用结构体block_device表示一个具体的块设备对象,比如一个硬盘或者分区,定义在include/linux/fs.h 文件中;其中,使用指向通用磁盘结构体gendisk的bd_disk成员变量来表示一个独立的磁盘设备或分区,该结构体定义在include/linux/genhd.h中。和字符设备驱动一样,块设备也需要向内核注册、申请设备号和注销等操作。块设备注册函数为register_blkdev,注销函数为unregister_blkdev。编写块的设备驱动的时候需要分配并初始化gendisk,使用函数alloc_disk申请一个gendisk,使用del_gendisk删除一个gendisk。需要注意,使用alloc_disk申请到gendisk以后系统还不能使用,必须使用add_disk函数将申请到的gendisk 添加到内核中。每一个磁盘都会有容量,因此在初始化gendisk的时候需要使用函数set_capacity设置其容量。而不管设备物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以set_capacity函数设置的是块设备实际容量除以512字节得到的扇区数量。除此之外,内核会通过get_disk和put_disk这两个函数来调整gendisk的引用计数,get_disk增加gendisk的引用计数,put_disk减少gendisk的引用计数。
和字符设备的file _operations一样,块设备也有操作集,内核使用结构体 block_device_operations定义了一组与块设备操作相关的函数指针,实现了块设备的打开、关闭、读写和ioctl等功能,使得不同的块设备能够通过统一的接口与内核进行交互,此结构体定义在 include/linux/blkdev.h 中。
struct block_device_operations {
blk_qc_t (*submit_bio) (struct bio *bio);
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, unsigned int);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
unsigned int (*check_events) (struct gendisk *disk,
unsigned int clearing);
void (*unlock_native_capacity) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
/* this callback is with swap_lock and sometimes page table lock held */
void (*swap_slot_free_notify) (struct block_device *, unsigned long);
int (*report_zones)(struct gendisk *, sector_t sector,
unsigned int nr_zones, report_zones_cb cb, void *data);
char *(*devnode)(struct gendisk *disk, umode_t *mode);
struct module *owner;
const struct pr_ops *pr_ops;
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
ANDROID_OEM_DATA(1);
};
其中,submit_bio函数用于提交块IO(bio)请求,Linux内核从2.6版本开始使用bio作为底层块IO的抽象,在较新的内核版本中,更是采用了多队列(blk-mq)调度器,使得每个请求队列可以有自己的特定的submit_bio方法。open/release函数用于打开和关闭设备。rw_page函数用于读写指定页。ioctl 函数用于块设备的I/O控制接口。compat_ioctl函数和ioctl函数一样,都是用于块设备的I/O控制。区别在于在64位系统上,32位应用程序的ioctl会调用compat_iotl函数。在 32 位系统上运行的 32 位应用程序调用的就是 ioctl 函数。check_events函数用于检查设备事件,如介质是否发生改变。unlock_native_capacity函数用于解锁设备原生容量。getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。swap_slot_free_notify函数是当交换槽被释放时的通知函数,用于处理内存管理。owner表示此结构体属于哪个模块,一般直接设置为THIS_MODULE。pr_ops指向打印操作的结构体,用于设备日志和调试输出。
很明显,block_device_operations 结构体中并没有像字符设备中read和write这样的读写函数,块设备读写数据是通过request_queue、request和bio实现的。内核将对块设备的读写都发送到请求队列request_queue中,request_queue中是大量的request(请求结构体),而 request 又包含了 bio,bio中保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。其中,gendisk结构体中就包含了一个需要填充的request_queue结构体指针类型的成员变量queue。
初始化请求队列
request_queue也是需要申请和初始化的,在之前版本的内核中使用blk_init_queue函数来完成request_queue的申请与初始化,函数原型如下:
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
其中,rfn为请求处理函数指针,每个request_queue都要有一个请求处理函数,请求处理函数request_fn_proc原型如下:
void (request_fn_proc) (struct request_queue *q);
而在较新版本内核中,由于采用了多队列(blk-mq)调度器,对于request_queue的申请与初始化函数也发生了变化。此次实验使用的Ubuntu虚拟机内核版本为5.4.0,在该版本下,请求队列初始化有两种方式,一是使用blk_mq_init_sq_queue函数进行初始化,二是使用blk_mq_init_queue申请并初始化请求队列。其中,blk_mq_init_queue函数原型如下,函数定义在linux/blk-mq.h。
struct request_queue *blk_mq_init_queue(struct blk_mq_tag_set *);
该函数返回值为申请并初始化好的请求队列结构体指针,参数为struct blk_mq_tag_set类型的指针,该结构体是块设备多队列调度的核心,提供了管理请求标签的必要信息和功能,使得设备能够高效处理并发的IO请求。内核中使用blk_mq_alloc_tag_set函数初始化并申请一个标签集用于管理并发请求;使用blk_mq_free_tag_set函数释放块设备标签集。blk_mq_tag_set结构体内容如下:
struct blk_mq_tag_set {
struct blk_mq_queue_map map[HCTX_MAX_TYPES];
unsigned int nr_maps;
const struct blk_mq_ops *ops;
unsigned int nr_hw_queues;
unsigned int queue_depth;
unsigned int reserved_tags;
unsigned int cmd_size;
int numa_node;
unsigned int timeout;
unsigned int flags;
void *driver_data;
atomic_t active_queues_shared_sbitmap;
struct sbitmap_queue __bitmap_tags;
struct sbitmap_queue __breserved_tags;
struct blk_mq_tags **tags;
struct mutex tag_list_lock;
struct list_head tag_list;
};
其中, const struct blk_mq_ops *ops指向操作集函数的指针,该操作集定义了与标签相关的操作,如请求分配和释放等。该结构体具体内容如下:
struct blk_mq_ops {
/**
* @queue_rq: Queue a new request from block IO.
*/
blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *,
const struct blk_mq_queue_data *);
/**
* @commit_rqs: If a driver uses bd->last to judge when to submit
* requests to hardware, it must define this function. In case of errors
* that make us stop issuing further requests, this hook serves the
* purpose of kicking the hardware (which the last request otherwise
* would have done).
*/
void (*commit_rqs)(struct blk_mq_hw_ctx *);
/**
* @get_budget: Reserve budget before queue request, once .queue_rq is
* run, it is driver's responsibility to release the
* reserved budget. Also we have to handle failure case
* of .get_budget for avoiding I/O deadlock.
*/
bool (*get_budget)(struct request_queue *);
/**
* @put_budget: Release the reserved budget.
*/
void (*put_budget)(struct request_queue *);
/**
* @timeout: Called on request timeout.
*/
enum blk_eh_timer_return (*timeout)(struct request *, bool);
/**
* @poll: Called to poll for completion of a specific tag.
*/
int (*poll)(struct blk_mq_hw_ctx *);
/**
* @complete: Mark the request as complete.
*/
void (*complete)(struct request *);
/**
* @init_hctx: Called when the block layer side of a hardware queue has
* been set up, allowing the driver to allocate/init matching
* structures.
*/
int (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int);
/**
* @exit_hctx: Ditto for exit/teardown.
*/
void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);
/**
* @init_request: Called for every command allocated by the block layer
* to allow the driver to set up driver specific data.
*
* Tag greater than or equal to queue_depth is for setting up
* flush request.
*/
int (*init_request)(struct blk_mq_tag_set *set, struct request *,
unsigned int, unsigned int);
/**
* @exit_request: Ditto for exit/teardown.
*/
void (*exit_request)(struct blk_mq_tag_set *set, struct request *,
unsigned int);
/**
* @initialize_rq_fn: Called from inside blk_get_request().
*/
void (*initialize_rq_fn)(struct request *rq);
/**
* @cleanup_rq: Called before freeing one request which isn't completed
* yet, and usually for freeing the driver private data.
*/
void (*cleanup_rq)(struct request *);
/**
* @busy: If set, returns whether or not this queue currently is busy.
*/
bool (*busy)(struct request_queue *);
/**
* @map_queues: This allows drivers specify their own queue mapping by
* overriding the setup-time function that builds the mq_map.
*/
int (*map_queues)(struct blk_mq_tag_set *set);
#ifdef CONFIG_BLK_DEBUG_FS
/**
* @show_rq: Used by the debugfs implementation to show driver-specific
* information about a request.
*/
void (*show_rq)(struct seq_file *m, struct request *rq);
#endif
};
其中,blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *, const struct blk_mq_queue_data *) 指向的函数负责将请求(request)排入队列,是块设备IO请求处理的核心函数。
而blk_mq_init_sq_queue函数原型如下,函数同样定义在linux/blk-mq.h。
struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,
const struct blk_mq_ops *ops,
unsigned int queue_depth,
unsigned int set_flags);
该函数返回值为申请并初始化好的请求队列结构体指针,参数分别为struct blk_mq_tag_set类型的指针,与标签相关的操作集函数指针,队列深度以及与标签相关的标志信息。
删除请求队列
当卸载块设备驱动的时候需要删除申请到的request_queue,删除请求队列使用函数blk_cleanup_queue,函数原型如下:
void blk_cleanup_queue(struct request_queue *q);
请求(request)
请求队列(request_queue)里面包含的是一系列的请求(request),request 也是一个结构体,同样定义在 include/linux/blkdev.h 里面。request结构体里面有一个struct bio *bio成员变量,类型为 bio 结构体指针,真正的数据就保存在bio里面。驱动程序需要从request_queue中取出一个个的request,然后再从每个request里面取出bio,最后根据 bio 的描述将数据写入到块设备,或者从块设备中读取数据。从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,函数原型如下:
request *blk_peek_request(struct request_queue *q);
使用 blk_peek_request 函数获取到下一个要处理的请求之后使用blk_start_request 函数开启请求处理。而对于较新版本内核,通过标签操作集中的请求队列函数获取请求,也即blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *, const struct blk_mq_queue_data *),该函数中的blk_mq_queue_data结构体中就包含有请求request。获取到请求之后,使用blk_mq_start_request函数开启请求处理,函数原型如下:
void blk_mq_start_request(struct request *rq);
使用blk_mq_end_request函数结束请求处理,原型如下:
void blk_mq_end_request(struct request *rq, blk_status_t error);
块IO(bio)
前面说过,每个request里面会有多个bio,bio保存着最终要读写的数据、地址等信息。上层应用程序对于块设备的读写会被构造成一个或多个bio结构体,bio 结构体中描述了要读写的起始扇区、要读写的扇区数量、读写方向、页偏移、数据长度等等信息。上层会将bio提交给I/O调度器,I/O调度器会将这些 bio 构造成request结构体,而一个物理存储设备对应一个request_queue,request_queue里面顺序存放着一系列的request。新产生的bio可能被合并到request_queue里现有的request 中,也可能产生新的 request,然后插入到 request_queue 中合适的位置,这一切都是由 I/O调度器来完成的。request_queue、request 和 bio 之间的关系如图所示:
对于物理存储设备的操作不外乎就是将RAM中的数据写入到物理存储设备中,或者将物理设备中的数据读取到RAM中去处理。Linux中bio是块设备最小的数据传输单元,bio 是个结构体,定义在 include/linux/blk_types.h 中,其中的bvec_iter 结构体成员变量描述了要操作的设备扇区等信息,用于描述物理存储设备地址信息,定义在/linux/bvec.h。
struct bvec_iter {
sector_t bi_sector; /* device address in 512 byte sectors */
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
unsigned int bi_bvec_done; /* number of bytes completed in current bvec */
};
而bio_vec 结构体成员变量描述了RAM信息,包括数据传输页地址,传输大小和页偏移。
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
bio、bvec_iter以及bio_vec这三个结构体之间的关系如图所示:
实验程序编写
此次实验使用Ubuntu虚拟机上的RAM模拟一个块设备,定义为ramdisk,并编写块设备驱动程序,验证其功能。
#include <linux/vmalloc.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/blk-mq.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/hdreg.h>
#include <linux/kdev_t.h>
#include <linux/genhd.h>
#include <linux/blkdev.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! */
struct ramdisk_dev{
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk内存空间,用于模拟块设备 */
spinlock_t lock;
struct gendisk *gendisk;
struct request_queue *queue;
struct blk_mq_tag_set bd_tag_set; /* 标签 */
};
struct ramdisk_dev *ramdisk;
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release\r\n");
}
/* 获取磁盘信息,是相对于机械硬盘的概念 */
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,
};
/* 处理传输过程函数 */
static int ramdisk_transfer(struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9; /*获取扇区地址,左移9位转换为字节地址 */
unsigned long len = blk_rq_cur_bytes(req);
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;
}
/* 队列操作核心函数 */
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,
};
/* 队列初始化函数 */
static struct request_queue* init_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;
}
/* gendisk初始化函数 */
static int init_req_gendisk(struct ramdisk_dev *set)
{
struct ramdisk_dev *dev = set;
dev->gendisk = alloc_disk(RADMISK_MINOR);
if(dev == NULL)
return -ENOMEM;
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;
}
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;
}
printk("ramdisk major = %d\r\n", dev->major);
/* 4、创建多队列 */
dev->queue = init_req_queue(&dev->bd_tag_set);
if(dev->queue == NULL) {
goto init_queue_fail;
}
/* 5、创建块设备 */
ret = init_req_gendisk(dev);
if(ret < 0) {
goto init_gendisk_fail;
}
return 0;
init_gendisk_fail:
blk_cleanup_queue(dev->queue);
blk_mq_free_tag_set(&dev->bd_tag_set);
init_queue_fail:
unregister_blkdev(dev->major, RAMDISK_NAME);
register_blkdev_fail:
kfree(dev->ramdiskbuf); /* 释放内存 */
kfree(dev);
return -ENOMEM;
}
static void __exit ramdisk_exit(void)
{
printk("ramdisk exit\r\n");
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("tester");
测试驱动程序按照上述分析流程编写,其中对于块设备操作集只填充了打开、关闭和获取磁盘信息操作函数;对于队列操作集函数,只填充了核心的块IO请求处理函数queue_rq,并通过ramdisk_transfer函数封装了bio处理过程,该函数内部通过调用 blk_rq_pos 函数从请求中获取要操作的块设备扇区地址,使用blk_rq_cur_bytes 函数获取请求要操作的数据长度,使用bio_data函数获取请求中bio保存的数据,最后调用rq_data_dir函数判断当前读写方向,如果是写的话就使用memcpy将bio中的数据拷贝到ramdisk 指定地址(扇区),如果是读的话就从ramdisk 中的指定地址(扇区)读取数据放到bio中。而对于请求队列以及标签集的初始化则封装在init_req_queue函数中,并给出了多队列调度下的两种不同请求队列初始化方式。
运行测试
将编写好的测试驱动程序进行模块化编译,并加载驱动模块。
另开终端查看内核打印信息,可以看到驱动成功加载,块设备成功注册,主设备号为252。
使用fdisk -l命令查看系统磁盘信息,可以看到成功生成ramdisk设备。
格式化ramdisk,为了实验现象更加明显,将其挂载到桌面,进入桌面目录,新建.txt文件并写入hello test测试生成的块设备功能是否正常。
总结:本篇详细分析了Linux块设备驱动架构,并对比了多队列调度机制下Linux块设备驱动的不同,最后通过编写驱动测试程序,实现了块设备的注册,打开和数据传输等功能。
标签:set,struct,mq,request,queue,之块,blk,Linux,驱动 From: https://blog.csdn.net/woaidandanhou/article/details/144427004