1 块设备驱动简介
块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动。块设备驱动比字符设备复杂,不同类型的存储设备又对应不同的驱动子系统,如MTD(memory technology device内存技术设备)是用于访问memory设备(RAM、ROM、flash)的Linux的子系统,又例如mmc子系统如下图:
1.1 块设备特点
块设备驱动相比字符设备驱动的主要区别如下:
读写单位 | 是否支持随机访问 | 是否支持缓冲区 | |
---|---|---|---|
块设备 | 块为单位 | 支持 | 支持 |
字符设备 | 字节为单位 | 不支持 | 不支持 |
块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。 这么做的目的为了提高块设备寿命,有些硬盘或者 NAND Flash 就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块 设备寿命引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命。
不同的存储其 I/O 算法也会不同,比如对于 EMMC、SD 卡、NAND Flash 这类没有 任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。
2 块设备驱动框架
2.1 数据结构和块设备API
2.1.1 block_device
通常不会直接去放问存储块设备,而是通过文件系统的方式去访问,比如:open(“filename”); write(fd, buf, len);
block_device定义在include/linux/fs.h
struct block_device {
ev_t bd_dev; /* not a kdev_t - it's a search key */
nt bd_openers;
truct inode *bd_inode; /* will die */
truct super_block *bd_super;
truct mutex bd_mutex; /* open/close mutex */
truct list_head bd_inodes;
oid * bd_claiming;
oid * 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;
struct request_queue *bd_queue;
struct list_head bd_list;
/*
* Private data. You must have bd_claim'ed the block_device
* to use this. NOTE: bd_claim allows an owner to claim
* the same device multiple times, the owner must take special
* care to not mess up bd_private for that case.
*/
unsigned long bd_private;
/* The counter of freeze processes */
int bd_fsfreeze_count;
/* Mutex for freeze */
struct mutex bd_fsfreeze_mutex;
};
内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区。bd_disk成员是一个gendisk 结构体指针类型,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。
2.1.1.1 注册/注销块设备
int register_blkdev(unsigned int major, const char *name);
向内核注册新的块设备、申请设备号,块设备注册函数为 register_blkdev。
major:主设备号(1-255)如果填0表示动态分配主设备号。
name:块设备名字
返回值:如果major不是0,那么成功范围0,失败返回错误码。如果major传入0,那么返回值表示主设备号。
void unregister_blkdev(unsigned int major, const char *name);
2.1.2 gendisk
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;
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 结构体指针数组,此数组每一项都对应一个分区信息。
fops:块设备操作集,为 block_device_operations 结构体类型。和字符设备操作 集 file_operations 一样,是块设备驱动中的重点!
queue:磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求
2.1.2.1 申请/删除 gendisk
struct gendisk *alloc_disk(int minors);
minors:次设备号数量,也就是 gendisk 对应的分区数量。
返回值:成功:返回申请到的 gendisk,失败:NULL。
void del_gendisk(struct gendisk *gp);
2.1.2.2 将 gendisk 添加到内核
将申请到的 gendisk 添加到内核中。
void add_disk(struct gendisk *disk);
2.1.2.3 设置 gendisk 容量
void set_capacity(struct gendisk *disk, sector_t size);
disk:要设置容量的 gendisk。
size:磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区 一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以 512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(2*1024*1024)/512=4096
。
2.1.2.4 调整 gendisk 引用计数
truct kobject *get_disk(struct gendisk *disk);//增加 gendisk 的引用计数
void put_disk(struct gendisk *disk);//减少 gendisk 的引用计数
2.1.3 block_device_operations(块设备操作集)
//和字符设备的 file _operations 一样,块设备也有操作集。include/linux/blkdev.h
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);//open 函数用于打开指定的块设备
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);//rw_page 函数用于读写指定的页
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 *);//getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。
/* 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;
};
2.1.4 块设备 I/O 请求过程
仔细点可以看到块设备fops中并没有read,write函数。那么块设备是怎么从物理块设备中读写数据?
2.1.4.1 请求队列 request_queue
在内核中,对块设备的读写都会发送到请求队列 request_queue,request_queue是request的集合,request里面包含bio结构,bio 保存了读写相关数据,比如从块设备的哪个 地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。这些数据结构都定义在:include/linux/blkdev.h
2.1.4.1.1 初始化请求队列
- 申请并初始化一个 request_queue,申请成功后需把request_queue地址赋值给 gendisk 的 queue 成员变量。
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
rfn:请求处理函数,原型为:void (request_fn_proc) (struct request_queue *q);
驱动人员需要自己定义实现请求处理函数。
lock:自旋锁,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用 这个自旋锁。
返回值: 成功返回请求队列指针,失败返回NULL。
blk_init_queue
的请求队列有去绑定请求处理函数,这个一般用于像机械硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。但是对于 EMMC、SD 卡这样的非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。因此可以直接申请request_queue:
struct request_queue *blk_alloc_queue(gfp_t gfp_mask);
gfp_mask:内存分配掩码,具体可选择的掩码值请参考 include/linux/gfp.h 中的相关宏定义, 一般为 GFP_KERNEL。
返回值:申请到的无 I/O 调度的 request_queue。
2.1.4.1.2 删除请求队列
当卸载块设备驱动的时候还需要删除请求队列。
void blk_cleanup_queue(struct request_queue *q);
2.1.4.1.3 请求队列绑定制造请求函数
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn);
q:需要绑定的请求队列,也就是 blk_alloc_queue 申请到的请求队列。
mfn:“制造”请求函数,原型如下: void (make_request_fn) (struct request_queue *q, struct bio *bio);
“制造请求”函数需要驱动编写人员实现。
2.1.4.2 请求 request
以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio, 最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。
2.1.4.2.1 获取请求
request *blk_peek_request(struct request_queue *q);
从request_queue中依次获取每个request。
2.1.4.2.2 开启请求
void blk_start_request(struct request *req);
2.1.4.2.3 一次性获取和开启请求
struct request *blk_fetch_request(struct request_queue *q) {
struct request *rq;
rq = blk_peek_request(q);
if (rq)
blk_start_request(rq);
return rq;
}
2.1.4.3 请求有关的函数
函数 | 描述 |
---|---|
blk_end_request() | 请求中指定字节数据被处理完成 |
blk_end_request_all() | 请求中所有数据全部处理完成 |
blk_end_request_cur() | 当前请求中的 chunk |
blk_end_request_err() | 处理完请求,直到下一个错误产生 |
__blk_end_request() | 和 blk_end_request 函数一样,但是需要持有队列锁 |
__blk_end_request_all() | 和 blk_end_request_all 函数一样,但是需要持有队列锁 |
__blk_end_request_cur() | 和 blk_end_request_cur 函数一样,但是需要持有队列锁 |
__blk_end_request_err() | 和 blk_end_request_err 函数一样,但是需要持有队列锁 |
2.1.5 bio
每个 request 里面会有多个 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 操作,读或写 */
unsigned int bi_phys_segments;
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;
#endif
};
unsigned short bi_vcnt; /* bio_vec 列表中元素数量 */
unsigned short bi_max_vecs; /* bio_vec 列表长度 */
atomic_t bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* bio_vec 列表 */
struct bio_set *bi_pool;
struct bio_vec bi_inline_vecs[0];
};
bvec_iter和bi_io_vec结构体
//设备扇区等信息
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 中已经处理完成的字节数 */
};
//页,长度,偏移信息
struct bio_vec {
struct page *bv_page; /* 页 */
unsigned int bv_len; /* 长度 */
unsigned int bv_offset; /* 偏移 */
};
bio和bvec_iter,bio_vec结构关系:
2.1.5.1 bio处理
2.1.5.1.1 遍历请求中的bio
遍历 请求中的 bio 使用函数__rq_for_each_bio
:
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
_bio 就是遍历出来的每个 bio,rq 是要进行遍历操作的请求,_bio 参数为 bio 结构体指针类 型,rq 参数为 request 结构体指针类型。
2.1.5.1.2 遍历 bio 中的所有段
bio 包含了最终要操作的数据,因此还需要遍历 bio 中的所有段,这里要用到 bio_for_each_segment
函数:
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
第一个 bvl 参数就是遍历出来的每个 bio_vec,第二个 bio 参数就是要遍历的 bio,类型为 bio 结构体指针,第三个 iter 参数保存要遍历的 bio 中 bi_iter 成员变量。
2.1.5.1.3 通知 bio 处理结束
void bio_endio(struct bio *bio, int error);
bio:要结束的 bio
error:如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
3 块设备驱动实验
3.1 使用请求队列实验
使用开发板上的 RAM 模拟一段块设备,也就是 ramdisk,然后编写块设备驱动。
参考linux 内核 drivers/block/z2ram.c
3.1.1 块设备驱动模块源码编写
#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 <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.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内存空间,用于模拟块设备 */
spinlock_t lock; /* 自旋锁 */
struct gendisk *gendisk; /* gendisk */
struct request_queue *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,
};
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.ramdiskbuf + start, len);
else if(rq_data_dir(req) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuf + start, buffer, len);
}
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;
/* 1、申请用于ramdisk内存 */
ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if(ramdisk.ramdiskbuf == NULL) {
ret = -EINVAL;
goto ram_fail;
}
/* 2、初始化自旋锁 */
spin_lock_init(&ramdisk.lock);
/* 3、注册块设备 */
ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
if(ramdisk.major < 0) {
goto register_blkdev_fail;
}
printk("ramdisk major = %d\r\n", ramdisk.major);
/* 4、分配并初始化gendisk */
ramdisk.gendisk = alloc_disk(RADMISK_MINOR);
if(!ramdisk.gendisk) {
ret = -EINVAL;
goto gendisk_alloc_fail;
}
/* 5、分配并初始化请求队列 */
ramdisk.queue = blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
if(!ramdisk.queue) {
ret = EINVAL;
goto blk_init_fail;
}
/* 6、添加(注册)disk */
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;
blk_init_fail:
put_disk(ramdisk.gendisk);
//del_gendisk(ramdisk.gendisk);
gendisk_alloc_fail:
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
kfree(ramdisk.ramdiskbuf); /* 释放内存 */
ram_fail:
return ret;
}
static void __exit ramdisk_exit(void)
{
/* 释放gendisk */
del_gendisk(ramdisk.gendisk);
put_disk(ramdisk.gendisk);
/* 清除请求队列 */
blk_cleanup_queue(ramdisk.queue);
/* 注销块设备 */
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
/* 释放内存 */
kfree(ramdisk.ramdiskbuf);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
3.1.2 分析
3.1.2.1 块设备驱动注册过程
-
调用
register_blkdev
注册块设备,得到主设备号 -
alloc_disk
和adddisk
申请和将gendisk添加到内核- 初始化gendisk, 3个次设备号,gendisk 对应的分区数量为3
- 利用request_queue,初始化请求队列
- 设置gendisk属性,包括设备号,fops, 磁盘对应的请求队列。
- 设置gendisk容量为2M。注意大小是扇区数,不是字节数,一个扇区是 512 字节
- 将gendisk注册到内核。
3.1.2.2 块设备驱动卸载过程
3.1.2.3 块设备驱动读写过程
读写通过请求处理函数完成:
ramdisk_request_fn
主要工作就是依次处理请求队列中的所有请求。blk_fetch_request
获取请求队列中第一个请求,如果请求不为空的话就调用ramdisk_transfer
函数进行对请求做进一 步的处理,然后就是 while 循环依次处理完请求队列中的每个请求。__blk_end_request_cur
检查是否为最后一个请求,如果不是的话就继续获取下一个,直至整个请求队列处理完成。
再来看ramdisk_transfer
:
针对一次请求做具体的数据传输。
- 首先要获取要访问的磁盘地址和大小。
bio_data
获取bio的数据缓冲区。- 从磁盘对应地址读出数据到缓冲区或者写入数据到磁盘对应地址。
3.1.3 运行测试
编译改驱动成ramdisk.ko。
depmod //第一次加载驱动的时候需要运行此命令
modprobe ramdisk.ko //加载驱动模块
动加载成功以后就会在/dev/目录下生成一个名为“ramdisk”的设备:
fdisk -l//查看磁盘信息
可以看出,ramdisk 已经识别出来了,大小为 2MB,但是同时也提示/dev/ramdisk 没有分区表,因为我们还没有格式化/dev/ramdisk。
使用 mkfs.vfat 命令格式化/dev/ramdisk
,将其格式化成 vfat 格式:
mkfs.vfat /dev/ramdisk
格式化完后挂载到/tmp目录下:mount /dev/ramdisk /tmp
。挂载成功以后就可以通过/tmp 来访问 ramdisk 这个磁盘。
3.2 不使用请求队列实验
blk_init_queue
的请求队列有去绑定请求处理函数,这个一般用于像机械硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。但是对于 EMMC、SD 卡这样的非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。因此可以直接申请request_queue。
-
这里使用
blk_alloc_queue
分配一个request_queue。通过blk_queue_make_request
让请求队列绑定一个”制造请求“函数。 -
制造请求函数:
ramdisk_make_request_fn
里面是全部是对 bio 的操作,所有的处理内容都在 bio 参数里面。再次打开bio的结构图:
①读取 bio 的 bi_iter 成员变量的 bi_sector 来获取要操作的设备地址(扇区)
②处理每个段
③调用 bio_endio 函数,结束 bio。