Linux 通用块层 bio 详解

2018年8月10日 2.07k 次阅读 0 条评论 1 人点赞

Linux Block 层在 Linux 内核设计之初就作为几大子系统存在,当然这也是得益于他的前辈 Unix 等优秀的设计。作为 IO 子系统的中间层,他为上层输出接口,为下层提供数据,像个勤劳的小蜜蜂,本文介绍通用块层中的最具传奇色彩的 bio,他就像是一个原子,是在整个 block 层的最小单位,不可分割。

bio 的组成

作为最小单位以及传输介质,那么具体应该长得如何?他又承载着那些信息?

struct bio {
        struct bio          *bi_next;   /* request queue link */
        struct block_device *bi_bdev;
        unsigned int        bi_flags;   /* status, command, etc */
        int                 bi_error;
        unsigned long       bi_rw;      /* 末尾 bit 表示 READ/WRITE,
                                         * 起始 bit 表示优先级
                                         */
        struct bvec_iter    bi_iter;

        /* 当完成物理地址合并之后剩余的段的数量 */
        unsigned int        bi_phys_segments;

        /*
         * To keep track of the max segment size, we account for the
         * sizes of the first and last mergeable segments in this bio.
         */
        unsigned int        bi_seg_front_size;
        unsigned int        bi_seg_back_size;

        /* 关联 bio 的数量 */
        atomic_t            __bi_remaining;
        bio_end_io_t        *bi_end_io;
        void                *bi_private;
        unsigned short      bi_vcnt;    /* how many bio_vec's */

        /*
         * Everything starting with bi_max_vecs will be preserved by bio_reset()
         */
        unsigned short      bi_max_vecs;    /* max bvl_vecs we can hold */
        /* 当前 bio 的引用计数,当该数据为 0 时才可以 free */
        atomic_t            __bi_cnt;       /* pin count */
        struct bio_vec      *bi_io_vec;     /* the actual vec list */
        struct bio_set      *bi_pool;

        /*
         * We can inline a number of vecs at the end of the bio, to avoid
         * double allocations for a small number of bio_vecs. This member
         * MUST obviously be kept at the very end of the bio.
         * 表示跟在 bio 后面的数据集合
         */
        struct bio_vec      bi_inline_vecs[0];
};

bio 结构体包含了大量的基础信息,这些都是一个基本单元的属性,他们代表着当前这个 bio 的状态,比如是读还是写或者是一些特殊的操作命令等,在 bio 的尾部携带了一个 bi_inline_vecs 数组1,这就是一个 bio 数据所在部分,相当于这个结构体描述的都只是元数据部分,实际数据都包含在紧跟其后的 bio_vec 中,那么这个 bio_vec 何许人也?

最小数据单位 bio_vec

struct bio_vec {
        struct page     *bv_page;
        unsigned int    bv_len;
        unsigned int    bv_offset;
};

顾名思义,bio_vec 就是一个 bio 的数据容器,专门用来保存 bio 的数据,当然他是这个 bio 大集体的一个最小项,刚刚说了 bio 是通用块层的最小集,而这个 bio_vec 则是组成 bio 数据的最小单位,他包含了一块数据所在的页,这块数据所在的页内偏移以及长度,通过这些信息就可以很清晰的描述数据具体位于什么位置,通过对这些数据的整合,可以将他们添加到 SGL(散列表) 中直接发送给后端硬件设备。

迭代器 bvec_iter

寻遍整个 bio 发现居然没有携带需要下盘的扇区编号以及当前 bio 的大小,这个很尴尬,但确实如此,相当于 bio 作为一辆汽车,他携带了货物但是没告诉他目的地这不是完蛋了吗?不是,真正的目的地保存在这个 bvec_iter 中,作为一个迭代器,自然他的使命就是用来遍历 bvec,也就是 bio 数据区。那么他好比就是这辆 bio 汽车的货物分拣员,自然我的目的地不必贴到车上,直接告诉分拣员也是可以的,因为后面的事情可不是这辆汽车再做,而是分拣员需要逐个卸货的时候用。一起来看看迭代器长什么样?

struct bvec_iter {
        sector_t        bi_sector;      /* device address in 512 byte sectors */
        unsigned int    bi_size;        /* residual I/O count */
        unsigned int    bi_idx;         /* current index into bvl_vec */
        unsigned int    bi_bvec_done;   /* number of bytes completed in current bvec */
};

bio 的逻辑架构图

关于 bi_io_vec 和 bi_inline_vecs 的关系

细心的朋友会发现,一个 bio 会有两个字段用来描述 bio 携带的数据,对于没有深入了解的人来讲会比较困惑,其实也比较容易理解,bio 结构在申请内存的时候会多申请 4 个 bvec 的位置跟随 bio 结构体上,这就是 bi_inline_vecs ,他是用来存放内联数据(不能太多,因为申请了不用就会造成内存浪费),当往该 bio 注入数据时,小块的 bio 会直接使用这个内联的数据区域保存小于 4 个 bvec 的信息,而无需重新调用 kmalloc 申请内存,减少了一次内存申请操作,牺牲一小部分内存达到对小 bio 的加速(内存申请过程很长且相对较慢),然后为了兼容后端的其他接口,会直接将 bi_io_vec 指针直接指向 bi_inline_vecs 所在的位置,目的就是告诉后端,数据存放在 bi_inline_vecs 位置上。如果超过 4 个以上的 bvec 才足够完整描述本次 IO 的全部数据,那么 bi_inline_vecs 字段是被直接忽略的,尽管他内存很早就申请好了,但是并不会被采用,而是需要重新自内存管理单元重新申请足额的内存保存这一次的 IO 数据,这就是前面说的一点点的内存浪费的原因。


  1. 这是一个 0 长度数组,这不是一个标准 C 的做法,用 std=c99 肯定是编译不过去的,这是一个 std=gnu99 扩展标准的一个特定用法,主要是在结构体中插入一个可以标识紧跟之后的内存的方法,这种方式在 Linux 内核中很常见,是个很巧妙的办法。 ↩︎
标签:
最后编辑:2019年5月17日

文章评论(0)