第六十八章 Linux 块设备驱动实验

前面我们都是在学习字符设备驱动,本章我们来学习一下块设备驱动框架,块设备驱动是Linux三大驱动类型之一。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本章我们重点学习一下块设备相关驱动概念,不涉及到具体的存储设备。最后,我们使用ALPHA开发板板载RAM模拟一个块设备,学习块设备驱动框架的使用。

68.1 什么是块设备?
块设备是针对存储设备的,比如SD卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是linux虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。这么做的目的为了提高块设备寿命,大家如果仔细观察的话就会发现有些硬盘或者NAND Flash就会标明擦除次数(flash的特性,写之前要先擦除),比如擦除100000次等。因此,为了提高块设备寿命而引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命。
字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。
块设备结构的不同其I/O算法也会不同,比如对于EMMC、SD卡、NAND Flash这类没有任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能,linux里面针对不同的存储设备实现了不同的I/O调度算法。
68.2 块设备驱动框架
68.2.1 block_device结构体
linux内核使用block_device表示块设备,block_device为一个结构体,定义在include/linux/fs.h文件中,结构体内容如下:

示例代码68.2.1.1 block_device结构体
1  struct block_device {
2   	dev_t           bd_dev;  /* not a kdev_t - it's a search key */
3   	int         	bd_openers;
4   	struct inode 			*bd_inode; /* will die */
5   	struct super_block 	*bd_super;
6   	struct mutex     	bd_mutex;   	/* open/close mutex */
7   	struct list_head  bd_inodes;
8   	void *          	bd_claiming;
9   	void *          	bd_holder;
10  	int         		bd_holders;
11  	bool            	bd_write_holder;
12 #ifdef CONFIG_SYSFS
13  	struct list_head 	bd_holder_disks;
14 #endif
15  	struct block_device 	*bd_contains;
16  	unsigned        		bd_block_size;
17  	struct hd_struct 		*bd_part;
18  /*number of times partitions within this device have been opened.*/
19  	unsigned        		bd_part_count;
20  	int         			bd_invalidated;
21  	struct gendisk 		*bd_disk;
22 	 	struct request_queue *bd_queue;
23  	struct list_head    	bd_list;
24  	/*
25   	 * Private data.  You must have bd_claim'ed the block_device
26   	 * to use this.  NOTE:  bd_claim allows an owner to claim
27   	 * the same device multiple times, the owner must take special
28   	 * care to not mess up bd_private for that case.
29   	 */
30  	unsigned long       	bd_private;
31 
32  	/* The counter of freeze processes */
33  	int         			bd_fsfreeze_count;
34  	/* Mutex for freeze */
35  	struct mutex        	bd_fsfreeze_mutex;
36 };
对于block_device结构体,我们重点关注一下第21行的bd_disk成员变量,此成员变量为gendisk结构体指针类型。内核使用block_device来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话bd_disk就指向通用磁盘结构gendisk。
1、注册块设备
和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:

int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:主设备号。
name:块设备名字。
返回值:如果参数major在1255之间的话表示自定义主设备号,那么返回0表示注册成功,如果返回负值的话表示注册失败。如果major为0的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1255),如果返回负值那就表示注册失败。
2、注销块设备
和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:
void unregister_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:要注销的块设备主设备号。
name:要注销的块设备名字。
返回值:无。
68.2.2 gendisk结构体
linux内核使用gendisk来描述一个磁盘设备,这是一个结构体,定义在include/linux/genhd.h中,内容如下:

示例代码68.2.2.1 gendisk结构体
1  struct gendisk {
2   	/* major, first_minor and minors are input parameters only,
3    	* don't use directly.  Use disk_devt() and disk_max_parts().
4    	*/
5   	int major;       	/* major number of driver */
6   	int first_minor;
7   	int minors;      	/* maximum number of minors, =1 for
8                          	 * disks that can't be partitioned. */
9  
10  	char disk_name[DISK_NAME_LEN];  /* name of major driver */
11  	char *(*devnode)(struct gendisk *gd, umode_t *mode);
12 
13  	unsigned int events;        	/* supported events */
14  	unsigned int async_events;  /* async events, subset of all */
15 
16  	/* Array of pointers to partitions indexed by partno.
17   	* Protected with matching bdev lock but stat and other
18   	* non-critical accesses use RCU.  Always access through
19   	* helpers.
20   	*/
21  	struct disk_part_tbl __rcu *part_tbl;
22  	struct hd_struct part0;
23 
24  	const struct block_device_operations *fops;
25  	struct request_queue *queue;
26  	void *private_data;
27 
28  	int flags;
29  	struct device *driverfs_dev;  // FIXME: remove
30  	struct kobject *slave_dir;
31 
32  	struct timer_rand_state *random;
33  	atomic_t sync_io;       /* RAID */
34  	struct disk_events *ev;
35 #ifdef  CONFIG_BLK_DEV_INTEGRITY
36  	struct blk_integrity *integrity;
37 #endif
38  	int node_id;
39 };
我们简单看一下gendisk结构体中比较重要的几个成员变量:

