24_块设备驱动

一、块设备驱动框架

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 挂载磁盘。

  • 创建文本文件测试磁盘是否正常。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值