块设备介绍

块设备是针对存储设备的,比如 SD 卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。

linux 内 核 使 用 block_device 表 示 块 设 备 , block_device 为 一 个 结 构 体 , 定 义 在include/linux/fs.h 文件中,结构体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 };

注册块设备和注销块设备和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,

1
2
3
int register_blkdev(unsigned int major, const char *name)/*如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。*/
void unregister_blkdev(unsigned int major, const char *name)//无返回值
//major: 主设备号,name: 块设备名字。

linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在 include/linux/genhd.h中,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 disks that can't be partitioned. */
8
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,使用的 API 函数。

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

1
2
3
struct gendisk *alloc_disk(int minors)
//minors: 次设备号数量, 也就是 gendisk 对应的分区数量。
//返回值: 成功:返回申请到的 gendisk,失败: NULL。

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

1
2
void del_gendisk(struct gendisk *gp)
//gp: 要删除的 gendisk。无返回值

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

1
2
void add_disk(struct gendisk *disk)
//disk: 要添加到内核的 gendisk。返回值: 无。

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

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

块设备中最小的可寻址单元为扇区,扇区大小一般是2的整数倍,最常见的大小是512字节,扇区大小是设备的物理属性,扇区是所有块设备的基本单元,块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次传输多个扇区。

5、调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数, get_disk 是增加 gendisk 的引用计数, put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:

1
2
struct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)

块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);//用于块设备的 I/O 控制,在32位系统
6 int (*compat_ioctl) (struct block_device *, fmode_t, unsigned,unsigned long);//用于块设备的 I/O 控制,64位
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 lockheld */
17 void (*swap_slot_free_notify) (struct block_device *,unsigned long);
18 struct module *owner; //表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE。
19 };

块设备 I/O 请求过程

在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue ,内核将对块设备的读写都发送到请求队列 request_queue 中, request_queue 中是大量的request(请求结构体),而 request 又包含了 bio, bio 保存了读写相关数据。

通常一个bio对应一个I/O请求。调度算法可以将连续的bio合并成一个请求,所以一个请求包含多个bio。

请求队列API函数解析

1
2
3
4
5
6
7
8
9
10
11
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
/*rfn: 请求处理函数指针 lock: 自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用
这个自旋锁。返回值: 如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。*/
void blk_cleanup_queue(struct request_queue *q)
/*q: 需要删除的请求队列。返回值: 无。*/
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
/*gfp_mask: 内存分配掩码,返回值: 申请到的无 I/O 调度的 request_queue。*/
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)
/*返回值: 无*/

请求API函数解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
request *blk_peek_request(struct request_queue *q)
/*q: 指定 request_queue。
返回值: request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回NULL。*/
void blk_start_request(struct request *req)
/*req: 要开始处理的请求。返回值: 无。*/
//一步到位处理请求
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;
}

每个 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 调度器来完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
					//bio 是个结构体,定义在 include/linux/blk_types.h 中
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 结构体描述了要操作的设备扇区等信息。

1
2
3
4
5
6
7
8
9
10
11
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 中已经处理完成的字节数 */
};
struct bio_vec {
struct page *bv_page; /* 页 */
unsigned int bv_len; /* 长度 */
unsigned int bv_offset; /* 偏移 */
};

①、遍历请求中的 bio
请求中包含有大量的 bio,因此就涉及到遍历请求中所有 bio 并进行处理。遍历请求中的 bio 使用函数______rq_for_each_bio,这是一个宏,内容如下:

1
#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 函数,此函数也是一个宏,内容如下:

1
#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 函数,函数原型如下:

1
bvoid bio_endio(struct bio *bio, int error)

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

小结

block_device: 描述一个分区或整个磁盘对内核的一个块设备实例
gendisk: 描述一个通用硬盘(generic hard disk)对象。
hd_struct: 描述分区应有的分区信息
bio: 描述块数据传送时怎样完成填充或读取块给driver
request: 描述向内核请求一个列表准备做队列处理。
request_queue: 描述内核申请request资源建立请求链表并填写BIO形成队列。

在Linux中,驱动对块设备的输入或输出(I/O)操作,都会向块设备发出一个请求,在驱动中用>request结构体描述。但对于一些磁盘设备而言请求的速度很慢,这时候内核就提供一种队列的机制把这些I/O请求添加到队列中(即:请求队列),在驱动中用request_queue结构体描述。在向块设备提交这些请求前内核会先执行请求的合并和排序预操作,以提高访问的效率,然后再由内核中的I/O调度程序子系统来负责提交 I/O 请求, 调度程序将磁盘资源分配给系统中所有挂起的块 I/O 请求,其工作是管理块设备的请求队列,决定队列中的请求的排列顺序以及什么时候派发请求到设备。
由通用块层(Generic Block Layer)负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系。在通用块层中,通常用一个bio结构体来对应一个I/O请求。
Linux提供了一个gendisk数据结构体,用来表示一个独立的磁盘设备或分区,用于对底层物理磁盘进行访问。在gendisk中有一个类似字符设备中file_operations的硬件操作结构指针,是block_device_operations结构体。
当多个请求提交给块设备时,执行效率依赖于请求的顺序。如果所有的请求是同一个方向(如:写数据),执行效率是最大的。内核在调用块设备驱动程序例程处理请求之前,先收集I/O请求并将请求排序,然后,将连续扇区操作的多个请求进行合并以提高执行效率(内核算法会自己做,不用你管),对I/O请求排序的算法称为电梯算法(elevator algorithm)。电梯算法在I/O调度层完成。

代码框架