Linux块设备驱动-ramdisk模拟磁盘
作者:互联网
前言
写文章的目的是想通过记录自己的学习过程,以便以后使用到相关的知识点可以回顾和参考。
一、块设备驱动框架
.
1、 block_device 结构体
linux 内核使用 block_device 表示块设备, block_device 为 一 个 结 构 体 , 定 义 在 include/linux/fs.h 文件中,结构体内容如下:
struct block_device {
dev_t bd_dev; /* not a kdev_t - it's a search key */
int bd_openers;
struct inode * bd_inode; /* will die */
struct super_block * bd_super;
struct mutex bd_mutex; /* open/close mutex */
struct list_head bd_inodes;
void * bd_claiming;
void * bd_holder;
int bd_holders;
bool bd_write_holder;
#ifdef CONFIG_SYSFS
struct list_head bd_holder_disks;
#endif
struct block_device * bd_contains;
unsigned bd_block_size;
struct hd_struct * bd_part;
/* number of times partitions within this device have been opened. */
unsigned bd_part_count;
int bd_invalidated;
struct gendisk * bd_disk; /* 描述一个磁盘的结构体gendisk */
struct request_queue * bd_queue;
........
};
对于 block_device 结构体,重点关注一下 bd_disk 成员变量,此成员变量为
gendisk 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。
. 注册块设备
和字符设备驱动一样,需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:
int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:主设备号。
name:块设备名字。
返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。
. 注销块设备
如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:
void unregister_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:要注销的块设备主设备号。
name:要注销的块设备名字。
返回值:无。
.
2、 gendisk 结构体
linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在 include/linux/genhd.h中,内容如下:
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
/* 磁盘设备的主设备号 */
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 */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
struct disk_part_tbl __rcu *part_tbl; /* 磁盘对应的分区表 */
struct hd_struct part0;
const struct block_device_operations *fops; /* 块设备操作集 */
struct request_queue *queue; /* 磁盘对应的请求队列 */
void *private_data; /* 私有数据 */
.......
};
里面比较重要的成员,我已中文注释在代码中。编写块的设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数,常用的 API 函数如下面这些。
. 申请 gendisk
使用 gendisk 之前要先申请,allo_disk 函数用于申请一个 gendisk,函数原型如下:
struct gendisk *alloc_disk(int minors)
函数参数和返回值含义如下:
minors:次设备号数量,也就是 gendisk 对应的分区数量。
返回值:成功:返回申请到的 gendisk,失败:NULL。
. 删除 gendisk
如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:
void del_gendisk(struct gendisk *gp)
函数参数和返回值含义如下:
gp:要删除的 gendisk。
返回值:无。
. 将 gendisk 添加到内核
使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中,add_disk 函数原型如下:
void add_disk(struct gendisk *disk)
函数参数和返回值含义如下:
disk:要添加到内核的 gendisk。
返回值:无。
. 设置 gendisk 容量
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:
void set_capacity(struct gendisk *disk, sector_t size)
函数参数和返回值含义如下:
disk:要设置容量的 gendisk。
size:磁盘容量大小,注意这里是扇区数量。
返回值:无。
. 调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道,get_disk 是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:
truct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)
.
3、 block_device_operations 结构体
和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
int (*release) (struct gendisk *, fmode_t);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*direct_access) (struct block_device *, sector_t,
void **, unsigned long *);
unsigned int (*check_events) (struct gendisk *disk,
unsigned int clearing);
/* ->media_changed() is DEPRECATED, use ->check_events() instead */
int (*media_changed) (struct gendisk *);
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);
struct module *owner;
};
可以看出,block_device_operations 结构体里面的操作集函数和字符设备的 file_operations 操作集基本类似,不过块设备中的读写操作不再通过read,write操作函数来完成,因此在上面找不到这两个函数指针。除了open,release,ioctl外,里面有一个叫 getgeo 的也要了解下,接下来的驱动编写实践会用到,它用于获取磁盘信息,包括磁头、柱面和扇区等信息,其中有个参数
struct hd_geometry *
,它是 hd_geometry 结构体类型的指针,定义在 include/linux/hdreg.h 文件中,内容如下:
/* 描述一个磁盘的结构 */
struct hd_geometry {
unsigned char heads; /* 磁头 */
unsigned char sectors; /* 一条磁道的扇区数 */
unsigned short cylinders; /* 柱面,即磁道数 */
unsigned long start;
};
.
4、块设备 I/O 请求过程
在上面讲到 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引处理块设备驱动中非常重要的 request_queue、request 和 bio。
它们之间的关系如下图所示:
内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的 request(请求结构体),而 request 又包含了 bio,bio 包含的bvec_iter、bio_vec 结构体描述了该I/O请求的开始扇区、数据方向(读、写)、数据放入的页,即bio保存了读写相关数据。bio相关代码定义如下:
. bio结构体
struct bio {
struct bio *bi_next; /* 请求队列的下一个 bio */
struct block_device *bi_bdev; /* 指向块设备 */
unsigned long bi_flags; /* bio 状态等信息 */
unsigned long bi_rw; /* I/O 操作,读或写 */
struct bvec_iter bi_iter; /* I/O 操作,读或写 */
.......
struct bio_vec *bi_io_vec; /* bio_vec 列表 */
struct bio_set *bi_pool;
struct bio_vec bi_inline_vecs[0];
};
. bvec_iter结构体
struct bvec_iter {
sector_t bi_sector; /* I/O 请求的设备起始扇区(512 字节) */
unsigned int bi_size; /* 剩余的 I/O 数量 */
unsigned int bi_idx; /* blv_vec 中当前索引 */
unsigned int bi_bvec_done; /* 当前 bvec 中已经处理完成的字节数 */
};
. bio_vec结构体
struct bio_vec {
struct page *bv_page; /* 页 */
unsigned int bv_len; /* 长度 */
unsigned int bv_offset; /* 偏移 */
};
我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度。bio、bvec_iter 以及 bio_vec 这三个结构体之间的关系如下图所示:
5、涉及处理 request_queue、request 和 bio 的API
从上面知道了 request_queue、request 和 bio 这三者之间的关系,下面来看一下内核提供的涉及处理 request_queue、request 和 bio 的API函数。
1、初始化请求队列
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
函数参数和返回值含义如下:
rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数需要驱动编写人员自行实现。
lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。
2、清除请求队列
void blk_cleanup_queue(struct request_queue *q)
3、分配请求队列
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
对于 ramdisk 这种完全随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,可以直接 “踢开” I/O调度器,使用如下函数来绑定请求队列和 “制造请求” 函数(make_request_fn)。
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
通常 blk_cleanup_queue() 和 blk_queue_make_request() 都是结合起来使用的,逻辑一般是:
xxx_queue = blk_cleanup_queue(GFP_KERNEL);
blk_queue_make_request(xxx_queue, xxx_make_request_fn);
4、提取请求
struct request *blk_peek_request(struct request_queue *q)
上述函数用于返回下一个要处理的请求,如果没有请求则返回NULL。
5、启动请求
void blk_start_request(struct request *req)
使用 blk_peek_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,使用上述函数启动请求。
6、一步到位处理请求
struct request *blk_fetch_request(struct request_queue *q)
我们可以考虑使用 blk_fetch_request() 函数完成请求的提取和启动工作。
7、遍历 bio 和片段
__rq_for_each_bio(_bio, rq) 遍历一个请求的所有 bio
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
bio_for_each_segment 遍历一个 bio 的所有 bio_vec
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
rq_for_each_segment 迭代遍历一个请求所有 bio 中的所有 bio_vec
#define rq_for_each_segment(bvl, _rq, _iter) \
__rq_for_each_bio(_iter.bio, _rq) \
bio_for_each_segment(bvl, _iter.bio, _iter.iter)
8、报告完成
void __blk_end_request_all(struct request *rq, int error)
void blk_end_request_all(struct request *rq, int error)
上述两个函数用于报告请求是否完成,error为0表示完成,小于0表示失败,__blk_end_request_all 需要在持有队列锁的场景下使用。
如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要使用 bio_endio 函数通知处理完成:
bvoid bio_endio(struct bio *bio, int error)
.
二、实践:编写 ramdisk 驱动
ramdisk 是一种模拟磁盘,其数据实际上存储在 RAM 中。它通过 kzalloc() 分配出来的内存空间来模拟出一个磁盘,以块设备的方式访问这片内存。
下面的驱动程序通过设置 request_mode 的值,实现 使用请求队列 或者 不使用请求队列 两种方式来访问ramdisk。
ramdisk.c驱动程序
#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/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/bio.h>
#include <linux/hdreg.h>
#define RAMDISK_SIZE (2 * 1024 * 1024)
#define RAMDISK_NAME "ramdisk"
#define RAMDISK_MINORS (3) /* 3个分区 */
#define RAMDISK_QUEUE (1) /* 使用请求队列 */
#define RAMDISK_NOQUEUE (2) /* 不使用请求队列 */
const unsigned char request_mode = RAMDISK_QUEUE; /* 选择是否使用请求队列 */
/* ramdisk 设备结构体 */
struct ramdisk_dev{
int major;
unsigned char *ramdiskbuf; /* ramdisk 内存空间,用于模拟块设备 */
spinlock_t lock;
struct gendisk *gendisk; /* 描述一个磁盘设备, gendisk 结构体 */
struct request_queue *queue; /* 请求队列 */
};
struct ramdisk_dev ramdiskdev; /* ramdisk 设备 */
static int setup_device(struct ramdisk_dev *dev);
/* 使用请求队列时的传输过程 */
static void ramdisk_transfer(struct ramdisk_dev *dev, struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9;
unsigned long len = blk_rq_cur_bytes(req);
char *buffer = bio_data(req->bio);
if(rq_data_dir(req) == READ)
memcpy(buffer, dev->ramdiskbuf + start, len);
else if(rq_data_dir(req) == WRITE)
memcpy(dev->ramdiskbuf + start, buffer, len);
}
/* 请求处理函数,处理请求队列中每一个请求 */
static void ramdisk_request_fn(struct request_queue *q)
{
struct request *req;
struct bio_vec *bvec;
struct req_iterator iter;
req = blk_fetch_request(q);
#if 0
while(req != NULL){
struct ramdisk_dev *dev = req->rq_disk->private_data;
ramdisk_transfer(dev, req); /* 针对请求做具体的传输处理 */
__blk_end_request_all(req, 0);
req = blk_fetch_request(q);
}
#endif
while(req != NULL){
struct ramdisk_dev *dev = req->rq_disk->private_data;
rq_for_each_segment(bvec, req, iter){ /* 遍历一个请求所有 bio 中的所有 bio_vec */
ramdisk_transfer(dev, req);
}
__blk_end_request_all(req, 0);
req = blk_fetch_request(q);
}
}
/*
* "制造请求"函数,
* 不使用请求队列时使用
*/
static void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
{
struct ramdisk_dev *dev = q->queuedata;
int offset;
struct bio_vec *bvec;
unsigned long len = 0;
int i = 0;
offset = (bio->bi_sector) << 9;
/* 处理 bio 中的所有 bio_vec */
bio_for_each_segment(bvec, bio, i){
char *ptr = __bio_kmap_atomic(bio, bio->bi_idx,i);
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;
__bio_kunmap_atomic(ptr,i);
}
set_bit(BIO_UPTODATE, &bio->bi_flags);
bio_endio(bio, 0);
}
static int ramdisk_open(struct block_device *bdev, fmode_t mode)
{
//struct ramdisk_dev *dev = bdev->bd_disk->private_data;
printk(KERN_EMERG "ramdisk open!\r\n");
return 0;
}
static int ramdisk_release(struct gendisk *disk, fmode_t mode)
{
//struct ramdisk_dev *dev = disk->private_data;
printk(KERN_EMERG "ramdisk release!\r\n");
return 0;
}
static int ramdisk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
//struct ramdisk_dev *dev = bdev->bd_disk->private_data;
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 __init ramdisk_init(void)
{
int ret = 0;
/* 申请 ramdisk 内存 */
ramdiskdev.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if(ramdiskdev.ramdiskbuf == NULL){
ret = -EINVAL; /* 申请内存空间失败 */
goto ram_fail;
}
/* 初始化自旋锁 */
spin_lock_init(&ramdiskdev.lock);
/* 注册块设备 */
ramdiskdev.major = register_blkdev(0, RAMDISK_NAME); /* 自动分配主设备号 */
if(ramdiskdev.major < 0){
goto register_blkdev_fail;
}
printk(KERN_EMERG "major = %d\r\n",ramdiskdev.major);
ret = setup_device(&ramdiskdev);
if(ret != 0)
return ret;
return 0;
register_blkdev_fail:
kfree(ramdiskdev.ramdiskbuf); /* 释放内存 */
ram_fail:
return ret;
}
/* 驱动出口函数 */
static void __exit ramdisk_exit(void)
{
put_disk(ramdiskdev.gendisk); /* 引用计数-1 */
del_gendisk(ramdiskdev.gendisk); /* 删除 gendisk */
blk_cleanup_queue(ramdiskdev.queue); /* 清除请求队列 */
unregister_blkdev(ramdiskdev.major, RAMDISK_NAME); /* 注销块设备 */
kfree(ramdiskdev.ramdiskbuf); /* 释放内存 */
}
static int setup_device(struct ramdisk_dev *dev)
{
int ret = 0;
switch(request_mode){
case RAMDISK_NOQUEUE: /* 不使用请求队列 */
dev->queue = blk_alloc_queue(GFP_KERNEL);
if(dev->queue == NULL){
ret = -EINVAL;
goto blk_queue_fail;
}
blk_queue_make_request(dev->queue, ramdisk_make_request_fn); /* 设置 "制造请求" 函数*/
break;
default: /* 没有指定模式,则默认使用 请求队列 */
printk(KERN_EMERG "Bad request mode %d, using simple\r\n", request_mode);
case RAMDISK_QUEUE: /* 使用请求队列 */
dev->queue = blk_init_queue(ramdisk_request_fn, &dev->lock);
if (dev->queue == NULL){
ret = -EINVAL;
goto blk_queue_fail;
}
break;
}
dev->queue->queuedata = dev; /* 私有数据 */
/* 分配并初始化gendisk */
dev->gendisk = alloc_disk(RAMDISK_MINORS);
if (dev->gendisk == NULL)
{
printk(KERN_EMERG "alloc_disk fail\r\n");
ret = -EINVAL; /* 分配 gendisk 失败 */
goto gendisk_alloc_fail;
}
/* 初始化 gendisk ,并添加进内核 */
dev->gendisk->major = dev->major;
dev->gendisk->first_minor = 0;
dev->gendisk->fops = &ramdisk_fops;
dev->gendisk->private_data = dev; /* 私有数据 */
dev->gendisk->queue = dev->queue;
sprintf(dev->gendisk->disk_name, RAMDISK_NAME);
set_capacity(dev->gendisk, RAMDISK_SIZE/512); /* 设置 gendisk 容量 */
add_disk(dev->gendisk); /* 将 gendisk 添加到内核 */
return 0;
blk_queue_fail:
unregister_blkdev(dev->major, RAMDISK_NAME);
gendisk_alloc_fail:
put_disk(dev->gendisk); /* 引用计数-1 */
kfree(dev->ramdiskbuf); /* 释放内存 */
return ret;
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzj");
.
测试
加载驱动 insmod
格式化 ramdisk-----mkfs.vfat /dev/ramdisk
查看所有硬盘及分区信息------fdisk -l
已经识别出 ramdisk 设备了,容量和磁盘信息跟上面驱动程序中的一样。
把磁盘挂载到 tmp目录------mount /dev/ramdisk /tmp
在 tmp 目录中随便新建一个.txt来测试 ramdisk 的访问是否正常,接着卸载。然后重新挂载到 mnt 目录,可以看到 mnt 目录里面有之前在 tmp 目录下创建的.txt,说明 ramdisk驱动正常工作。
标签:ramdisk,bio,struct,request,dev,gendisk,Linux,磁盘 来源: https://blog.csdn.net/qq_43286311/article/details/118055569