存档

2012年9月 的存档

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 标签: ,

我的生命不过是温柔的疯狂

2012年9月17日 没有评论

我是被天上的彩虹罚下地狱

幸福曾是我的灾难

我的忏悔 我的蛆虫

我的生命如此辽阔

不仅仅献于力与美

–阿尔蒂尔·兰波 地狱一季·言语炼金术

在你的身上, 似乎看到了我的影子, 种种矛盾不安, 狂野的性格, 还有那对自由的崇尚. 是城市的喧嚣, 是自己内心的对宁静的渴望, 抑或是压抑的内心的躁动, 让我不可自拔的 爱上你的诗.

兰波, 你为什么让我如此的感动.

分类: 文艺 标签:

Spice 分析(2) – Spice Server 架构分析

2012年9月17日 没有评论

概述

Spice 是一个 Virtual Device Interfaces(VDI) 的库, 它以 libspice 库的形式提供给 VDI 后端, 这个后端可以是 QEMU, 或者 Xorg 等.

架构模型

根据上面提到的 VDI 的概念, 它是一个非常复杂的模型, 包括无数的组件: 显示模块, 输入输出模块, 各种 guest agent模块, 所以需要一个精心设计 的架构来使得各个模块流畅的运行.

其实处理 VDI 的这种编程模型, 有两种比较流行的架构模型

事件驱动架构

在一个 main loop 里面通过 select 或者 poll 各种句柄, 来对 用户的事件进行处理. 典型的比如大部分基于 gtk 程序.

并行处理架构

开辟多个进程/线程分别处理不同的模块.

Spice server 严格说来属于上面的事件驱动架构, 即它是通过抛出一系列的文件句柄给 调用者, 然后在 mainloop(甚至 几乎所有 mainloop 都抛出给caller) 上轮询这些 句柄, 实现他的架构的. 这个架构写过 GTK 或者用过 libevent 的人应该不会陌生.

SS 通过提供接口的注册函数(spice_server_add_interface()), 使得 caller 可以把想要处理的模块注册到 SS 系统中, 比如想要实现声音的播放, 需要注册 SpicePlaybackInterface 实例到 SS 系统中, 当不想用这个模块了, 可以用 spice_server_remove_interface() 将它从 SS 中移除.

在所有的模块中, 其中有一个接口(Core interface)比较特殊, 这个接口是在 SS 初始化的时候被注册的. 这个接口主要用于监听 Spice client 的连接请求, 管理 channels 的注册和移除等.

初始化

SS 提供了一个 API spice_server_init() 来初始化 SS. 它的原型如下:

/** 
 * 初始化 SS
 * 
 * @param s SS 的实例, 用 spice_server_new() 创建
 * @param core 前面提到的 Core interface
 * 
 * @return 
 */
int spice_server_init(SpiceServer *s, SpiceCoreInterface *core);

其中 SpiceCoreInterface 这个 API 的核心. 下面重点介绍:

struct SpiceBaseInterface {
    /* 接口类型 */
    const char *type;
    /* 接口描述 */
    const char *description;
    /* 接口的主版本号, 初始化这个接口的时候会判断用户传递的值和 SS 允许的
     是否相等, 不相等初始化失败 */
    uint32_t major_version;
    /* 接口的次版本号, 初始化的时候比较用户传递的和 SS 允许的大小, 大于
     SS 允许的则初始化失败 */
    uint32_t minor_version;
};
 
struct SpiceCoreInterface {
    /* 接口信息, 见上面的分析 */
    SpiceBaseInterface base;
 
    /* 添加一个定时器, 旧版本的 SS 会用这个 API 自动创建一个 timer, 新版本要求
     * caller 传递 timer 给这个 API, 这个 timer 主要用来在超时的时候调用
     * callback, 超时时间是由下面的 timer_start() 设置的 */
    SpiceTimer *(*timer_add)(SpiceTimerFunc func, void *opaque);
 
    /* 设置超时时间, 在 ms 毫秒之后调用 callback */
    void (*timer_start)(SpiceTimer *timer, uint32_t ms);
 
    /* 取消上面设置的超时时间 */
    void (*timer_cancel)(SpiceTimer *timer);
 
    /* 移除 timer */
    void (*timer_remove)(SpiceTimer *timer);
 
    /* 添加一个 watch, 前面说过, SS 是事件驱动模型的, SS 会在内部创建事件
     * 源(客户端连接),把这些事件的句柄通过文件的形式做成接口, caller
     * 实现这些接口, 比如 用 select 轮询这些句柄. */
    SpiceWatch *(*watch_add)(int fd, int event_mask, SpiceWatchFunc func, void *opaque);
    /* 修改文件句柄的读写属性 */
    void (*watch_update_mask)(SpiceWatch *watch, int event_mask);
    /* 移除对该文件句柄的g监听 */
    void (*watch_remove)(SpiceWatch *watch);
 
    /* 当有一些特殊的事件发生时, 会调用这个 API, 比如新客户端连接, 断开等,
     * caller 可以根据自己的需求做一些操作, 比如 QEMU 会创建一个 QMP 消息
     * 等 */
    void (*channel_event)(int event, SpiceChannelEventInfo *info);
};

注册接口

上面说过, 通过注册接口的函数, 可以把想要处理的模块添加到 SS 系统中, 这个 API 就是:

int spice_server_add_interface(SpiceServer *s, SpiceBaseInstance *sin);

以注册键盘为例:

static void kbd_push_key(SpiceKbdInstance *sin, uint8_t frag)
{
}
 
static uint8_t kbd_get_leds(SpiceKbdInstance *sin)
{
}
 
static const SpiceKbdInterface kbd_interface = {
    .base.type          = SPICE_INTERFACE_KEYBOARD,
    .base.description   = "keyboard",
    .base.major_version = SPICE_INTERFACE_KEYBOARD_MAJOR,
    .base.minor_version = SPICE_INTERFACE_KEYBOARD_MINOR,
    /* Client的键盘输入 */
    .push_scan_freg     = kbd_push_key,
    /* Client查询 led 的状态 */
    .get_leds           = kbd_get_leds,
};
 
SpiceKbdInstance *kbd;
kbd = g_malloc0(sizeof(*kbd));
kbd->base.sif = &kbd_interface.base;
/* 调用 API 注册 */
spice_server_add_interface(spice_server, &kbd->base);

分类: QEMU, Spice 标签: ,

Spice 分析(1) – 构建 Spice 开发环境

2012年9月17日 没有评论

Spice 基本的组件包括:

spice-protocol
Spice 协议, 全是以头文件的形式提供的, 这个头文件 expose 给外部用到spice的相关程序, e.g. QEMU, spice-gtk
spice-common
提供了一些公共的模块, 比如内存分配的 API, ssl 的 API 等, 是给 spice-server 和 spice-gtk 共同使用的.
spice-server
以库的形式提供接口, 一方面用 spice-protocol 和 client 通信, 一方面, hypervise 调用其提供的接口提供 VDI 的能力, 如果是做客户端开发, 这个组件是不需要的.
spice-gtk
用 gtk 封装的一系列客户端与 spice-server 通信的 API, 包括 连接 spice 实例, 传递鼠标键盘事件等等, 如果是做 hypervise 端的开发, 比如 只使用 QEMU/KVM, 那么不需要这个组件.

clone 项目仓库

在 git 仓库里, spice-common 作为 spice 和 spice-gtk 的一个子项目存在, spice-protocol 又是在 spice-common 里的一个子项目, 所以只需要 clone spice 和 spice-gtk 就可以了.

$ git clone git://git.freedesktop.org/git/spice/spice
$ git clone git://git.freedesktop.org/git/spice/spice-gtk

编译安装

$ cd spice && ./autogen.sh --prefix=/usr && make -j5 && sudo make install
$ cd spice-gtk && ./autogen.sh --prefix=/usr && make -j5 && sudo make install

当然可以在编译的时候做一些更深的定制, 比如我的:

$ cd spice && ./autogen.sh --prefix=/usr --build=x86_64-pc-linux-gnu \
--host=x86_64-pc-linux-gnu --mandir=/usr/share/man --infodir=/usr/share/info \
--datadir=/usr/share --sysconfdir=/etc --localstatedir=/var/lib \
--libdir=/usr/lib64 --disable-dependency-tracking --disable-static \
--disable-tunnel --enable-client --enable-gui --without-sasl \
--disable-static-linkage && make -j5 && sudo make install
 
$ cd spice-gtk && ./autogen.sh --prefix=/usr --build=x86_64-pc-linux-gnu \
--host=x86_64-pc-linux-gnu --mandir=/usr/share/man --infodir=/usr/share/info \
--datadir=/usr/share --sysconfdir=/etc --localstatedir=/var/lib \
--libdir=/usr/lib64 --disable-dependency-tracking --disable-maintainer-mode \
--disable-static --enable-introspection --with-audio=pulse --without-python \
--without-sasl --enable-polkit --disable-vala --with-gtk=3.0 --enable-werror \
--enable-usbredir && make -j5 && sudo make install

分类: QEMU, Spice 标签: ,

以只读方式启动虚拟机

2012年9月5日 2 条评论

有时候想在虚拟机里面做一些测试, 又不想让这些测试损坏虚拟机镜像, 这时候就 需要能有一种只读的方式可以用来启动虚拟机.

其实, QEMU 支持各种各样的快照模式, Live Snapshot, Temporary Snapshot 等等. 利用这些模式, 就可以实现上面所说的功能.

创建一个快照

用 qemu-img 指令创建一个原始镜像的快照.

$ qemu-img create -f qcow2 -b vm-base.img vm-append.img

vm-base.img 是原始的镜像, vm-append.img 是快照名字. 之后对 VM 所做的所有修改都只会改变 vm-append.img, vm-base.img 将是只读的.

启动 VM 的时候, 使用创建的这个快照文件即可.

$ qemu-kvm -hda vm-append.img

据我所知, 有些基于 QEMU/KVM 的虚拟化管理平台(e.g. oVirt)有一种叫 stateless 的启动模式, 就是用的上面的方式, 启动虚拟机之前创建一个快照, 虚拟机结束之后再把快照删除.

NB: raw 不支持快照, 所以最好用 qcow2 的镜像格式(qed 也可以)

使用临时快照

用上面的方式来实现 Read-only 略显麻烦, 比如每次启动虚拟机的时候都要手动 创建一个快照, 感觉有点奇怪, 再比如更严重的, QEMU 异常退出怎么办? 快照文件 岂不是会占用而外的空间, 难道需要开启另外一个 daemon 程序监控 QEMU?

好在, QEMU 支持另一种快照模式 – 临时快照, 这种模式不需要经过 1)创建快照, 2)指定快照名称, 3)VM 关机后删除快照就可以实现上述功能.

使用也很简单, 在 QEMU 启动的时候, 增加一个 -snapshot 的参数就可以了.

$ qemu-kvm -hda vm.img -snapshot

下面简叙一下原理: 如果传递了 -snapshot 的参数, 会在初始化 img 的时候创建 一个临时的快照. 并且在具体打开这个 img 的时候把这个文件删除. 请注意, Linux 允许一个文件打开之后删除, 其实只是删除了文件名, 对这类文件的正常操作, 内核会有一个比较优雅的方式来处理.

但是为什么要删除呢而不是让用户visable呢? 在我看来有两点:

  1. 一旦这个进程结束(正常或者是异常), 这个文件的内容马上就被系统回收, 不会造成空间浪费. 感觉是不是比上面的方式优雅?
  2. 处于安全性的考虑, 一旦文件被删除以后, 除了这个进程(VM进程), 其它 是不可能打开和操作这个文件的, 文件的安全性得到保障. 之前在邮件列表 上看到有人提到把这个文件名让用户来设置, 看了它的代码实现后, 发觉 QEMU 的开发者想问题的时候还是很靠谱的.(当然, 这个提议没有通过)

之后, 如果用户想要把快照的内容写回去, QEMU 提供了方式可以写回去的方式, 例如在 nographic 模式下用 Ctrl-a s 把数据写回去, 过多的 write back 不再讨论, 毕竟这里主要研究”只读”.

int get_tmp_filename(char *filename, int size)
{
    int fd;
    const char *tmpdir;
    tmpdir = getenv("TMPDIR");
    if (!tmpdir)
        tmpdir = "/tmp";
    if (snprintf(filename, size, "%s/vl.XXXXXX", tmpdir) >= size) {
        return -EOVERFLOW;
    }
    fd = mkstemp(filename);
    if (fd < 0 || close(fd)) {
        return -errno;
    }
    return 0;
}
 
/* 检查用户是否启用临时快照 */
if (flags & BDRV_O_SNAPSHOT) {
    ret = get_tmp_filename(tmp_filename, sizeof(tmp_filename));
}
 
/* 打开文件的时候检查是否是临时快照, 如果是, 把文件删除 */
static int bdrv_open_common(BlockDriverState *bs, const char *filename,
    int flags, BlockDriver *drv)
{
    if (bs->is_temporary) {
        unlink(filename);
    }
}

分类: kvm, QEMU 标签: ,