KVM虚拟化原理中的块设备IO虚拟化是怎样的

37次阅读
没有评论

KVM 虚拟化原理中的块设备 IO 虚拟化是怎样的,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

块设备 IO 虚拟化简介

作为另外一个重要的虚拟化资源,块设备 IO 的虚拟化也是同样非常重要的。同网络 IO 虚拟化类似,块设备 IO 也有全虚拟化和 virtio 的虚拟化方式(virtio-blk)。现代块设备的工作模式都是基于 DMA 的方式,所以全虚拟化的方式和网络设备的方式接近,同样的 virtio-blk 的虚拟化方式和 virtio-net 的设计方式也是一样,只是在 virtio backend 端有差别。

传统块设备架构块设备 IO 协议栈

KVM 虚拟化原理中的块设备 IO 虚拟化是怎样的

如上图所示,我们把块设备 IO 的流程也看做一个 TCP/IP 协议栈的话,从最上层说起。

Page cache 层,这里如果是非直接 IO,写操作如果在内存够用的情况下,都是写到这一级后就返回。在 IO 流程里面,属于 writeback 模式。需要持久化的时候有两种选择,一种是显示的调用 flush 操作,这样此文件(以文件为单位)的 cache 就会同步刷到磁盘,另一种是等待系统自动 flush。

VFS,也就是我们通常所说的虚拟文件系统层,这一层给我们上层提供了统一的系统调用,我们常用的 create,open,read,write,close 转化为系统调用后,都与 VFS 层交互。VFS 不仅为上层系统调用提供了统一的接口,还组织了文件系统结构,定义了文件的数据结构,比如根据 inode 查找 dentry 并找到对应文件信息,并找到描述一个文件的数据结构 struct file。文件其实是一种对磁盘中存储的一堆零散的数据的一种描述,在 Linux 上,一个文件由一个 inode 表示。inode 在系统管理员看来是每一个文件的唯一标识,在系统里面,inode 是一个结构,存储了关于这个文件的大部分信息。这个数据结构有几个回调操作就是提供给不同的文件系统做适配的。下层的文件系统需要实现 file_operation 的几个接口,做具体的数据的读写操作等。

struct file_operations {
 // 文件读操作
 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
 // 文件写操作
 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
 int (*readdir) (struct file *, void *, filldir_t);
 // 文件打开操作
 int (*open) (struct inode *, struct file *);
};

再往下就是针对不同的文件系统层,比如我们的 ext3,ext4 等等。我们在 VFS 层所说的几个文件系统需要实现的接口,都在这一层做真正的实现。这一层的文件系统并不直接操作文件,而是通过与下层的通用块设备层做交互,为什么要抽象一层通用块设备呢?我们的文件系统适用于不同的设备类型,比如可能是一个 SSD 盘又或者是一个 USB 设备,不同的设备的驱动不一样,文件系统没有必要为每一种不同的设备做适配,只需要兼容通用快设备层的接口就可以。

位于文件系统层下面的是通用快设备层,这一层在程序设计里面是属于接口层,用于屏蔽底层不同的快设备做的抽象,为上层的文件系统提供统一的接口。

通用快设备下层就是 IO 调度层。用如下命令可以看到系统的 IO 调度算法.

➜ ~ cat /sys/block/sda/queue/scheduler
noop deadline [cfq]

noop,可以看成是 FIFO(先进先出队列),对 IO 做一些简单的合并,比如对同一个文件的操作做合并,这种算法适合比如 SSD 磁盘不需要寻道的块设备。

cfq,完全公平队列。此算法的设计是从进程级别来保证的,就是说公平的对象是每个进程。系统为此算法分配了 N 个队列用来保存来自不同进程的请求,当进程有 IO 请求的时候,会散列到不同的队列,散列算法是一致的,同一个进程的请求总是被散列到同一个队列。然后系统根据时间片轮训这 N 个队列的 IO 请求来完成实际磁盘读写。

deadline,在 Linux 的电梯调度算法的基础上,增加了两个队列,用来处理即将超时或者超时的 IO 请求,这两个队列的优先级比其他队列的优先级较高,所以避免了 IO 饥饿情况的产生。

