Linux内核设计与实现——块I/O层

块I/O层

系统中能够随机访问固定大小数据片的硬件设备称作块设备,这些固定大小的数据片就称作块。最常见的块设备是硬盘,还有软盘驱动器、蓝光光驱和闪存等。它们都是以安装文件系统的方式使用的——这是块设备一般的访问方式。另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘。这两种类型的设备区别在于是否可以随机访问数据。

1. 块设备

块设备中最小的可寻址单元是扇区。扇区有时会称作“硬扇区”或“设备块”;块有时会称作“文件块”或“I/O块”。扇区的大小是设备的物理属性,一般是2的整数倍,最常见的是512字节,其他大小的扇区也很常见,比如,很多CD-ROM盘的扇区都是2KB。内核执行的所有磁盘操作都是按照块进行的,块必须是扇区大小的2的整数倍,并且要小于页面大小,通常块大小是512字节、1KB或4KB,一个页可以容纳一个或多个内存中的块。

2. 缓冲区和缓冲区头

  1. 当一个块被调入内存时(在读入后或等待写出时),要存储在一个缓冲区中。每个缓冲区与一个块对应,相当于是磁盘块在内存中的表示。每一个缓冲区都有一个对应的描述符,用buffer_head结构体表示,称作缓冲区头,在<linux/buffer_head.h> 中定义,它包含了内核操作缓冲区所需要的全部信息:
struct buffer_head {
        unsigned long b_state;					/* 缓冲区状态标志 */
        struct buffer_head *b_this_page;		/* 页面中的缓冲区 */
        struct page *b_page;					/* 存储缓冲区的页面 */
        sector_t b_blocknr;						/* 起始块号 */
        size_t b_size;							/* 映像的大小 */
        char *b_data;							/* 页面内的数据指针 */
        struct block_device *b_bdev;			/* 相关联的块设备 */
        bh_end_io_t *b_end_io;					/* I/O完成方法 */
        void *b_private;						/* io完成方法 */
        struct list_head b_assoc_buffers;		/* 相关的映射链表 */
        struct address_space *b_assoc_map;		/* 相关的地址空间 */
        atomic_t b_count;						/* 缓冲区使用计数 */
};
  1. 与缓冲区对应的磁盘物理块由b_blocknr-th域索引,该值是b_bdev域指明的块设备中的逻辑块号。b_state域表示缓冲区的状态,可以是表14-1中一种标志或多种标志的组合。合法的标志存放在bh_state_bits枚举中,在<linux/buffer_head.h> 中定义。
    在这里插入图片描述
    ​ a. bh_state_bits列表还包含了一个特殊标志——BH_PrivateStart,该标志不是可用状态标志,它指明可被其他代码使用的起始 位,块I/O层不会使用BH_PrivateStart或更高的位。只要不与块I/O层的专用位发生冲突,驱动程序就可以在这些位中定义自己 的状态标志。

​ b. b_count域表示缓冲区的使用记数,可通过两个定义在<linux/buffer_head.h>中的内联函数对此域进行增减。

static inline void get_bh(struct buffer_head *bh)
{
		atomic_inc(&bh->b_count);
}

static inline void put_bh(struct buffer_head *bh)
{
		atomic_dec(&bh->b_count);
}

​ 操作缓冲区头前应该使用get_bh()增加缓冲区头的引用计数;完成缓冲区头的操作后必须使用put_bh()减少引用计数。

3. bio结构体

内核中块I/O操作的基本容器由bio结构体表示,定义在<linux/bio.h>中。该结构体代表了正在现场的(活动的)以片断链表形式组织的块I/O操作,一个片段是一小块连续的内存缓冲区。这样的向量I/O就是聚散I/O。下面给出bio结构体和各个域的描述:

struct bio {
        sector_t 					bi_sector;				/* 磁盘上相关的扇区 */
        struct bio 					*bi_next;				/* 请求链表 */
        struct block_device 		*bi_bdev;				/* 相关的块设备 */
        unsigned long 				bi_flags;				/* 状态和命令标志 */
        unsigned long 				bi_rw;					/* 读还是写 */
        unsigned short 				bi_vcnt;				/* bio_vecs偏移的个数 */
        unsigned short 				bi_idx;					/* bio_io_vect的当前索引 */
        unsigned short 				bi_phys_segments;		/* 结合后的片断数目 */
        unsigned int 				bi_size;				/* I/O计数 */
        unsigned int 				bi_seg_front_size;		/* 第一个可合并的段大小 */
        unsigned int 				bi_seg_back_size;		/* 最后一个可合并的段大小 */
        unsigned int 				bi_max_vecs;			/* bio_vecs数目上限 */
        unsigned int 				bi_comp_cpu;			/* 结束CPU */
        atomic_t 					bi_cnt;					/* 使用计数 */
        struct bio_vec 				*bi_io_vec;				/* bio_vecs链表 */
        bio_end_io_t 				*bi_end_io;				/* I/O完成方法 */
        void 						*bi_private;			/* 拥有者的私有方法 */
        bio_destructor_t 			*bi_destructor;			/* 撤销方怯 */
        struct bio_vec 				bi_inline_vecs[0];		/* 内嵌bio向量 */
};

(1) I/O向量

  1. bi_io_vec指向一个bio_vec结构体链表——包含一个特定I/O操作所需要使用到的所有片段。每个bio_vec结构描述一个特定的片段:片段所在的物理页、块在物理页中的偏移位置、从给定偏移量开始的块长度。整个bi_io_vec结构体数组表示了一个完整的缓冲区。bio_vec结构定义在<linux/bio.h>中:
struct bio_vec {
        /* 指向这个缓冲区所驻留的物理页 */
        struct page *bv_page;

        /* 这个缓冲区以字节为单位的大小 */
        unsigned int bv_len;

        /* 缓冲区所驻留的页中以字节为单位的偏移量 */
        unsigned int by_offset;
};
  1. bi_idx域指向数组中的当前bio_vec片段,块I/O层通过它跟踪块I/O操作的完成进度。该域还可以分割bio结构体,像冗余廉价磁盘阵列(RAID,为了提高性能和可靠性,将单个磁盘的卷扩展到多个磁盘上)这样的驱动器可以把单独的bio结构体(原本是为单个设备使用准备的),分割到RAID阵列中的各个硬盘上去。

  2. bi_cnt域记录bio结构体的使用计数,如果该域值减为0,就应该撤销该bio结构体,并释放它占用的内存。通过下面两个函数管理使用计数,前者增加使用计数,后者减少使用计数。在操作正在活动的bio结构体时要先增加使用计数;操作完毕后要减少使用计数:

void bio_get(struct bio *bio)
void bio_put(struct bio *bio)
  1. bi_private域是一个属于拥有者(创建者)的私有域,只有创建了bio结构的拥有者可以读写该域。

bio结构体处理的是物理页而不是直接指针,很容易处理高端内存。bio结构体既可以代表普通页I/O,也可以代表直接I/O(不通过页高速缓存的I/O操作)。

4. 请求队列

  1. 块设备将它们挂起的块I/O请求保存在请求队列中,由reques_queue结构体表示,定义在<linux/blkdev.h>中,包含一个双向请求链表以及相关控制信息。通过内核中像文件系统这样高层的代码将请求加入到队列中。请求队列只要不为空,队列对应的块设备驱动程序就会从队列头获取请求,将其送入对应的块设备上。请求队列表中的每一项都是一个单独的请求,由reques结构体表示。

  2. 队列中的请求由结构体request表示,定义在<linux/blkdev.h> 中。一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。虽然磁盘上的块必须连续,但是在内存中这些块不一定要连续。

5. I/O调度程序

内核在提交请求前,先执行名为合并与排序的预操作。在内核中负责提交I/O请求的子系统称为I/O调度程序。I/O调度程序将磁盘I/O资源分配给系统中所有挂起的块I/O请求。进程调度程序和I/O调度程序都是将一个资源虚拟给多个对象,对进程调度程序来说,处理器被虚拟并被系统中的运行进程共享。这种虚拟提供给用户的就是多任务和分时操作系统,像Unix系统。I/O调度程序虚拟块设备给多个磁盘请求,以降低磁盘寻址时间。

