云计算国产化之路 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

Virtio-Balloon超详细分析

2017年03月20日

前言

分析celometer获取memory.usage的时候,发现功能实现是基于virtio_balloon,见本人这篇博文

Virtio-Balloon驱动即内存气泡,可以用来动态调整内存,这个驱动很早就已经出来了, 最近才发现这个驱动居然还有监控内存指标的功能,所以再一次review了一遍代码。这里就把这个驱动详细的介绍一遍。

功能介绍

Virtio-Balloon驱动就是通过在虚拟机中申请内存,然后将申请的内存通知给qemu,然后qemu侧再将内存释放掉,可以让其他虚拟机使用,达到内存复用的能力。

要开启这个功能,就是在启动时候需要添加Virtio-Balloon后端设备,并且要在Guest里面安装 Virtio-Balloon驱动。

安装

  • 添加设备 如果用libvirt启动 在libvirt的虚拟机配置侧添加如下xml
 <devices>
 <memballoon model='virtio'>
      <alias name='balloon0'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>
      <stats period='10'/>
    </memballoon>
  </devices>

在qemu中启动中添加如下设备

-device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0×4
  • 安装驱动 windows驱动这里省略了在网上教程很多。 linux下在kernel 很早就加入了Virtio-Balloon驱动,所以在主流linux发行版中一般都具有这个驱动。
[root@localhost _posts]# modinfo virtio-balloon
filename:       /lib/modules/3.10.0-327.el7.x86_64/kernel/drivers/virtio/virtio_balloon.ko
license:        GPL
description:    Virtio balloon driver
rhelversion:    7.2
srcversion:     F2D65C53D0AFD06A3668942
alias:          virtio:d00000005v*
depends:        virtio,virtio_ring
intree:         Y
vermagic:       3.10.0-327.el7.x86_64 SMP mod_unload modversions 
signer:         CentOS Linux kernel signing key
sig_key:        79:AD:88:6A:11:3C:A0:22:35:26:33:6C:0F:82:5B:8A:94:29:6A:B3
sig_hashalgo:   sha256

使用

有了上面的准备,启动虚拟机后就可以体验到内存气泡的功能。

  • 查看内存: 在libvirt侧使用
virsh # dommemstat test

在qemu的hmp中查看内存

info balloon
  • 设置目标虚拟机的内存,注意这里是直接设置虚拟机当前可以使用的内存。 比如起始的时候内存是8G,然后想缩减2G,实际这里操作要设置成目前为6G。

libvirt侧

virsh # setmem test 4096

在qemu的hmp中查看内存

balloon 4096

代码分析

从代码侧来具体分析Virtio-Balloon的。先从linux驱动侧看起吧 相对其他virtio驱动 ,内存气泡的驱动相对简单好看。 它的代码全部哎driver/virtio/virtio_balloon.c中

首先来看 virtio_balloon_driver驱动的定义, 这里很容易看到驱动的处理函数并不多virtballoon_probe,在驱动加载时候, virtballoon_remove在驱动卸载时候,剩下只有virtballoon_changed那么肯定所有的功能都是在这个里面来实现。

static struct virtio_driver virtio_balloon_driver = {
    .feature_table = features,
    .feature_table_size = ARRAY_SIZE(features),
    .driver.name =  KBUILD_MODNAME,
    .driver.owner = THIS_MODULE,
    .id_table = id_table,
    .probe =    virtballoon_probe,
    .remove =   virtballoon_remove,
    .config_changed = virtballoon_changed,
#ifdef CONFIG_PM_SLEEP
    .freeze =   virtballoon_freeze,
    .restore =  virtballoon_restore,
#endif
};

virtballoon_changed的函数也相当简单,就是启动了一个work 这个work主要来执行update_balloon_size_work 这个回调。

static void virtballoon_changed(struct virtio_device *vdev)
{
    struct virtio_balloon *vb = vdev->priv;
    unsigned long flags;

    spin_lock_irqsave(&vb->stop_update_lock, flags);
    if (!vb->stop_update)
        queue_work(system_freezable_wq, &vb->update_balloon_size_work);
    spin_unlock_irqrestore(&vb->stop_update_lock, flags);
}

