存档

‘QEMU’ 分类的存档

QEMU 使用 API 直接访问各种存储后端中的镜像

2014年6月17日 没有评论

现在的虚拟化(云计算)环境的存储技术越来越复杂, 各种存储协议层出不穷, 从古老的 NFS, 到最近流行的 Ceph, GlusterFS 等. QEMU/KVM 作为开源虚拟机的事实标准, 发展是相当地与时俱进, 对大部分存储协议, 他都有相对应的驱动直接和这些后端存储通信. 使用这些驱动来访问存储具有很多好处: 包括绕过用户空间和内核空间的数据传输层(FUSE) 提高 I/O 性能, 使得非 root 用户运行能访问这些存储设备等. 下面来做一个总结.

Ceph(RBD)

优势

  • 和 QEMU 的磁盘工具 qemu-img 完美整合
  • 由于直接利用 librbd 和 Ceph 通信, 大大增加了性能.
  • 通过 librbd, 可以通过调整 RBD 的缓存来达到性能调优的目的.
  • 可以使用 librbd 使得虚拟磁盘拥有 TRIM 指令.

使用

安装客户端, 参考官方文档, 不累述.

在 Ceph 集群中先创建一个名为 qemu, 大小为 8G 的 rbd 设备 用来测试

# rbd create --size 8192 qemu
# rbd map qemu --pool rbd

在启动 QEMU 的时候, 磁盘的参数为

# -device virtio-blk-pci,drive=drive0 \
# -drive file=rbd:rbd/qemu:id=admin:key=AQDVmFJT0JnIJxAAjXG/22KnhIC91W4Gd9iJMg==,if=none,id=drive0

nfs

优势

  • 不需要挂载 NFS, 直接连接
  • 由于不需要挂载 NFS, 普通用户也可以使用

使用

由于该后端使用 libnfs, 需要安装以下地址提供的 libnfs:

# git clone git://github.com/sahlberg/libnfs.git
# cd libnfs 
# ./bootstrap && ./configure && make && make install

使用以下指令使用

# -device virtio-blk-pci,drive=drive0 \
# -drive file=nfs://192.168.3.155/test/test1.qcow2,if=none,id=drive0

iSCSI

优势

  • 不需要先登录 iSCSI 设备到本地, 有很多虚拟机和很多 iSCSI 设备的时候很方便
  • 由于上一个原因, 一定程度上增强了安全性
  • 非 root 用户也可以访问 iSCSI 设备.

使用

# -device lsi -device scsi-generic,drive=drive0 \
# -drive file=iscsi://192.168.3.31/iqn.qemu.test/1,if=none,id=drive0

http

优势

  • 对于在网络上的镜像, 不需要下载直接就可以访问

使用

由于是承载在 http 上镜像, 不需要下载, 因此该镜像是只读的, 需要以 readonly 参数打开, 或者以 snapshot 方式打开.

# -device virtio-blk-pci,drive=drive0 \
# -drive file=http://192.168.3.155:8000/test1.qcow2,if=none,id=drive0,snapshot=on

ftp

优势

同 http

使用

打开方式同 http, 支持传入用户名和密码, 实际上, http 和 ftp 都是使用 libcurl 这个后端来实现的.

# -device virtio-blk-pci,drive=drive0 \
# -drive file=ftp://hao32:hao32@192.168.3.155/test1.qcow2,if=none,id=drive0,snapshot=on

tftp

优势

同 http

使用

打开方式同 http

# -device virtio-blk-pci,drive=drive0 \
# -drive file=tftp://192.168.3.155/test1.qcow2,if=none,id=drive0,snapshot=on

ssh

优势

  • 对于可以使用 ssh 的镜像资源, 不需要把镜像 scp 到本地直接就可以访问

使用

目前只支持使用 ssh-agent 的认证方式, 所以必须先设置好 ssh-agent

# eval `ssh-agent`
# ssh-add

连接到 ssh 服务器上的镜像文件:

# -device virtio-blk-pci,drive=drive0 \
# -drive file=ssh://root@192.168.3.155:22/test/test1.qcow2,if=none,id=drive0

host_device

优势

可以直接打开主机上文件作为虚拟机的设备

使用

# -device virtio-blk-pci,drive=drive0 -drive file=host_device:/dev/sdb,if=none,id=drive0

GlusterFS

TODO

NBD

TODO

sheepdog

TODO

分类: QEMU 标签:

深入分析 QEMU 的 cache 机制

2013年10月22日 没有评论

Cache 的一些基本概念

Cache 最先指的是高速缓存, 用于平衡 CPU 和内存之间的速度差异, 后来, 进一步发展为 一种技术, 用在速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构.

