一、块设备驱动框架
1、block_device结构体
linux 内核使用 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;
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。
2、块设备注册函数
块设备注册函数为 register_blkdev,函数原型如下:
int register_blkdev(unsigned int major, const char *name)
major:主设备号。
name:块设备名字。
返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,负值注册失败;如果 major 为 0 的话表示由系统自动分配主设备号,注册成功返回分配的主设备号(1~255),负值注册失败。
3、块设备注销函数
块设备注销函数为 unregister_blkdev,函数原型如下:
void unregister_blkdev(unsigned int major, const char *name)
major:注销的块设备主设备号。
name:块设备名字。
4、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; /* 磁盘设备主设备号 */
int first_minor; /* 磁盘第一个次设备号 */
int minors; /* 磁盘的分区数量 */
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; /* 磁盘分区表,disk_part_tbl核心是一个hd_struct结构体指针数组,数组每一项对应一个分区信息 */
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;
};
5、gendisk操作函数
1)申请gendisk
struct gendisk *alloc_disk(int minors)
minors:次设备号数量,也就是分区数量。
返回值:成功:返回申请到的 gendisk,失败:NULL。
2)删除gendisk
void del_gendisk(struct gendisk *gp)
gp:要删除的 gendisk。
3)将gendisk添加到内核
申请 gendisk 后要添加到内核才能正常使用。
void add_disk(struct gendisk *disk)
disk:要添加到内核的 gendisk。
4)设置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。
5)调整gendisk引用计数(释放gendisk时要减少引用计数)
truct kobject *get_disk(struct gendisk *disk) //增加引用计数
void put_disk(struct gendisk *disk) //减少引用计数
6、block_device_operations结构体
block_device_operations 结构体是块设备的操作函数集合,结构体定义在 include/linux/blkdev.h 中:
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); //块设备IO控制
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); //64位系统中32位应用程序调用的IO控制函数
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; //一般是THIS_MODULE
};
7、块设备IO请求过程(读写操作)
1)请求队列request_queue
内核将块设备的读写都发送到请求队列中,request_queue 由大量的 request 组成,而 request 又包含很多 bio,bio 中保存了读写相关数据。request_queue 结构体定义在 include/linux/blkdev.h 中,每一个 gendisk 都要分配一个 request_queue。
① 机械类块设备的 request_queue 申请
使用 blk_init_queue 函数来完成申请和初始化,函数原型如下:
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数原型如下:
void (request_fn_proc) (struct request_queue *q)
lock:自旋锁指针,请求队列时使用。
返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。
② 非机械类块设备的 request_queue 申请
- 首先使用 blk_alloc_queue 函数申请 request_queue,函数原型如下:
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
gfp_mask:内存分配掩码,具体可选择的掩码值请参考 include/linux/gfp.h 中的相关宏定义,一般为 GFP_KERNEL。
返回值:申请到的无 I/O 调度的 request_queue。
- 然后使用 blk_queue_make_request 函数绑定一个制造请求函数,函数原型如下:
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
q:申请到的 request_queue。
mfn:需要绑定的制造请求函数,函数原型如下:
void (make_request_fn) (struct request_queue *q, struct bio *bio)
③ 删除请求队列
卸载驱动时需要使用 blk_cleanup_queue 删除 request_queue,函数原型如下:
void blk_cleanup_queue(struct request_queue *q)
q:需要删除的 request_queue。
2)请求requset
request 结构体定义在 include/linux/blkdev.h,需要从 request_queue 中依次取出 request,然后从每个 request 中取出 bio,最后根据 bio 的描述将数据读写到块设备。
① 获取 request
使用 blk_peek_request 函数从请求队列中获取 request,函数原型如下:
request *blk_peek_request(struct request_queue *q)
q:指定的 request_queue。
返回值:request_queue 中下一个要处理的 request,如果没有要处理的请求就返回 NULL。
② 开始处理 request
获取到下一个要处理的 request 后就要开始处理这个 request,使用 blk_start_request 函数,函数原型如下:
void blk_start_request(struct request *req)
req:需要处理的 request。
③ 一次性获取并处理 request
使用 blk_fetch_request 函数可以一次性获取并处理 request,函数原型如下:
struct request *blk_fetch_request(struct request_queue *q)
q:指定的 request_queue。
返回值:request_queue 中下一个要处理的 request,如果没有要处理的请求就返回 NULL。
④ 其他和 request 有关的 API
函数 | 说明 |
---|---|
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函数一样,但是需要持有队列锁。 |
3)bio结构
bio 结构描述了要读写的起始扇区、扇区数量、读取还是写入、页偏移、数据长度等信息。每个 request 中包含多个 bio,上层程序对块设备的读写都会被构成一个或多个 bio 结构。新产生的 bio 可能被合并到 request_queue 里现有的 request,也可能产生新的 request,然后插入到 request_queue 中合适的位置,这一切都由 I/O 调度器完成。bio 结构体定义在 include/linux/blk_types.h 中:
struct bio {
struct bio *bi_next; /* 请求队列的下一个bio */
struct block_device *bi_bdev; /* 指向块设备 */
unsigned long bi_flags; /* bio状态等信息 */
unsigned long bi_rw; /* 最低位读或写*/
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; /* bio_vec中元素数量 */
/*
* 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; /* bio_vec列表 */
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];
};
bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_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 结构体描述了 RAM 信息。bio_vec 结构体定义如下:
struct bio_vec {
struct page *bv_page; /* 物理页 */
unsigned int bv_len; /* 数据长度 */
unsigned int bv_offset; /* 所处页的地址偏移 */
};
数据在物理存储设备和 RAM 中传输。
① 遍历 request 中的 bio
使用宏 __rq_for_each_bio 来遍历 request 中所有的 bio,宏定义如下:
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
_bio:遍历出来的每个 bio,为 bio 结构体指针类型。
rq:需要遍历的 request,为 request 结构体指针类型。
② 遍历 bio 中所有段
使用宏 bio_for_each_segment 遍历 bio 中所有段,宏定义如下:
#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 成员变量。
③ 通知 bio 处理结束(非机械类磁盘)
如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通知内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:
bvoid bio_endio(struct bio *bio, int error)
bio:要结束的 bio。
error:如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如 -EIO。
二、块设备驱动实验(非机械类存储器)
1、添加设备结构体
typedef struct {
int major; //主设备号
unsigned char *ramdiskbuf; //模拟块设备
struct gendisk *gendisk; //磁盘
spinlock_t lock; //自旋锁
struct request_queue *queue; //请求队列
}ramdisk_dev;
ramdisk_dev ramdisk;
3、编写加载和卸载注册函数
加载和卸载注册函数如下:
/* 模块加载 */
module_init(Ramdisk_Init);
module_exit(Ramdisk_Exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("LZK");
入口函数:
/* 入口函数 */
static int __init Ramdisk_Init(void)
{
int ret;
/* 初始化模拟块设备 */
ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if(ramdisk.ramdiskbuf == NULL){
ret = -EINVAL;
goto ram_fail;
}
/* 初始化自旋锁 */
spin_lock_init(&ramdisk.lock);
/* 块设备注册 */
ramdisk.major = register_blkdev(0, RAMDISK_NAME); //自动分配
if(ramdisk.major < 0){
ret = -EINVAL;
goto register_blkdev_fail;
}
printk("ramdisk major = %d\r\n", ramdisk.major);
/* 申请gendisk */
ramdisk.gendisk = alloc_disk(RAMDISK_MINORS);
if(ramdisk.gendisk == NULL){
ret = -EINVAL;
goto alloc_disk_fail;
}
/* 申请request_queue */
ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
if(ramdisk.queue == NULL){
ret = -EINVAL;
goto alloc_queue_fail;
}
/* 绑定制造请求函数 */
blk_queue_make_request(ramdisk.queue, mfn);
/* 添加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); //添加gendisk到内核
return 0;
alloc_queue_fail:
del_gendisk(ramdisk.gendisk);
kfree(ramdisk.ramdiskbuf);
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
alloc_disk_fail:
kfree(ramdisk.ramdiskbuf);
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
kfree(ramdisk.ramdiskbuf);
ram_fail:
return ret;
}
出口函数:
/* 出口函数 */
static void __exit Ramdisk_Exit(void)
{
/* 释放gendisk */
put_disk(ramdisk.gendisk);
del_gendisk(ramdisk.gendisk);
/* 清除请求队列 */
blk_cleanup_queue(ramdisk.queue);
/* 注销块设备 */
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
/* 释放内存 */
kfree(ramdisk.ramdiskbuf);
}
3、编写请求制造函数
/* 制造请求函数 */
void mfn(struct request_queue *q, struct bio *bio)
{
int offset;
struct bio_vec bvec;
struct bvec_iter iter;
unsigned long len = 0;
/* 获取块设备偏移地址 */
offset = (bio->bi_iter.bi_sector) << 9;
/* 处理bio中所有段 */
bio_for_each_segment(bvec, bio, iter){
char *ptr = page_address(bvec.bv_page)+bvec.bv_offset; //获取RAM地址
len = bvec.bv_len; //数据长度
if(bio_data_dir(bio) == READ) //读数据
memcpy(ptr, ramdisk.ramdiskbuf + offset, len);
else if(bio_data_dir(bio) == WRITE) //写数据
memcpy(ramdisk.ramdiskbuf + offset, ptr, len);
offset += len;
}
set_bit(BIO_UPTODATE, &bio->bi_flags);
bio_endio(bio, 0); //通知bio处理结束
}
4、编写设备的具体操作函数
/* open函数 */
static int ramdisk_open(struct block_device *dev, fmode_t mode)
{
return 0;
}
/* release函数 */
static void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
}
/* getgeo函数 */
static 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 const struct block_device_operations ramdisk_fops = {
.owner = THIS_MODULE,
.open = ramdisk_open,
.release = ramdisk_release,
.getgeo = ramdisk_getgeo,
};
5、添加头文件
参考 linux 内核的驱动代码时,找到可能用到的头文件,添加进工程。在调用系统调用函数或库函数时,在终端使用 man 命令可查看调用的函数需要包含哪些头文件。
man 命令数字含义:1:标准命令 2:系统调用 3:库函数
添加以下头文件:
#include <linux/module.h>
#include <linux/ratelimit.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/i2c.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/debugfs.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
#include <linux/blk_types.h>
#define RAMDISK_SIZE (2*1024*1024) //磁盘大小容量为2MB
#define RAMDISK_NAME "ramdisk" //磁盘名字
#define RAMDISK_MINORS 3 //分区数量
11、测试驱动
-
加载模块后,使用命令 fdisk -l 查看块设备是否存在。
-
使用 mkfs.vat /dev/ramdisk 格式化磁盘。
-
使用 mount /dev/ramdisk /tmp 挂载磁盘。
-
创建文本文件测试磁盘是否正常。