Linux I/O
首发于Linux I/O
Linux IO请求处理流程-bio和request

Linux IO请求处理流程-bio和request

说明

从这里开始,我们要深入进入每个IO请求内部,探测它的生命轨迹。

数据结构

与块设备层IO相关的主要数据结构有以下两个:

struct bio {
    sector_t     bi_sector;   
    struct bio    *bi_next;       /* request queue link */
    struct block_device *bi_bdev;
    unsigned long       bi_flags; /* status,command,etc */
    unsigned long       bi_rw;
    unsigned short      bi_vcnt;  /* how many bio_vec's */
    unsigned short      bi_idx;         
    unsigned int        bi_phys_segments;
    ......
    // bio完成时的回调函数
    bio_end_io_t            *bi_end_io;
    void                    *bi_private;
    bio_destructor_t        *bi_destructor;
    struct bio_vec          bi_inline_vecs[0];
 };

struct request {
    struct list_head queuelist;
    struct call_single_data csd;

    struct request_queue *q;

    unsigned int cmd_flags;
    enum rq_cmd_type_bits cmd_type;
    unsigned long atomic_flags;
    ......
}


将linux io相关最重要的两个数据结构列在这里,不作过多分析,都很简单。以后需要再仔细分析吧。

提交bio请求


请求的提交是由上层文件系统发起的,文件系统准备好所需读写参数(初始化bio),接下来调用Linux块设备层提供的submit_bio接口提交读写请求:

void submit_bio(int rw, struct bio *bio)
{
    bio->bi_rw |= rw;
    ......
    // 根据bio构造request,接下来的下层主要与request打交道
    generic_make_request(bio);
}

void generic_make_request(struct bio *bio)
{
    struct bio_list bio_list_on_stack; 
    if (!generic_make_request_checks(bio))
        return;

    // 保证每个进程同时只有一个线程在执行make_request_fn
    // 否则在某些情况下有可能出错
    // 但是为什么这里不加锁呢?加入线程A和B同时进来
    // 不是就出错了嘛?
    if (current->bio_list) {
        bio_list_add(current->bio_list, bio);
        return;
    }

    bio_list_init(&bio_list_on_stack);
    current->bio_list = &bio_list_on_stack;
    do {
        // bio->bi_bdev指向bio请求所属块设备描述符
        // 进而可以找到gendisk,找到其request_queue
        // 最后调用request_queue->make_request_fn方法
        // 这个方法被初始化为__make_request
        struct request_queue *q = bdev_get_queue(bio->bi_bdev);
        q->make_request_fn(q, bio);
        bio = bio_list_pop(current->bio_list);
    } while (bio);
    current->bio_list = NULL; /* deactivate */
}


注意:到这里开始出现了OO编程思想:调用了request_queue的make_request_fn()方法,我们不作过多纠结,在后面会仔细梳理request_queue内容。但是我们必须知道,对于scsi磁盘,该request_queue的make_request_fn被初始化为__make_request

bio处理-合并


经过上面的处理,每个bio到达了磁盘设备的request_queue,接下来需要对该bio进行深加工,为什么需要深加工,提高IO效率,谁叫你是巨人的阿喀琉斯之踵呢?

static int __make_request(struct request_queue *q, struct bio *bio)
{
    struct request *req;
    int el_ret;

    spin_lock_irq(q->queue_lock);
    // 尝试进行请求合并,将bio合并至请求req中
    // 返回值el_ret表明了该bio需要合并的方向:前向合并还是后向合并   
    el_ret = elv_merge(q, &req, bio);
    switch (el_ret) {
    // 可后向合并
    case ELEVATOR_BACK_MERGE:
        ......
    // 可前向合并    
    case ELEVATOR_FRONT_MERGE:
    // 无法做合并
    default:
    }
    ......
}