为了弄清楚这个work我们还是需要看一下virtio_balloon的初始化函数virtballoon_probe。 截取片段来看,初始化时候就注册了stats_request 这个callback,用来响应后端发来的stats请求。定义了2个任务 update_balloon_size_func是用来修改内存, 而update_balloon_stats_func这个是用来更新内存信息的,这个也是再次浏览代码时候的发现。

static int virtballoon_probe(struct virtio_device *vdev)
{
  struct virtqueue *vqs[3];
    vq_callback_t *callbacks[] = { balloon_ack, balloon_ack, stats_request };
    static const char * const names[] = { "inflate", "deflate", "stats" };
    int err, nvqs;

    nvqs = virtio_has_feature(vb->vdev, VIRTIO_BALLOON_F_STATS_VQ) ? 3 : 2;
    err = vb->vdev->config->find_vqs(vb->vdev, nvqs, vqs, callbacks, names,
            NULL);

 INIT_WORK(&vb->update_balloon_stats_work, update_balloon_stats_func);
 INIT_WORK(&vb->update_balloon_size_work, update_balloon_size_func);
}

先来介绍基本功能update_balloon_size_func,代码非常好理解,拿到diff和本身现在的内存进行比较,然后开始增加或者减少内存,接着update_balloon_size更新下当前内存的信息。

static void update_balloon_size_func(struct work_struct *work)
{
    struct virtio_balloon *vb;
    s64 diff;

    vb = container_of(work, struct virtio_balloon,
              update_balloon_size_work);
    diff = towards_target(vb);

    if (diff > 0)
        diff -= fill_balloon(vb, diff);
    else if (diff < 0)
        diff += leak_balloon(vb, -diff);
    update_balloon_size(vb);

    if (diff)
        queue_work(system_freezable_wq, work);
}

fill_balloon和leak_alloon其实很相似,这里就看下fill_balloon的实现。 调用balloon_page_enqueue函数进行内存的添加,然后tell_host去更新表项。 balloon_page_enqueue函数是kernel自己实现的堆内存进行增减的功能。 所以无论是kvm还是xen的气球驱动,最终都是调用这个函数去进行实现。 该函数在mm/balloon_compaction.c。这里就不继续往下追了,本编只是介绍virtio-balloon驱动原理。要想更细一步了解内存的实现,在以后专门讲内存操作时会再次提及该函数。

static unsigned fill_balloon(struct virtio_balloon *vb, size_t num)
{
    struct balloon_dev_info *vb_dev_info = &vb->vb_dev_info;
    unsigned num_allocated_pages;

    /* We can only do one array worth at a time. */
    num = min(num, ARRAY_SIZE(vb->pfns));

    mutex_lock(&vb->balloon_lock);
    for (vb->num_pfns = 0; vb->num_pfns < num;
         vb->num_pfns += VIRTIO_BALLOON_PAGES_PER_PAGE) {
        struct page *page = balloon_page_enqueue(vb_dev_info);

        if (!page) {
            dev_info_ratelimited(&vb->vdev->dev,
                         "Out of puff! Can't get %u pages\n",
                         VIRTIO_BALLOON_PAGES_PER_PAGE);
            /* Sleep for at least 1/5 of a second before retry. */
            msleep(200);
            break;
        }
        set_page_pfns(vb, vb->pfns + vb->num_pfns, page);
        vb->num_pages += VIRTIO_BALLOON_PAGES_PER_PAGE;
        if (!virtio_has_feature(vb->vdev,
                    VIRTIO_BALLOON_F_DEFLATE_ON_OOM))
            adjust_managed_page_count(page, -1);
    }

    num_allocated_pages = vb->num_pfns;
    /* Did we get any? */
    if (vb->num_pfns != 0)
        tell_host(vb, vb->inflate_vq);
    mutex_unlock(&vb->balloon_lock);

    return num_allocated_pages;
}

