块设备驱动程序,linux内核中的一类驱动程序。通过传输固定大小的随机数据来访问设备。
初始化
注册块设备驱动
向内核注册设备驱动程序使用函数int register_blkdev(unsigned int major, const char *name);
(linux/fs.h),参数是该设备使用的主设备号和名称(名字在/proc/devices
中显示)。若传入的主设备号为0,内核将分配一个新的主设备号,并将该设备号返回给调用者;失败则返回负值。
1 | static int __init sbull_init(void) |
对于register_blkdev
的调用是可选的,该接口所做的事情是:如果需要的话分配一个动态的主设备号;在/proc/devices
中创建一个入口项。
注册磁盘
内核使用gendisk
结构(linux/genhd.h
)来表示一个独立的磁盘设备。内核还使用该结构体表示分区。该结构中的许多成员必须由驱动程序进行初始化。
- major
int major;
主设备号 - first_minor
int first_minor;
一个驱动器至少使用一个次设备号,如果驱动器是可被分区的用户将要为每个可能的分区都分配一个次设备号。 - minors
int minors;
常取16,一个完整的磁盘可以包含15个分区。 - disk_name
char disk_name[32];
磁盘设备名字,该名字显示在/proc/partitions
和sysfs
中。 - fops
struct block_device_operations *fops;
块设备操作 - queue
struct request_queue *queue;
设备I/O请求队列 - flags
int flags;
驱动器状态标志。可移动介质将被设置为GENHD_FL_REMOVABLE
;CD-ROM设备被设置为GENHD_FL_CD
;若不希望在/proc/partitions
中显示分区信息可设置为GENHD_FL_SUPPRESS_PARTITION_INFO
- capacity
sector_t capacity;
以512字节为一个扇区,该驱动器可以包含的扇区数。驱动程序不能直接设置该成员,而要将扇区数传给set_capacity
- private_data
void *private_data;
块设备驱动可以使用该成员保存指向其内部数据的指针
gendisk
是一个动态分配的结构,驱动程序不能自己动态分配该结构,必须使用alloc_disk
分配,使用del_gendisk
回收。
- alloc_disk
struct gendisk *alloc_disk(int minors);
参数minors
是该磁盘使用的次设备号的数目。为了能正常工作,minors
传入后就不能更改了。 - add_disk
void add_disk(struct gendisk *gd);
使用alloc_disk
分配的gendisk
不能使磁盘对系统可用,还需要add_disk
将磁盘设备激活,并随时准备调用它提供的方法。在驱动程序完全被初始化并且能够响应对磁盘的请求前,请不要调用add_disk
- get_disk and put_disk
gendisk
是一个引用技术结构,get_disk
和put_disk
函数负责处理引用计数。 - del_gendisk
void del_gendisk(struct gendisk *gd);
调用del_gendisk
会删除gendisk
中的最终计数。当没有用户继续使用时,将真正删除该结构。此后,在系统中不会找到该设备信息。
1 | static void setup_device(struct sbull_dev *dev,int which) |
逆初始化
注销块设备驱动
与register_blkdev
对应的注销函数为int unregister_blkdev(unsigned int major, const char *name);
,传入的参数与传递给register_blkdev
的参数必须匹配。
1 | static void sbull_exit(void) |
块设备操作
字符设备使用file_operations
结构,块设备使用类似的结构block_device_operations
(linux2.6在linux/fs.h
中,linux4.4在linux/blkdev.h
中)
1 | struct block_device_operations { |
- open
int (*open) (struct inode *inode, struct file *filp);
当设备被打开时调用它。 - release
int (*release) (struct inode *inode, struct file *filp);
当设备被关闭时调用它。 - ioctl
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
实现ioctl系统调用的函数,块设备层会先截取大量的标准请求,因此大多数块设备的ioctl函数都十分短小。 - media_changed
int (*media_changed) (struct gendisk *gd);
内核调用该函数检查用户是否更换了驱动器内的介质,如果更换返回一个非零值。该函数只适用于那些支持可移动介质。 - revalidate_disk
int (*revalidate_disk) (struct gendisk *gd);
当介质被更换时被调用。 - owner
struct module *owner;
指向拥有该结构的模块指针,通常被初始化为THIS_MODULE
open
1 | static int sbull_open(struct inode *inode,struct file *filp) |
release
1 | static int sbull_release(struct inode *inode,struct file *filp) |
media_changed
open
中调用check_disk_change
函数触发media_changed
检查介质是否被改变。如果介质改变则返回非零值。
1 | int sbull_media_changed(struct gendisk *gd) |
revalidate_disk
介质改变后,内核会调用revalidate_disk
,调用完成后,内核将重新读取设备的分区表。
1 | int sbull_revalidate(struct gendisk *gd) |
ioctl
1 | int sbull_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg) |
内核对块设备的物理信息并不感兴趣,它只把设备看成是线性的扇区数组。但一些用户空间的应用程序依然需要查询磁盘的物理信息。特别是fdisk
工具。
请求处理
块设备中没有字符设备中的read、write函数,那么块设备是如何处理I/O请求的呢?早在sbull_init
中初始化queue时需要传入一个queue处理函数,待内核接收到磁盘I/O时,会调用queue处理函数来处理。
request queue
一个请求队列就是一个动态的数据结构,该结构必须由块设备的I/O子系统创建。
创建删除
- 创建初始化
request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
参数request
是处理这个队列的函数指针,lock是控制访问队列权限的自旋锁。由于创建过程会分配内存,因此会有失败的可能,所以在使用队列前一定要检查返回值。 - 删除
void blk_cleanup_queue(request_queue_t *);
调用该函数后,驱动程序将不会再得到这个队列中的请求,也不能再引用这个队列了。
队列元素
- 从队列中获取请求
struct request *elv_next_request(request_queue_t *queue);
返回一个需要处理的请求指针,该指针由I/O调度器决定,如果没有请求需要处理返回NULL。该函数被调用后,请求依然保存在队列中,但是为其做了活动标记,该标记保证了当开始执行该请求时I/O调度器不再将该请求与其他请求合并。 - 从队列中删除请求
void blkdev_dequeue_request(struct request *req);
将请求从队列中删除。 - 请求返回队列
void elv_requeue_request(request_queue_t *queue, struct request *req);
将拿出队列的请求再返回给队列。当驱动需要同时处理同一队列中的多个请求时,一般多用blkdev_dequeue_request
和elv_requeue_request
request
一个块请求队列可以包含那些实际并不向磁盘读写数据的请求,生产商信息、底层诊断操作、与特殊设备模式相关指令、介质写模式设定等。每个request结构都代表一个块设备的I/O请求,这个I/O请求可以通过对多个独立请求的合并而来。
1 | static void sbull_request(request_queue_t *q) |
elv_next_request
用来获取队列中第一个未完成的请求,当没有请求需要处理时,返回NULL。请求被获取后并不从队列中删除。blk_fs_request
用来判断该请求是否是一个文件系统请求。end_request
void end_request(struct request *req, int succeeded);
传递当前请求的指针和完成结果(0表示不成功,非0表示成功)。req->sector
在设备上开始扇区的索引号req->buffer
传输或者接收数据的缓冲区指针rq_data_dir
传输方向,返回0表示从设备读数据,非0表示向设备写入数据
如果多个请求都是对磁盘中相邻扇区进行操作,则内核将合并它们,内核不会合并在单独request结构中的读写操作,如果合并的结果会打破对请求队列的限制,则内核也不会对请求进行合并。
bio
一个request结构是作为一个bio结构的链表实现的,保证在执行请求的时候驱动程序能知道执行的位置。当内核以文件系统、虚拟内存子系统或者系统调用的形式决定从块I/O设备输入、输出块数据时,它将再结合一个bio结构,用来描述这个操作。该结构被传递给I/O代码,代码会把它合并到一个已存在的request结构中,或者根据需要再创建一个新的request结构。bio结构包含了驱动程序执行请求的全部信息,而不必与初始化这个请求的用户空间的进程相关联。
1 | struct bio { |
bio的核心是一个名为bi_io_vec
的数组
1 | struct bio_vec { |
遍历request中的bio
1 | static int sbull_xfer_request(struct sbull_dev *dev,struct request *req) |
遍历bio中的segment
遍历bio中的segment,也就是遍历bio结构中的bi_io_vec
。
1 | static int sbull_xfer_bio(struct sbull_dev *dev,struct bio *bio) |
参考&鸣谢
- 《设备驱动程序》