【虚拟化】QEMU/KVM(一)——QEMU基本组件

本篇介绍QEMU的事件循环机制、QOM和线程模型,从而为更深入地了解QEMU的具体部件做准备。

1. 事件循环机制

Windows平台上,QEMU需要从三个地方进行poll:

  • 全局变量GArray* gpollfds,其中的每一个元素是GPollFD
  • glib提供的完整框架。glib是一个跨平台的底层库,提供的接口均以g打头;
  • 全局变量WaitObjects wait_objects,是一个自定义的结构,包含事件类型和回调函数等。

但不管是什么机制,思路都是一样的:

  • 确定要poll的fd和event集合;
  • 执行poll(或者select);
  • 处理所有poll到的事件,调用其回调函数。

下面简单分析。

1.1. main_loop初始化

qemu_init_main_loop中完成初始化。

首先完成的是时钟和信号的初始化。信号是Linux机制,QEMU通过signalfd系统调用将一个信号集合变成可读的fd,进而用qemu_set_fd_handler设置该fd的回调函数,并加入到iohandler_ctx事件源里。在Windows上这一函数直接返回0。

    int ret;

    init_clocks(qemu_timer_notify_cb);

    ret = qemu_signal_init(errp);
    if (ret) {
        return ret;
    }

接下来就是和事件机制直接相关的了,首先是qemu_aio_context

    qemu_aio_context = aio_context_new(&local_error);
    if (!qemu_aio_context) {
        error_propagate(errp, local_error);
        return -EMFILE;
    }
    src = aio_get_g_source(qemu_aio_context);
    g_source_set_name(src, "aio-context");
    g_source_attach(src, NULL);
    g_source_unref(src);

然后是iohandler_ctx,它没有像上面一样专门new一下,因为iohandler_get_g_sourceqemu_set_fd_handler(如果是Linux平台,在设置信号时就会调用这个函数)都会判断iohandler_ctx是否已经初始化,否则调用aio_context_new

    src = iohandler_get_g_source();
    g_source_set_name(src, "io-handler");
    g_source_attach(src, NULL);
    g_source_unref(src);

上面两个东西都被称为事件源,它们是AioContext类型,是GSource子类(不是QOM里面的继承机制!),通过g_source_attach注册到给定的上下文中。传入的上下文为NULL,则注册到默认上下文。

这些都是用于glib的概念。glib库中设计了主事件循环(GMainLoop)机制,每一个主事件循环都有其主上下文(GMainContext)。glib还提供了一个默认的主上下文,它可以通过g_main_context_default获得。在不指定GMainLoop时,这一上下文就是默认采用的上下文。

1.2. 三种事件机制

注意下面是围绕Windows平台来讲的,Linux平台将有所不同,特别是gpollfds的部分。

首先介绍对gpollfds的poll操作。gpollfds数组中存放的是GPollFD结构体,它包含fdeventsrevents三个变量,fd不用多说,events用来表示该fd上有什么样的事件需要poll,而revents表示已经发生的事件的类型。

pollfds_fillgpollfds中的所有需要监听的事件加入监听列表,随后用select系统调用检查是否有就绪的fd。如果有,pollfds_poll会将就绪的fd在gpollfds对应的表项上设revents位。

要我说这些代码还是封装在一个额外的函数更好,这样就和glib的框架API是一个等级了,看起来比较清楚。

    FD_ZERO(&rfds);
    FD_ZERO(&wfds);
    FD_ZERO(&xfds);
    nfds = pollfds_fill(gpollfds, &rfds, &wfds, &xfds);
    if (nfds >= 0) {
        select_ret = select(nfds + 1, &rfds, &wfds, &xfds, &tv0);
        if (select_ret != 0) {
            timeout = 0;
        }
        if (select_ret > 0) {
            pollfds_poll(gpollfds, nfds, &rfds, &wfds, &xfds);
        }
    }

可是后面找不到对应执行回调的代码了,我也不知道为什么。