这里的关键在于将bio合并至已存在request内,所谓的合并指的是该bio所请求的io是否与当前已有request在物理磁盘块上连续,如果是,无需分配新的request,直接将该请求添加至已有request,这样一次便可传输更多数据,提升IO效率,这其实也是整个IO系统的核心所在。
根据elv_merge()的判断结果,可能会出现以下三种情况:

1. 可以后向合并:该bio可以合并至某个request的尾部;
2. 可以前向合并:该bio可以合并至某个request的头部;
3. 无法合并:该bio无法与任何request进行合并。


我们以“后向合并”和“无法合并”为例阐述具体实现逻辑。

后向合并

case ELEVATOR_BACK_MERGE:
        BUG_ON(!rq_mergeable(req));
        // 检查该req是否由于硬件限制而无法再进行合并
        // elv_merge()只能判断是否可以进行合并
        if (!ll_back_merge_fn(q, req, bio))
            break;

        // 执行后向合并,将bio链接到req的bio链表的尾部
        req->biotail->bi_next = bio;
        req->biotail = bio;
        req->__data_len += bytes;
        req->ioprio = ioprio_best(req->ioprio, prio);
        if (!blk_rq_cpu_valid(req))
            req->cpu = bio->bi_comp_cpu;
        drive_stat_acct(req, 0);
        // 尚不知道这个函数到底干啥?
        // 目前好像只有CFQ算法用到了
        elv_bio_merged(q, req, bio);
        // 后向合并完成后检查req是否与下一个req可以继续合并
        // 如果可以则将两个request再作一次合并,好复杂
        if (!attempt_back_merge(q, req))
            // 因为做了request合并,可能需要调整request在具体
            // 调度算法中的位置,对于deadline算法来说,实现是
            // deadline_merged_request()
            // 它对于前向合并过的request,调整了其在RB树中的位置
            elv_merged_request(q, req, el_ret);
        goto out;


如果某个request可以做后向合并,那么:

1. 调用ll_back_merge_fn()判断是否可以真的合并,因为request内的数据大小可能受限于硬件;
2. 将bio添加到request的尾部,因为是后向合并;
3. 判断合并后的request是否可以与其他的request再做一次合并,调用了attempt_back_merge
4. 因为request做过合并,可能需要调整其在调度算法中的位置,调用了elv_merged_request,因为不同的调度算法可能内部实现不同,因此,内部实现其实是一个接口,由具体调度算法实现。

无法合并

get_rq:
    rw_flags = bio_data_dir(bio);
    if (sync)
        rw_flags |= REQ_SYNC;

    // 这里可能会陷入sleep 
    req = get_request_wait(q, rw_flags, bio);
    // 根据bio初始化req
    init_request_from_bio(req, bio);

    spin_lock_irq(q->queue_lock);
    if (test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags) ||
        bio_flagged(bio, BIO_CPU_AFFINE))
        req->cpu = blk_cpu_to_group(smp_processor_id());
    if (queue_should_plug(q) && elv_queue_empty(q))
        blk_plug_device(q);

    /* insert the request into the elevator */
    drive_stat_acct(req, 1);
    // 将req添加到调度算法队列之中
    // 这个函数需要处理判断req是否可以合并至现有req逻辑
    // 因为在get_request_wait返回后,这个bio已经可以合并了
    __elv_add_request(q, req, where, 0);

无法合并的bio请求的处理逻辑就相对简单:为bio分配一个request结构,注意:这里可能会阻塞。接下来初始化该request,将request添加到queue(__elv_add_request)。
需要注意的一点就是:由于在申请request的时候可能会阻塞,在此期间,其他进程提交的bio可能与本次bio在物理位置上连续,因此在__elv_add_request()内必须判断该request是否可合并,而不仅仅将其添加到request_queue中就完事。
由于前向合并和后向合并的逻辑极其相似,我们就不再此赘述了,感兴趣的读者可自行分析。

总结


在这里,我们着重分析了上层请求bio是如何被下发并添加到系统的request中。接下来我们要来仔细分析相关request是如何被下发到具体的物理设备执行。

编辑于 2018-07-07

文章被以下专栏收录