1. 简介
Linux块设备是针对存储设备,比如SD卡、EMMC、Nand Flash等。
与字符设备相对,主要有以下特点:
- 块设备只能以块为单位进行读写访问,块是Linux虚拟文件系统(VFS)的基本 数据传输单位,一般为512字节或者4K,而字符设备是以字节为单位进行数据传输的,不需要缓冲。
- 块设备在结构上是可以随机访问的。Linux块设备以固定大小的块(通常为512字节或4KB)为单位进行数据读写。这种块设备的结构允许系统直接访问任意块的数据,而不必按照顺序逐个读取。因此,Linux块设备可以实现随机访问,提高数据读写的效率。
- 块设备支持缓冲和缓存。操作系统通常会使用缓冲区来存储从块设备读取或写入的数据。这可以提高系统的性能,因为它可以减少对实际物理设备的访问次数。
- 块设备可以被多个进程同时访问。多个进程可以并发地读取或写入块设备,而无需互斥访问。这使得块设备非常适合用于共享和分布式系统。
- 块设备可以通过文件系统进行组织和管理。文件系统是操作系统中用于管理和访问数据的一种抽象层。它可以在块设备上创建文件和目录,并提供对这些文件和目录的高级操作。
2. 块设备驱动框架
Linux块设备驱动框架是Linux内核用于管理和控制块设备的框架,主要包括:
- 块设备结构体,用于表示和管理块设备的信息
- 请求队列:块设备驱动框架通过请求队列来管理和调度I/O请求,确保数据的顺序性和一致性。
- I/O接口:用于实现块设备的读写操作
- 注册和注销接口
2.1 block_device 块设备结构体
linux 内 核 使 用 block_device 表 示 块 设 备 , block_device 为 一 个 结 构 体 , 定 义 在include/linux/fs.h 文件中,结构体内容如下:
struct block_device {
dev_t bd_dev; /* not a kdev_t - it's a search key */
int bd_openers;
struct inode * bd_inode; /* will die */
struct super_block * bd_super;
struct mutex bd_mutex; /* open/close mutex */
struct list_head bd_inodes;
void * bd_claiming;
void * bd_holder;
int bd_holders;
bool bd_write_holder;
#ifdef CONFIG_SYSFS
struct list_head bd_holder_disks;
#endif
struct block_device * bd_contains;
unsigned bd_block_size;
struct hd_struct * bd_part;
/* number of times partitions within this device have been opened. */
unsigned bd_part_count;
int bd_invalidated;
struct gendisk * bd_disk;
struct request_queue * bd_queue;
struct list_head bd_list;
/*
* Private data. You must have bd_claim'ed the block_device
* to use this. NOTE: bd_claim allows an owner to claim
* the same device multiple times, the owner must take special
* care to not mess up bd_private for that case.
*/
unsigned long bd_private;
/* The counter of freeze processes */
int bd_fsfreeze_count;
/* Mutex for freeze */
struct mutex bd_fsfreeze_mutex;
};
其中需要重点关注bd_disk
成员变量,类型为gendisk
结构体。
2.2 注销、注册块设备函数
注册块设备函数
int register_blkdev(unsigned int major, const char *name)
- major: 主设备号
- name:块设备名字
- 返回值:如果major在1-255之间,返回0表示注册成功,返回负值表示注册失败;如果major为0,返回系统自动分配的设备号(1~255),如果返回负值,表示注册失败
注销块设备函数
void unregister_blkdev(unsigned int major, const char *name)
- major:要注销的块设备的主设备号
- name:要注销的块设备的名字
2.3 gendisk 结构体
Linux使用gendisk来描述一个磁盘设备,这是一个结构体,定义在include/linux/genhd.h
中。结构体定义如下
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
int major; /* major number of driver */
int first_minor;
int minors; /* maximum number of minors, =1 for
* disks that can't be partitioned. */
char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
struct disk_part_tbl __rcu *part_tbl;
struct hd_struct part0;
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
- major为磁盘设备的主设备号
- first_minor为磁盘的第一个次设备号
- minor为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,次设备号不同
- part_tbl为磁盘对应的分区表。
disk_part_tbl
类型,核心是一个hd_struct
的结构体数组,此数组每一项都对应一个分区信息。 - fops为块设备操作集
- queue为磁盘对应的请求队列,所有针对该磁盘设备的请求都放到该请求队列中,驱动程序需要处理此队列的所有请求。
2.4 gendisk操作函数
2.4.1 申请gendisk
在使用gendisk之前,要先进行申请,函数原型:
struct gendisk *alloc_disk(int minors)
- minors:次设备号的数量,也就是gendisk对应的分区数量
- 返回值:成功返回申请的gendisk,失败返回NULL
2.4.2 删除gendisk
使用del_gendisk删除gendisk,函数原型:
void del_gendisk(struct gendisk *gp)
- gp:要删除的gendisk
2.4.3 将gendisk添加到内核
使用alloc_gendisk
申请到的gendisk系统还无法使用,需要使用add_disk将申请到的gendisk添加到内核中,函数原型:
void add_disk(struct gendisk *disk)
2.4.4 设置gendisk的容量
每一个磁盘都有容量,所以在初始化gendisk的时候也需要设备容量,函数原型如下:
void set_capacity(struct gendisk *disk, sector_t size)
- disk:要设置容量的gendisk
- size:磁盘容量的大小,这里指的是扇区的数量。块设备中的最小可寻址单元是扇区,一般一个扇区是512字节,但是有些也不是。不管物理扇区是多少,内核和块设备驱动之间的扇区都是512字节(4K)。例如,2MB的磁盘,扇区数量就是(2 * 1024 * 1024)/ 512 = 4096
2.4.5 调整gendisk的引用计数
内核通过get_disk和put_disk这两个函数来调整引用计数
struct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)
2.5 block_device_operations结构体
与字符设备的file_operations一样,块设备也有操作集,此结构体定义在include/linux/blkdev.h中,结构体内容如下:
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;
};
2.6 块设备I/O请求
在block_device_operations结构体中,并没有read和write函数。块设备使用request_queue、request和bio实现读写操作
2.6.1 请求队列request queue
内核对于块设备的读写都发送到请求队列request_queue中,request_queue中是大量的request,request又包含bio,在bio中保存了读写相关的数据,比如从块设备的哪个地址开始读、读取的长度是多少,如果是写入、还包括要写入的数据。
在gendisk结构体中,有一个request_queue结构体指针类型的成员变量queue,在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个request_queue
请求队列使用流程:
- 初始化请求队列
首先需要申请并初始化request_queue,使用blk_init_queue函数来完成request_queue的申请与初始化,函数原型:
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
- rfn:是请求处理函数指针,原型void (request_fn_proc) (struct request_queue *q)
,需要由驱动人员实现
- lock,自旋锁指针 - 删除请求队列
当卸载块设备驱动的时候,需要删除前面申请的reques_queue,使用blk_cleanup_queue
函数原型void blk_cleanup_queue(struct request_queue *q)
- 分配请求队列并绑定制造请求函数
blk_init_queue函数完成了请求队列的申请和处理函数䣌绑定,这个一般用于像机械硬盘这样的存储设备,需要I/O调度器优化数据读写过程。但是对于EMMC\SD卡这样的非机械设备,可以进行完全随机访问,所以不需要复杂的I/O调度器。
对于非机械设备,可以先申请request_queue,然后将申请到的reques_queue与制造请求的函数绑定在一起。
申请request_queue函数blk_queue,函数原型:
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
- gfp_mask:内存分配掩码,一般为GFK_KERNEL。