linux块IO层

块设备和字符设备

块设备可以随机访问,字符设备只能有序字符流访问。
最常见的块设备是硬盘,还有软盘驱动器,蓝光光驱,闪存等。他们都是以安装文件系统的方式使用的。
另一种块设备类型是字符设备,字符设备以字符流的方式被有序访问,像串口和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请求,尚未和磁盘块关联。。)
    // 其他字段和操作省略...
};

块io操作的结构体:bio

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设备的请求队列

块设备将他们挂起的块io请求保留在请求队列中。
因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。(在磁盘上必须连续,但是在内存中不需要连续,每个bio结构体都可以描述多个内存区域),每个请求也可以包含多个bio。

io调度程序

在交给块io设备前,内核会将请求合并和排序,从而提高性能。

电梯:当电梯到达最顶层或最底层时,改变方向。

预测

最终操作

完全公平的排队

CFQ的主要原理如下:

时间片调度: CFQ引入了时间片的概念,为每个进程分配一个时间片。每个进程在其时间片内可以发送一定数量的I/O请求。

公平队列: CFQ维护一个I/O请求队列,对每个进程都分配一个独立的队列。队列的调度按照时间片的顺序进行,确保每个队列都有机会进行磁盘I/O。

权重控制: 不同的进程可能有不同的权重,这些权重决定了它们分配到的时间片大小。权重高的进程获得更多的磁盘带宽。

SSTF调度: 在每个队列内,CFQ使用最短寻道时间优先(SSTF)的方式来进行调度,以尽量减少寻道时间。

防止饥饿: CFQ采用了一些机制来防止某个队列永远得不到服务,确保每个队列都有机会访问磁盘。

空操作

猜你喜欢

转载自blog.csdn.net/qq_35693377/article/details/136059760