工做学习中,没有比图表更好的东西了(虽然不少人在嘲笑PPT),尤为是描述精准的图表。当你想画图说明一个结构或一个流程时,必须对其已经充分理解。而在讲解一张图时,也必须对其有基本的理解。这真的不简单,反正对我来讲是这样。关于Linux Storage架构,就有一张描述很精准的图,“Linux Storage Stack Diagram”。这张图总结的实在是太好了,Storage涉及的模块都有描述,让学习者能清晰的了解复杂的系统。本文试图对该图的各部分作个简介,但不会涉及具体的实现。前端
https://www.thomas-krenn.com/...linux
图中使用颜色来区分不一样的组成部分。算法
VFS是Linux内核提供的一个虚拟文件系统层。VFS提供给用户层一些标准的系统调用来操做文件系统,如open()、read()、write()等,让用户态应用无需关心底层的文件系统和存储介质。同时VFS还要对底层文件系统进行约束,提供统一的抽象接口和操做方式。后端
Linux支持的文件系统众多,大体能够分为如下几类。缓存
Block Layer是Linux Storage系统中的中间层,链接着文件系统和块设备。它将上层文件系统的读写请求抽象为BIOs,经过调度策略将BIOs传输给设备。Block Layer包含图中的蓝绿色、黄色和中间BIOs传输过程。网络
Linux系统在打开文件时能够经过O_DIRECT标识来却别是否使用Page cache。当带有O_DIRECT时,I/O读写会绕过cache,直接访问块设备。不然,读写须要经过Page cache进行,Page cache的主要行为以下。数据结构
BIO表明对Block设备的读写请求,在内核中使用一个结构体来描述。架构
struct bvec_iter { sector_t bi_sector; // 设备地址,以扇区(512字节)为单位 unsigned int bi_size; // 传输数据的大小,byte unsigned int bi_idx; // 当前在bvl_vec中的索引 unsigned int bi_bvec_done; // 当前bvec中已经完成的数据大小,byte }; struct bio { struct bio *bi_next; // request队列 struct block_device *bi_bdev; // 指向block设备 int bi_error; unsigned int bi_opf; // request标签 unsigned short bi_flags; // 状态,命令 unsigned short bi_ioprio; struct bvec_iter bi_iter; unsigned int bi_phys_segments; // 物理地址合并后,BIO中段的数量 /* * 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; // 最后一个可合并段的大小 atomic_t __bi_remaining; bio_end_io_t *bi_end_io; // BIO结束时的回调函数,通常用于通知调用者该BIO的完成状况 ...... unsigned short bi_vcnt; // bio_vec的计数 unsigned short bi_max_vecs; // bvl_vecs的最大数量 atomic_t __bi_cnt; // 使用计数 struct bio_vec *bi_io_vec; // vec list的指针 struct bio_set *bi_pool; ...... };
一个BIO构建完成后,就能够经过generic_make_request()来建立传输Request,将Request加入到请求队列中。请求队列在内核中有结构体request_queue来描述,它包含一个双向请求链表以及相关控制信息。请求链表中每一项都是一个Request,Request由BIOs组成,BIO中又可能包含不一样的Segment。由于一个BIO只能连续的磁盘块,但一个Request可能不连续的磁盘块,因此一个Request可能包含一个或多个BIOs。尽管BIO中的磁盘块是连续的,但它们在内存中多是不连续的,因此BIO中可能包含几个Segments。app
读写数据组织成请求队列后,就是访问磁盘的过程,这个过程由IO调度完成。BIOs访问的指定的磁盘扇区,首先要进行寻址的操做。寻址就是定位磁盘磁头到特定块上的某个位置,这个过程相对来讲很慢。为了优化寻址操做,内核既不会简单地按请求接收次序,也不会当即将其提交给磁盘,而是在提交前,先执行名为合并与排序的预操做,这种预操做能够极大地提升系统的总体性能。这就是IO调度须要完成的工做。socket
当前内核中,支持两种模式的IO调度器:single-queue和multi-queue。single-queue在图中标识为“I/O Scheduler”,multi-queue标识为blkmq。两者应该都是Scheduler,只是请求的组织方式不一样。
single-queue经过合并和排序来减小磁盘寻址时间。合并指将多个连续请求合成一个更大的IO请求,以便充分发挥硬件性能。排序使用电梯调度,将整个请求队列将按扇区增加方向有序排列。排列的目的不只是为了缩短单独一次请求的寻址时间,更重要的优化在于,经过保持磁盘头以直线方向移动,缩短了全部请求的磁盘寻址的时间。目前single-queue使用的调度策略包括:noop、deadline、cfq等。
早先的内核只有single-queue,当时存储设备主要时HDD,HDD的随机寻址性能不好,single-queue就能够知足传输需求。当SSD发展起来后,它的随机寻址性能很好,传输的瓶颈就转移到请求队列上。结合多核CPU,multi-queue被设计出来。multi-queue为每一个CPU core或socket配置一个Software queue,这也解决了single-queue中多核锁竞争的问题。若是存储设备支持并行多个Hardware dispatch queues,传输性能又会大幅度提高。目前multi-queue支持的调度策略包括:mq-deadline、bfq、kyber等。
设备文件是Linux系统访问硬件设备的接口,驱动程序将硬件设备抽象为设备文件,以便应用程序访问。设备驱动加载时在/dev/下建立设备文件描述符,若是是Block设备,同时会建立一个软连接到/dev/block/下,并根据设备号来命名。图中将Block设备分为如下几类。
图中橙色部分表示了Block设备所依赖的技术实现,多是硬件规范的软件实现,也多是一种软件架构。图中把SCSI和LIO单独圈出来,由于这两部分相对比较复杂。SCSI包含的硬件规范不少,最经常使用的是经过libata来访问HDD和SSD。
LIO(Linux-IO)是基于SCSI engine,实现了SCSI体系模型(SAM)中描述的SCSI Target。LIO在linux 2.6.38后引入内核,其支持的SAN技术包括Fibre Channel、FCoE、iSCSI、iSER 、SRP、USB等,同时还能为本机生成模拟的SCSI设备,以及为虚拟机提供基于virtio的SCSI设备。LIO使用户可以使用相对廉价的Linux系统实现SCSI、SAN的各类功能,而不用购买昂贵的专业设备。能够看到LIO的前端是Fabric模块(Fibre Channel、FCoE、iSCSI等),用来访问模拟的SCSI设备。Fabric模块就是实现SCSI命令的传输协议,例如iSCSI技术就是把SCSI命令放在TCP/IP中传输,vhost技术就是把SCSI命令放在virtio队列中传输。LIO的后端实现了访问磁盘数据的方法。FILEIO经过Linux VFS来访问数据,IBLOCK访问Linux Block设备,PSCSI 直接访问SCSI设备,Memory Copy RAMDISK用来放访问模拟SCSI的ramdisk。
图中天蓝色部分,就是实际的硬件存储设备。其中virtio_pci、para-virtualized SCSI、VMware's para-virtualized scsi是虚拟化的硬件设备。