本文主要指的是协调内存和硬盘速度的 Cache.

同步在 Cache 和他的后端存储一般有以下几种方式:

  • Write through: 数据一旦在 Cache 中, 就马上同步到真实设备中, 也就是保持同步
  • Write back: 见闻知意, writeback 的意思就是数据一旦在 Cache 中, 这个操作就算完成了, 并不需要马上同步到真实设备中, 除非用户手动指定(fsync), 或者此 Cache 中的内容发生了改变

QEMU 中的 Cache 模型

由于虚拟化的关系, QEMU 中的 Cache 模型有一点复杂.

在虚拟化的世界中, 一个对象通常有两端, guest 端和 host 端, 或者称为 frontend 和 backend. 比如 vcpu 对象, 在 frontend 是一个 CPU, 在 backend 端, 它只是 一个线程, 对磁盘来讲, frontend 端看到 的是一个磁盘设备, 在 backend 端, 仅仅 是一个普通的文件而已.

所以 QEMU 中的 Cache, 就有两种情况, guest(frontend) 看到的 disk 的 Cache, 和 host(backend)看到的那个普通文件的 Cache. QEMU 需要对前者进行模拟, 对后者 需要管理. 后面会用代码详细解释 QEMU 是怎么实现的.

先遍历一下 QEMU 中 Cache 模式:

Cache Mode Host Cache Guest Disk Cache
none off on
writethrough on off
writeback on on
unsafe on ignore
directsync off off

实现的代码分析

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

host end 初始化 block 的流程

根据上面所说的, 模拟磁盘的初始化分为前端(磁盘设备)和后端(镜像文件)的初始化, 在后端的初始化中, 入口函数是 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")) {
        /* 由上, 这个组合表示的是 host end 不用 Cache, 数据直接在用户空间(QEMU)
         * 和真实设备之间通过 DMA 直接传输, 但是同时, 告诉 guest 模拟的磁盘设备
         * 是有 cache 的, guest 能发起 flush 的操作(fsync/fdatasync) */
        *flags |= BDRV_O_NOCACHE | BDRV_O_CACHE_WB;
    } else if (!strcmp(mode, "directsync")) {
        /* 很好理解, 完全没有 cache, host end 和 guest end 都没有 cache, guest
         * 不会发起 flush 的操作 */
        *flags |= BDRV_O_NOCACHE;
    } else if (!strcmp(mode, "writeback")) {
        /* 和上面相反, host side 和 guest side 都有 cache, 性能最好, 但是如果
         * host 掉电, 会导致数据的损失 */
        *flags |= BDRV_O_CACHE_WB;
    } else if (!strcmp(mode, "unsafe")) {
        /* 见文可知意, 最不安全的模式, guest side 有cache, 但是 host side 不理睬
         * guest 发起的 flush 操作, 完全忽略, 这种情况性能最高, snapshot 默认使用
         * 的就是这种模式 */
        *flags |= BDRV_O_CACHE_WB;
        *flags |= BDRV_O_NO_FLUSH;
    } else if (!strcmp(mode, "writethrough")) {
        /* host end 有 cache, guest 没有 cache, 其实通过后面的代码分析可以知道,
         * 这种模式其实是 writeback + flush 的组合, 也就是每次写操作同时触发
         * 一个 host flush 的操作, 会带来一定的性能损失, 尤其是非 raw(e.g. qcow2)
         * 的网络存储(e.g. ceph), 但是很遗憾, 这是 QEMU 默认的 Cache 模式 */
        /* this is the default */
    } else {
        return -1;
    }
 
    return 0;
}
 
DriveInfo *drive_init(QemuOpts *all_opts, BlockInterfaceType block_default_type)
{
    /* code snippet */
 
    value = qemu_opt_get(all_opts, "cache");
    if (value) {
        int flags = 0;
 
        /* 解析命令行 -drive 的 cache= 选项 */
        if (bdrv_parse_cache_flags(value, &flags) != 0) {
            error_report("invalid cache option");
            return NULL;
        }
 
        /* Specific options take precedence */
        if (!qemu_opt_get(all_opts, "cache.writeback")) {
            qemu_opt_set_bool(all_opts, "cache.writeback",
                              !!(flags & BDRV_O_CACHE_WB));
        }
        if (!qemu_opt_get(all_opts, "cache.direct")) {
            qemu_opt_set_bool(all_opts, "cache.direct",
                              !!(flags & BDRV_O_NOCACHE));
        }
        if (!qemu_opt_get(all_opts, "cache.no-flush")) {
            qemu_opt_set_bool(all_opts, "cache.no-flush",
                              !!(flags & BDRV_O_NO_FLUSH));
        }
        qemu_opt_unset(all_opts, "cache");
    }
 
    return blockdev_init(all_opts, block_default_type);
}

接下来的流程稍微复杂一点:

blockdev_init
  -> bdrv_open
    -> bdrv_file_open
      -> raw_open
        -> raw_open_common
          -> raw_parse_flags
          -> qemu_open

首先是从 blockdev_init 到 raw_open 的流程, 简单分析如下:

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 函数, 这个函数和 host side 的 Cache 相关, 通过上面 的分析, 已经比较简单了.

/* 这个函数其实是 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 请求设备寄存器的时候, QEMU 会相应地用这个值填充寄存器的位, 下面以 IDE 硬盘来做例子.

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

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;
}

Cache 的工作模式

首先简单分析一下 guest 到 QEMU 的 I/O 执行路径:

下面的函数 bdrv_co_do_writev(bdrv_co_do_rw), 很好的阐释了 QEMU 怎么模拟 磁盘的 cache

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;
        }
    }
 
flush_parent:
    return bdrv_co_flush(bs->file);
}

那么对于 guest side, 如果磁盘有 cache, 那么 guest 是如何保证同步到磁盘上的:

guest 会根据磁盘的 cache 情况指定相应的调度策略, 来把数据从内存同步到磁盘 中, 或者用户手动指定 fsync, 这时也会发生 flush.

关于 host 刷新的机制, 没有什么好说的, 无非就是调用系统调用 fdatasync, 如果系统不支持 fdatasync, 那么调用 fsync.

# hdparm -I /dev/sdb #cache=writeback or none
Commands/features:
    Enabled Supported:
       *    SMART feature set
       *    Write cache
       *    NOP cmd
       *    48-bit Address feature set
       *    Mandatory FLUSH_CACHE
       *    FLUSH_CACHE_EXT
# hdparm -I /dev/sdc #cache=writethrough or directsync
Commands/features:
    Enabled Supported:
       *    SMART feature set
            Write cache
       *    NOP cmd
       *    48-bit Address feature set
       *    Mandatory FLUSH_CACHE
       *    FLUSH_CACHE_EXT

测试

使用 ubuntu-13.04 server 版本, 内核版本号 3.8.0-19. 24 cores, 20GB的 memory.

PS: 这里只是比较几种 cache 模式的性能, 不能作为专业的测试结果.

FIO 配置:

[test]
rw=rw
rwmixread=50
ioengine=libaio
iodepth=1
direct=1
bs=4k
filename=/dev/sdb
runtime=180
group_reporting=1

Cache Mode Read IOPS Write IOPS
none 293/235/218/200/200 292/234/215/199/200
writethrough 5/36/72/93/113 5/35/73/93/113
writeback 1896/1973/1944/1627/2027 1894/1979/1947/1627/2023

writethough 的性能相当糟糕, 写的性能我还能理解, 读的性能完全不能理解, 仔细 想了一下, 应该是”混合读写”导致的, “写”拖慢了”读”, 我随后单独做了一个随机读 的测试, IOPS 达到 四千多, 这次应该是正常的.

结论

通过以上的分析, writethrough 的性能很差, 因为它几乎相当于 writeback + flush, writeback 的性能最好, 但是在掉电/迁移的情况下不能保证数据安全, none 的读性能 很差, 因为他完全绕过了 kernel 的 buffer.

总的来说, 选择 cache 模式的时候, 至少要考虑以下几种情况:

  • 你的存储是本地还是分布式的, 具体是那种存储, 支不支持 Direct IO(FUSE?), 一般来说网络文件系统使用 writethrough cache 性能很糟糕
  • 是否需要迁移, 有的 cache 模式迁移会导致数据丢失.

分类: QEMU, Storage 标签: ,

Get the guest IP address of Virtual Machine

2013年9月23日 没有评论

These days, I am developing a small project. One of the convenient functions it provides is that user can access QEMU virtual machine directly via ssh as long as user knows the name of VM.

There are two methods to archive this if user knows the guest’s MAC address:

Using ARP

ARP is a protocol which is used to convert an IP address to MAC Address.

With the ARP, we can get the MAC Address of a machine by its IP address, but what we have known is guest’s MAC address and what we want to know is IP address.

A hack method is to query all IP<->MAC address, then filter them by guest’s MAC.

#!/usr/bin/python
 
import commands
import sys
 
def find_ipaddr(mac):
    outtext = commands.getoutput('arp -n')
    for l in outtext.split('\n'):
        if l.split()[2] == mac:
            return l.split()[0]
    return None
 
if __name__ == '__main__':
    if len(sys.argv) != 2:
        print 'please input MAC Address'
        exit(1)
    ip = find_ipaddr(sys.argv[1])
    if ip:
        print '%s <-> %s' % (sys.argv[1], ip)
    else:
        print '%s <-> None' % (sys.argv[1])

Using GuestAgent

GuestAgent is a tool provided by QEMU official which aims to provide access to a system-level agent via standard QMP commands.

There are many commands available, one of them named “guest-network-get-interfaces”, outputs guest network information we need.

This way is more useful and graceful than using ARP, we can also get the guest’s IP address even if guest is on another subnet.

The following code shows how it works:

Start guest, append following arguments:

-chardev socket,id=qga0,path=/tmp/qga.sock,server,nowait \
-device virtio-serial -device virtserialport,chardev=qga0,name=org.mathslinux.qemu

Or if you want to use libvirt to start guest, append following xml under <device> tag

    <channel type='unix'>
      <source mode='bind' path='/tmp/qga.sock'/>
      <target type='virtio' name='org.mathslinux.qemu'/>
    </channel>

Then in the guest, run qemu-ga

./qemu-ga -p /dev/virtio-ports/org.mathslinux.qemu

After guest starts, we can fetch its IP address by connecting and sending “guest-network-get-interface” command to unix socket defined above.

#!/usr/bin/python
 
import socket
import sys
 
path = '/tmp/qga.sock'
 
def find_ipaddr(mac):
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.connect(path)
 
    sock.send('{"execute": "guest-network-get-interfaces"}')
    data = sock.recv(4096)
    for l in eval(data)['return']:
        if l['hardware-address'] == mac:
            ip_list = l['ip-addresses']
            for ip in ip_list:
                if ip['ip-address-type'] == 'ipv4':
                    return ip['ip-address']
 
    return None
 
if __name__ == '__main__':
    if len(sys.argv) != 2:
        print 'please input MAC Address'
        exit(1)
    ip = find_ipaddr(sys.argv[1])
    if ip:
        print '%s <-> %s' % (sys.argv[1], ip)
    else:
        print '%s <-> None' % (sys.argv[1])

分类: QEMU 标签:

QEMU CPU Hotplug

2013年8月22日 没有评论

// this post has not be done yet.

In the world of cloud compute, Scaling the compute resources without shutdown the compute instance is important. e.g. Add/Reduce cpu number, memory size, and so on.

In the version 1.5, QEMU has implemented the feature: CPU Hotplug, it allows virtual machine to add cpu core without shutdown.

Below is how to do it:

Before try this feature, please make sure your QEMU version >= 1.5.0, or this feature is not supported.

# ~/usr/bin/qemu-system-x86_64 --version
QEMU emulator version 1.6.50, Copyright (c) 2003-2008 Fabrice Bellard

Use QEMU command directly to enable this feature

# qemu-system-x86_64 -enable-kvm -m 512 -smp 1,maxcpus=4 ArchLinux.raw -serial \
   tcp::3333,nowait,server -qmp tcp::5555,nowait,server -nographic

the maxcpus means the maximum number of hotpluggable CPUs.

[root@myhost ~]# cat /proc/cpuinfo | grep processor
processor       : 0

After active the CPU we just add

# telnet localhost 5555
......
{ "execute": "qmp_capabilities" }  
{"return": {}}
{ "execute": "cpu-add", "arguments": { "id": 1 }}
{"return": {}}

[root@myhost ~]# cat /proc/cpuinfo | grep processor
processor       : 0
processor       : 1

Use libvirt to enable this feature

Make sure the version of libvirt >= 1.1.0, or else it wont work

[root@hdr245 ~]# libvirtd --version
libvirtd (libvirt) 1.1.1

分类: QEMU 标签:

Spice 分析(4) – spice 客户端实现

2013年6月28日 8 条评论

目前上游社区支持的客户端是 spice-gtk, 它其实是一个库的项目, 编译后能得到两个库:

  • libspice-client-glib: spice client 的协议处理部分
  • libspice-client-gtk: 整合了 gtk 的更完善的实现

virt-manager 通过 spice-gtk 实现了 spice client 的功能, 可执行的程序称是 remote-view.

下面对现在市面上存在的各种客户端做一下分类和探讨, 其中对 spice-gtk 重点做 一下探讨, 毕竟这是东宫太子.

使用 spice-gtk

如果要实现 spice 客户端, 最省事最简单的办法莫过于在 libspice-client-gtk 这个库的基础上进行开发了. 比如我曾经介绍过的 编写简单的 Spice Client

但是由于以下种种原因, 基于 spice-gtk 的客户端可能不会是最佳的选择:

gtk3 不支持 X11 的共享内存

如果使用 gtk3, 那么渲染的后端只能是 cairo, 无法选择 X11, 同时意味者 X11 和应用程序的共享内存无法使用, 即每次做 bitmap 渲染的时候, 会在 spice client 和 X11 server 之间多做一次内存拷贝, 损失不必要的性能.

某些平台没有 gtk

在其它平台并没有 gtk 实现, 比如 android, IOS 等, 要移植 gtk 到这些 平台, 不亚于重写 spice client.

性能的考虑

同上面, 如果渲染后端使用 cairo, 在某些平台达不到最好的效果, 实际上, 在 Linux 下, 应该使用 X11 的共享内存技术, 在 windows 或者其它平台也应该使用 相应的技术.

以下几个客户端即是基于 libspice-client-gtk 实现的.

  • remote-view
  • spice-osx
  • spicy(just to be used to test spice-gtk)

使用 spice-glib

libspice-client-glib 完整的实现了 spice 协议的客户端, 只不过用的 C 库是 glib 而已, 它完全和 gtk 无关. 理论上, 每个平台都应该调用 libspice-client-glib, 而图形的渲染 部分, 则由平台相关的 API 来实现, 也即实现 spice-widget.c 部分.

PS: 在 Bitmap 的处理部分, 应该使用平台相关的 API, 这样能达到最佳的性能(最大化的 使用显卡的加速功能), 比如在 linux(X86/ARM) 平台, 使用硬件加速(opengl?) 来处理 Bitmap, 在 windows 平台, 使用 GDI 来处理, 在 macos 平台(?). 遗憾的是, 虽然开发者留下了这些平台独立的接口和文件, 但是并没有完全实现, 这些接口都没有 用到, 目前的实现都是用统一的 pixman 库(纯 CPU 运算, 虽然对各个平台也会做一些优化) 操作, 具体的实现在 common/sw_canvas.c.

言归正传, 下面描述怎么使用 libspice-client-glib, 大部分代码参照以前我在 android 上的 实现的 spice-client.

首先需要实现一个 display 的类(python/c++/c)用来做渲染, 在这个类的构造函数里面, 注册相应的信号处理函数(这些信号由 SpiceSession 在有事件到来的时候通知你), 例如:

    AndroidDisplay *display;
    AndroidDisplayPrivate *d;
 
        // ......
 
    if (!d->session) {
        g_error("AndroidDisplay constructed without a session");
    }
 
    spice_g_signal_connect_object(d->session, "channel-new",
                                  G_CALLBACK(channel_new), display, 0);
    spice_g_signal_connect_object(d->session, "channel-destroy",
                                  G_CALLBACK(channel_destroy), display, 0);

其中 channel-new 这个信号, 表示 SpiceSession 监听到有新的 channel(display, input) 在的时候, 主动通知我们的 AndroidDisplay 对象, 我们使用 channel_new() 来处理这个 信号:

static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
{
    AndroidDisplay *display = data;
    AndroidDisplayPrivate *d = ANDROID_DISPLAY_GET_PRIVATE(display);
    int id;
 
    g_object_get(channel, "channel-id", &id, NULL);
    if (SPICE_IS_MAIN_CHANNEL(channel)) {
            // ......
    }
 
    if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
        SpiceDisplayPrimary primary;
        if (id != d->channel_id)
            return;
        d->display = channel;
                // 处理 primary 信号, 下面会讲到
        spice_g_signal_connect_object(channel, "display-primary-create",
                                      G_CALLBACK(primary_create), display, 0);
        spice_g_signal_connect_object(channel, "display-primary-destroy",
                                      G_CALLBACK(primary_destroy), display, 0);
        // 这个信号比较重要, 每次 guest 做任何 UI 的操作, blit, copy 等, 必然会
        // 重绘某些区域, 等到 SpiceDisplay 处理完 bitmap 的变换之后, 会给注册
        // 了这个信号的组件发送 display-invalidate 信号, 告诉这些组件哪些区域需要
        // 重绘.
        spice_g_signal_connect_object(channel, "display-invalidate",
                                      G_CALLBACK(invalidate), display, 0);
        spice_g_signal_connect_object(channel, "display-mark",
                                      G_CALLBACK(mark), display, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
        if (spice_display_get_primary(channel, 0, &primary)) {
            primary_create(channel, primary.format, primary.width, primary.height,
                           primary.stride, primary.shmid, primary.data, display);
            mark(display, primary.marked);
        }
        spice_channel_connect(channel);
        spice_main_set_display_enabled(d->main, get_display_id(display), TRUE);
        return;
    }
 
    // input 通道, 处理鼠标键盘事件
    if (SPICE_IS_INPUTS_CHANNEL(channel)) {
        d->inputs = SPICE_INPUTS_CHANNEL(channel);
        spice_channel_connect(channel);
        return;
    }
 
    // handle other channels, e.g. curse, usb-redir
    return;
}

下面是 invalidate 的实现.

static void invalidate(SpiceChannel *channel,
        gint x, gint y, gint w, gint h, gpointer data)
{
    AndroidDisplay *display = data;
    AndroidDisplayPrivate *d = ANDROID_DISPLAY_GET_PRIVATE(display);
    android_pd = d;
    /* 正如上面讲到的, 这个是重绘的处理函数, 不同的平台这里可能会不同.
       如果是 gtk: 应该调用相应的 gtk_widget_queue_draw_area() 等函数通知上层
       UI 更新相应的显示区域. 因为这里 android 没有 gtk, 所以这里使用了 java
       的 JNI 技术通知 Android UI 需要重绘某些区域, 把控制权交给 Android UI. */
    spice_callback("OnGraphicsUpdate", "(IIII)V", x, y, w, h);
 
    /* 另外提醒一点, 所有的 bitmap 数据都存放在 d->data 里, 所以真正重绘的时候,
       可以从这里取数据, 如果后端的渲染是 cairo 的话, 每次 "display-primary-create
       发生的时候,  spice-gtk 会使用 cairo_image_surface_create_for_data()
       把 d->data 映射到它的画板(canvas)上去. */
 
    return ;
}

如果要写 IOS 或者 Android 客户端的话, 应该采用这种方式.

不依赖于其它模块

如果没有 glib 用, 或者其它的什么原因, 那么只能从头自己实现. spice-html5 就是 这么做的, 因为这是浏览器上运行的客户端, 编程语言是 javascript, 也没有 glib.

小玩具

如果对 spice-client 的渲染了如指掌的话, 可以做一些小 hack, 比如我喜欢用酷狗 来听音乐, 因为它的歌词显示得比较好, 但是我用的是 linux 操作系统, 酷狗音乐没有 linux 的版本. 所以如果能只显示虚拟机里面的酷狗歌词到我的 linux 桌面, 那也蛮帅的.

下面是相关的实现代码(trick and ugly):

// file: spice-widget.c
static void invalidate(SpiceChannel *channel,
                       gint x, gint y, gint w, gint h, gpointer data)
{
    // ......
    if (d->ad_drawing_area) {
        // 我在 SpiceDisplayPrivate 定义了一个 GtkWidget *ad_drawing_area 用来
        // 画歌词的窗口区域, hard code
        gtk_widget_queue_draw_area(d->ad_drawing_area, 0, 0, 1024, 113);
    }
}
 
// file: spice-widget-cairo.c
int spicex_image_create(SpiceDisplay *display)
{
    // other code
    if (d->ad_drawing_area) {
        // 我用歌词窗口的信息来创建在 linux 上的窗口的 bitmap 数据.
        d->ad_ximage = cairo_image_surface_create_for_data
                ((guint8 *)d->data + 1024 * 446 * 4, CAIRO_FORMAT_RGB24, 1024, 113, 1024 * 4);
    }
    return 0;
}
 
// file: spicy.c
static gboolean ad_draw_event(GtkWidget *widget, cairo_t *cr, void *opaque)
{
    int ww, wh;
    SpiceDisplayPrivate *d = opaque;
 
    ww = gdk_window_get_width(gtk_widget_get_window(widget));
    wh = gdk_window_get_height(gtk_widget_get_window(widget));
 
    cairo_rectangle(cr, 0, 0, ww, wh);
 
    cairo_set_source_surface(cr, d->ad_ximage, 0, 0);
    cairo_paint(cr);
 
    return TRUE;
}
 
static SpiceWindow *create_spice_window(spice_connection *conn, SpiceChannel *channel, int id, gint monitor_id)
{
    // other codes
    d->ad_drawing_area = gtk_drawing_area_new();
    // 注册 draw 信号, 当 gtk_widget_queue_draw_area() 被调用的时候,
    // 会触发 draw 信号, 表示某些区域需要被更新.
    g_signal_connect(d->ad_drawing_area, "draw",
                     G_CALLBACK(ad_draw_event), d);
    // other codes
}

附上截图一张 kugou-geci-redir

分类: QEMU, Spice 标签: ,

QEMU VGA 分析(0) – 显卡组件剖析

2013年5月8日 没有评论

显卡的基本功能就是允许 CPU 操作显卡的内存(显存), 然后在对存储在这些 内存中的信息进行一些变换, 产生可以供显示器使用的信号.

VGA_Overview

Frame Buffer

Frame Buffer 存储了显示器需要显示的像素, 是显卡的核心部件. 几乎所有的操作都是使用里面存储的数据. Frame Buffer 以内存的形式存在的, 或者, 有的显卡会实用 VRAM 来做显存. 显存的大小决定了显卡所能支持的最大分 辨率, Frame Buffer 被映射到 CPU 的地址空间上, 应用程序就可以像访问普通内存 一样的访问它.

The Sequencer

Sequencer 的主要作用是把显存的数据转换成像素的 color index, DAC 会用 这些信息进行数模转换等操作.

Graphics Controller

GC 是操纵 frame buffer 的接口, 是显卡芯片最主要的部分之一. 它可以让 CPU 操作 frame buffer. 允许执行一些标准的显卡操作, 例如在加速 芯片里面的画线, 填充区域, 颜色转换, 3D加速渲染等, 这些操作有效的降低了 CPU 的负荷.

各组件如何工作

流程是这样的: 把数据从 frame Buffer 里面取出来, 把像素从数字信号转 换为显示器需要的模拟信号. 首先从显存中顺序地读取像素数据, 然后通过 一个调色表 (palette look-up table)转换成模拟信号. 同时, CRT 控制器 发射定时信号, 使得显示器显示模拟的颜色信息.

例如, sequencer 首先从 frame buffer 中读取数据, 然后转化为像素颜色数据, 读的同时给 CRT 控制器发送定时信号, 使得 CRT 控制器能提供显示器需要的定时信号. 被转换的颜色数据被传送到 attribute controller 进行格式化为 DAC 识别的格式. DAC 通过 查询 palette table, 把这些数据转换为模拟信号, 然后随着定时信号, 输出到 显示器上.

分类: QEMU, VGA 标签: ,

QEMU 的 CPU 配置

2013年4月11日 没有评论

根据前面描述 CPU 的基本知识, 可以知道 CPU 有物理 CPU, 多核 CPU, 超线程 CPU 之分.

事实上, QEMU 支持所有这些配置, 下面一一举例来说明如何模拟这些 CPU.

基本的 CPU 模拟

下面的指令模拟了一个具有 1 个物理 CPU, 两个逻辑 CPU 的系统

$ qemu -enable-kvm -m 1024 ArchLinux.img -smp 2,sockets=1

在 guest 上看看 cpuinfo 的信息:

$ cat /proc/cpuinfo
processor   : 0
physical id : 0
siblings    : 2
core id     : 0
cpu cores   : 2
 
processor   : 1
physical id : 0
siblings    : 2
core id     : 1
cpu cores   : 2

可以看到两个逻辑 CPU 是双核的, 没有使用超线程技术.

指定核心数

模拟一个具有 1 个物理 CPU(双核), 四个逻辑 CPU 的系统. 此时为了满足双核 四线程的概念, 得启用超线程技术, 如下

$ qemu -enable-kvm -m 1024 ArchLinux.img -smp 4,sockets=1,cores=2
$ cat /proc/cpuinfo
processor   : 0
physical id : 0
siblings    : 4
core id     : 0
cpu cores   : 2
 
processor   : 1
physical id : 0
siblings    : 4
core id     : 0
cpu cores   : 2
 
processor   : 2
physical id : 0
siblings    : 4
core id     : 1
cpu cores   : 2
 
processor   : 3
physical id : 0
siblings    : 4
core id     : 1
cpu cores   : 2

指定 thread 数

模拟一个具有 2 个物理 CPU, 四个逻辑 CPU 的系统, 启用超线程技术, 每个核心两个 线程. 不难算出, 此时每个 CPU 都是单核的(4 = 2*2*1).

$ qemu -enable-kvm -m 1024 ArchLinux.img -smp 4,sockets=2,threads=2
$ cat /proc/cpuinfo
processor   : 0
physical id : 0
siblings    : 2
core id     : 0
cpu cores   : 1
 
processor   : 1
physical id : 0
siblings    : 2
core id     : 0
cpu cores   : 1
 
processor   : 2
physical id : 1
siblings    : 2
core id     : 0
cpu cores   : 1
 
processor   : 3
physical id : 1
siblings    : 2
core id     : 0
cpu cores   : 1

其它

事实上, QEMU 还有更强大的 CPU 的配置, 比如配置 CPU 指令级, 配置 NUMA, 等等, 这里不一一列举.

分类: QEMU 标签:

How to install spice client on Raspberry Pi

2013年1月7日 没有评论

Download source

First, we need to download the source of spice client from spice official website, the latest stable version is 0.14.

$ wget http://spice-space.org/download/gtk/spice-gtk-0.14.tar.bz2

Install dependencies packages

Spice client depends many other packages, e.g. jpeg, gtk, audio … We must install these packages before compiling source.

Furthermore, since we install spice client from source, packages related to compile are also needed, e.g. gcc, autoconf, libtool

The celt package in raspbian’s repository(0.7.1) is newer than Spice client requires(0.5.1), so we have to install this required version from source.

$ sudo apt-get install libogg-dev
$ wget https://launchpadlibrarian.net/59154526/celt_0.5.1.3.orig.tar.gz
$ tar xf celt_0.5.1.3.orig.tar.gz
$ cd celt-0.5.1.3
$ ./configure --prefix=/usr
$ make && sudo make install

All packages we need to install are following:

$ sudo apt-get install build-essential autoconf libtool intltool libspice-protocol-dev libgtk2.0-dev libssl-dev libpulse-dev gobject-introspection libgirepository1.0-dev libjpeg8-dev pulseaudio

Compile and install source

This step, we are ready to compile and install Spice client.

A accelerated X driver for Raspberry Pi are being developped, and a test version has been released. The driver takes advantage of hardware acceleration for display. So spice client could run much smoother.

But by default, spice-gtk uses cairo as display backend, which does not use hardware acceleration. In order to use hardware acceleration, we must configure “–with-gtk=2.0”.

$ tar xf spice-gtk-0.14.tar.bz2
$ cd spice-gtk-0.14
$ ./configure --prefix=/usr --disable-maintainer-mode --disable-static --enable-introspection --without-python --without-sasl --disable-polkit --disable-vala --enable-smartcard=no --with-gtk=2.0
$ make
$ sudo make install

Ok, if no errors occur, we have completed the compilation and installation, it’s time to enjoy it.

Done, connect to server

Now you can connect to your VM using following command:

$ spicy -h spice_server -p port

That’s all, enjoy it!

Drag And Drop support in QEMU/Spice

2012年11月25日 4 条评论

好久没有写 Blog 了, 最近的业余时间我在给 Spice 社区挖一个坑, 让QEMU/Spice 的虚拟机也支持文件拖拽, 简单的讲, 就是通过鼠标拖拽文件/文件夹, 使得可以在 client 和 guest 之间传输文件, 这个功能 Vmware 和 Virtualbox 已经有了.

我决定给 spice 也加上这个功能, 目前的状态是:

  1. 只支持 client -> guest 的拖拽
  2. 代码基本实现了, 正在 merge 到 upstream. 目前正在和上游的 maintainer 交流一些细节方面的问题, 得到了很多 Developper 的支持.

相信不久这个功能就可以在正式版中用了.

分类: QEMU, Spice 标签: ,

Spice 分析(3) – 编写 Spice Client

2012年9月20日 4 条评论

spice-gtk 的帮助下, spice client 的编写非常简单. 以致于我在做 Spice Server 的测试的时候, 顺手写了一个简单的 spice client.

把下面的一些核心部分做一个剖析:

static void channel_new(SpiceSession *s, SpiceChannel *c, gpointer *data);
 
/* 创建一个 Spice session */
spice_session = spice_session_new();
 
/* 设置 Spice 的地址和端口 */
g_object_set(spice_session, "host", host, NULL);
g_object_set(spice_session, "port", port, NULL);
 
/* 设置当 Spice Channel 建立之后的 callback, 也就是说这个时候可以
 * 获取 Spice Gtk 建立的 Spice Widget, 包括 Spice Window 等 */
g_signal_connect(spice_session, "channel-new",
                 G_CALLBACK(channel_new), NULL);
 
/* 最后调用这个 API, 连接 Server 就可以了 */
spice_session_connect(spice_session);
 
static void channel_new(SpiceSession *s, SpiceChannel *c, gpointer *data)
{
    int id = 0;
 
    /* 获取通道 ID */
    g_object_get(c, "channel-id", &id, NULL);
 
    if (SPICE_IS_DISPLAY_CHANNEL(c)) {
        /* 对 Display 通道, 获取 spice window, 然后把它加入我们的容器(主窗口,
         * VBox 等), 这里的 main_window 是我用 gtk_window_new() 创建的主窗口 */
        spice_display = spice_display_new(s, id);
        gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(spice_display));
        gtk_widget_show_all(main_window);
    }
 
}

事实上, 核心代码不超过 10 行, 如此的简单, 当然更多的鼠标, 键盘事件, USB 重定向 等, 只要自己写 signal 的 callback 就可以了.

完整的代码在 我的github 上:

$ git clone git://github.com/mathslinux/qemu-tools
$ cd qemu-tools/spice-tools/
$ make
$ ./spice-client

分类: QEMU, Spice 标签: ,