驱动开发学习笔记---块设备

一、块设备简介

块设备驱动是存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:

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

②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。

二、块设备驱动框架

1、注册注销块设备

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

major: 要注销的块设备主设备号。 name: 要注销的块设备名字。

2、申请和删除磁盘设备

struct gendisk *alloc_disk(int minors)  

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

void del_gendisk(struct gendisk *gp)

gp: 要删除的 gendisk。

3、将 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 的磁盘,其扇区数量就是(210241024)/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)

6、块设备操作集

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

块设备数据读写过程:

(1)、请求队列 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: 自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。

②、删除请求队列

当卸载块设备驱动的时候我们还需要删除掉前面申请到的 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 与“制造请求”函数绑定在一起。

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

gfp_mask: 内存分配掩码

2、 请求 request

请求队列(request_queue)里面包含的就是一系列的请求(request), request 是一个结构体, 需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

①、 获取请求

request *blk_peek_request(struct request_queue *q)  

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

②、开启请求

void blk_start_request(struct request *req)

③、一步到位处理请求

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

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

 
posted @ 2022-12-04 10:44  晚安z  阅读(291)  评论(0编辑  收藏  举报