块设备驱动分析

块设备驱动是Linux 三大驱动类型之一。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本文重点学习一下块设备相关驱动概念,不涉及到具体的存储设备。

1. 什么是块设备?

​ 块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备

驱动的主要区别如下:

①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。

②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。这么做的目的为了提高块设备寿命,大家如果仔细观察的话就会发现有些硬盘或者 NAND Flash就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块设备寿命而引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命。

③、字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。

④、块设备结构的不同其 I/O 算法也会不同,比如对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能,linux 里面针对不同的存储设备实现了不同的 I/O 调度算法。

2. 块设备驱动框架

2.1 block_device结构体

linux 内核使用 block_device表示块设备,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;		/* block_device核心 */
	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 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。

2.1.1 注册块设备

​ 和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:

int register_blkdev(unsigned int major, const char *name)

major:主设备号。

name:块设备名字。

​ **返回值:**如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。

2.1.2 注销块设备

​ 和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:

void unregister_blkdev(unsigned int major, const char *name)

2.1.3 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;			/* 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;
};

简单看一下 gendisk 结构体中比较重要的几个成员变量:

major 为磁盘设备的主设备号。

first_minor 为磁盘的第一个次设备号。

minors 为磁盘的此设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,此设备号不同。

part_tbl 为磁盘对应的分区表,为结构体 disk_part_tbl 类型,disk_part_tbl 的核心是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。

fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作集 file_operations 一样,是块设备驱动中的重点!

queue 为磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求。

​ 编写块的设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数,我们来看一下一些常用的 API 函数。

a. 申请gendisk

​ 使用 gendisk 之前要先申请,allo_disk 函数用于申请一个 gendisk,函数原型如下:

struct gendisk *alloc_disk(int minors)

minors:次设备号数量,也就是 gendisk 对应的分区数量。

**返回值:**成功:返回申请到的 gendisk,失败:NULL。

b. 删除gendisk

​ 如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:

void del_gendisk(struct gendisk *gp)

gp:要删除的 gendisk。

**返回值:**无。

c. 将gendisk添加到内核

​ 使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中,add_disk 函数原型如下:

void add_disk(struct gendisk *disk)
d. 设置gendisk容量

​ 每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:

void set_capacity(struct gendisk *disk, sector_t size)

disk:要设置容量的 gendisk。

size:磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(2x1024x1024)/512=4096。

e. 调整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)

2.1.4 block_device_operations 结构体

​ 和字符设备的 file _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);
	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;
};

​ 可以看出,block_device_operations 结构体里面的操作集函数和字符设备的 file_operations操作集基本类似,但是块设备的操作集函数比较少,具体分析其中比较重要的几个成员函数:

open 函数用于打开指定的块设备。

release 函数用于关闭(释放)指定的块设备。

rw_page 函数用于读写指定的页。

ioctl 函数用于块设备的 I/O 控制。

compat_ioctl 函数和 ioctl 函数一样,都是用于块设备的 I/O 控制。区别在于在 64位系统上,32 位应用程序的 ioctl 会调用 compat_iotl 函数。在 32 位系统上运行的 32 位应用程序调用的就是 ioctl 函数。

getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。

owner 表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE。

2.1.5 块设备IO请求过程

​ 仔细观察的话会在 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引出处理块设备驱动中非常重要的 request_queue、request 和 bio

a. 请求队列 request_queue

​ 内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的request(请求结构体),而 request 又包含了 bio,bio 保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。我们先来看一下 request_queue,这是一个结构体,定义在文件 include/linux/blkdev.h 中,gendisk结构体里面有一个 request_queue 结构体指针类型成员变量 queue,也就说在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue。

1). 初始化请求队列

​ 首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个request_queue 地址赋值给 gendisk 的 queue 成员变量。使用 blk_init_queue 函数来完成request_queue 的申请与初始化,函数原型如下:

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数

​ request_fn_proc 原型如下:

void (request_fn_proc) (struct request_queue *q)

请求处理函数需要驱动编写人员自行实现。

lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。请求队列会使用这个自旋锁。

​ **返回值:**如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。

2). 删除请求队列

​ 当卸载块设备驱动的时候我们还需要删除掉前面申请到的 request_queue,删除请求队列使用函数 blk_cleanup_queue,函数原型如下:

void blk_cleanup_queue(struct request_queue *q)
3). 分配请求队列并绑定制造请求函数

​ blk_init_queue 函数完成了请求队列的申请以及请求处理函数的绑定,这个一般用于像机械硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。

​ 但是对于 EMMC、SD 卡这样的非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。对于非机械设备可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。

​ 先来看一下 request_queue 申请函数 blk_alloc_queue,函数原型如下:

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

gfp_mask:内存分配掩码,具体可选择的掩码值请参考 include/linux/gfp.h 中的相关宏定义,一般为 GFP_KERNEL。

**返回值:**申请到的无 I/O 调度的 request_queue。

​ 还需要为 blk_alloc_queue 函数申请到的请求队列绑定一个“制造请求”函数(其他参考资料将其直接翻译为“制造请求”函数)。这里需要用到函数 blk_queue_make_request,函数原型如下:

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)

“制造请求”函数需要驱动编写人员实现。

**返回值:**无。

​ 一般 blk_alloc_queue 和 blk_queue_make_request 是搭配在一起使用的,用于那么非机械的存储设备、无需 I/O 调度器,比如 EMMC、SD 卡等。blk_init_queue 函数会给请求队列分配一个 I/O 调度器,用于机械存储设备,比如机械硬盘等。

b. 请求request

​ 请求队列(request_queue)里面包含的就是一系列的请求(request),request 是一个结构体,定义在 include/linux/blkdev.h 里面,这里就不展开 request 结构体了,太长了。request 里面有一个名为“bio”的成员变量,类型为 bio 结构体指针。前面说了,真正的数据就保存在 bio 里面,所以需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

1). 获取请求

​ 从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,函数原型如下:

request *blk_peek_request(struct request_queue *q)

q:指定 request_queue。

​ **返回值:**request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回NULL。

2). 开启请求

​ 使用 blk_peek_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,这里要用到 blk_start_request 函数,函数原型如下:

void blk_start_request(struct request *req)
3). 一步到位处理请求

​ 也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启,blk_fetch_request函数很简单,内容如下:

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; 
}

​ 可以看出,blk_fetch_request 就是直接调用了 blk_peek_request 和 blk_start_request 这两个函数。

c. bio

​ 每个 request 里面里面会有多个 bio,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 之间的关系如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u83A38d9-1605964509738)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201026165414090.png)]

​ bio 是个结构体,定义在 include/linux/blk_types.h 中,结构体中重要的成员内容如下(有删减):

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];
};

​ 重点来看一下第 6 行和第 30 行,第 6 行为 bvec_iter 结构体类型的成员变量,第 30 行为bio_vec 结构体指针类型的成员变量。

struct bvec_iter	bi_iter;	
struct bio_vec *bi_io_vec; 		/* bio_vec 列表 */

