块设备可以随机访问,字符设备只能有序字符流访问。
最常见的块设备是硬盘,还有软盘驱动器,蓝光光驱,闪存等。他们都是以安装文件系统的方式使用的。
另一种块设备类型是字符设备,字符设备以字符流的方式被有序访问,像串口和U盘就属于字符设备。
当一个块被调入内存时,它要存储在一个缓冲区中,每个缓冲区和一个块对应,它相当于是磁盘块在内存中的表示,但是它存储在磁盘上。
每个块在磁盘上都有一个块头(block header)或称为缓冲区头(buffer head)用于存储一些元数据信息。
这个块头通常与存储在磁盘上的实际数据部分组合在一起,形成一个完整的块。当文件系统需要读取或写入特定的块时,它会先读取块头以了解有关块的信息,然后再处理块中的实际数据。
缓冲区头的目的在于描述磁盘块和物理内存缓冲区(页)之间的映射关系,仅仅描述,并不操作。
struct buffer_head {
unsigned long b_blocknr; // 缓冲区页对应的块号
struct buffer_head *b_this_page; // 缓冲区页链表中的下一个页
struct page *b_page; // 存储缓冲区的页
sector_t b_blocknr; // 缓冲区页对应的块号
atomic_t b_count; // 缓冲区页的引用计数
unsigned long b_state; // 缓冲区页的状态标志(脏,有io请求,尚未和磁盘块关联。。)
// 其他字段和操作省略...
};
bio的目的是代表正在执行的io操作。
当进行块读取或写入时,内核会创建一个或多个 struct bio 结构,代表相应的 I/O 操作。设备驱动程序负责处理这些 struct bio,并与硬件进行实际的块I/O 操作。
struct bio {
struct bio *bi_next; // 下一个bio结构体
struct block_device *bi_bdev; // 目标块设备
unsigned long bi_flags; // 标志位
sector_t bi_sector; // 起始扇区号
struct bio_vec *bi_io_vec; // 存储I/O数据的bio向量
unsigned int bi_vcnt; // 向量数
unsigned int bi_idx; // 向量索引
unsigned long bi_size; // 数据传输的字节数
struct bio *bi_private; // 私有数据指针
// 其他字段和操作省略...
};
每个块io请求都通过一个bio结构体来表示,每个请求包含一个或者多个块内容,这些块存储在bio_vec结构体数据组中。bio_vec描述了每个块在物理页中的实际位置,并且像向量一样组织在一起。
struct bio_vec {
struct page *bv_page; // 缓冲区所驻留的物理页
unsigned int bv_len; // 这个缓冲区的大小
unsigned int bv_offset; // 在缓冲区中的偏移量
};
以下是一个简单示例,假设我们要将两个页面的数据加载到内存:
struct bio *bio = bio_alloc(GFP_KERNEL, 2); // 分配一个包含两个 bio_vec 的 bio
struct bio_vec *bvec1 = bio_kmalloc(GFP_KERNEL, 1);
bvec1->bv_page = alloc_page(GFP_KERNEL); // 分配第一个页面
bvec1->bv_len = PAGE_SIZE; // 页面大小
bvec1->bv_offset = 0;
struct bio_vec *bvec2 = bio_kmalloc(GFP_KERNEL, 1);
bvec2->bv_page = alloc_page(GFP_KERNEL); // 分配第二个页面
bvec2->bv_len = PAGE_SIZE; // 页面大小
bvec2->bv_offset = 0;
// 将 bio_vec 添加到 bio 中,形成链表
bio_add_page(bio, bvec1->bv_page, bvec1->bv_len, bvec1->bv_offset);
bio_add_page(bio, bvec2->bv_page, bvec2->bv_len, bvec2->bv_offset);
// 提交 bio,进行块I/O 操作
submit_bio(READ, bio);
// 释放资源
cleanup_bio(bio);
块设备将他们挂起的块io请求保留在请求队列中。
因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。(在磁盘上必须连续,但是在内存中不需要连续,每个bio结构体都可以描述多个内存区域),每个请求也可以包含多个bio。
在交给块io设备前,内核会将请求合并和排序,从而提高性能。
CFQ的主要原理如下:
时间片调度: CFQ引入了时间片的概念,为每个进程分配一个时间片。每个进程在其时间片内可以发送一定数量的I/O请求。
公平队列: CFQ维护一个I/O请求队列,对每个进程都分配一个独立的队列。队列的调度按照时间片的顺序进行,确保每个队列都有机会进行磁盘I/O。
权重控制: 不同的进程可能有不同的权重,这些权重决定了它们分配到的时间片大小。权重高的进程获得更多的磁盘带宽。
SSTF调度: 在每个队列内,CFQ使用最短寻道时间优先(SSTF)的方式来进行调度,以尽量减少寻道时间。
防止饥饿: CFQ采用了一些机制来防止某个队列永远得不到服务,确保每个队列都有机会访问磁盘。