系列文章目录
块设备概念和字符设备区别
块设备驱动框架
块设备驱动程序及验证
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
驱动分为三大部分:字符设备驱动,块设备驱动,网络设备驱动;
字符设备: 以字节为单位的即时数据传输的设备。
块设备:以块为单位的数据传输(读写)非随机即时性访问;
一、块设备概念及和字符设备区别?
块设备主要是存储类设备: 例如 SD卡、EMMC、NAND FLASH Nor Flash、 SPI Flash QSpi Flash、机械硬盘 、固态硬盘等。
块设备驱动就是对于这些存储类设备的操作。
块设备传输特点:
1、以块为单位对数据操作(读写),块是linux虚拟文件系统(VFS)传输的单位,而虚拟文件系统提供了对文件系统的读写操作;
2、块设备在文件或者说是结构上可以随机访问,但是对物理设备都是按照块进行的。块设备驱动都会有缓冲区暂存数据,一定程度后一次性将缓冲区写入块设备(例如缓冲区快满了,数据全部请求完成等等);
为什么需要缓冲区?
1)块设备一般都需要擦除再写入并有一定的擦除次数标准;如果经常性的擦除会导致块设备寿命缩短;
2)块设备结构不同I/O算法也不同,如SD卡这类可以任意跳转扇区读取,但机械硬盘就不行,因为存在磁头,读取磁道数据需要先移动磁头;为了提供性能或者寿命有着不同的I/O算法;最常见的就是电梯算法来保护块设备寿命。
二、块设备驱动框架
1.常用结构体
block_device 结构体:include\linux\fs.h
重点关注成员:
struct gendisk * bd_disk; //通用磁盘结构
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 */
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;
u8 bd_partno;
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 backing_dev_info *bd_bdi;
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;
} __randomize_layout;
1、注册块设备
和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为
register_blkdev,函数原型如下:
int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major: 主设备号。
name: 块设备名字。
返回值: 如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成
功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那
么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。
2、注销块设备
和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为
unregister_blkdev,函数原型如下:
void unregister_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major: 要注销的块设备主设备号。
name: 要注销的块设备名字。
返回值: 无
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]; /* 设备名 */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
struct disk_part_tbl __rcu *part_tbl; /* 磁盘对应分区表 */
struct hd_struct part0;
const struct block_device_operations *fops; /* 块设备操作集 类似 file_operations*/
struct request_queue *queue;
void *private_data;
int flags;
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct kobject integrity_kobj;
#endif /* CONFIG_BLK_DEV_INTEGRITY */
int node_id;
struct badblocks *bb;
};
内核提供操作gendisk的API集:
1、 申请 gendisk
使用 gendisk 之前要先申请, allo_disk 函数用于申请一个 gendisk,函数原型如下:
struct gendisk *alloc_disk(int minors)
函数参数和返回值含义如下:
minors: 次设备号数量, 也就是 gendisk 对应的分区数量。
返回值: 成功:返回申请到的 gendisk,失败: NULL。
2、删除 gendisk
如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:
void del_gendisk(struct gendisk *gd);
gp: 要删除的 gendisk。返回值: 无
3、将gendisk添加到内核
使用alloc_disk申请到gendisk以后系统还不 能使用,必须使用add_gendisk添加到内核;
void add_gendisk(struct gendisk * gd);
gd:要添加到内核的gendisk返回值:无
4、设置 gendisk 容量
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数
set_capacity,函数原型如下:
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 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以
知道, get_disk 是增加 gendisk 的引用计数, put_disk 是减少 gendisk 的引用计数,这两个函数原
型如下所示:
truct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)
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 *, bool);
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);
/* ->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;
const struct pr_ops *pr_ops;
};
open: 打开块设备文件
release: 关闭块设备文件
rw_page: 读写指定页
ioctl: 块设备的I/O控制32位系统上
compat: 块设备io控制64位系统上32位引用程序调用ioctl时底层调用这个
getgeo: 获取磁盘信息,磁头、柱面、扇区
media_changed: 修改承载媒介
owner: 所属模块
块设备读取数据流程
1、访问流程
之前就说过,块设备的读写操作都是通过VFS来进行的,block_device_operations没有直接提供读写函数
如下: vfs->通过通用块层->struct bio 来访问一个扇区
struct bio 描述了整个请求的块io的信息;
这个哥们儿写的还不错BIO画的图比较清晰
(1条消息) 关于BIO结构分析_zhanghaiyang9999的专栏-CSDN博客_bio结构
bio最后一个成员是数组,但是数组长度是0;但是
struct bio {
struct bio *bi_next; /*指向下一个bio */
struct gendisk *bi_disk; //将要操作gendisk
u8 bi_partno; //分区号
blk_status_t bi_status;
unsigned int bi_opf;
unsigned short bi_flags; /* status, etc and bvec pool number */
unsigned short bi_ioprio;
unsigned short bi_write_hint;
struct bvec_iter bi_iter;
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
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#ifdef CONFIG_BLK_DEV_THROTTLING_LOW
void *bi_cg_private;
struct blk_issue_stat bi_issue_stat;
#endif
#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 */
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];
};
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_done; /* number of bytes completed */
unsigned int bi_bvec_done; /* number of bytes completed in current bvec */
};
成员结构关系图:
2、请求队列 requeset_queue
block_device_operations 中并没有read|write函数,如何进行读取数据?
内核(也就是虚拟文件系统)会把对块设备的请求放到请求队列(requeset_queue)中,requeset_queue包含大量的requeset请求结构,而requet中又包含了bio,bio保存了读写相关数据,如从那个地址开始读取、读取的长度,结束地址等等,
requeset_queue结构超大
转载的这个图:图片来源
实验:
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/bitops.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <asm/setup.h>
#include <linux/gfp.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/hdreg.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/workqueue.h>
/****************************************
*文件名: ramdisk.c
*功能: 内存模拟硬盘,实现块设备驱动,使用请求队列
*参考: driver/block/z2ram.c
****************************************/
struct ramdisk_dev{
int major; /* 保存主设备号 */
unsigned char *ramdisk_buff; /* 申请内存(模拟硬盘)空间 */
struct request_queue *queue; /* 请求队列 */
spinlock_t lock; /* 自旋锁*/
struct gendisk *gendisk; /* gendis */
};
static struct ramdisk_dev *g_ramdisk_dev;
#define RAMDISK_PARTION_NUM 4
#define RAMDISK_SIZE (2*1024*1024)
#define RAMDISK_NAME "cjmyram"
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 void ramdisk_transfer(struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9; /* blk_rq_pos 获取到的是扇区地址,左移9 位转换为字节地址 512 = 1<<9*/
unsigned long len = blk_rq_cur_bytes(req); /* 大小 当前段中剩余的字节数*/
/* bio 中的数据缓冲区
* 读:从磁盘读取到的数据存放到 buffer 中
* 写: buffer 保存这要写入磁盘的数据
*/
void *buffer = bio_data(req->bio);
printk("ramdisk_transfer start:0x%lx \n", start);
if(rq_data_dir(req) == READ) /* 读数据 */
memcpy(buffer, g_ramdisk_dev->ramdisk_buff + start, len);
else if(rq_data_dir(req) == WRITE) /* 写数据 */
memcpy(g_ramdisk_dev->ramdisk_buff + start, buffer, len);
}
static void do_ramdisk_request(struct request_queue *q)
{
int err = 0;
struct request *req;
/* 循环处理请求队列中的每个请求 */
req = blk_fetch_request(q);
printk("do_ramdisk_request \n");
while(req != NULL) {
/* 针对请求做具体的传输处理 */
ramdisk_transfer(req);
/* 判断是否为最后一个请求,如果不是的话就获取下一个请求
* 循环处理完请求队列中的所有请求。
*/
if (!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release");
}
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}
static const struct block_device_operations ramdisk_fops =
{
.owner = THIS_MODULE,
.open = ramdisk_open,
.release = ramdisk_release,
.getgeo = ramdisk_getgeo,
};
static int mysbull_major = 0;
static int myramdisk_init(void)
{
unsigned int ret = 0;
/* 1、分配空间 并初始化*/
g_ramdisk_dev = vmalloc(sizeof(struct ramdisk_dev)); /* 整体分配空间 */
memset(g_ramdisk_dev, 0, sizeof(struct ramdisk_dev));
g_ramdisk_dev->ramdisk_buff = vmalloc(RAMDISK_SIZE); /* 给模拟硬盘的内存分配空间*/
memset(g_ramdisk_dev->ramdisk_buff, 0, RAMDISK_SIZE);
spin_lock_init(&g_ramdisk_dev->lock); /* 初始化自旋锁 */
g_ramdisk_dev->major = 0;
printk(" requeset mem ok \n ");
/* 2、注册块设备 */
g_ramdisk_dev->major = register_blkdev(mysbull_major, RAMDISK_NAME);
if (!g_ramdisk_dev->major)
{
goto register_blkdev_fail;
}
printk(" register_blkdev ok \n ");
/* 3、构建gendisk */
g_ramdisk_dev->gendisk = alloc_disk(RAMDISK_PARTION_NUM); /* 创建gendisk */
if(!g_ramdisk_dev->gendisk) {
ret = -EINVAL;
goto gendisk_alloc_fail;
}
printk(" alloc gendisk ok \n ");
/* 4、 初始化请求队列 实现请求队列处理函数 */
g_ramdisk_dev->queue = blk_init_queue(do_ramdisk_request, &g_ramdisk_dev->lock);
if (!g_ramdisk_dev->queue )
{
printk(" blk_init_queue error \n");
goto out_queue;
}
printk(" blk_init_queue gendisk ok \n");
/* 5、设置gendisk结构 */
g_ramdisk_dev->gendisk->major = g_ramdisk_dev->major;
g_ramdisk_dev->gendisk->first_minor = 0;
g_ramdisk_dev->gendisk->fops = &ramdisk_fops;
sprintf(g_ramdisk_dev->gendisk->disk_name, RAMDISK_NAME);
g_ramdisk_dev->gendisk->queue = g_ramdisk_dev->queue;
set_capacity(g_ramdisk_dev->gendisk, RAMDISK_SIZE/512); /* 设备容量(单位为扇区)*/
printk("set capacity \n");
add_disk(g_ramdisk_dev->gendisk);
printk("add disk \n");
return ret;
out_queue:
put_disk(g_ramdisk_dev->gendisk);
gendisk_alloc_fail:
unregister_blkdev(g_ramdisk_dev->major, RAMDISK_NAME);
register_blkdev_fail:
printk("major:%d \n", g_ramdisk_dev->major);
kfree(g_ramdisk_dev->ramdisk_buff);
kfree(g_ramdisk_dev);
return ret;
}
static void myramdisk_exit(void)
{
del_gendisk(g_ramdisk_dev->gendisk);
put_disk(g_ramdisk_dev->gendisk);
/* 清除请求队列 */
blk_cleanup_queue(g_ramdisk_dev->queue);
/* 注销块设备 */
unregister_blkdev(g_ramdisk_dev->major, RAMDISK_NAME);
kfree(g_ramdisk_dev->ramdisk_buff);
kfree(g_ramdisk_dev);
}
module_init(myramdisk_init);
module_exit(myramdisk_exit);
MODULE_LICENSE("GPL");
总结
块设备驱动步骤:
1、申请私有数据空间 vmalloc
2、注册块设备 register_blkdev
3、构建gendisk alloc_disk 并初始化gendisk 设置容量set_capacity
4、初始化请求队列实现请求队列处理函数 blk_init_queue();
实现请求队列处理函数
1、获取请求(request) request = blk_fetch_request(request_queue)
2、处理请求,获取扇区地址,剩余字节数, 读取/写入bio数据bio_data(req->bio);
3、获取下个请求 request = blk_fetch_request
5、将gendisk注册到块设备链表 add_disk