这个基本功能介绍完了,回到本篇最初的问题,Virtio-Balloon驱动顺便还实现了一个内存的定时监控信息。 刚才在初始化的时候可以看到当后端主动调用stat指令时候,stats_request就会触发另一个work update_balloon_stats_func。

static void stats_request(struct virtqueue *vq)
{
    struct virtio_balloon *vb = vq->vdev->priv;

    spin_lock(&vb->stop_update_lock);
    if (!vb->stop_update)
        queue_work(system_freezable_wq, &vb->update_balloon_stats_work);
    spin_unlock(&vb->stop_update_lock);
}

update_balloon_stats_func 这里主要调用stats_handle_request

static void update_balloon_stats_func(struct work_struct *work)
{
    struct virtio_balloon *vb;

    vb = container_of(work, struct virtio_balloon,
              update_balloon_stats_work);
    stats_handle_request(vb);
}

然后update_balloon_stats这个函数将会收集所有的信息到达vb这个结构体,然后将vb发送给后端。

static void stats_handle_request(struct virtio_balloon *vb)
{
    struct virtqueue *vq;
    struct scatterlist sg;
    unsigned int len;

    update_balloon_stats(vb);

    vq = vb->stats_vq;
    if (!virtqueue_get_buf(vq, &len))
        return;
    sg_init_one(&sg, vb->stats, sizeof(vb->stats));
    virtqueue_add_outbuf(vq, &sg, 1, vb, GFP_KERNEL);
    virtqueue_kick(vq);
}

update_balloon_stats这个函数就是实际采集guest连内存使用的情况。

static void update_balloon_stats(struct virtio_balloon *vb)
{
    unsigned long events[NR_VM_EVENT_ITEMS];
    struct sysinfo i;
    int idx = 0;
    long available;

    all_vm_events(events);
    si_meminfo(&i);

    available = si_mem_available();

    update_stat(vb, idx++, VIRTIO_BALLOON_S_SWAP_IN,
                pages_to_bytes(events[PSWPIN]));
    update_stat(vb, idx++, VIRTIO_BALLOON_S_SWAP_OUT,
                pages_to_bytes(events[PSWPOUT]));
    update_stat(vb, idx++, VIRTIO_BALLOON_S_MAJFLT, events[PGMAJFAULT]);
    update_stat(vb, idx++, VIRTIO_BALLOON_S_MINFLT, events[PGFAULT]);
    update_stat(vb, idx++, VIRTIO_BALLOON_S_MEMFREE,
                pages_to_bytes(i.freeram));
    update_stat(vb, idx++, VIRTIO_BALLOON_S_MEMTOT,
                pages_to_bytes(i.totalram));
    update_stat(vb, idx++, VIRTIO_BALLOON_S_AVAIL,
                pages_to_bytes(available));
}

到此,Vritio-Balloon驱动的相关实现都描述完了。

qemu后端代码比较简单了,首先来看气球初始化时候, 通过qemu_add_balloon_handler,注册了在调用qmp时候的回调函数,用来发起请求。 初始化了队列,ivq,dvq,svq用来接收相关的信息,然后调用回调进行相应操作。

static void virtio_balloon_device_realize(DeviceState *dev, Error **errp)
{
    VirtIODevice *vdev = VIRTIO_DEVICE(dev);
    VirtIOBalloon *s = VIRTIO_BALLOON(dev);
    int ret;

    virtio_init(vdev, "virtio-balloon", VIRTIO_ID_BALLOON,
                sizeof(struct virtio_balloon_config));

    ret = qemu_add_balloon_handler(virtio_balloon_to_target,
                                   virtio_balloon_stat, s);

    if (ret < 0) {
        error_setg(errp, "Adding balloon handler failed");
        virtio_cleanup(vdev);
        return;
    }

    s->ivq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
    s->dvq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
    s->svq = virtio_add_queue(vdev, 128, virtio_balloon_receive_stats);
    reset_stats(s);

    register_savevm(dev, "virtio-balloon", -1, 1,
                    virtio_balloon_save, virtio_balloon_load, s);

    object_property_add(OBJECT(dev), "guest-stats", "guest statistics",
                        balloon_stats_get_all, NULL, NULL, s, NULL);

    object_property_add(OBJECT(dev), "guest-stats-polling-interval", "int",
                        balloon_stats_get_poll_interval,
                        balloon_stats_set_poll_interval,
                        NULL, s, NULL);
}

