云计算国产化之路 vnc登录 文件系统直通(virtio-9p) 扩展qemu接口 gpg WARNING 珍藏博客 虚拟内存情况dommemstat分析 免密码自动登录脚本 Linux网桥 测试网段IP占用情况 Linux 进程状态 systemc强制依赖 调试openstack ut uefi p2v 重做ubuntu内核 virsh创建虚拟机简介 virtio IO路径 虚拟化层升级后磁盘无法卸载卷 vmtouch使用 Taint flags 主机和虚拟机文件共享处理的几种方法 kvm分析工具 kvm中对磁盘的io cache 虚拟化不同导致的guestos中软件安装差异(未解决) 设备直通(PCI Assignment)到底是咋整的 virtio到底是咋整的 内核启动参数 虚拟化实时性提升(零)之配置步骤 中断虚拟化(pic)到底是咋整的 中断虚拟化(apic)到底是咋整的 Raid卡配置丢失导致服务器无法启动 tainted kernels cpu stuck for 23s问题分析 虚拟化实时性提升(一)之hostOS切换为强实时系统 内存虚拟化到底是咋整的 qemu-kvm中vcpu虚拟化到底是咋整的 风河虚拟化技术点分析 使用qga的好处 qemu迁移代码分析 虚拟机串口配置及其导出到主机pts和console.log System-based I/O vs. Raw I/O 虚拟机使用Hugepage(大页) 硬件辅助分页(hardware assisted paging) 修改centos7默认启动项目 virtio的工作流程——kernel中virtio-pci初始化(2) virtio的工作流程——qemu中virtio-backend初始化(1) qmp ceilometer取不到memory.usage指标 Virtio-Balloon超详细分析 slabtop输出 虚拟机磁盘cache导致的host os kernel崩溃 虚拟机cpu和memory性能优化测评 PCI配置空间(PCI Configuration Space) centos下网卡设备直通(VT-dpci passthrough)遇到的问题及其解决思路 libguestfs详解 yum卸载软件包及其依赖 通过原始Centos ISO来定制自己的ISO centos下网卡设备直通(VT-d,pci passthrough) (占位符)window虚拟机中拔盘如何通知到libvirt和qemu后端的 cirrus漏洞分析CVE-2017-2615 XSA-208 qcow2随笔 控制寄存器概览 ceilometer对接postgresql 解压initrd和vmlinuz qemu guest agent验证 QEMU升级指南(待续) ubuntu中kdump的配置 qemu(2.3)接口梳理 热迁移导致的FC存储场景下的multipath卷残留问题分析 virsh命令(4)secret,snapshot,pool,volume部分 virsh命令(3)之interface,filter,network virsh命令(2)monitor,host,nodedev部分 virsh命令(1)之domain部分 QEMU内存管理之FlatView模型(QEMU2.0.0) ovirt基于sanock的高可用(主机粒度HA) Sanlock防脑裂场景功能测试用例 gnocchi配置及与ceilometer对接指南 make patch for libvirt in centos centos使用sanlock指导 高可用nfs资料 ubuntu14中使用sanlock指导 LVM操作指南 sanlock相关功能验证流程汇总 make patch for libvirt in ubuntu libvirt.so.0-version `LIBVIRT_PRIVATE_1.2.7' not found gdb debug libvirt 基于ubuntu社区源码包编译libvirt compile libvirt(centos) No PCI buses available nfs lead to Linux halt nfs install and config anti-virus for cloud platform nova fetch image from glance(something about _base) token auth process ovs入门指南 virt software anti-virus something about ceilometer disk sample context interview questions openstack vm injection openstack Restful and RPC murano 概览 创建虚拟机流程(compute节点)之网络创建 创建虚拟机流程之compute_api之虚拟机实例填充之配额检测 创建虚拟机流程之compute_api之基本参数生成 创建虚拟机流程之compute_api 创建虚拟机流程(主) 创建虚拟机之image 创建虚拟机流程之准备网桥 创建虚拟机流程之virt 创建虚拟机流程之compute节点 CI/CD研发流程之工程创建 CI/CD研发流程之代码合入 CI/CD研发流程之UT(单元测试) 向openstack社区合入代码记 openstack/ceilometer/gnocchi杂谈 影子页表原理 mem_add(exec.c) qemu编译安装调试 openstack/ceilometer/gnocchi之Grafana简介 openstack wiki etcd openstack计量ceilometer openstack计费cloudKitty enventlet backdoor USB 安装VMWARE ESX pycharm设置指南 无法执行modprobe -a kvm-intel解决办法 QEMU配置项 网络不通小记 libvirt之XML(虚拟机定义文件) openstack-horizon 证书认证 ceilometer与ceph对接 openstack定时任务剖析(TODO) 服务器重启后mongodb进程无法启动 ubuntu14下新增openstack服务到service的导引 ERROR 1045(28000)-数据库连不上 Python两个内置函数—locals和globals unknown exit, hardware reason 31

kvm中对磁盘的io cache

2017年06月28日

背景

libvirt配置中,针对虚拟机的磁盘cache有如下几种策略

网上有个不错的图如下:

writeback采用了guest和host两层的page cache
none和writethrough都会绕过host层的page cache。
kvm默认的cache方式是writeback(下图是qemu-img -h的输出)
当打数据量的到时候,也就是高IO的情况下可能存在cache吃掉过多的内存,具体见这里

综合数据的安全性和性能,建议选择none模式。

但是,随着barrier passing技术的出现,writeback也能保证数据的一致性
所以,如果采用raw格式的image,建议选择none,如果采用qcow2格式的image,建议选择writeback。

以上摘自liujunwei1234的博客

流程

qemu 初始化 block

为了方便, 选择使用 ide 的 device 和 raw 的磁盘格式

根据上面所说的, 模拟磁盘的初始化分为前端(磁盘设备)和后端(镜像文件)的初始化,
在后端的初始化中, 入口函数是 drive_init(), 到最后的 qemu_open() 终止.
上图 中红色得模块就是 Cache 初始化最重要的两个地方.

int bdrv_parse_cache_flags(const char *mode, int *flags) { *flags &= ~BDRV_O_CACHE_MASK;   /*
    * - BDRV_O_NOCACHE: host end 绕过 cache
    * - BDRV_O_CACHE_WB: guest 的磁盘设备启用 writeback cache
    * - BDRV_O_NO_FLUSH: 在 host end 永远不要把 cache 里的数据同步到文件里
    * 这几个宏的具体应用后面分析到数据读写的时候会进行分析*/
    * 
     if (!strcmp(mode, "off") || !strcmp(mode, "none")) { 
        *flags |= BDRV_O_NOCACHE | BDRV_O_CACHE_WB; } 
     /* 由上, 这个组合表示的是 host end 不用 Cache, 数据直接在用户空间(QEMU)
      * 和真实设备之间通过 DMA 直接传输, 但是同时, 告诉 guest 模拟的磁盘设备
      * 是有 cache 的, guest 能发起 flush 的操作(fsync/fdatasync) */ 


      else if (!strcmp(mode, "directsync")) { 
      *flags |= BDRV_O_NOCACHE; }
      /* 很好理解, 完全没有 cache, host end 和 guest end 都没有 cache, 
       *guest不会发起 flush 的操作 */ 
      
       else if (!strcmp(mode, "writeback")) {
        *flags |= BDRV_O_CACHE_WB; }           
        /* 和上面相反, host side 和 guest side 都有 cache, 
         * 性能最好, 但是如果host 掉电, 会导致数据的损失 */  
      
      else if (!strcmp(mode, "unsafe")) { 
      *flags |= BDRV_O_CACHE_WB;  
      *flags |= BDRV_O_NO_FLUSH;  }
      /* 见文可知意, 最不安全的模式, guest side 有cache, 
       *但是 host side 不理睬guest 发起的 flush 操作, 完全忽略, 这种情况性能最高, 
       *snapshot 默认使用 的就是这种模式 */  
      
      else if (!strcmp(mode, "writethrough")) { 
      
      /* host end 有 cache, guest 没有 cache, 其实通过后面的代码分析可以知道,
       *这种模式其实是 writeback + flush 的组合, 也就是每次写操作同时触发
       *一个 host flush 的操作, 会带来一定的性能损失, 尤其是非 raw(e.g. qcow2)
       *的网络存储(e.g. ceph), 但是很遗憾, 这是 QEMU 默认的 Cache 模式 */ 
      } 
      else { return -1; }   
      return 0; 
     }  

然后是

static DriveInfo *blockdev_init(QemuOpts *all_opts, BlockInterfaceType block_default_type) { 
    DriveInfo *dinfo;   /* code snippet 解析和配置各种各样的参数, e.g. 磁盘格式, 启动顺序等等,最后填充到 dinfo 对象中 */  
    snapshot = qemu_opt_get_bool(opts, "snapshot", 0);  
    file = qemu_opt_get(opts, "file");   
    if (qemu_opt_get_bool(opts, "cache.writeback", true)) { 
        bdrv_flags |= BDRV_O_CACHE_WB; } 
    if (qemu_opt_get_bool(opts, "cache.direct", false)) { 
        bdrv_flags |= BDRV_O_NOCACHE; } 
    if (qemu_opt_get_bool(opts, "cache.no-flush", false)) { 
        bdrv_flags |= BDRV_O_NO_FLUSH; }   
    if (snapshot) { 
    /* 前面讲过, snapshot 打开磁盘时候, 使用 unsafe 的 cache 模式 */ 
        bdrv_flags &= ~BDRV_O_CACHE_MASK; 
        bdrv_flags |= (BDRV_O_SNAPSHOT|BDRV_O_CACHE_WB|BDRV_O_NO_FLUSH); }  
    bdrv_flags |= ro ? 0 : BDRV_O_RDWR;   /* 使用 bdrv_open 打开文件 */ 
    ret = bdrv_open(dinfo->bdrv, file, bs_opts, bdrv_flags, drv, &error);   /* 返回配置好的 DriveInfo *dinfo, 这个对象在初始化模拟磁盘设备的时候被传入, 写入该磁盘设备的 PCI config space */ 
    return dinfo; }   
    
int bdrv_open(BlockDriverState *bs, const char *filename, QDict *options, int flags, BlockDriver *drv, Error **errp) { 
    BlockDriverState *file = NULL; 
    const char *drvname;   
    /* 打开文件, QEMU 支持的镜像格式都有一个后端的 format, 比如 raw 和* qcow2 这个 format 就是 file, 其他的还有 sheepdog, glusterfs 的gluster 等. 所以这里其实是打开一个本地文件 */ 
    
    ret = bdrv_file_open(&file, filename, file_options, bdrv_open_flags(bs, flags | BDRV_O_UNMAP), &local_err);   
    /* 其实 bdrv_file_open() 就会调用 bdrv_open_common 函数, 只不过那个时候调用 bdrv_open_common() 用的是 file 这个 BlockDriver, 现在是直接磁盘文件的format 的 BlockDriver(qcow2, raw 等), 所以这里的函数是来初始化特定格式的磁盘文件, 如 qcow2_open 等 */ 
    
    ret = bdrv_open_common(bs, file, options, flags, drv, &local_err); }   
    
int bdrv_file_open(BlockDriverState **pbs, const char *filename, QDict *options, int flags, Error **errp) { BlockDriverState *bs;   
/* 找到相应的格式的 BlockDriver, 由于这里是 file 的 open, 因为 file来没有被打开, 所以这里第二个指针传递的是空, 注意 drv 这个参数, 表示file 的 BlockDriver */ 

   ret = bdrv_open_common(bs, NULL, options, flags, drv, &local_err);   return ret; }   
   
static int bdrv_open_common(BlockDriverState *bs, BlockDriverState *file, QDict *options, int flags, BlockDriver *drv, Error **errp) { 
bs->open_flags = flags;  
open_flags = bdrv_open_flags(bs, flags);  
/* 注意这里, flags 保存着之前 bdrv_parse_cache_flags 获取到的 flags,如果用户没有指定 none, writeback, 或者是 unsafe, 那么 guest 看到的这个磁盘设备是没有 cache 的, 后面我会以 hdparm 这个工具来验证,同时, 这个变量(bs->enable_write_cache)控制着 QEMU 怎么模拟 cache行为, 后面写文件的时候会分析到 */ 
 * 
 * bs->enable_write_cache = !!(flags & BDRV_O_CACHE_WB);   
 * /* 打开文件, 因为从上面的流程可以看到, 这里作为 file 打开, 而不是作为 image format(e.g. qcow2) 打开, 所以这里调用的是 bdrv_file_open()方法, 也就是 raw-posix.c 中 format_name="file" 的 BlockDriver 里面的的 raw_open */ 
 * 
 * if (drv->bdrv_file_open) { 
 *     assert(file == NULL); 
 *     assert(drv->bdrv_parse_filename || filename != NULL); 
 *     ret = drv->bdrv_file_open(bs, options, open_flags, &local_err); } 
 * else { 
 * if (file == NULL) { 
 *     error_setg(errp, "Can't use '%s' as a block driver for the " "protocol level", drv->format_name); 
 *     ret = -EINVAL; goto free_and_fail; }
 *  bs->file = file; 
 *  ret = drv->bdrv_open(bs, options, open_flags, &local_err); }  
 *  return ret; }

最后剩下核心的 raw_open

/* 这个函数其实是 raw_open_common */ 
static int raw_open(BlockDriverState *bs, QDict *options, int flags, Error **errp){ 
   BDRVRawState *s = bs->opaque;  
   s->type = FTYPE_FILE;
   return raw_open_common(bs, options, flags, 0); }   

static int raw_open_common(BlockDriverState *bs, QDict *options, int bdrv_flags, int open_flags) { 
    s->open_flags = open_flags; /* 解析 open 的参数, 把 QEMU 的 BDRV_O_* 映射成 open 的 O_*, 下面详细分析 */ 
    raw_parse_flags(bdrv_flags, &s->open_flags);  
    s->fd = -1;   /* 用上面解析到得参数打开文件 */ 
    fd = qemu_open(filename, s->open_flags, 0644); s->fd = fd; 
 }   

static void raw_parse_flags(int bdrv_flags, int *open_flags) { assert(open_flags != NULL);   /* 首先清空其他标志  */ 
 *open_flags |= O_BINARY; 
 *open_flags &= ~O_ACCMODE; /* 设置读写权限位 */ 
 if (bdrv_flags & BDRV_O_RDWR) { 
     *open_flags |= O_RDWR; } 
 else { 
     *open_flags |= O_RDONLY; }   /* 如果设置了 cache=none, 那么直接用 O_DIRECT 打开文件, 这个标志保证数据
                                    的传输将不会通过内核空间, 而是使用 DMA 直接在用户空间到存储设备之间
                                   传送, 不保证数据是否同步. 这就是 cache=none 的由来 */ 
  
 * if ((bdrv_flags & BDRV_O_NOCACHE)) { 
 * *open_flags |= O_DIRECT; } }

guest end cache 的设置

在上面的的 bdrv_open_common() 函数中, 会设置 BlockDriverState->enable_write_cache 成员, 这个成员表示 guest 默认是否启用 writeback 的 Cache.
接下来会看到, guest 获取设备寄存器的时候, 会相应地用这个值填充寄存器的位, 下面以 IDE 硬盘来做例子.

guest 在初始访问 IDE 设备时, 会发送 IDENTIFY DRIVE (0xec) 指令, 设备收到这个 指令后, 需要返回一个 512 字节的信息, 包括设备的状态, 扇区数目, 等等的信息.

static void ide_identify(IDEState *s) { 
    uint16_t *p; unsigned int oldsize; 
    IDEDevice *dev = s->unit ? s->bus->slave : s->bus->master;  
    memset(s->io_buffer, 0, 512); 
    p = (uint16_t *)s->io_buffer;   
    /* 看这里, bit 85 表示 writeback Cache 的状态, cache=none 之类的模式
     * 是会设置这个位的, 这样, guest 会发 fsync 指令过来, 否则, geust
     * 不会自动发送 fsync 同步数据, 当然, guest 可以在稍后设置是否启用
     * writeabck cache, QEMU 的新版本已经支持这个功能了. */ 
     * /* 14 = NOP supported, 5=WCACHE enabled, 0=SMART feature set enabled */ 
    if (bdrv_enable_write_cache(s->bs)) put_le16(p + 85, (1 << 14) | (1 << 5) | 1); 
    else put_le16(p + 85, (1 << 14) | 1);  
    memcpy(s->identify_data, p, sizeof(s->identify_data)); s->identify_set = 1; }

guest 到 QEMU 的 I/O 执行路径:

static int coroutine_fn bdrv_co_do_writev(BlockDriverState *bs, int64_t sector_num, int nb_sectors, QEMUIOVector *qiov, BdrvRequestFlags flags) { 
    if (ret < 0) { /* Do nothing, write notifier decided to fail this request */ } 
    else if (flags & BDRV_REQ_ZERO_WRITE) { 
        ret = bdrv_co_do_write_zeroes(bs, sector_num, nb_sectors); } 
    else { 
        ret = drv->bdrv_co_writev(bs, sector_num, nb_sectors, qiov); }   
        /* enable_write_cache 为 false, 即 cache=writethrough或者directsync, 那么
         * 每次写完都执行一次 flush 操作, 保证数据的同步, 当然, 这样会损失很大的性能 */ 
 * 
 * if (ret == 0 && !bs->enable_write_cache) { 
 * ret = bdrv_co_flush(bs); }   
 * return ret; }   
 * 
int coroutine_fn bdrv_co_flush(BlockDriverState *bs) { 
int ret;   /* 对于 unsafe 的cache, 任何时候都不需要把数据真正同步到磁盘 */ 
if (bs->open_flags & BDRV_O_NO_FLUSH) { goto flush_parent; }  
BLKDBG_EVENT(bs->file, BLKDBG_FLUSH_TO_DISK); 
if (bs->drv->bdrv_co_flush_to_disk) { 
ret = bs->drv->bdrv_co_flush_to_disk(bs); } 
else if (bs->drv->bdrv_aio_flush) {
    BlockDriverAIOCB *acb; 
    CoroutineIOCompletion co = { .coroutine = qemu_coroutine_self(), };  
    acb = bs->drv->bdrv_aio_flush(bs, bdrv_co_io_em_complete, &co); 
    if (acb == NULL) { ret = -EIO; } else { qemu_coroutine_yield(); ret = co.ret; } }  
    dflush_parent: return bdrv_co_flush(bs->file); }