接下来介绍glib框架机制。通过g_main_context_acquire来获取默认上下文context的所有权,以便后面使用preparequerycheckdispatch等函数。acquire是可以看成对一个可递归锁非阻塞获取。接下来下面调用g_main_context_prepare进行准备,然后调用g_main_context_query获取需要poll的fds,它们放在poll_fds数组中。

    g_main_context_acquire(context);
    /* ... */
    g_main_context_prepare(context, &max_priority);
    n_poll_fds = g_main_context_query(context, max_priority, &poll_timeout,
                                      poll_fds, ARRAY_SIZE(poll_fds));

下面设置超时时间。

    if (poll_timeout < 0) {
        poll_timeout_ns = -1;
    } else {
        poll_timeout_ns = (int64_t)poll_timeout * (int64_t)SCALE_MS;
    }

    poll_timeout_ns = qemu_soonest_timeout(poll_timeout_ns, timeout);

接下来打开QEMU全局锁qemu_global_mutex,它也称QEMU大锁(BQL),在后面会有介绍。然后打开重放锁replay,它用于向日志中写入接下来将要发生的读写事件。然后就是poll读写事件,poll完后顺次关上两把锁。

qemu_poll_ns函数和g_poll是一个功能,只不过时间粒度精确到纳秒。poll_fds数组中存放的是GPollFD结构体,g_poll会将所有已发生事件写入到对应结构体的revents域中。

    qemu_mutex_unlock_iothread();

    replay_mutex_unlock();

    g_poll_ret = qemu_poll_ns(poll_fds, n_poll_fds + w->num, poll_timeout_ns);

    replay_mutex_lock();

    qemu_mutex_lock_iothread();

然后调用g_main_context_check,将上次query到的结果传回给主循环,再通过g_main_context_dispatch完成事件分发,最后通过g_main_context_release释放上下文。猜测WaitObjectsg_main_context_dispatch分别调用不同的回调函数;WaitObjects是为支持Windows所需要的特性。

    if (g_main_context_check(context, max_priority, poll_fds, n_poll_fds)) {
        g_main_context_dispatch(context);
    }

    g_main_context_release(context);

最后介绍WaitObjects机制。它的待poll集合是由上面的qemu_poll_ns统一poll的,所以用下面的操作append到poll_fds

    for (i = 0; i < w->num; i++) {
        poll_fds[n_poll_fds + i].fd = (DWORD_PTR)w->events[i];
        poll_fds[n_poll_fds + i].events = G_IO_IN;
    }

poll完之后,各回各家,各找各妈。这些事件源没有加入glib框架,所以只能手动回调。

    if (g_poll_ret > 0) {
        for (i = 0; i < w->num; i++) {
            w->revents[i] = poll_fds[n_poll_fds + i].revents;
        }
        for (i = 0; i < w->num; i++) {
            if (w->revents[i] && w->func[i]) {
                w->func[i](w->opaque[i]);
            }
        }
    }

但是从debug的过程看这三种机制:

  • gpollfds中仅有一个事件,但从未发生过,难怪不执行回调函数也没有问题。
  • WaitObjects从整个repo中看只有一个函数win_stdio_wait_func会调用。
  • 其它应该都是glib事件框架管理的。

所以后面还是会重点学习glib事件机制。

2. QEMU Object Model

本节以edu设备为例,讲解各种C语言大型项目都有的东西——面向对象设计。

2.1. 类型注册

pci_edu_register_types构造了TypeInfo类型的edu_info。它用来描述一个新类:

  • 类名是TYPE_PCI_EDU_DEVICE
  • 继承自TYPE_PCI_DEVICE
  • 给定占用空间和类初始化、实例初始化的函数;
  • 接口,就是让你想起多继承的那个接口。

type_init注册类型。

static void pci_edu_register_types(void)
{
    static InterfaceInfo interfaces[] = {
        {INTERFACE_CONVENTIONAL_PCI_DEVICE},
        {},
    };
    static const TypeInfo edu_info = {
        .name = TYPE_PCI_EDU_DEVICE,
        .parent = TYPE_PCI_DEVICE,
        .instance_size = sizeof(EduState),
        .instance_init = edu_instance_init,
        .class_init = edu_class_init,
        .interfaces = interfaces,
    };

    type_register_static(&edu_info);
}
type_init(pci_edu_register_types)

不妨把type_init宏打开一看:

static void __attribute__((constructor))
do_qemu_init_pci_edu_register_types(void) {
    register_module_init(pci_edu_register_types, MODULE_INIT_QOM);
}

constructor是编译器属性,这些函数的执行将早于main,即所有QOM类型在main之前即完成注册。看一下注册函数:

void register_module_init(void (*fn)(void), module_init_type type)
{
    ModuleEntry *e;
    ModuleTypeList *l;

    e = g_malloc0(sizeof(*e));
    e->init = fn;
    e->type = type;

    l = find_type(type);

    QTAILQ_INSERT_TAIL(l, e, node);
}

QOM类型的type都是MODULE_INIT_QOM,传入e->type;而e->init是构造函数,未来在module_call_init中被调用。注册结束前将该QOM类型加入类型链表。module_call_init会顺着类型链表往下,逐个初始化。

void module_call_init(module_init_type type)
{
    ModuleTypeList *l;
    ModuleEntry *e;

    if (modules_init_done[type]) {
        return;
    }

    l = find_type(type);

    QTAILQ_FOREACH(e, l, node) {
        e->init();
    }

    modules_init_done[type] = true;
}

执行初始化函数的时候,type_register_static最终调用到type_register_internal,在其中根据传入的TypeInfo创建一个TypeImpl结构体,并加入到的哈希表中,哈希表接口由glib提供。

static void type_table_add(TypeImpl *ti)
{
    assert(!enumerating_types);
    g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}

static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

2.2. 类层次结构

下面讲述QOM是怎样实现类和继承概念的。edu设备及其父类的内容我就不写了,继承关系是这样:

  TYPE_PCI_EDU_DEVICE("edu")
->TYPE_PCI_DEVICE("pci-device")
->TYPE_DEVICE("device")
->TYPE_OBJECT("object")

TYPE_OBJECT是所有可实例化类的祖先,这一点和Python、Java等面向对象语言很像。那么它们是如何在QEMU中组织起来的呢?

在类的初始化函数type_initialize中,分配类的存储结构:

ti->class = g_malloc0(ti->class_size);

Object存储结构如下,可以看到属性是用哈希表存放的。

/**
 * ObjectClass:
 *
 * The base for all classes.  The only thing that #ObjectClass contains is an
 * integer type handle.
 */
struct ObjectClass
{
    /*< private >*/
    Type type;
    GSList *interfaces;

    const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
    const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];

    ObjectUnparent *unparent;

    GHashTable *properties;
};

然后对于device类:

typedef struct DeviceClass {
    /*< private >*/
    ObjectClass parent_class;
    /*< public >*/

    DECLARE_BITMAP(categories, DEVICE_CATEGORY_MAX);
    const char *fw_name;
    const char *desc;

    Property *props_;

    bool user_creatable;
    bool hotpluggable;

    DeviceReset reset;
    DeviceRealize realize;
    DeviceUnrealize unrealize;

    /* device state */
    const VMStateDescription *vmsd;

    /* Private to qdev / bus.  */
    const char *bus_type;
} DeviceClass;

继承链就这样一直往下,再到PCI类。edu类没有显式地定义一个新类,而是表现为一个特殊的PCI类,见如下初始化代码。在初始化该类之前,其父类和接口已经递归地被初始化,就像DFS。

这个函数是在type_initialize中调用的。可以看到需要设置一些父类的属性,用DEVICE_CLASSPCI_DEVICE_CLASS即可实现从ObjectClass类的转换。这实际上是dynamic cast,转换时有类型检查,最终调用到object_class_dynamic_cast以及type_is_ancestor。检查方式很简单,就是逐级往上查找。

2.3. 构建实例

传入参数-device edu,查看初始化情况。函数调用如下:

qdev_device_add中,通过如下方式获取设备名,之后创建设备:

    driver = qemu_opt_get(opts, "driver");
    /* ... */
    /* create device */
    dev = DEVICE(object_new(driver));

object_new会根据设备名搜索到类信息TypeImpl,再传入object_new_with_type进行解析(Type就是TypeImpl *),且分配实例空间。颇有反射的味道。

一直调用到类信息的构造函数instance_init

static void edu_instance_init(Object *obj)
{
    EduState *edu = EDU(obj);

    edu->dma_mask = (1UL << 28) - 1;
    object_property_add_uint64_ptr(obj, "dma_mask",
                                   &edu->dma_mask, OBJ_PROP_FLAG_READWRITE);
}

EDU是指针类型转换宏,依然是dynamic cast。EduState是一个显式定义的类,继承链为PCIDeviceDeviceStateObjectClass

2.4. 属性

在这一句代码之后,edu设备才可用:

    object_property_set_bool(OBJECT(dev), true, "realized", &err);

QOM依然提供了反射机制,用于添加类属性或者成员属性,以及修改其值。查看上面这句代码的调用栈:

device_set_realized函数的调用可以看出,这是专门为realized属性准备的回调函数,它是BoolPropertyset成员:

typedef struct BoolProperty
{
    bool (*get)(Object *, Error **);
    void (*set)(Object *, bool, Error **);
} BoolProperty;

property_set_bool又是专门准备的回调函数,是ObjectProperty的成员。可以想到这些函数句柄的赋值都是在添加属性时完成的。

其实device_set_realized又向下调用PCI的realize函数,然后再调用edu的realize函数。这些都是在Device、PCI、edu类初始化时绑定上去的。很像动态绑定,但因为C语言的灵活性,能够做更多与设备相关的事情,理解透彻了属于是。

realized具现化)是一种bool属性。除bool外,QOM属性还有childlink等。前者表示设备的从属关系,后者表示设备的引用关系。

学到这里对QOM的作用已经有了一个简单的认识;面向对象的实现细节很多,更具体的内容后面碰到了再说。

3. 线程模型

一个QEMU进程对应一个虚拟机。线程的作用比较多了,首先主线程当然是线程,负责调用glib的事件监听和分发接口;每个VCPU都是一个线程,通过KVM、WHPX、HAXM之类的接口创建;还有VNC线程、I/O线程等。

3.1. QBL

线程之间用QEMU大锁同步,第一节有讲主线程在poll时释放了锁(休眠当然要放锁),dispatch时获取锁。

3.2. WHPX VCPU线程

又是设备具现化,神奇吧?这是WHPX启动VCPU的代码:

static void qemu_whpx_start_vcpu(CPUState *cpu)
{
    char thread_name[VCPU_THREAD_NAME_SIZE];

    cpu->thread = g_malloc0(sizeof(QemuThread));
    cpu->halt_cond = g_malloc0(sizeof(QemuCond));
    qemu_cond_init(cpu->halt_cond);
    snprintf(thread_name, VCPU_THREAD_NAME_SIZE, "CPU %d/WHPX",
             cpu->cpu_index);
    qemu_thread_create(cpu->thread, thread_name, qemu_whpx_cpu_thread_fn,
                       cpu, QEMU_THREAD_JOINABLE);
#ifdef _WIN32
    cpu->hThread = qemu_thread_get_handle(cpu->thread);
#endif
}

qemu_whpx_cpu_thread_fn函数是VCPU线程的起点。到whpx_cpu_run函数,可以看到一个非常明显的CPU执行循环:pre->run->post->handle exit->pre->run…

whp_dispatch.WHvRunVirtualProcessor负责CPU执行,它来自WinHvPlatform.dll

    do {
        if (cpu->vcpu_dirty) {
            whpx_set_registers(cpu, WHPX_SET_RUNTIME_STATE);
            cpu->vcpu_dirty = false;
        }

        whpx_vcpu_pre_run(cpu);

        if (atomic_read(&cpu->exit_request)) {
            whpx_vcpu_kick(cpu);
        }

        hr = whp_dispatch.WHvRunVirtualProcessor(
            whpx->partition, cpu->cpu_index,
            &vcpu->exit_ctx, sizeof(vcpu->exit_ctx));

        if (FAILED(hr)) {
            error_report("WHPX: Failed to exec a virtual processor,"
                         " hr=%08lx", hr);
            ret = -1;
            break;
        }

        whpx_vcpu_post_run(cpu);

        switch (vcpu->exit_ctx.ExitReason) {
        case WHvRunVpExitReasonMemoryAccess:
            ret = whpx_handle_mmio(cpu, &vcpu->exit_ctx.MemoryAccess);
            break;

        case ...:
            break;

        default:
            /* error */
            break;
        }

    } while (!ret);

下面是调用栈:

我们可以通过硬件断点来观察这个函数句柄是什么时候初始化的。首先设置launch.jsonstopAtEntrytrue,然后启动时在VSCode的调试台输入命令:

-exec watch whp_dispatch.WHvRunVirtualProcessor

之后会在断点处停下,可以发现是在configure_accelerators中初始化的。这些WHPX的API在<WinHvPlatform.h>中声明。

VSCode将手动设置的监视点视为Exception,问题不大。

下面学习一个奇妙的宏写法:

LIST_WINHVPLATFORM_FUNCTIONS(WHP_LOAD_FIELD)

这一句就完成了所有WHPX函数句柄的初始化。LIST_WINHVPLATFORM_FUNCTIONS是一个以函数为参数的宏,由一个函数列表组成。不妨先看传入的参数WHP_LOAD_FIELD,它的原型是:

#define WHP_LOAD_FIELD(return_type, function_name, signature)

再看LIST_WINHVPLATFORM_FUNCTIONS列表中的一项:

#define LIST_WINHVPLATFORM_FUNCTIONS(X) \
  X(HRESULT, WHvGetCapability, (WHV_CAPABILITY_CODE CapabilityCode, VOID* CapabilityBuffer, UINT32 CapabilityBufferSizeInBytes, UINT32* WrittenSizeInBytes)) \
  ...

所以简单展开,这第一项就变成:

WHP_LOAD_FIELD(HRESULT, WHvGetCapability, (WHV_CAPABILITY_CODE CapabilityCode, VOID* CapabilityBuffer, UINT32 CapabilityBufferSizeInBytes, UINT32* WrittenSizeInBytes))

恰好变成了又一个函数调用,展开就得到从DLL文件加载函数以及为whp_dispatch的函数句柄赋值的代码。这一黑魔法还被用于WHPDispatch这个工厂类的定义。

struct WHPDispatch {
    LIST_WINHVPLATFORM_FUNCTIONS(WHP_DECLARE_MEMBER)
    LIST_WINHVEMULATION_FUNCTIONS(WHP_DECLARE_MEMBER)
    LIST_WINHVPLATFORM_FUNCTIONS_SUPPLEMENTAL(WHP_DECLARE_MEMBER)
};

extern struct WHPDispatch whp_dispatch;

3.3. I/O线程

I/O线程是专门处理I/O事件的线程,它的作用在于减少对BQL的占据,从而把更多时间留给VCPU。为此QEMU创建了一个新的类型叫TYPE_IOTHREAD,其它需要创建I/O线程的设备只需创建一个指向TYPE_IOTHREAD的link属性即可。

static Property virtio_scsi_properties[] = {
    DEFINE_...
    DEFINE_PROP_LINK("iothread", VirtIOSCSI, parent_obj.conf.iothread,
                     TYPE_IOTHREAD, IOThread *),
    DEFINE_PROP_END_OF_LIST(),
};

后面的文章会结合实际设备谈I/O线程。

发表评论

您的电子邮箱地址不会被公开。