​ 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 结构体描述了内容如下 :

struct bio_vec {
    struct page *bv_page; /* 页 */
    unsigned int bv_len; /* 长度 */
    unsigned int bv_offset; /* 偏移 */
};

​ 可以看出 bio_vec 就是“page,offset,len”组合,page 指定了所在的物理页,offset 表示所处页的偏移地址,len 就是数据长度。

​ 我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。 bi_io_vec 指向 bio_vec 数组首地址, bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度,“页地址”是 linux 内核里面内存管理相关的概念,对于 RAM 的操作最终会转换为页相关操作。bi_iter 与 bio_vec 数组的关系,如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H6WUrBkx-1605964509743)(C:\Users\Administrator\Desktop\my_markdown\bio_iter_bio_vec.PNG)]

1). 遍历请求中的 bio

​ 请求中包含有大量的 bio,因此就涉及到遍历请求中所有 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). 遍历 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 成员变量。

3). 通知 bio 处理结束

​ 如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:

bvoid bio_endio(struct bio *bio, int error)

bio: 要结束的 bio。

error: 如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
返回值: 无

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 成员变量。  



3). 通知 bio 处理结束  

​		如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:  

```c
bvoid bio_endio(struct bio *bio, int error)

bio: 要结束的 bio。

error: 如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
返回值: 无

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值