第5行,major为磁盘设备的主设备号。
第6行,first_minor为磁盘的第一个次设备号。
第7行,minors为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,次设备号不同。
第21行,part_tbl为磁盘对应的分区表,为结构体disk_part_tbl类型,disk_part_tbl的核心是一个hd_struct结构体指针数组,此数组每一项都对应一个分区信息。
第24行,fops为块设备操作集,为block_device_operations结构体类型。和字符设备操作集file_operations一样,是块设备驱动中的重点!
第25行,queue为磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求。
编写块的设备驱动的时候需要分配并初始化一个gendisk,linux内核提供了一组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 *gp)
函数参数和返回值含义如下:
gp:要删除的gendisk。
返回值:无。
3、将gendisk添加到内核
使用alloc_disk申请到gendisk以后系统还不能使用,必须使用add_disk函数将申请到的gendisk添加到内核中,add_disk函数原型如下:
void add_disk(struct gendisk disk)
函数参数和返回值含义如下:
disk:要添加到内核的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的磁盘,其扇区数量就是(21024
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)
68.2.3 block_device_operations结构体
和字符设备的file _operations一样,块设备也有操作集,为结构体block_device_operations,此结构体定义在include/linux/blkdev.h中,结构体内容如下:

示例代码68.2.3.1 block_device_operations结构体
1  struct block_device_operations {
2   	int (*open) (struct block_device *, fmode_t);
3   	void (*release) (struct gendisk *, fmode_t);
4   	int (*rw_page)(struct block_device *, sector_t, struct page *, 
int rw);
5   	int (*ioctl) (struct block_device *, fmode_t, unsigned, 
unsigned long);
6   	int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, 
unsigned long);
7   	long (*direct_access)(struct block_device *, sector_t,
8                   void **, unsigned long *pfn, long size);
9   	unsigned int (*check_events) (struct gendisk *disk,
10                    unsigned int clearing);
11  /* ->media_changed() is DEPRECATED, use ->check_events() instead */
12  	int (*media_changed) (struct gendisk *);
13  	void (*unlock_native_capacity) (struct gendisk *);
14  	int (*revalidate_disk) (struct gendisk *);
15  	int (*getgeo)(struct block_device *, struct hd_geometry *);
16  /* this callback is with swap_lock and sometimes page table lock held */
17  	void (*swap_slot_free_notify) (struct block_device *, 
unsigned long);
18  	struct module *owner;
19 };
可以看出,block_device_operations结构体里面的操作集函数和字符设备的file_operations操作集基本类似,但是块设备的操作集函数比较少,我们来看一下其中比较重要的几个成员函数:
第2行,open函数用于打开指定的块设备。
第3行,release函数用于关闭(释放)指定的块设备。
第4行,rw_page函数用于读写指定的页。
第5行,ioctl函数用于块设备的I/O控制。
第6行,compat_ioctl函数和ioctl函数一样,都是用于块设备的I/O控制。区别在于在64位系统上,32位应用程序的ioctl会调用compat_iotl函数。在32位系统上运行的32位应用程序调用的就是ioctl函数。
第15行,getgeo函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。
第18行,owner表示此结构体属于哪个模块,一般直接设置为THIS_MODULE。

68.2.4 块设备I/O请求过程
大家如果仔细观察的话会在block_device_operations结构体中并没有找到read和write这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引出了块设备驱动中非常重要的request_queue、request和bio。
1、请求队列request_queue
内核将对块设备的读写都发送到请求队列request_queue中,request_queue中是大量的request(请求结构体),而request又包含了bio,bio保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。我们先来看一下request_queue,这是一个结构体,定义在文件include/linux/blkdev.h中,由于request_queue结构体比较长,这里就不列出来了。大家回过头看一下示例代码68.2.2.1的gendisk结构体就会发现里面有一个request_queue结构体指针类型成员变量queue,也就说在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个request_queue。
①、初始化请求队列
我们首先需要申请并初始化一个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地址。
②、删除请求队列
当卸载块设备驱动的时候我们还需要删除掉前面申请到的request_queue,删除请求队列使用函数blk_cleanup_queue,函数原型如下:
void blk_cleanup_queue(struct request_queue *q)
函数参数和返回值含义如下:
q:需要删除的请求队列。
返回值:无。
③、分配请求队列并绑定制造请求函数
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)
函数参数和返回值含义如下:
q:需要绑定的请求队列,也就是blk_alloc_queue申请到的请求队列。
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调度器,用于机械存储设备,比如机械硬盘等。
2、请求request
请求队列(request_queue)里面包含的就是一系列的请求(request),request是一个结构体,定义在include/linux/blkdev.h里面,这里就不展开request结构体了,太长了。request里面有一个名为“bio”的成员变量,类型为bio结构体指针。前面说了,真正的数据就保存在bio里面,所以我们需要从request_queue中取出一个一个的request,然后再从每个request里面取出bio,最后根据bio的描述讲数据写入到块设备,或者从块设备中读取数据。
①、获取请求
我们需要从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,函数原型如下:
request *blk_peek_request(struct request_queue *q)
函数参数和返回值含义如下:
q:指定request_queue。
返回值:request_queue中下一个要处理的请求(request),如果没有要处理的请求就返回NULL。
②、开启请求
使用blk_peek_request函数获取到下一个要处理的请求以后就要开始处理这个请求,这里要用到blk_start_request函数,函数原型如下:
void blk_start_request(struct request *req)
函数参数和返回值含义如下:
req:要开始处理的请求。
返回值:无。
③、一步到位处理请求
我们也可以使用blk_fetch_request函数来一次性完成请求的获取和开启,blk_fetch_request函数很简单,内容如下:

示例代码68.2.4.1 blk_fetch_request函数源码
1 struct request *blk_fetch_request(struct request_queue *q)
2 {
3   	struct request *rq;
4 
5   	rq = blk_peek_request(q);
6   	if (rq)
7       		blk_start_request(rq);
8   	return rq;
9 }

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

④、其他和请求有关的函数

关于请求的API还有很多,常见的见表68.2.4.1:

EMMC和硬盘_物联网


表68.2.4.1 请求相关API函数

3、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之间的关系如图68.2.4.1所示:

EMMC和硬盘_EMMC和硬盘_02

图68.2.4.1 request_queue、request和bio之间的关系
bio是个结构体,定义在include/linux/blk_types.h中,结构体内容如下:

示例代码68.2.4.2 bio结构体
1  struct bio {
2   	struct bio     		*bi_next;   	/* 请求队列的下一个bio 	*/
3   	struct block_device 	*bi_bdev;		/* 指向块设备			*/
4   	unsigned long       	bi_flags;  	/* bio状态等信息		*/
5   	unsigned long       	bi_rw;  		/* I/O操作,读或写		*/
6   	struct bvec_iter    	bi_iter;		/* I/O操作,读或写		*/
7   	unsigned int       	 bi_phys_segments;
8   	unsigned int        	bi_seg_front_size;
9   	unsigned int        	bi_seg_back_size;
10  	atomic_t        		bi_remaining;
11  	bio_end_io_t        	*bi_end_io;
12  	void            		*bi_private;
13 #ifdef CONFIG_BLK_CGROUP
14  /*
15   * Optional ioc and css associated with this bio.  Put on bio
16   * release.  Read comment on top of bio_associate_current().
17   */
18  	struct io_context   	*bi_ioc;
19  	struct cgroup_subsys_state *bi_css;
20 #endif
21  	union {
22 #if defined(CONFIG_BLK_DEV_INTEGRITY)
23      	struct bio_integrity_payload *bi_integrity; 
24 #endif
25  	};
26 
27  	unsigned short      bi_vcnt;    		/* bio_vec列表中元素数量	*/
28  	unsigned short      bi_max_vecs;    	/* bio_vec列表长度			*/
29  	atomic_t        		bi_cnt;     	/* pin count */
30  	struct bio_vec      	*bi_io_vec; 	/* bio_vec列表 */
31  	struct bio_set      	*bi_pool;
32  	struct bio_vec      	bi_inline_vecs[0];
33 };
重点来看一下第6行和第30行,第6行为bvec_iter结构体类型的成员变量,第30行为bio_vec结构体指针类型的成员变量。
bvec_iter结构体描述了要操作的设备扇区等信息,结构体内容如下:
示例代码68.2.4.3 bvec_iter结构体
1 struct bvec_iter {
2   	sector_t      	bi_sector;  	/* I/O请求的设备起始扇区(512字节) */
3   	unsigned int  	bi_size;    	/* 剩余的I/O数量 					*/
4   	unsigned int 	bi_idx;     	/* blv_vec中当前索引 				*/
5   	unsigned int 	bi_bvec_done; 	/* 当前bvec中已经处理完成的字节数	*/
6 };
bio_vec结构体描述了内容如下:
示例代码68.2.4.4 bio_vec结构体
1 struct bio_vec {
2   	struct page 	*bv_page;		/* 页 	*/
3  	 	unsigned int   bv_len;		/* 长度	*/
4   	unsigned int  	bv_offset; 	/* 偏移	*/
5 };
可以看出bio_vec就是“page,offset,len”组合,page指定了所在的物理页,offset表示所处页的偏移地址,len就是数据长度。
我们对于物理存储设备的操作不外乎就是将RAM中的数据写入到物理存储设备中,或者将物理设备中的数据读取到RAM中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到RAM中的哪个地址处、读取的数据长度是多少。既然bio是块设备最小的数据传输单元,那么bio就有必要描述清楚这些信息,其中bi_iter这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_io_vec指向bio_vec数组首地址,bio_vec数组就是RAM信息,比如页地址、页偏移以及长度,“页地址”是linux内核里面内存管理相关的概念,这里我们不深究linux内存管理,我们只需要知道对于RAM的操作最终会转换为页相关操作。
bio、bvec_iter以及bio_vec这三个机构体之间的关系如图68.4.2.2所示:

EMMC和硬盘_块设备_03

图68.4.2.2 bio、bio_iter与bio_vec之间的关系
①、遍历请求中的bio
前面说了,请求中包含有大量的bio,因此就涉及到遍历请求中所有bio并进行处理。遍历请求中的bio使用函数__rq_for_each_bio,这是一个宏,内容如下:

示例代码68.2.4.5 __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结构体指针类型。
②、遍历bio中的所有段

bio包含了最终要操作的数据,因此还需要遍历bio中的所有段,这里要用到bio_for_each_segment函数,此函数也是一个宏,内容如下:

示例代码68.2.4.6 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成员变量。
③、通知bio处理结束
如果使用“制造请求”,也就是抛开I/O调度器直接处理bio的话,在bio处理完成以后要通过内核bio处理完成,使用bio_endio函数,函数原型如下:

bvoid bio_endio(struct bio *bio, int error)
函数参数和返回值含义如下:
bio:要结束的bio。
error:如果bio处理成功的话就直接填0,如果失败的话就填个负值,比如-EIO。
返回值:无
68.3 使用请求队列实验
关于块设备架构就讲解这些,接下来我们使用开发板上的RAM模拟一段块设备,也就是ramdisk,然后编写块设备驱动。
68.3.1 实验程序编写
本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 24_ramdisk_withrequest。
首先是传统的使用请求队列的时候,也就是针对机械硬盘的时候如何编写驱动。由于实验程序稍微有点长,因此我们就分步骤来讲解一下,本实验参考自linux内核drivers/block/z2ram.c。打开实验源码,我们先来一下一相关的宏定义和结构体,代码如下:

示例代码68.3.1.1 宏定义和结构体
1  #include <linux/types.h>
......
21 #include <asm/io.h>
22 /***************************************************************
23 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
24 文件名      	: ramdisk.c
25 作者       	: 左忠凯
26 版本       	: V1.0
27 描述      	: 内存模拟硬盘,实现块设备驱动,本驱动使用请求队列。
28          		: 参考:drivers/block/z2ram.c
29 其他       	: 无
30 论坛       	: www.openedv.com
31 日志       	: 初版V1.0 2020/5/22 左忠凯创建
32 ***************************************************************/
33 
34 #define RAMDISK_SIZE (2 * 1024 * 1024)   /* 容量大小为2MB */
35 #define RAMDISK_NAME "ramdisk"  /* 名字 */
36 #define RADMISK_MINOR    3     	/* 表示三个磁盘分区!不是次设备号为3! */
37 
38 /* ramdisk设备结构体 */
39 struct ramdisk_dev{
40  	int major;                  	/* 主设备号 							*/
41  	unsigned char *ramdiskbuf; 	/* ramdisk内存空间,用于模拟块设备 	*/
42  	spinlock_t lock;            	/* 自旋锁 							*/
43  	struct gendisk *gendisk;    	/* gendisk 						*/
44  	struct request_queue *queue;/* 请求队列 							*/
45 };
46 
47 struct ramdisk_dev ramdisk;   	/* ramdisk设备 					*/
第34~36行,实验相关宏定义,RAMDISK_SIZE就是模拟块设备的大小,这里设置为2MB,也就是说本实验中的虚拟块设备大小为2MB。RAMDISK_NAME为本实验名字,RADMISK_MINOR是本实验此设备号数量,注意不是次设备号!此设备号数量决定了本块设备的磁盘分区数量。
第39~45行,ramdisk的设备结构体。
第47行,定义一个ramdisk示例。
接下来看一下驱动模块的加载与卸载,内容如下:
示例代码68.3.1.2 驱动模块加载与卸载
1  /*
2   * @description 	: 驱动入口函数
3   * @param        	: 无
4   * @return       	: 无
5   */
6  static int __init ramdisk_init(void)
7  {
8   	int ret = 0;
9  
10  	/* 1、申请用于ramdisk内存 */
11 	 	ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
12  	if(ramdisk.ramdiskbuf == NULL) {
13      	ret = -EINVAL;
14      	goto ram_fail;
15  	}
16 
17  	/* 2、初始化自旋锁 */
18  	spin_lock_init(&ramdisk.lock);
19 
20  	/* 3、注册块设备 */
21  	ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 自动分配 */
22  	if(ramdisk.major < 0) {
23      	goto register_blkdev_fail;
24  	}  
25  	printk("ramdisk major = %d\r\n", ramdisk.major);
26 
27  	/* 4、分配并初始化gendisk */
28  	ramdisk.gendisk = alloc_disk(RADMISK_MINOR);
29  	if(!ramdisk.gendisk) {
30      	ret = -EINVAL;
31      	goto gendisk_alloc_fail;
32  	}
33 
34  	/* 5、分配并初始化请求队列 */
35  	ramdisk.queue = blk_init_queue(ramdisk_request_fn, 
&ramdisk.lock);
36  	if(!ramdisk.queue) {
37      	ret = -EINVAL;
38      	goto blk_init_fail;
39  	}
40 
41  	/* 6、添加(注册)disk */
42  	ramdisk.gendisk->major = ramdisk.major;	/* 主设备号 		*/
43  	ramdisk.gendisk->first_minor = 0;           /*起始次设备号) */
44  	ramdisk.gendisk->fops = &ramdisk_fops;   	/* 操作函数 		*/
45  	ramdisk.gendisk->private_data = &ramdisk;	/* 私有数据 		*/
46  	ramdisk.gendisk->queue = ramdisk.queue; 	/* 请求队列 		*/
47  	sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME);/* 名字 */
48  	set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512);  /* 设备容量(单位
为扇区)*/
49  	add_disk(ramdisk.gendisk);
50 
51  	return 0;
52 
53 blk_init_fail:
54  	put_disk(ramdisk.gendisk);
55 gendisk_alloc_fail:
56  	unregister_blkdev(ramdisk.major, RAMDISK_NAME);
57 register_blkdev_fail:
58  	kfree(ramdisk.ramdiskbuf); /* 释放内存 */
59 ram_fail:
60  	return ret;
61 }
62 
63 /*
64  * @description  	: 驱动出口函数
65  * @param        	: 无
66  * @return       	: 无
67  */
68 static void __exit ramdisk_exit(void)
69 {
70  	/* 释放gendisk */
71		del_gendisk(ramdisk.gendisk);
72  	put_disk(ramdisk.gendisk);  	
73 
74  	/* 清除请求队列 */
75  	blk_cleanup_queue(ramdisk.queue);
76 
77  	/* 注销块设备 */
78  	unregister_blkdev(ramdisk.major, RAMDISK_NAME);
79 
80  	/* 释放内存 */
81  	kfree(ramdisk.ramdiskbuf); 
82 }
83 
84 module_init(ramdisk_init);
85 module_exit(ramdisk_exit);
86 MODULE_LICENSE("GPL");
87 MODULE_AUTHOR("zuozhongkai");
ramdisk_init和ramdisk_exit这两个函数分别为驱动入口以及出口函数,我们依次来看一下这两个函数。
第11行,因为本实验是使用一块内存模拟真实的块设备,因此这里先使用kzalloc函数申请用于ramdisk实验的内存,大小为2MB。
第18行,初始化一个自旋锁,blk_init_queue函数在分配并初始化请求队列的时候需要用到一次自旋锁。
第21行,使用register_blkdev函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里我们让内核自动分配一个主设备号,因此register_blkdev函数的第一个参数为0。
第28行,使用alloc_disk分配一个gendisk。
第35行,使用blk_init_queue函数分配并初始化一个请求队列,请求处理函数为ramdisk_request_fn,具体的块设备读写操作就在此函数中完成,这个需要驱动开发人员去编写,稍后讲解。
第42~47行,初始化第28行申请到的gendisk,重点是第44行设置gendisk的fops成员变量,也就是设置块设备的操作集。这里设置为ramdisk_fops,需要驱动开发人员自行编写实现,稍后讲解。
第48行,使用set_capacity函数设置本块设备容量大小,注意这里的大小是扇区数,不是字节数,一个扇区是512字节。
第49行,gendisk初始化完成以后就可以使用add_disk函数将gendisk添加到内核中,也就是向内核添加一个磁盘设备。
ramdisk_exit函数就比较简单了,在卸载块设备驱动的时候需要将前面申请的内容都释放掉。第71和72行使用put_disk和del_gendis函数释放前面申请的gendisk,第75行使用blk_cleanup_queue函数消除前面申请的请求队列,第78行使用unregister_blkdev函数注销前面注册的块设备,最后调用kfree来释放掉申请的内存。
在ramdisk_init函数中设置了gendisk的fops成员变量,也就是块设备的操作集,具体内容如下:
示例代码68.3.1.3 gendisk的fops操作集
1  /*
2   * @description   	: 打开块设备
3   * @param - dev  	: 块设备
4   * @param - mode  	: 打开模式
5   * @return         	: 0 成功;其他 失败
6   */
7  int ramdisk_open(struct block_device *dev, fmode_t mode)
8  {
9   	printk("ramdisk open\r\n");
10  	return 0;
11 }
12 
13 /*
14  * @description  	: 释放块设备
15  * @param - disk  	: gendisk
16  * @param - mode 	: 模式
17  * @return        	: 0 成功;其他 失败
18  */
19 void ramdisk_release(struct gendisk *disk, fmode_t mode)
20 {
21  	printk("ramdisk release\r\n");
22 }
23 
24 /*
25  * @description	: 获取磁盘信息
26  * @param - dev  	: 块设备
27  * @param - geo  	: 模式
28  * @return         	: 0 成功;其他 失败
29  */
30 int ramdisk_getgeo(struct block_device *dev, 
struct hd_geometry *geo)
31 {
32  	/* 这是相对于机械硬盘的概念 */
33  	geo->heads = 2;         /* 磁头 */
34  	geo->cylinders = 32;    /* 柱面 */
35  	geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 磁道上的扇区数量 */
36  	return 0;
37 }
38 
39 /* 
40  * 块设备操作函数 
41  */
42 static struct block_device_operations ramdisk_fops =
43 {
44  	.owner   = THIS_MODULE,
45  	.open    = ramdisk_open,
46  	.release = ramdisk_release,
47  	.getgeo  = ramdisk_getgeo,
48 };
第42~48行就是块设备的操作集block_device_operations,本例程实现的比较简单,仅仅实现了open、release和getgeo,其中open和release函数都是空函数。重点是getgeo函数,第30~37行就是getgeo的具体实现,此函数用户获取磁盘信息,信息保存在参数geo中,为结构体hd_geometry类型,如下:
示例代码68.3.1.4 hd_geometry结构体
1 struct hd_geometry {
2       unsigned char 		heads;    	/* 磁头 				*/
3       unsigned char 		sectors;  	/*一个磁道上的扇区数量 	*/
4       unsigned short 	cylinders;	/* 柱面 				*/
5       unsigned long 		start;    
6 };
本例程中设置ramdisk有2个磁头(head)、一共有32个柱面(cylinderr)。知道磁盘总容量、磁头数、柱面数以后我们就可以计算出一个磁道上有多少个扇区了,也就是hd_geometry中的sectors成员变量。
最后就是非常重要的请求处理函数,使用blk_init_queue函数初始化队列的时候需要指定一个请求处理函数,本例程中注册的请求处理函数如下所示:
示例代码68.3.1.5 请求处理函数
1  /*
2   * @description  : 处理传输过程
3   * @param-req    : 请求
4   * @return       : 无
5   */
6  static void ramdisk_transfer(struct request *req)
7  {    
8   unsigned long start = blk_rq_pos(req) << 9;  /* blk_rq_pos获取到的是

扇区地址,左移9位转换为字节地址 */

9   unsigned long len  = blk_rq_cur_bytes(req);    /* 大小   */
10 
11  /* bio中的数据缓冲区
12   * 读:从磁盘读取到的数据存放到buffer中
13   * 写:buffer保存这要写入磁盘的数据
14   */
15  void *buffer = bio_data(req->bio);      
16  
17  	if(rq_data_dir(req) == READ)        	/* 读数据 */ 
18      	memcpy(buffer, ramdisk.ramdiskbuf + start, len);
19  	else if(rq_data_dir(req) == WRITE)  /* 写数据 */
20      	memcpy(ramdisk.ramdiskbuf + start, buffer, len);
21 
22 }
23 
24 /*
25  * @description  	: 请求处理函数
26  * @param-q  		: 请求队列
27  * @return       	: 无
28  */
29 void ramdisk_request_fn(struct request_queue *q)
30 {
31  	int err = 0;
32  	struct request *req;
33 
34  	/* 循环处理请求队列中的每个请求 */
35  	req = blk_fetch_request(q);
36  	while(req != NULL) {
37 
38      	/* 针对请求做具体的传输处理 */
39      	ramdisk_transfer(req);
40 
41      	/* 判断是否为最后一个请求,如果不是的话就获取下一个请求
42       	 * 循环处理完请求队列中的所有请求。
43       	 */
44      	if (!__blk_end_request_cur(req, err))
45          	req = blk_fetch_request(q);
46  	}
47 }
请求处理函数的重要内容就是完成从块设备中读取数据,或者向块设备中写入数据。首先来看一下29~47行的ramdisk_request_fn函数,这个就是请求处理函数。此函数只要一个参数q,为request_queue结构体指针类型,也就是要处理的请求队列,因此ramdisk_request_fn函数的主要工作就是依次处理请求队列中的所有请求。第35行,首先使用blk_fetch_request函数获取请求队列中第一个请求,如果请求不为空的话就调用ramdisk_transfer函数进行对请求做进一步的处理,然后就是while循环依次处理完请求队列中的每个请求。第44行使用__blk_end_request_cur函数检查是否为最后一个请求,如果不是的话就继续获取下一个,直至整个请求队列处理完成。
ramdisk_transfer函数完成清楚中的数据处理,第8行调用blk_rq_pos函数从请求中获取要操作的块设备扇区地址,第9行使用blk_rq_cur_bytes函数获取请求要操作的数据长度,第15行使用bio_data函数获取请求中bio保存的数据。第17	~20行调用rq_data_dir函数判断当前是读还是写,如果是写的话就将bio中的数据拷贝到ramdisk指定地址(扇区),如果是读的话就从ramdisk中的指定地址(扇区)读取数据放到bio中。

