系统中能够随机访问特定大小数据片的设备被称作块设备,这些数据片就称作块。最常见的块设备是硬盘。注意,它们都是以安装文件系统的方式使用的——这也是块设备通常的访问方式。
另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘就属于字符设备。
这两种设备的根本区别在于它们是否可以被随机访问。内核管理块设备比字符设备复杂的多,有一个专门的子系统来管理块设备和对块设备的请求,这一部分在内核中被称作块I/O层。
我们在应用层对文件的读写访问,比如我们读写一个 1.txt,这个文件实际是存储在块设备上的,那么是谁完成了文件读写到具体块设备扇区的读写转换呢?是文件系统!我们内核中支持 vfat,ext2,ext3,yaffs2,jffs2 等非常多种文件系统,然而为了统一多种文件系统,内核抽象出了VFS虚拟文件系统,实际的文件系统向上为VFS提供统一的接口。比如一个 write 操作大体的操作流程如下图所示:
整个块设备的框架:
一、基本概念
块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,而最常见的大小是512字节。内核执行的所有磁盘操作都是按照块进行的,由于扇区是块设备的最小可寻址单元,所以块要比扇区2的整数倍,所以通常是 512 字节 、1k 、2k 。
和磁盘相关的其它术语还有——簇、柱面、磁头等,这些术语都和具体的块设备相关,一般情况下用户空间的软件都用不到这些概念。
二、缓冲区和缓冲区头
当一个块被调入内存时(也就是说,在读入后或等待写出时),它要存储在一个缓冲区中,每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示。由于内核在处理数据时需要一些相关的控制信息,比如属于哪一个块设备、块对应于哪个缓冲区等,所以内一个缓冲区都有一个对应的描述符,该描述符用 buffer_head 结构体表示,被称作缓冲区头。
struct buffer_head {
unsigned long b_state; /* 缓冲区状态标志 */
struct buffer_head *b_this_page;/* 页面中的缓冲区 */
struct page *b_page; /* 存储缓冲区的页面 */
sector_t b_blocknr; /* 逻辑块号 */
size_t b_size; /* 块大小 */
char *b_data; /* 页面中的缓冲区 */
struct block_device *b_bdev; <span style="font-family: Arial, Helvetica, sans-serif;">/* 所属的块设备 */</span>
bh_end_io_t *b_end_io; /* I/O 完成方法 */
void *b_private; /* reserved for b_end_io */
struct list_head b_assoc_buffers; /* associated with another mapping */
struct address_space *b_assoc_map; /* mapping this buffer is
associated with */
atomic_t b_count; /* 引用计数 */
};
与缓冲区对应的磁盘物理块由 b_blockbr 索引,该值是 b_bdev 所指明的块设备中的逻辑块号。
与缓冲区对应的内存物理页由 b_page 表示,另外,b_data 直接指向相应的块(它位于 b_page),块大小由 b_size 表示。
缓冲区头的目的在于描述磁盘和物理内存缓冲区之间的映射关系。
缓冲区头仅仅能描述一个缓冲区,当作为所有I/0操作的容器使用时,就会变成对多个 buffer_head 结构体进行操作,势必会造成不必要的负担和空间浪费,因此引入了一个灵活且轻量级的容器——bio 结构体。
三、bio 结构体
每一个块 I/O 请求都通过一个 bio 结构体表示。每一个请求包含一个或者多个块,这些块存储在 bio_vec 结构体数组中,这些结构体描述了每一个片段在物理页面中的实际位置,并像向量一样被组织在一起。I/O 操作的第一个片段由 b_io_vec 结构体所指向,其他的片段在其后依次放置,共有 bi_vcnt 个片段。当块 I/O 层开始执行请求,需要使用各个片段时,bi_idx 会不断更新,从而总指向当前的片段。
struct bio {
//该bio结构所要传输的第一个(512字节)扇区:磁盘的位置
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_vesc偏移的个数
unsigned short bi_idx; //bi_io_vec的当前索引
unsigned short bi_phys_segments; //结合后的片段数目
unsigned short bi_hw_segments; //重映射后的片段数目
unsigned int bi_size; <span style="white-space:pre"> </span> //I/O计数
unsigned int bi_hw_front_size; //第一个可合并的段大小;
unsigned int bi_hw_back_size; //最后一个可合并的段大小
unsigned int bi_max_vecs; //bio_vecs数目上限
struct bio_vec *bi_io_vec; //bio_vec链表:内存的位置
bio_end_io_t *bi_end_io;//I/O完成方法
atomic_t bi_cnt; //使用计数
void *bi_private; //拥有者的私有方法
bio_destructor_t *bi_destructor;//销毁方法
};
struct bio_vec {
struct page *bv_page; // 这个缓冲区所在的物理页面
unsigned int bv_len; // 这个缓冲区的大小
unsigned int bv_offset; // 缓冲区在页面的偏移
};
struct gendisk {
int major; //主设备号
int first_minor; //第一个从设备号
int minors;
/* 描述被磁盘使用的设备号的成员.一个驱动器必须使用最少一个次编号.
*如果你的驱动会是可分区的,但是(并且大部分应当是),你要分配一个次编号给每个可能 的分区.次编号的一个普通的值是 16,
*它允许"全磁盘"设备盒 15 个分区. 一些磁盘驱动使用 64 个次编号给每个设备.
*/
char disk_name[32]; //应当被设置为磁盘驱动器名子的成员. 它出现在 /proc/partitions 和 sysfs.
struct hd_struct **part; /* [indexed by minor] */
struct block_device_operations *fops;// 设备操作集合.
struct request_queue *queue;//被内核用来管理这个设备的 I/O 请求的结构;
void *private_data;//块驱动可使用这个成员作为一个指向它们自己内部数据的指针.
sector_t capacity;
// 这个驱动器的容量,以512-字节扇区来计.sector_t类型可以是64位宽.驱动不应当直接设置这个成员;相反,传递扇区数目给set_capacity.
int flags;
// 一套标志(很少使用),描述驱动器的状态.如果你的设备有可移出的介质,
// 你应当设置GENHD_FL_REMOVABLE.CD-ROM驱动器可设置 GENHD_FL_CD.
// 如果, 由于某些原因, 你不需要分区信息出现在 /proc/partitions, 设置 GENHD_FL_SUPPRESS_PARTITIONS_INFO.
struct device *driverfs_dev; // FIXME: remove
struct device dev;
struct kobject *holder_dir;
struct kobject *slave_dir;
struct timer_rand_state *random;
int policy;
atomic_t sync_io; /* RAID */
unsigned long stamp;
int in_flight;
#ifdef CONFIG_SMP
struct disk_stats *dkstats;
#else
struct disk_stats dkstats;
#endif
struct work_struct async_notify;
};
七、内核 ll_rw_block 函数调用
分析ll_rw_block
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
submit_bh(rw, bh);
struct bio *bio; // 使用bh来构造bio (block input/output)
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);