一、概念
- 块设备:Linux设备类型中的一种。例如硬盘、U盘、NandFlash等存储设备。
- 扇区:任何块设备硬件对数据处理的基本单位。通常,1个扇区的大小为512byte。
- 块:由Linux制定对内核或文件系统等数据处理的基本单位。通常,1个块由1个或多个扇区组成。
- 段:由若干个相邻的块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。
二、操作特点
相比与字符设备,块设备有以下几点不同:
-
块设备只能以块为单位接收输入和返回输出,而字符设备则以字节为单位。
-
块设备对于I/O请求有对应的缓冲区,可以调整响应顺序,而字符设备没有缓冲、直接读写。
-
块设备可以随机访问,而字符设备只能顺序读写。
三、整体框架
app: open,read,write "1.txt"
---------------------------------------------
文件系统: vfat, ext2, ext3, yaffs2, jffs2
---------------------------------------------
通用块层
I/O调度层
内核: 块设备驱动程序
---------------------------------------------
硬件: 硬盘,flash
当应用层读写某一文件"1.txt"时,文件系统把文件的读写转换为扇区的读写,
通用块层负责维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系
所有的I/O请求都会先放入一个队列,统一由I/O调度层管理,它决定队列中的请求的排列顺序以及什么时候派发请求到块设备驱动,最终由块设备驱动程序操作到相关的硬件
四、相关数据结构
-
block_device:代表了内核中的一个块设备。它可以表示整个磁盘或一个特定的分区。
当这个结构代表一个块设备时,bd_disk成员指向设备的gendisk结构。struct block_device { dev_t bd_dev; int bd_openers; struct super_block * bd_super; struct mutex bd_mutex; // 打开/关闭锁 void * bd_claiming; void * bd_holder; int bd_holders; bool bd_write_holder; struct block_device * bd_contains; unsigned bd_block_size; // 分区块大小 struct hd_struct * bd_part; unsigned bd_part_count; // 打开次数 int bd_invalidated; struct gendisk * bd_disk; struct request_queue * bd_queue; // 请求队列 struct list_head bd_list; unsigned long bd_private; // 私有数据 };
-
gendisk:代表一个独立的磁盘设备或分区。
struct gendisk { int major; // 主设备号 int first_minor; // 次设备号 int minors; // 占据的次设备号个数 char disk_name[DISK_NAME_LEN]; // 主设备名 char *(*devnode)(struct gendisk *gd, umode_t *mode); unsigned int events; // 支持的事件 unsigned int async_events; struct disk_part_tbl __rcu *part_tbl; // 分区表 struct hd_struct part0; // 分区信息 const struct block_device_operations *fops; // 操作函数 struct request_queue *queue; // 设备管理I/O请求 void *private_data; // 私有数据 .... };
-
block_device_operations:块设备对应的操作接口,
是连接抽象的块设备操作与具体块设备操作之间的枢纽。struct block_device_operations { int (*open) (struct block_device *, fmode_t); // 打开设备时调用 int (*release) (struct gendisk *, fmode_t); // 关闭设备时调用 int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); // I/O控制 int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*direct_access) (struct block_device *, sector_t, void **, unsigned long *); unsigned int (*check_events) (struct gendisk *disk, unsigned int clearing); int (*media_changed) (struct gendisk *); // 介质改变 void (*unlock_native_capacity) (struct gendisk *); int (*revalidate_disk) (struct gendisk *); // 使介质有效 int (*getgeo)(struct block_device *, struct hd_geometry *); // 获取驱动器信息 void (*swap_slot_free_notify) (struct block_device *, unsigned long); struct module *owner; // 模块指针,通常为THIS_MODULE };
-
bio:通常一个bio对应上层传递给块层的I/O请求。
struct bio { sector_t bi_sector; // 设备地址 struct bio *bi_next; // 请求队列链表 struct block_device *bi_bdev; unsigned long bi_flags; // 标志位 unsigned long bi_rw; // 读写位 unsigned short bi_vcnt; // bio_vec数量 unsigned short bi_idx; // 当前的bvl_vec unsigned int bi_phys_segments; .... struct bio_vec *bi_io_vec; bio_end_io_t *bi_end_io; void *bi_private; // 私有数据 bio_destructor_t *bi_destructor; struct bio_vec bi_inline_vecs[0]; // 用于描述这个boi请求对应的所有内存 };
-
request_queue:块设备的请求队列
struct request { //用于挂在请求队列链表的节点,使用函数elv_next_request()访问它,而不能直接访问 struct list_head queuelist; struct list_head donelist; // 用于挂在已完成请求链表的节点 struct request_queue *q; // 指向请求队列 unsigned int cmd_flags; // 命令标识 enum rq_cmd_type_bits cmd_type; // 读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写 sector_t sector; // 要提交的下一个扇区偏移位置(offset) .... unsigned int current_nr_sectors; // 当前需要传送的扇区数(长度) .... char *buffer; // 当前请求队列链表的申请里面的数据,用来读写扇区数据(源地址) .... };
五、内核代码分析
ll_rw_block
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
submit_bh(rw, bh);
struct bio *bio; // 使用bh来构造bio
submit_bio(rw, bio);
// 通用的构造请求: 使用bio来构造请求(request)
generic_make_request(bio);
__generic_make_request(bio);
request_queue_t *q = bdev_get_queue(bio->bi_bdev); // 找到队列
// 调用队列的"构造请求函数"
ret = q->make_request_fn(q, bio);
// 默认的函数是__make_request
__make_request
// 先尝试合并
elv_merge(q, &req, bio);
// 如果合并不成,使用bio构造请求
init_request_from_bio(req, bio);
// 把请求放入队列
add_request(q, req);
// 执行队列
__generic_unplug_device(q);
// 调用队列的"处理函数"
q->request_fn(q);
六、驱动编写步骤
-
分配一个 gendisk 结构体
alloc_disk (int minors)
- 参数“minor+s”是指次设备号个数,即“分区个数+0”。
0 是指整个磁盘。为 1 时,就是把整个磁盘当成一个分区,则在上面不能再创建分区了。
如写成 16,则最多可以创建 15 个分区。
- 参数“minor+s”是指次设备号个数,即“分区个数+0”。
-
分配/设置队列
blk_init_queue (request_fn_proc * rfn, spinlock_t * lock)
- 参数1:执行处理队列的函数。
- 参数2:一个“自旋锁”。
-
注册块设备
register_blkdev(0, "ramblock"); //自动分配主设备号
-
设置其他属性
-
设置第一个此设备号和块设备的名字
ramblock_disk->first_minor = 0; //第一个次设备号写为0,则从0~16都对应这个块设备。 printf(ramblock_disk->disk_name, "ramblock"); //块设备的名字
-
设置操作函数
ramblock_disk->fops = &ramblock_fops;
-
设置容量
set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);
-
注册
add_disk(ramblock_disk)