块设备驱动层就是针对不同的块设备的真实驱动层了,块设备驱动层完成块设备的内存映射并处理块设备的中断,完成块设备的读写。

块设备就是真实的存储设备,包括 SAS,SATA,SSD 等等。块设备也可能有 cache,一般称为 Disk cache,对于驱动层来说,cache 的存在是很重要的,比如 writeback 模式下,驱动层只需要写入到 Disk cache 层就可以返回,块设备层保证数据的持久化以及一致性。
通常带有 Disk cache 的块设备都有电池管理,当掉电的时候保证 cache 的内容能够保持一段时间,下次启动的时候将 cache 的内容写入到磁盘中。

块设备 IO 流程

应用层的读写操作,都是通过系统调用 read,write 完成,由 Linux VFS 提供的系统调用接口完成,屏蔽了下层块设备的复杂操作。write 操作有直接 IO 和非直接 IO 之分(缓冲 IO),非直接 IO 的写操作直接写入到 page cache 后就返回,后续的数据依赖系统的 flush 操作,如果在 flush 操作未完成的时候发生了系统掉电,那可能会丢失一部分数据。直接 IO(Direct IO),绕过了 page cache,数据必须达到磁盘后才返回 IO 操作完成。

对于 I / O 的读写流程,逻辑比较复杂,这里以写流程简单描述如下:
KVM 虚拟化原理中的块设备 IO 虚拟化是怎样的

如上图所示,在初始化 IO 设备的时候,会为 IO 设备分配一部分物理内存,这个物理内存可以由 CPU 的 MMU 和连接 IO 总线的 IOMMU 管理,作为共享内存存在。以一个读取操作为例子,当 CPU 需要读取块设备的某个内容的时候,CPU 会通过中断告知设备内存地址以及大小和需要读取的块设备地址,然后 CPU 返回,块设备完成实际的读取数据后,将数据写入到共享的内存,并以中断方式通知 CPU IO 流程完成,并设置内存地址,接着 CPU 直接从内存中读取数据。
写请求类似,都是通过共享内存的方式,这样可以解放 CPU,不需要 CPU 同步等待 IO 的完成并且不需要 CPU 做过多的运算操作。
因为块设备 IO 的虚拟化需要经过两次 IO 协议栈,一次 Guest,一次 HV。所以需要把块设备 IO 协议栈说的很具体一点。

至此,Linux 块设备的 IO 层就基本介绍完整了,以上内容也只是做一个简单的介绍,这部分的内容可以很深入的去了解,在此限于篇幅限制,就不做过多介绍了。

块设备 IO 虚拟化

块设备的全虚拟化方式和网络 IO 的 DMA 设备虚拟化方式类似,这里就不过多介绍了,主要介绍一下 virtio-blk。

KVM 虚拟化原理中的块设备 IO 虚拟化是怎样的

如上图所示,蓝色表示 writethrough,黄色表示 none,红色表示 writeback。其中虚线表示写到哪一个层次后 write 调用返回。

cache=writethrough(蓝色线)
表示 v -backend 打开镜像文件并写入时候采用非直接 IO+flush 操作,也就是说每次写入到 Page cache 并 flush 一次,直到数据被真实写入到磁盘后 write 调用返回,这样必然会导致数据写入变慢,但是好处就是安全性较高。

cache=none(黄色线)
cache 为 none 模式表示了 v -backend 写入文件时候使用到了 DIRECT_IO,将会绕过 HV 的 Page cache,直接写入磁盘,如果磁盘有 Disk cache 的话,写入 Disk cache 就返回,此模式的好处在于保证性能的前提下,也能保证数据的安全性,在使用了 Disk cache 电池的情况下。但是对于读操作,因为没有写入 HV Page cache,所以会有一定性能影响。

cache=writeback(红色线)
此模式表示 v -backend 使用了非直接 IO,写入到 HV 的 Page 后就返回,有可能会导致数据丢失。

块设备 IO 的虚拟化方式也是统一的 virtio- x 模式,但是 virtio-blk 需要经过两次 IO 协议栈,带来了不必要的开销。

看完上述内容,你们掌握 KVM 虚拟化原理中的块设备 IO 虚拟化是怎样的的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注丸趣 TV 行业资讯频道,感谢各位的阅读!