这里以刷新内存信息为例往下讲,内存增减功能类似。 在上面注册了guest-stats-polling-interval属性,这个是设置查询周期的,在设置了周期后,就可以看到启动了一个定时任务,balloon_stats_poll_cb来实时查询内存的信息。

static void balloon_stats_set_poll_interval(Object *obj, struct Visitor *v,
                                            void *opaque, const char *name,
                                            Error **errp)
{
    VirtIOBalloon *s = opaque;
    Error *local_err = NULL;
    int64_t value;

    visit_type_int(v, &value, name, &local_err);
    if (local_err) {
        error_propagate(errp, local_err);
        return;
    }

    if (value < 0) {
        error_setg(errp, "timer value must be greater than zero");
        return;
    }

    if (value > UINT32_MAX) {
        error_setg(errp, "timer value is too big");
        return;
    }

    if (value == s->stats_poll_interval) {
        return;
    }

    if (value == 0) {
        /* timer=0 disables the timer */
        balloon_stats_destroy_timer(s);
        return;
    }

    if (balloon_stats_enabled(s)) {
        /* timer interval change */
        s->stats_poll_interval = value;
        balloon_stats_change_timer(s, value);
        return;
    }

    /* create a new timer */
    g_assert(s->stats_timer == NULL);
    s->stats_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL, balloon_stats_poll_cb, s);
    s->stats_poll_interval = value;
    balloon_stats_change_timer(s, 0);
}

而virtio_balloon_receive_stats主要是从vq中取到前面讲的内存信息,然后用balloon_stats_enabled进行更新。

static void virtio_balloon_receive_stats(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIOBalloon *s = VIRTIO_BALLOON(vdev);
    VirtQueueElement *elem = &s->stats_vq_elem;
    VirtIOBalloonStat stat;
    size_t offset = 0;
    qemu_timeval tv;

    if (!virtqueue_pop(vq, elem)) {
        goto out;
    }

    /* Initialize the stats to get rid of any stale values.  This is only
     * needed to handle the case where a guest supports fewer stats than it
     * used to (ie. it has booted into an old kernel).
     */
    reset_stats(s);

    while (iov_to_buf(elem->out_sg, elem->out_num, offset, &stat, sizeof(stat))
           == sizeof(stat)) {
        uint16_t tag = virtio_tswap16(vdev, stat.tag);
        uint64_t val = virtio_tswap64(vdev, stat.val);

        offset += sizeof(stat);
        if (tag < VIRTIO_BALLOON_S_NR)
            s->stats[tag] = val;
    }
    s->stats_vq_offset = offset;

    if (qemu_gettimeofday(&tv) < 0) {
        fprintf(stderr, "warning: %s: failed to get time of day\n", __func__);
        goto out;
    }

    s->stats_last_update = tv.tv_sec;

out:
    if (balloon_stats_enabled(s)) {
        balloon_stats_change_timer(s, s->stats_poll_interval);
    }
}

总结

到此可以看到Virtio-Balloon的相关实现,和周期性查询内存信息的功能。 Virtio-Balloon进行内存复用本身存在一些问题

  • Guest对内存变化会进行感知,Balloon特性本身是kernel所具备的,本身是通过修改识别的内存信息来限制Guest中的使用。所以不是很友好。
  • 内存复用需要实时监控,发现客户虚拟机内存使用过多还要及时归还内存,对于系统本身做这个功能有很大的局限性。
  • Balloon特性的内存复用并不是本质上进行内存的冗余复用,仅仅是借东墙补西墙,当虚拟机都大量使用内存时候,并不能实际突破物理内存上限。

本文转自肖丁博客