(1) Linus电梯

当一个请求加入到队列中时,可能发生四种操作:

a. 如果队列中已存在一个对相邻磁盘扇区操作的请求,新请求将和这个已存在的请求合并成一个请求。

b. 如果队列中存在一个驻留时间过长的请求,新请求将被插入到队列尾部,防止其他旧的请求饥饿。

c. 如果队列中以扇区方向为序存在合适的插入位置,新请求将被插入到该位置。

d. 如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部。

(2) 最后期限I/O调度程序

在最后期限I/O调度程序中,每个请求都有一个超时时间。默认情况下,读请求的超时时间是500ms,写请求的是5s。最后期限I/O调度请求类似Linus电梯,也以磁盘物理位置为次序维护请求队列,这个队列称为排序队列。当一个新请求递交给排序队列时,最后期限I/O调度程序在执行合并和插入请求时类似于Linus电梯,但是最后期限I/O调度程序同时会以请求类型为依据将它们插入到额外队列中。读请求按次序被插入到特定的读FIFO队列中;写请求被插入到特定的写FIFO队列中;而对于普通操作,最后期限I/O调度程序将请求从排序队列的头部取下,推入到派发队列中,派发队列将请求提交给磁盘驱动。如果在写FIFO队列头或是在读FIFO队列头的请求超时,最后期限I/O调度程序便从FIFO队列中提取请求进行服务。最后期限I/O调度程序的实现在block/deadline-iosched.c中。

(3) 预测I/O调度程序

  1. 预测I/O调度的基础是最后期限I/O调度程序,预测I/O调度程序增加了预测启发能力。预测I/O调度程序的请求提交后不直接返回处理其他请求,而是有意空闲片刻(实际空闲时间可以设置,默认为6ms)。这几ms中,对相邻磁盘位置操作的请求都会立刻得到处理。

  2. 预测I/O调度程序跟踪并统计每个应用程序块I/O操作的习惯行为,以便正确预测应用程序的未来行为。

  3. 预测I/O调度程序的实现在文件内核源代码树的block/as-iosched.c中,它是Linux内核中缺省的I/O调度程序,对大多数工作负荷都执行良好,对服务器也是理想的。不过在某些非常见而又有严格工作负荷的服务器(包括数据库挖掘服务器)上执行的效果不好。

(4) 完全公正的排队I/O调度程序

  1. 完全公正的排队I/O调度程序(Complete Fair Queuing, CFQ)把进入的I/O请求放入特定的队列中,这种队列是根据引起I/O请求的进程组织的,例如,来自foo进程的I/O请求进入foo队列,来自bar进程的I/O请求进入bar队列。在每个队列中,刚进入的请求与相邻请求合并在一起,并进行插入分类。

  2. CFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取请求数(默认值为4,可以进行配置),然后进行下一轮调度。完全公正的排队I/O调度程序位于block/cfq-iosched.c,主要给桌面工作负荷使用,但在几乎所有的工作负荷中都能很好地执行。

(5) 空操作的I/O调度程序

空操作I/O调度程序位于block/noop_iosched.c,是专为随机访问设备而设计的,如闪存卡。空操作(Noop)I/O调度程序不进行排序和预寻址操作。除了合并操作,空操作I/O调度程序只是维护请求队列以近乎FIFO的顺序排列。如果块设备只有一点或者没有“寻道”的负担,那么空操作I/O调度程序是理想的候选者。

(6) I/O调度程序的选择

作为缺省,块设备使用完全公平的I/O调度程序。在启动时,可以通过命令行选项elevator=foo来覆盖缺省,这里foo是一个有效而激活的I/O调度程序,例如,内核命令行选项elevator=as会启用预测I/O调度程序给所有的块设备。

注:本文摘自《Linux内核设计与实现(第三版)》
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值