68.3.2 运行测试

编译上一小节的驱动,得到ramdisk.ko驱动模块,然后拷贝到rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录lib/modules/4.1.15中。输入如下命令加载ramdisk.ko这个驱动模块。

depmod //第一次加载驱动的时候需要运行此命令

modprobe ramdisk.ko //加载驱动模块

1、查看ramdisk磁盘

驱动加载成功以后就会在/dev/目录下生成一个名为“ramdisk”的设备, 输入如下命令查看ramdisk磁盘信息:

fdisk -l //查看磁盘信息

上述命令会将当前系统中所有的磁盘信息都打印出来,其中就包括了ramdisk设备,如图68.3.2.1所示:

EMMC和硬盘_EMMC和硬盘_04

图68.3.2.1 ramdisk磁盘信息
从图68.3.2.1可以看出,ramdisk已经识别出来了,大小为2MB,但是同时也提示/dev/ramdisk没有分区表,因为我们还没有格式化/dev/ramdisk。
2、格式化/dev/ramdisk
使用mkfs.vfat命令格式化/dev/ramdisk,将其格式化成vfat格式,输入如下命令:
mkfs.vfat /dev/ramdisk
格式化完成以后就可以挂载/dev/ramdisk来访问了,挂载点可以自定义,这里笔者就将其挂载到/tmp目录下,输入如下命令:
mount /dev/ramdisk /tmp
挂载成功以后就可以通过/tmp来访问ramdisk这个磁盘了,进入到/tmp目录中,可以通过vi命令新建一个txt文件来测试磁盘访问是否正常。
68.4 不使用请求队列实验
68.4.1 实验程序编写
本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 25_ramdisk_norequest。
前面我们学习了如何使用请求队列,请求队列会用到I/O调度器,适合机械硬盘这种存储设备。对于EMMC、SD、ramdisk这样没有机械结构的存储设备,我们可以直接访问任意一个扇区,因此可以不需要I/O调度器,也就不需要请求队列了,这个我们前面已经说过了。本实验就来学习一下如何使用“制造请求”方法,本实验在上一个实验的基础上修改而来,参考了linux内核drivers/block/zram/zram_drv.c。重点来看一下与上一个实验不同的地方,首先是驱动入口函数ramdisk_init,ramdisk_init函数大部分和上一个实验相同,只是本实验中改为使用blk_queue_make_request函数设置“制造请求”函数,修改后的ramdisk_init函数内容如下(有省略):

