块设备是Linux驱动三大设备之一。与字符设备有很大的区别。块设备是针对存储设备的,比如 SD 卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动。
摘自Linux驱动设备开发详解(宋宝华)
1、block_device_operations
block_device_operations是和file_operations字符设备操作集类似的块设备操作集。它的具体定义如下:
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
long (*direct_access)(struct block_device *, sector_t,
void **, unsigned long *pfn, long size);
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;
};
主要用到的的函数:
open:当块设备打开的时候会调用此函数
release:当块设备关闭的时候会调用此函数
getgeo:此函数用来根据驱动器的几何信息填充一个hd_geometry结构体,该结构体包含磁头、扇区、柱面等信息。
2、gendisk结构体
Linux内核使用gendisk结构体描述一个独立的磁盘,该结构体的定义如下:
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;
int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
major:为磁盘设备的主设备号。
first_minor:为磁盘的第一个次设备号。
minors:为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一
样, 次设备号不同。
part_tbl:为磁盘对应的分区表,为结构体 disk_part_tbl 类型, disk_part_tbl 的核心是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。
part0:disk->part_tbl->part[0] = &part0;
fops:块设备操作集,为 block_device_operations 结构体类型。
queue:磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求
3、request_queue、request、bio
request_queue直译为请求队列,定义在gendisk中,因此每个块设备都对应于有一个请求队列。而请求队列中包含很多request直译为请求队列项。请求队列项又由一个或者多个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 之间的关系如图:
bio结构体的定义如下:
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};
unsigned short bi_vcnt; /* how many bio_vec's */
/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
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:page 指定了所在的物理页, offset 表示所处页的偏移地址, len 就是数据长度。
struct bio_vec {
struct page *bv_page; /* 页 */
unsigned int bv_len; /* 长度 */
unsigned int bv_offset; /* 偏移 */
};
其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇
区地址。 bi_io_vec 指向 bio_vec 数组首地址, bio_vec 数组就是 RAM 信息,比如页地址、页偏
移以及长度。
1、注册和注销块设备
int register_blkdev(unsigned int major, const char *name)
void unregister_blkdev(unsigned int major, const char *name)
major: 主设备号。
name: 块设备名字。
2、gendisk
struct gendisk *alloc_disk(int minors) // 注册gendisk
void del_gendisk(struct gendisk *gp) //注销gendisk
void add_disk(struct gendisk *disk) //向内核添加gendisk
void set_capacity(struct gendisk *disk, sector_t size) //设置gendisk容量
3、请求队列 request_queue
申请并初始化:
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
//初始化请求队列,并绑定rfn请求处理函数
rfn: 请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数
request_fn_proc 原型如下:
void (request_fn_proc) (struct request_queue *q)
请求处理函数需要驱动编写人员自行实现。
lock: 自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用
这个自旋锁。
删除请求队列:
void blk_cleanup_queue(struct request_queue *q)
分配请求队列并绑定制造请求函数:
blk_init_queue 函数完成了请求队列的申请已经请求处理函数的绑定,这个一般用于像机械
硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。但是对于 EMMC、 SD 卡这样的
非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。对于非机械设备可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。
分配一个request_queue
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
将request_queue和制造请求函数绑定 、
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
mfn:制造请求函数,该函数需要自己编写,函数原型如下:
void (make_request_fn) (struct request_queue *q, struct bio *bio)
本实验利用开发板上的RAM模拟一段内存,编写块设备驱动,代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*定义磁盘大小*/
#define RAMDISK_SIZE (2 * 1024 * 1024) /*大小2MB*/
#define RAMDISK_NAME "ramdisk"
#define RAMDISK_MINOR 3 /*磁盘分区*/
struct ramdisk_dev {
int major; /* major主设备号*/
unsigned char *ramdiskbuffer; /*ramdisk内存空间,用来模拟块设备*/
spinlock_t spinlock; /*spinlock自旋锁*/
struct gendisk *gendisk; /*gendisk*/
struct request_queue *queue; /*request_queue请求队列*/
};
struct ramdisk_dev ramdisk; /*定义一个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,
};
/*
* @description : 处理传输过程
* @param-req : 请求
* @return : 无
*/
static void 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.ramdiskbuffer + start, len);
else if(rq_data_dir(req) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuffer + start, buffer, len);
}
/*
* @description : 请求处理函数
* @param-q : 请求队列
* @return : 无
*/
void ramdisk_request_fn(struct request_queue *q)
{
int err = 0;
struct request *req;
/* 循环处理请求队列中的每个请求 */
req = blk_fetch_request(q);
while(req != NULL) {
/* 针对请求做具体的传输处理 */
ramdisk_transfer(req);
/* 判断是否为最后一个请求,如果不是的话就获取下一个请求
* 循环处理完请求队列中的所有请求。
*/
if (!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}
static int __init ramdisk_init(void)
{
int ret = 0;
printk("ramdisk init\r\n");
/*1、给准备模拟块设备的内存申请空间*/
ramdisk.ramdiskbuffer = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if(ramdisk.ramdiskbuffer == NULL) { /*内存申请失败*/
ret = -EINVAL;
goto failed_ram;
}
/*2、初始化自旋锁*/
spin_lock_init(&ramdisk.spinlock);
/*3、注册块设备*/
ramdisk.major = register_blkdev(0, RAMDISK_NAME); /*自动分配设备号*/
if(ramdisk.major < 0) { /*块设备注册失败*/
goto failed_register;
}
/*4、分配初始化gendisk*/
ramdisk.gendisk = alloc_disk(RAMDISK_MINOR); /*初始化3个分区*/
if(!ramdisk.gendisk) { /*初始化gendisk失败*/
ret = -EINVAL;
goto failed_gendisk;
}
/*5、分配并初始化请求队列*/
ramdisk.queue = blk_init_queue(&ramdisk_request_fn, &ramdisk.spinlock);
if(!ramdisk.queue) {
ret = -EINVAL;
goto failed_queue;
}
/*6、注册添加gendisk*/
ramdisk.gendisk->major = ramdisk.major; /*主设备号*/
ramdisk.gendisk->first_minor = 0; /*起始次设备号*/
ramdisk.gendisk->fops = &ramdisk_fops; /*块设备操作函数*/
ramdisk.gendisk->private_data = &ramdisk; /* 私有数据 */
ramdisk.gendisk->queue = ramdisk.queue; /* 请求队列 */
sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME);/* 名字 */
set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512); /* 设备容量(单位为扇区)*/
add_disk(ramdisk.gendisk);
return 0;
failed_queue:
put_disk(ramdisk.gendisk);
failed_gendisk:
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
failed_register:
kfree(ramdisk.ramdiskbuffer);
failed_ram:
return ret;
}
static void __exit ramdisk_exit(void)
{
printk("ramdisk exit\r\n");
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZYC");
实验验证:
可以看出, ramdisk 已经识别出来了,大小为 2MB,但是同时也提示/dev/ramdisk
没有分区表,因为我们还没有格式化/dev/ramdisk。
格式化/dev/ramdisk
mkfs.vfat /dev/ramdisk
格式化完成后如下图所示: