8.1.1 qemu内部命令架构
(1) 管理模块的char device
qemu采用monitor来管理内部命令, 当用户在qemu虚拟机界面上输入Ctrl+ALT+2将进入qemu monitor; 然后按CTRL+ALT+1将恢复当正常的虚拟机窗口。 monitor的初始化代码如下:
 main(vl.c) ==>  
 if (qemu_opts_foreach(qemu_find_opts("mon"),mon_init_func, NULL, 1) != 0)
        exit(1);
 
static intmon_init_func(QemuOpts *opts, void *opaque) {
    ......
    chr = qemu_chr_find(chardev); //chr用来控制用户输入输出
    monitor_init(chr, flags);
    return 0;
}
monitor 需要与char device绑定,用于管理命令的输入与信息输出。
目前qemu 的在vl.c中会用到的chardev示例如下:
    if (foreach_device_config(DEV_SERIAL,serial_parse) < 0)
        exit(1);
    if (foreach_device_config(DEV_PARALLEL,parallel_parse) < 0)
        exit(1);
    if (foreach_device_config(DEV_VIRTCON,virtcon_parse) < 0)
        exit(1);
    if (foreach_device_config(DEV_DEBUGCON,debugcon_parse) < 0)
        exit(1);
virtcon_parse ==>  qemu_chr_new(label, devname, NULL);
CharDriverState*qemu_chr_new(const char *label, const char *filename, void (*init)(structCharDriverState *s))
{
    ......
    if (strstart(filename,"chardev:", &p)) { //若filename对应的chardev已存在
        return qemu_chr_find(p);
    }
    //根据filename设置opt属性
    opts = qemu_chr_parse_compat(label, filename);
    if (!opts)
        return NULL;
 
    chr = qemu_chr_new_from_opts(opts, init);
    if (chr && qemu_opt_get_bool(opts,"mux", 0)) {
        monitor_init(chr,MONITOR_USE_READLINE);
    }
    qemu_opts_del(opts);
    return chr;
}
qemu_chr_new_from_opts ==》    for(i = 0; i < ARRAY_SIZE(backend_table); i++) {
        if (strcmp(backend_table[i].name,qemu_opt_get(opts, "backend")) == 0)
            break;
    }
    chr = backend_table[i].open(opts);
backend_table定义在qemu-char.c中:
static const struct {
    const char *name;
    CharDriverState *(*open)(QemuOpts *opts);
} backend_table[] = {
    { .name = "null",      .open = qemu_chr_open_null },
    { .name = "socket",    .open = qemu_chr_open_socket },
    { .name = "udp",       .open = qemu_chr_open_udp },
    { .name = "msmouse",   .open = qemu_chr_open_msmouse },
    { .name = "vc",        .open = text_console_init },
#ifdef _WIN32
    。。。。。。。
#else
    { .name = "file",      .open = qemu_chr_open_file_out },
    { .name = "pipe",      .open = qemu_chr_open_pipe },
    { .name = "pty",       .open = qemu_chr_open_pty },
    { .name = "stdio",     .open = qemu_chr_open_stdio },
#endif
。。。。。。。。。
{ .name ="tty",       .open =qemu_chr_open_tty },
}
 
chr =backend_table[i].open(opts);回建立具体的chardevice
 
chrdev创建后monitor_init会
voidmonitor_init(CharDriverState *chr, int flags)
{
    if (is_first_init) {
        monitor_protocol_event_init();
        is_first_init = 0;
    }
 
    mon = g_malloc0(sizeof(*mon));
    mon->chr = chr;
    mon->flags = flags;
    if (flags & MONITOR_USE_READLINE) {
        mon->rs = readline_init(mon,monitor_find_completion);
        monitor_read_command(mon, 0);
    }
 
    if (monitor_ctrl_mode(mon)) { //当启动参数设置ctrl后,会采用qmp支持的monitor
        mon->mc = g_malloc0(sizeof(MonitorControl));
        /* Control mode requires specialhandlers */
        qemu_chr_add_handlers(chr,monitor_can_read, monitor_control_read,
                             monitor_control_event, mon);
        qemu_chr_fe_set_echo(chr, true);
 
       json_message_parser_init(&mon->mc->parser,handle_qmp_command);
    } else {
        qemu_chr_add_handlers(chr,monitor_can_read, monitor_read,
                              monitor_event,mon);
    }
 
    QLIST_INSERT_HEAD(&mon_list, mon,entry);
    if(!default_mon || (flags & MONITOR_IS_DEFAULT))
        default_mon = mon;
    sortcmdlist();
}
 
最简单的stdio  char 为例:
qemu_chr_open_stdio ==》 qemu_set_fd_handler2(0, stdio_read_poll,stdio_read, NULL, chr);
stdio_read ==》 qemu_chr_be_write ==》s->chr_read
对于monitor 会调用monitor_read
static void monitor_read(void*opaque, const uint8_t *buf, int size)
{
    。。。。。
    if (cur_mon->rs) {
        for (i = 0; i < size; i++)
           readline_handle_byte(cur_mon->rs, buf[i]); //将数据读到buffer
    } else {
        if (size == 0 || buf[size - 1] != 0)
            monitor_printf(cur_mon,"corrupted command\n");
        else
            handle_user_command(cur_mon, (char*)buf); //收完数据则调用要执行的命令
    }
    cur_mon = old_mon;
}
 
(2) 管理模块架构
handle_user_command(monitor.c)
    cmd = monitor_parse_command(mon, cmdline,qdict); //根据名称与arg查找命令的实现结构
 
    if (handler_is_async(cmd)) {
        user_async_cmd_handler(mon, cmd,qdict);
    } else if (handler_is_qobject(cmd)) {
        QObject *data = NULL;
        cmd->mhandler.cmd_new(mon, qdict,&data);
        if (data) {
            cmd->user_print(mon, data);
            qobject_decref(data);
        }
    } else {
        cmd->mhandler.cmd(mon, qdict);
.......
monitor_find_command ==> search_dispatch_table(mon_cmds,cmdname);
 
static mon_cmd_t mon_cmds[] ={
#include"hmp-commands.h"
    { NULL, NULL, },
};
hmp-commands.h 在编译时由hmp-commands.hx 模板生成
下面是其中的一个代码片段:
{
.name      = "q|quit",
.args_type = "",
.params    = "",
.help      = "quit the emulator",
.user_print = monitor_user_noop,
.mhandler.cmd= hmp_quit,
},
mhandler.cmd为命令的实现函数
 
/root/qemu-kvm/scripts/hxtool -h  <  /root/qemu-kvm/hmp-commands.hx  >  hmp-commands.h  
 
8.1.2 qmp命令
qmp的全称为qemu machine protocol,其作用是让外部的程序来控制qemu. 如libvirt用qmp来向qemu发送管理消息。qmp采用了json数据交换格式。JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯
 
(1) json
JSON建构于两种结构:
 
“名称/值”对的集合(A collection of name/value pairs)。不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组 (associative array)。
值的有序列表(An ordered list ofvalues)。在大部分语言中,它被理解为数组(array)。
这些都是常见的数据结构。事实上大部分现代计算机语言都以某种形式支持它们。这使得一种数据格式在同样基于这些结构的编程语言之间交换成为可能。
 
JSON具有以下这些形式:
a. 对象是一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’ 对”之间使用“,”(逗号)分隔。
b. 数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。
c. 值(value)可以是双引号括起来的字符串(string)、数值(number)、true、false、 null、对象(object)或者数组(array)。这些结构可以嵌套。
d.字符串(string)是由双引号包围的任意数量Unicode字符的集合,使用反斜线转义。一个字符(character)即一个单独的字符串(character string)。
e.字符串(string)与C或者Java的字符串非常相似。
f.数值(number)也与C或者Java的数值非常相似。除去未曾使用的八进制与十六进制格式。除去一些编码细节。
g.空白可以加入到任何符号之间。
 
qemu中解析代码为:json-lexer.c; json-parser.c;json-streamer.c. 本文不分析这些代码
 
(2)QMP协议构成
最基本的使用是通过命令行参数,例子:
1. 启动虚拟机
# qemu [...] -qmptcp:localhost:4444,server,nowait
 
2. 运行 telnet
$ telnet localhost 4444
运行后会打印如下信息:
{"QMP":{"version": {"qemu": {"micro": 0,"minor": 6, "major": 1}, "package":""}, "capabilities": []}}
 
3. 发送 qmp_capabilities 命令e
{ "execute":"qmp_capabilities" }
当qmp 连接建立后,qmp 会发送问候 message,并且进入negotiation模式, 这时只支持qmp_capabliities命令;当收到qmp_capabilities命令后,会进入能处理一般命令的模式
4. 可以开始发送其他命令,如:
{ "execute":"query-commands" }
 
另一种应用场景是qemu-ga, qemu-qga是一个运行在虚拟机内部的普通应用程序(可执行文件名称默认为qemu-ga,服务名称默认为qemu-guest-agent),其目的是实现一种宿主机和虚拟机进行交互的方式,这种方式不依赖于网络,而是依赖于virtio-serial(默认首选方式)或者isa-serial,而QEMU则提供了串口设备的模拟及数据交换的通道,最终呈现出来的是一个串口设备(虚拟机内部)和一个unix socket文件(宿主机上)。
  qga通过读写串口设备与宿主机上的socket通道进行交互,宿主机上可以使用普通的unix socket读写方式对socket文件进行读写,最终实现与qga的交互,交互的协议与qmp(QEMU Monitor Protocol)相同(简单来说就是使用JSON格式进行数据交换),串口设备的速率通常都较低,所以比较适合小数据量的交换. 其源码为qemu-ga.c.
 
下面试qmp命令的数据示例:
 
S: {
        "QMP": {
            "version": {
                "qemu": {
                   "micro": 0,
                   "minor": 6,
                   "major": 1
                },
                "package":""
            },
           "capabilities": [
            ]
        }
    }
C: { "execute": "qmp_capabilities" }
S: { "return": {}}
 
The {"return": {} } response is QMP's success response. An errorresponse will contain the "error" keyword instead of"return".
Eject a medium
 
C: { "execute": "eject", "arguments": {"device": "ide1-cd0" } }
S: { "return": {}}
 
Query VM status
 
C: { "execute": "query-status" }
S: {
       "return": {
           "status":"running",
           "singlestep":false,
           "running": true
       }
   }
 
Asynchronous message
 
S: { "event": "BLOCK_IO_ERROR",
     "data": {"device": "ide0-hd1",
              "operation": "write",
               "action":"stop" },
     "timestamp": {"seconds": 1265044230, "microseconds": 450486 } }
 
 
 
(3) QMP命令实例分析
与hmp命令类似, qmp的模板头文件为qmp-commands-old.h:
static const mon_cmd_tqmp_cmds[] = {
#include"qmp-commands-old.h"
    { /* NULL */ },
};
 
typedef struct mon_cmd_t {
    const char *name;
    const char *args_type;
    const char *params;
    const char *help;
    void (*user_print)(Monitor *mon, constQObject *data);
    union {
        void (*info)(Monitor *mon);
        void (*cmd)(Monitor *mon, const QDict*qdict);
        int (*cmd_new)(Monitor *mon, const QDict *params, QObject **ret_data);
        int (*cmd_async)(Monitor *mon, const QDict *params,
                          MonitorCompletion*cb, void *opaque);
    } mhandler;
    int flags;
} mon_cmd_t;
例:int do_drive_del(Monitor*mon, const QDict *qdict, QObject **ret_data);
QDict时qemu为参数封装的数据类型, 其访问api定义在:
qdict.h; 实现代码为:qdict.c,qinit.c,qbool.c,qlist.c,qfloat.c,qstring.c
 
8.1.3 设备的添加与热插拔
本节分析qemu如何实现设备的动态添加:
{
.name       = "device_add",
.args_type  = "device:O",
.params     = "driver[,prop=value][,...]",
.help       = "add device, like -device on thecommand line",
.user_print =monitor_user_noop,
.mhandler.cmd_new =do_device_add,
},
 
do_device_add ==》 qdev_device_add(qdev-monitor.c)
a)  得到要建立设备的driver和类别信息:
driver = qemu_opt_get(opts,"driver");
obj = object_class_by_name(driver);
k = DEVICE_CLASS(obj);
 
b)  得到要建立设备的bus信息
path = qemu_opt_get(opts, "bus");
if (path != NULL) 
bus = qbus_find(path);
else
bus =qbus_find_recursive(sysbus_get_default(), NULL, k->bus_type);
if (!bus) 
bus = sysbus_get_default();   
 
c)  创建设备 
qdev = DEVICE(object_new(driver));
qdev_set_parent_bus(qdev, bus);
d)  为设备添加id属性: object_property_add_child
 
e)  初始化设备:qdev_init(qdev);
 
对于pci设备还支持动态添加:
pci_device_hot_add(pci_hotplug.c):
    const char *pci_addr = qdict_get_str(qdict,"pci_addr");
    const char *type = qdict_get_str(qdict,"type");
    const char *opts = qdict_get_try_str(qdict,"opts");
    if(strcmp(type, "nic") == 0) 
        dev = qemu_pci_hot_add_nic(mon,pci_addr, opts);
    } else if (strcmp(type,"storage") == 0) 
        dev = qemu_pci_hot_add_storage(mon,pci_addr, opts);
 
cpu的动态添加:
do_cpu_set_nr:
    status = qdict_get_str(qdict,"state");
    value = qdict_get_int(qdict,"cpu");
   //a.调用pc_new_cpu(global_cpu_model);启动一个vcpu线程
   //b. pm_update_sci更新虚拟机acpi信息
    qemu_system_cpu_hot_add(value, state);