示例代码68.4.1.1 ramdisk_init函数
1  static int __init ramdisk_init(void)
2  {
......
29 
30  	/* 5、分配请求队列 */
31  	ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
32  	if(!ramdisk.queue){
33      	ret = -EINVAL;
34      	goto blk_allo_fail;
35  	}
36 
37  	/* 6、设置“制造请求”函数 */
38  	blk_queue_make_request(ramdisk.queue, ramdisk_make_request_fn);
39 
40 	 	/* 7、添加(注册)disk */
41  	ramdisk.gendisk->major = ramdisk.major; 	/* 主设备号 */
42  	ramdisk.gendisk->first_minor = 0;  			/* 起始次设备号 */
43  	ramdisk.gendisk->fops = &ramdisk_fops;      	/* 操作函数 */
44  	ramdisk.gendisk->private_data = &ramdisk;   	/* 私有数据 */
45  	ramdisk.gendisk->queue = ramdisk.queue;     	/* 请求队列 */
46  	sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME); /* 名字 */
47  	set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512);  /* 设备容量*/
48  	add_disk(ramdisk.gendisk);
49 
......
60  	return ret;
61 }
ramdisk_init函数中第31~38行就是与上一个实验不同的地方,这里使用blk_alloc_queue和blk_queue_make_request这两个函数取代了上一个实验的blk_init_queue函数。
第31行,使用blk_alloc_queue函数申请一个请求队列。
第38行,使用blk_queue_make_request函数设置“制造请求”函数,这里设置的制造请求函数为ramdisk_make_request_fn,这个需要驱动编写人员去实现,稍后讲解。
第43行,设置块设备操作集为ramdisk_fops,和上一个实验一模一样,这里就不讲解了。
接下来重点看一下“制造请求”函数ramdisk_make_request_fn,函数内容如下:
示例代码68.4.1.2 ramdisk_make_request_fn函数
1  /*
2   * @description  	: “制造请求”函数
3   * @param-q  		: 请求队列
4   * @return       	: 无
5   */
6  void ramdisk_make_request_fn(struct request_queue *q, 
struct bio *bio)
7  {
8   	int offset;
9  	 	struct bio_vec bvec;
10  	struct bvec_iter iter;
11  	unsigned long len = 0;
12 
13  	offset = (bio->bi_iter.bi_sector) << 9; /* 获取设备的偏移地址 */
14 
15  	/* 处理bio中的每个段 */
16  	bio_for_each_segment(bvec, bio, iter){
17      	char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
18      	len = bvec.bv_len;
19 
20     	 	if(bio_data_dir(bio) == READ)   /* 读数据 */
21          		memcpy(ptr, ramdisk.ramdiskbuf + offset, len);
22      	else if(bio_data_dir(bio) == WRITE) /* 写数据 */
23          		memcpy(ramdisk.ramdiskbuf + offset, ptr, len);
24      	offset += len;
25  	}
26  	set_bit(BIO_UPTODATE, &bio->bi_flags);
27  	bio_endio(bio, 0);
28 }
虽然ramdisk_make_request_fn函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个bio参数里面,所以ramdisk_make_request_fn函数里面是全部是对bio的操作。
第13行,直接读取bio的bi_iter成员变量的bi_sector来获取要操作的设备地址(扇区)。
第16~25行,使用bio_for_each_segment函数循环获取bio中的每个段,然后对其每个段进行处理。

第17行,根据bio_vec中页地址以及偏移地址转换为真正的数据起始地址。
第18行,获取要出来的数据长度,也就是bio_vec的bv_len成员变量。
第20~23行,和上一个实验一样,要操作的块设备起始地址知道了,数据的存放地址以及长度也知道,接下来就是根据读写操作将数据从块设备中读出来,或者将数据写入到块设备中。
第27行,调用bio_endio函数,结束bio。
68.4.2 运行测试
测试方法和上一个实验一样,参考68.3.2小节即可。