Linux 块设备驱动分析(一)
Linux 块设备驱动分析(二)
Linux 块设备驱动分析(三)
Linux块设备IO子系统框架
块设备与字符设备的不同
块设备是与字符设备并列的概念,这两类设备在Linux中驱动的结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多,在I/O操作上表现出极大的不同,缓冲、I/O调度、请求队列等都是与块设备驱动相关的概念。
系统中能够随机(不需要按顺序)访问固定大小数据片的硬件设备称作块设备,这些固定大小的数据片就称作块。最常见的块设备是硬盘,除此以外,还有软盘驱动器、蓝光光驱和闪存等许多其他块设备。块设备的一般访问方式是通过文件系统。
另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘就属于字符设备。
块设备中最小的可寻址单元是扇区,扇区大小最常见的是512字节。虽然物理磁盘寻址是按照扇区级进行的,但是内核执行的所有磁盘操作都是按照块进行的。由于扇区是设备的最小可寻址单元,所以块不能比扇区还小,只能数倍于扇区大小。另外,内核(对有扇区的硬件设备)还要求块大小是2的整数倍,而且不能超过一个页的长度,通常块大小是512字节、1KB或4KB。
块设备的I/O操作特点
(1)块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位。
(2)块设备对于 I/O 请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲且被直接读写。对于存储设备而言调整读写的顺序作用巨大,因为在读写连续的扇区比分离的扇区更快。
(3)字符设备只能被顺序读写,而块设备可以随机访问。虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能。
Linux块设备IO子系统框架
VFS
内核都是通过文件系统访问块设备的,而不同的硬件平台采用的文件系统不一样,如Ext2、FAT、NFS等,不同的文件系统,访问的接口肯定是不太一样的,但是不管采用何种文件系统,应用层都可以通过open打开一个文件,然后read读取文件中的数据,也可以write往文件中写数据,这得归功于VFS,虚拟文件系统。VFS是对各种具体文件系统的一种封装,用户程序访问文件提供统一的接口。
Generic Block Layer
该层隐藏了底层硬件块设备的特性,将对不同块设备的操作转换成对逻辑数据块的操作,为文件系统层提供通用的块设备读写接口。
I/O Scheduler Layer
接收通用块层发出的I/O请求,为了优化寻址操作,内核既不会简单地按请求接收次序,也不会立即将其提交给磁盘,而是试图合并与排序请求,这可以极大地提高系统的整体性能。
主要的数据结构
gendisk结构体,描述一个通用的磁盘设备,对应一个具体的磁盘,结构体定义如下:
struct gendisk {
//磁盘的主设备号
int major;
//磁盘的第一个次设备号 ,如果有分区,每个分区都需要一个次设备号
int first_minor;
//次设备号个数,1表示不分区
int minors;
//磁盘的name
char disk_name[DISK_NAME_LEN];
......
//描述磁盘分区表信息
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;
......
};
字符设备有一个file_operations,对应的块设备有一个block_device_operations,定义如下:
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 *, bool);
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 **, pfn_t *,
long);
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;
const struct pr_ops *pr_ops;
};
一块磁盘是可以分成多个分区的,每个分区可以安装不同类型的文件系统,比如ext2、ext4等文件系统。
hd_struct结构体,描述一个具体的磁盘分区,定义如下:
struct hd_struct {
//分区在磁盘内的起始扇区编号
sector_t start_sect;
//分区的大小(扇区数)
sector_t nr_sects;
......
struct device __dev;
struct kobject *holder_dir;
//如果分区只读则policy为1,否则为0;partno为该分区的区号
int policy, partno;
......
};
gendisk里有一成员part_tbl,描述磁盘的分区表信息,定义如下:
struct disk_part_tbl {
struct rcu_head rcu_head;
//struct hd_struct数组的大小
int len;
struct hd_struct __rcu *last_lookup;
//struct hd_struct数组
struct hd_struct __rcu *part[];
};
Linux内核也把分区也当作一个块设备。
block_device结构体,描述一个逻辑上的块设备,对应一个磁盘或分区,定义如下:
struct block_device {
dev_t bd_dev; //设备号
int bd_openers; //这个块设备被打开的次数
......
//如果块设备代表一个分区,那么该项指向整个磁盘的block_device
struct block_device * bd_contains;
//块设备的块长度(以字节为单位)
unsigned bd_block_size;
//指向块设备的分区对象
struct hd_struct * bd_part;
//如果块设备代表一个磁盘,该域表示其分区被打开的次数;如果代表分区的话,该域为0
unsigned bd_part_count;
int bd_invalidated;
//指向所在磁盘的gendisk
struct gendisk * bd_disk;
struct request_queue * bd_queue;
//用于链接到全局链表all_bdevs
struct list_head bd_list;
......
};
block_device、hd_struct和gendisk之间的关系如下图: