Linux 块设备IO栈详解

 

一、概述

系统能够随机(不按顺序)的访问固定大小数据片的设备,被称为块设备。文件系统定义了块设备数据的格式,IO栈定义了传输块设备数据的流程。IO栈介于文件系统与块设备之间,起到承上启下的作用。IO层对文件数据请求进行转换、合并、排序、调度,然后发往块设备。

 

 

 

二、块设备相关概念

扇区:块设备中最小的可寻址单元是扇区。扇区大小一般是2的整数倍,而最常见的大小是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元—块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次传输多个扇区。虽然大多数块设备的扇区大小都是512字节。

块索引(sector indices)在32或64位系统上的变量类型为sector_t。

 

块(block):对于硬件设备来说,扇区是基本的数据传输单元;而对于VFS(虚拟文件系统),块是基本的数据传输单元。例如,当内核访问文件的数据时,它首先从磁盘上读取一个块,这个块有文件的inode,该块对应于磁盘上一个或多个扇区。由于扇区是块设备的最小可寻址单元,所以块不能比扇区还小,只能整数倍于扇区大小。另外内核(对有扇区的硬件设备)还要求块大小是2的整数倍,而且不能超过一页的长度。所以对块大小的最终要求是,必须是扇区大小的2的整数倍,并且要小于页面大小。所以通常块大小是512字节、1K或4K。至于块(block)大小,与具体的硬件设备无关。在块设备上创建文件系统时,管理员可以选择块大小;因此在同一个磁盘上,多个分区可能使用不同的块大小。

 

 

三、通用块设备层(generic block IO layer)

通用块设备层(Generic Block Layer)是内核的一个组成部分,它处理系统所有对块设备的请求。

 

• 将数据存放在高端内存—当CPU访问高端内存的数据时,页就被临时映射到内核的线性地址空间,    然后解除映射。

 

• 实现零拷贝(zero-copy),即磁盘数据直接拷贝到用户地址空间,而不需要先拷贝到内核地址空间。实际上就是,内核进行I/O数据传输使用的页面被映射到用户进程的地址空间中。

 

• 管理逻辑卷,如LVM(Logical Volume Manager)和RAID(Redundant Array of Inexpensive Disks)。

 

• 使用现在的磁盘控制器新特性,如大的磁盘缓存、增强的DMA功能、板上I/O请求调度等。

 

3.1 缓冲区和缓冲区头(buffer and buffer_head)

当一个块被调入内存时(也就是,在读入或等待写出时),它要存储在一个缓冲区中。

每个块都要有自己的缓冲区(page cache),内核使用内存上的这块缓冲区来保存块数据。当内核从块设备上读取一个块时,就用从硬件上读取的数据填充块缓冲区;同理,当内核向块设备写数据时,就用块缓冲区中的数据写到块设备上。

于内核在处理这些块数据时,需要一些相关的控制信息(比如块属于哪一个块设备,块对应于哪一个缓冲区等),所以每个缓冲区都有一个对应的描述符。Buffer_head正式扮演这个角色,从缓冲区到块的映射关系,其所描述块的状态(脏,干净,过期等),但是它并不与底层的块驱动程序打交道。具体的数据由bio结构体负责,后面会具体讲。

每个缓冲区都有一个缓冲区头(buffer head),描述类型为buffer_head,这个数据结构里包含了内核处理缓冲区所需要的信息;于是,在每个缓冲区上操作之前,内核都要先检查它的缓冲区头。buffer_head的定义在文件include/linux/buffer_head.h

 

 

 

  1. buffer_head是磁盘块的一个抽象,一个buffer_head对应一个磁盘块,buffer_head中保存对应的磁盘号
    2. buffer_head把page与磁盘块联系起来,由于page和磁盘块的大小可能不一样,所以一个page可能管理多个buffer_head
    这里假设page大小4K,块大小为1K, buffer_head,page和磁盘块关系如下

 

 

 

 

b_state域表示缓冲区的状态,可以是表1中一种标志或多种标志的组合。合法的标志存放在bh_state_bits枚举中,其定义也在文件include/linux/buffer_head.h中。

 

 

 

 

 

 

3.2 bio结构体

目前内核中块I/O操作的基本容器由bio结构体表示,定义在<linux/bio.h>中。该结构代表了正在现场(活动)的以片段(segment)链表形式组织的块I/O操作。一个片段是一小块连续的内存缓冲区。通过片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保障I/O操作的执行。像这样的向量I/O就是所谓的聚散I/O。
bio结构体中最重要的几个域是bi_io_ves、bi_vcnt、和bi_idx。

总之,每一个块I/O请求都是通过一个bio结构体表示。每个请求包含一个或多个块,这些块储存在bio_vec结构体数组中。

buffer head用来管理每一个文件块,指向buffer page所包含的buffer;每个buffer包含几个连续的sector。每个块设备传输的request中包含至少一个bio,每个bio都包含几个bio_vec,每个bio_vec都包含一个segment(ulk3中这么称呼),每个segment包含几个连续的buffer。 要写盘时,通过buffer_head为bio结构体赋值,以保证io操作能正确执行。
    简言之,buffer_head是用来管理buffer,而bio是用来传输buffer的。bio为通用层的主要数据结构,既描述了磁盘的位置,又描述了内存的位置,是上层内核vfs与下层驱动的连接纽带。

有几个重点:

第一:

一个BIO所请求的数据在块设备中是连续的,对于不连续的数据块需要放到多个BIO中。

第二:

一个BIO所携带的数据大小是有上限的,该上限值由bi_max_vecs间接指定,超过上限的数据块必须放到多个BIO中。

第三:

使用bio_for_each_segment来遍历 bio_vec

 

第四:

BIO、bi_io_vec、page之间的关系

 

 

 

迭代器 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 */

};

 

 

 

 

 

1. 块设备的上层抽象 - block_device

    block_device是伪文件系统bdevfs中对块设备或设备分区的抽象,它唯一的对应于一个设备号(对分区来说,主设备号相同,次设备号不同)。

    它的详细内容如下(include/linux/fs.h):

struct block_device {

    dev_t            bd_dev; /* 对应底层设备的设备号 */

    int            bd_openers; /* 该设备同时被多少进程打开 */

    struct inode *        bd_inode;    /* 块设备的inod,可利用bd_dev通过bdget获得 */

    struct super_block *    bd_super; /* 文件系统的超级块信息 */

    struct mutex        bd_mutex;    /* open/close mutex */

    struct list_head    bd_inodes;

    void *            bd_claiming;

    void *            bd_holder;

    int            bd_holders;

    bool            bd_write_holder;

#ifdef CONFIG_SYSFS

    struct list_head    bd_holder_disks;

#endif

    /* 首先block_device既可以是gendisk的抽象,又可以是hd_struct(分区)的抽象

     * 当作为分区的抽象时,bd_contains指向了该分区所属的gendisk对应的block_device

     * 当作为gendisk的抽象时,bd_contains指向自身的block_device

     */

    struct block_device *    bd_contains;

    unsigned        bd_block_size; /* 块的大小 */

    struct hd_struct *    bd_part; /* 指向分区指针,对于gendisk,指向内置的分区0 */

    /* number of times partitions within this device have been opened. */

    unsigned        bd_part_count; /* 该设备的所有分区同时被打开的次数 */

    int            bd_invalidated; /* 置1表示内存中的分区信息无效,下次打开设备时需要重新扫描分区表 */

    struct gendisk *    bd_disk; /* 通用磁盘抽象,当该block_device作为分区抽象时,指向该分区所属的gendisk,当作为gendisk的抽象时,指向自身 */

    struct list_head    bd_list;

    /*

     * Private data. You must have bd_claim'ed the block_device

     * to use this. NOTE: bd_claim allows an owner to claim

     * the same device multiple times, the owner must take special

     * care to not mess up bd_private for that case.

     */

    unsigned long        bd_private;

 

    /* The counter of freeze processes */

    int            bd_fsfreeze_count;

    /* Mutex for freeze */

    struct mutex        bd_fsfreeze_mutex;

};

 

 

 

 

 

2. 通用磁盘描述 - gendisk

    gendisk是对通用磁盘的一个描述,与真正的底层物理设备相关联。其详细内容如下(include/linux/genhd.h):

 

struct gendisk {

    /* major, first_minor and minors are input parameters only,

     * don't use directly. Use disk_devt() and disk_max_parts().

     */

    int major;            /* 主设备号 */

    int first_minor; /* 第一个次设备号 */

    /* 表示分区的个数,分区号从1开始,0表示gendisk本身 */

    int minors; /* maximum number of minors, =1 for

                                         * disks that can't be partitioned. */

 

    char disk_name[DISK_NAME_LEN];    /* 磁盘的名称,用于在sysfs和/proc/partitions中表示该磁盘 */

    char *(*devnode)(struct gendisk *gd, mode_t *mode);

 

    unsigned int events;        /* supported events */

    unsigned int async_events;    /* async events, subset of all */

 

    /* Array of pointers to partitions indexed by partno.

     * Protected with matching bdev lock but stat and other

     * non-critical accesses use RCU. Always access through

     * helpers.

     */

    struct disk_part_tbl __rcu *part_tbl; /* 分区表 */

    struct hd_struct part0; /* 用于表示gendisk本身 */

 

    const struct block_device_operations *fops; /* 指向底层具体设备的操作函数,一般由用户驱动程序实现,如ramdisk驱动实现的fops为brd_fops */

    struct request_queue *queue; /* 该disk关联的请求队列 */

    void *private_data; /* 私有数据,用于提供给用户驱动程序使用 */

 

    int flags;

    struct device *driverfs_dev; // FIXME: remove

    struct kobject *slave_dir;

 

    struct timer_rand_state *random;

    atomic_t sync_io;        /* RAID */

    struct disk_events *ev;

#ifdef CONFIG_BLK_DEV_INTEGRITY

    struct blk_integrity *integrity;

#endif

    int node_id;

};

 

 

3. 磁盘分区描述 - hd_struct

    hd_struct用于描述一个具体的磁盘分区,其详细内容如下(include/linux/genhd.h):

struct hd_struct {

    sector_t start_sect; /* 该分区的起始扇区号 */

    sector_t nr_sects; /* 该分区的扇区个数,也就是分区容量 */

    sector_t alignment_offset;

    unsigned int discard_alignment;

    struct device __dev;

    struct kobject *holder_dir;

    int policy, partno; /* 该分区的分区号 */

    struct partition_meta_info *info;

#ifdef CONFIG_FAIL_MAKE_REQUEST

    int make_it_fail;

#endif

    unsigned long stamp;

    atomic_t in_flight[2];

#ifdef    CONFIG_SMP

    struct disk_stats __percpu *dkstats;

#else

    struct disk_stats dkstats;

#endif

    atomic_t ref;

    struct rcu_head rcu_head;

};

 

 

 

4. 三者之间的关系

    如果你能读到我这篇文章,说明你应该很熟悉软件开发中的管理模式,下面我就类比软件开发的管理模式来说明三者之间的关系,可能不是十分贴切,但希望对你了解三者的关系能提供一些帮助。

    block_device就相当于每个程序猿的档案信息(如姓名、电话、邮件、职位以及leader等等),gendisk相当于一个项目组,而hd_struct相当于项目组中的每一个程序猿。如何解释呢?^_^

    想象一下,对于人力管理者(相当于VFS)来说,他其实并不关心底下干活的是哪个程序猿,是高富帅还是矮矬穷,是美女还是帅哥,他只需要知道你的档案信息(block_device)就可以了,因为只要有了你的档案信息,在需要你的时候就随时可以找到你(程序猿就是这么悲催)。

    而对于一个项目组(gendisk)来说,里面一个或多个程序猿(hd_struct)。因为项目组至少有一个leader吧,而leader本质上也是一个程序猿(相当于struct hd_struct part0)。当一个项目比较庞大时,可能一个leader会带领多个兄弟(就像一个硬盘管理着多个分区),然而如果是一个迷你项目,可能只需要项目组leader一个人就搞定了(就像一个磁盘不进行分区)。

    是不是有那么点意思?^_^

    当人力管理需要找某个程序猿(hd_struct或者part0)时,只需要找到他的档案信息(block_device)就可以了,因为二者是一对一的关系,而且根据程序猿找到他的项目组(gendisk)是不是也是一件很容易的事情?同理,一旦找到了项目组(gendisk),那么里面的所有程序猿(hd_struct)是不是也非常明朗了?总之一句话,档案信息(block_device)充当了人力管理(相当于VFS)和项目组成员(gendisk、hd_struct)之间的桥梁。

    不知道你有没有看明白,如果还有些混乱直接看下面的图好了:

 

 

 

 

 

 

struct request_queue {

    struct list_head    queue_head; //待处理请求的链表,请求队列中的请求用链表组织在一起

    struct request      *last_merge; //指向队列中首先可能合并的请求描述符

    struct elevator_queue   *elevator;//指向elevator对象的指针(电梯算法)

    struct request_list root_rl;///为分配请求描述符所使用的数据结构

 

    request_fn_proc     *request_fn;//实现驱动程序的策略例程入口点的方法,由他处理队列中请求

    make_request_fn     *make_request_fn;//将一个新请求插入请求队列时调用的方法

    prep_rq_fn      *prep_rq_fn; //该方法把这个处理请求的命令发送给硬件设备

 

    softirq_done_fn     *softirq_done_fn;

    rq_timed_out_fn     *rq_timed_out_fn;

 

    sector_t        end_sector;

    struct request      *boundary_rq;

    struct delayed_work delay_work;

 

    struct backing_dev_info backing_dev_info;

 

    /*

     * The queue owner gets to use this for whatever they like.

     * ll_rw_blk doesn't touch it.

     */

    void            *queuedata;

    spinlock_t      __queue_lock; //请求队列锁

    spinlock_t      *queue_lock; //指向请求队列锁的指针

 

 

    unsigned long       nr_requests;/* 请求队列中允许的最大请求数 */

 

    struct queue_limits limits;//队列的其他限制

};

 

 

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值