linux-kernel 2.6源码阅读[7]IO请求处理

一. 概述

本节开头还是先简要回顾一下linux IO栈。当对文件进行读写时,Page Cache或Direct IO层负责产生数据的IO请求,此时需要IO的数据保存在内存中,使用struct bio进行描述,随后映射层负责找到数据在磁盘中的位置,将此位置告诉通用层,如图1.1中所示。


图7.1 linux IO stack

二. 块设备数据结构

内核需要处理各式各样的块设备,通用块设备层抽象出所有块设备的公共特征,描述了一种逻辑上的块设备。通用层有3个关键的数据结构,他们之间的关系如图7.2所示,1)gendisk表示一个真正的物理磁盘,除了自身设备号、分区设备号等信息外,其中最重要的信息是一个分区表和一个请求队列,分区表是一个hd_struct指针数组;2)hd_struct描述一个分区的物理信息,包括起始扇区和总扇区数目,以及一些统计信息;3)block_device描述一个逻辑分区(或称为文件卷),每个block_device都与/dev中的一个块设备文件对应,因此不管是物理磁盘还是逻辑分区都有一个block_device,各逻辑分区的bd_contains指向物理盘的bd_contains,各分区的bd_disk字段指向gendisk,各分区的bd_part字段指向其对应的分区信息hd_struct。这部分内容基于一些默认的常识:1)同一个物理设备的逻辑分区的从设备号依次排布,并且第一个从设备号代表物理设备;2)LBA扇区编码队则中,第一个维度是柱面号,既,一段相邻的扇区号位于同一柱面。

图7.2 通用块层各数据结构之间的关系

以上数据结构与驱动程序密切相关,它们的建立过程不是我们关心的重点,我们重点理解如何使用它们。但是有一点需要知道,当一个块设备文件打开时,open系统调用默认使用blkdev_open函数,在该函数中调用rescan_partitions来探测磁盘的分区情况,并建立图7.2所示的数据结构,rescan_partitions创建一个新分区使用的函数是add_partition。

三. 处理IO请求

3.1 请求的数据结构

磁盘处理读写请求的模型是:当CPU发起读写请求之后,由DMA负责异步完成该请求,一次请求的数据在磁盘上必须连续,但在内存中却可以分散到各个页中,将一个页中的数据作为一个段。用bio_vec结构来表示一个段,该结构包含页的struct page以及数据在页中的起始和长度,所以每次请求都使用一个bio结构来表示,它记录了一次请求中内存中数据在磁盘上的位置,其核心成员包括bi_sector记录请求的起始扇区;bi_io_vec记录bio_vec链表;bi_flags记录读写命令。

@ /include/linux/boi.h
struct bio {
    sector_t        bi_sector;  //该请求的起始扇区
    struct bio      *bi_next;   // 同一请求中的bio链
    struct block_device *bi_bdev;
    unsigned long       bi_flags;   // status, command, etc
    unsigned long       bi_rw;      //请求的读写方向 READ/WRITE,

    unsigned short      bi_vcnt;    //该bio分散在多少页中
    unsigned short      bi_idx; // current index into bvl_vec 
    struct bio_vec      *bi_io_vec; //bio_vec链表,核心结构
    atomic_t        bi_cnt;     /* pin count */
    void            *bi_private;
};

通用层会对上层发来的请求进行合并和排序,多个相邻的bio会被合并到一个struct request中,struct request也是向设备驱动程序发送IO请求的单位。上一节提到,每个gendisk有一个请求链表,该设备的所有未处理请求都被放到该链表上,queuelist字段负责这个链表。nr_sectors表示提交扇区的总数目,current_nr_sectors表示当前bio的扇区数目,hard_sector、 hard_cur_sectors与结构中没有hard_前缀的对应成员语义相同,通常两组变量的值相同,但在使用RAID时可能会有差别。bio链表的链表头记录在struct bio *bio中,链表尾记录在struct bio *biotail中。最后, 所有等待该数据的进程在struct completion *waiting上挂起。

@ /include/linux/blkdev.h
struct request {
    struct list_head queuelist; 
    unsigned long flags;          // 请求的命令
    sector_t sector;              // 下一个要提交的扇区号
    unsigned long nr_sectors;    //提交扇区的总数目
    unsigned int current_nr_sectors; //当前bio的扇区数目

    struct bio *bio;                      // bio链表头
    struct bio *biotail;                 // bio链表尾
    void *elevator_private;         // 用于IO调度算法
    struct gendisk *rq_disk;       // 请求所属的gendisk

    int tag;
    char *buffer;
    request_queue_t *q;
    struct request_list *rl;
    struct completion *waiting;  // 等待该数据的进程在此挂起
    unsigned int timeout;
};

3.2 请求入队

在开始后两节的讨论之前,需要明确几个概念:1)请求队列有两个状态,阻塞状态(plug)下队列只进不出,只能向队列提交新的请求,但新的请求不会得到执行,排空状态(drain)下,队列只出不进,此时驱动程序正在处理队列中已有的请求,但是新的请求不能提交,需要阻塞等待;2)请求是按照逻辑分区而不是物理磁盘提交的,所以提交入队分为两个阶段,第一个阶段为逻辑分区中的提交函数,主要检查请求是否位于该分区内,并将逻辑扇区号转换为物理扇区号,第二阶段为物理设备的提交函数,负责真正的请求提交。
submit_bio负责根据传递的bio实例创建一个新请求,它实际上调用generic_make_request完成核心功能。该函数首先检查请求的扇区是否位于该分区内,随后检查队列是否处于排空状态(drain),如果是则阻塞请求的提交。最后,将逻辑分区中的扇区号转换成物理设备中的扇区号,调用物理设备的make_request_fn完成真正的提交。

@ /drivers/block/ll_rw_blk.c
void generic_make_request(struct bio *bio)
{
    // ......判断请求的扇区是否位于该分区内
    do {
        q = bdev_get_queue(bio->bi_bdev);
end_io:
        bio_endio(bio, bio->bi_size, -EIO);
        break;
        // 如果该队列处于排空(drain)状态,则该进程需要阻塞等待
        block_wait_queue_running(q);
        // 将逻辑分区中的扇区号转换成物理设备中的扇区号
        blk_partition_remap(bio);
        //调用物理设备的make_request_fn
        ret = q->make_request_fn(q, bio);
    } while (ret);
}

__make_request函数负责将新的bio请求插入请求队列中,该函数首先检查是否为空,若为空则代表之前的请求已经处理完成,将队列设为阻塞状态接收新的请求。随后尝试将新请求合并到原有请求上,如果合并成功就可以准备退出该函数。如果合并不超过,则准备构建一个请求,先获取新请求需要的存储空间,然后为新请求赋值,最后调用add_request将新请求添加到调度队列中,该功能由调度算法提供,新请求可能不直接插入调度队列,而是先插入调度算法的私有队列中,等到该请求可以执行时再插入调度队列。

@ /drivers/block/ll_rw_blk.c
static int __make_request(request_queue_t *q, struct bio *bio)
{
again:    // 检查队列是否为空,若空,将队列设为阻塞状态
    if (elv_queue_empty(q)) {
        blk_plug_device(q);
        goto get_rq;
    }
    //尝试将新请求合并到原有请求上
    el_ret = elv_merge(q, &req, bio);
    switch (el_ret) {
        case ELEVATOR_BACK_MERGE:
        case ELEVATOR_FRONT_MERGE:
        case ELEVATOR_NO_MERGE:
    }

get_rq:    // 获取新请求需要的存储空间
    if (freereq) {
        req = freereq;
        freereq = NULL;
    } else {
        if ((freereq = get_request(q, rw, GFP_ATOMIC)) == NULL) {
            freereq = get_request_wait(q, rw);
        }
        goto again;
    }
    // 为新请求赋值
    req->hard_sector = req->sector = sector;
    // 将新请求添加到调度队列及调度算法的私有队列中
    add_request(q, req);
out:
    if (bio_sync(bio))
        __generic_unplug_device(q);
    return 0;
end_io:
    bio_endio(bio, nr_sectors << 9, err);
    return 0;
}

3.3 请求出队

前文提到,为了请求的重排序与合并,队列有两个状态,阻塞状态(plug)和排空状态(drain)。当队列处于阻塞状态(plug)时,需要定时的切换到排空状态(drain)排空请求。有两个条件可以出发状态切换:1)时间,进入阻塞状态时,blk_plug_device函数会设置一个时钟,其默认值为3ms,时钟到时后,队列将自动切换到排空状态(drain)2)已积攒请求的数量,如果当前读写请求的数目达到unplug_thresh指定的阈值,则切换状态,使等待的请求得到处理。

三. 处理IO请求

IO调度程序又称为电梯算法,所有这类算法的目的都是减少磁盘的寻道时间(因为磁盘的寻道是机械运动,速度非常慢),其名称来源于Linux 0.11中使用的Linus电梯算法,该算法向电梯搭载乘客的顺序一样处理IO请求,该算法可以最大化磁盘的吞吐,但会带来严重的饿死问题。针对该问题,2.6版本中,在Linus电梯算法的基础上提出了4中电梯算法。

名称 描述 备注
deadline (最终期限) Linus电梯算法的改进版,增加了DDL,每个请求在DDL内会被响应。除调度队列外,还有4个辅助队列,其中读写各两个,分别按FIFO和块号排序。请求一开始不直接放入调度队列中,而是放入辅助队列中,等到排空请求时,再将辅助队列中的数据移动到调度队列并且执行。一般情况下,从块号排序的辅助队列中选取请求,这种情况下与Linus电梯算法类似,但如果有请求的计时器超时,则从FIFO队列中取出请求。由于读请求对性能的影响大,所以读请求的超时时限较长。 2.5.0成为默认,5.0被删除
as(预测) DDL调度算法的改进版,在读操作发出后等待6ms,期间如果有追加的读请求,这样做的好处是,进程在得到一个数据块之后很有可能继续请求下一个数据块。 2.6.0成为默认,2.6.33被删除
cfq(完全公平) 一共有64个辅助队列,用PID来hash索引每个进程对应队列,同一给定进程的请求,总是在同一队列中处理。时间片会分配到每个队列,内核使用一个轮转算法来处理各个队列。 2.6.18成为默认,5.0被删除
noop(不排序) 只合并,不排序。适用于flash等介质。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,653评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,321评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,833评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,472评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,306评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,274评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,658评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,335评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,638评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,697评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,454评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,311评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,699评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,986评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,254评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,647评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,847评论 2 335

推荐阅读更多精彩内容