Skip to the content.

QEMU 字符设备模拟

如果想要大致了解 printf 的实现,从上到下参考如下内容:

  1. 用户态 : musl 和 《程序员的自我修养》
  2. 内核态 : TTY 到底是什么? 或者我写的 tty 到底是什么
  3. 硬件: 参考 serial parallel 等字符设备的手册

当然这这是考虑一些很粗略的情况了,今天这些分析的是,QEMU 是如何模拟字符设备的,当 guest 读写 pio / mmio 导致 vmexit 出来,QEMU 进行一系列的操作。

Architecture

Guest 需要使用各种设备,比如 serial virtio-console,当 guest 在对应的 pio / mmio 上操作的时候,QEMU 正确的模拟出来。 这些设备模拟的部分被成为 Frontend,主要的代码出现在 /hw/char。

QEMU 的参数 -nographic 可以让 guest 是在 terminal 中运行还是在图形化的界面中运行,其实这是因为对于一个 guest 设备的输出, QEMU 可以将数据导入到不同的 host 载体中,比如 serial, file 或者 tcp,这种和 host 的载体打交道的部分被成为 Backend,主要的代码出现在 /chardev 上。

各种后端都是 Chardev 的子类,各种前端的共性较小,无法创建一个公共的 parent, 但是 QEMU 提供了一个 ChardevBackend 嵌入到结构体用于和 Backend 打交道。

Chardev

typedef struct ChardevClass {
    ObjectClass parent_class;

    bool internal; /* TODO: eventually use TYPE_USER_CREATABLE */
    void (*parse)(QemuOpts *opts, ChardevBackend *backend, Error **errp);

    void (*open)(Chardev *chr, ChardevBackend *backend,
                 bool *be_opened, Error **errp);

    int (*chr_write)(Chardev *s, const uint8_t *buf, int len);
    int (*chr_sync_read)(Chardev *s, const uint8_t *buf, int len);
    GSource *(*chr_add_watch)(Chardev *s, GIOCondition cond);
    void (*chr_update_read_handler)(Chardev *s);
    int (*chr_ioctl)(Chardev *s, int cmd, void *arg);
    int (*get_msgfds)(Chardev *s, int* fds, int num);
    int (*set_msgfds)(Chardev *s, int *fds, int num);
    int (*chr_add_client)(Chardev *chr, int fd);
    int (*chr_wait_connected)(Chardev *chr, Error **errp);
    void (*chr_disconnect)(Chardev *chr);
    void (*chr_accept_input)(Chardev *chr);
    void (*chr_set_echo)(Chardev *chr, bool echo);
    void (*chr_set_fe_open)(Chardev *chr, int fe_open);
    void (*chr_be_event)(Chardev *s, int event);
    /* Return 0 if succeeded, 1 if failed */
    int (*chr_machine_done)(Chardev *chr);
} ChardevClass;

使用 debugcon 作为一个例子:

CharBackend

/* This is the backend as seen by frontend, the actual backend is
 * Chardev */
struct CharBackend {
    Chardev *chr;
    IOEventHandler *chr_event;
    IOCanReadHandler *chr_can_read;
    IOReadHandler *chr_read;
    BackendChangeHandler *chr_be_change;
    void *opaque;
    int tag;
    int fe_open;
};

CharBackend 中四个 hook 都是前端注册上的

#4  0x0000555555a208c7 in serial_can_receive1 (opaque=<optimized out>) at /home/maritns3/core/xqm/hw/char/serial.c:609
#5  0x0000555555c4cca0 in mux_chr_read (opaque=<optimized out>, buf=<optimized out>, size=<optimized out>) at /home/maritns3/core/xqm/chardev/char-mux.c:223
#6  0x0000555555c4ac3d in fd_chr_read (chan=0x55555649e810, cond=<optimized out>, opaque=<optimized out>) at /home/maritns3/core/xqm/chardev/char-fd.c:68
#7  0x00007ffff704704e in g_main_context_dispatch () at /lib/x86_64-linux-gnu/libglib-2.0.so.0
#8  0x0000555555caf228 in glib_pollfds_poll () at /home/maritns3/core/xqm/util/main-loop.c:219
#9  os_host_main_loop_wait (timeout=<optimized out>) at /home/maritns3/core/xqm/util/main-loop.c:242
#10 main_loop_wait (nonblocking=<optimized out>) at /home/maritns3/core/xqm/util/main-loop.c:518

暂时不用看 fd_chr_readmux_chr_read,下面会分析的。

init

下面我们使用 serial 和 stdio 作为前端和后端来分析初始化的过程。

Chardev 的创建

在 vl.c:main 中间,当采用 -nographic 的时候,serial 是默认被导入到 stdio 中间的, 也就是执行 add_device_config(DEV_SERIAL, “stdio”);

    if (nographic) {
        if (default_parallel)
            add_device_config(DEV_PARALLEL, "null");
        if (default_serial && default_monitor) {
            add_device_config(DEV_SERIAL, "mon:stdio");
        } else {
            if (default_serial)
                add_device_config(DEV_SERIAL, "stdio");
            if (default_monitor)
                monitor_parse("stdio", "readline", false);
        }
    } else {
        if (default_serial)
            add_device_config(DEV_SERIAL, "vc:80Cx24C");
        if (default_parallel)
            add_device_config(DEV_PARALLEL, "vc:80Cx24C");
        if (default_monitor)
            monitor_parse("vc:80Cx24C", "readline", false);
    }

然后 在 foreach_device_config 中执行 hook serial_parse 来解析 “stdio”

SerialState

入口在 serial_hds_isa_init 中

void serial_hds_isa_init(ISABus *bus, int from, int to)
{
    int i;

    assert(from >= 0);
    assert(to <= MAX_ISA_SERIAL_PORTS);

    for (i = from; i < to; ++i) {
        if (serial_hd(i)) {
            serial_isa_init(bus, i, serial_hd(i));
        }
    }
}

serial_isa_init 中的参数 serial_hd(i) 就是之前创建的 Chardev

static Property serial_isa_properties[] = {
    // ...
    DEFINE_PROP_CHR("chardev",   ISASerialState, state.chr),
    // ...
    DEFINE_PROP_END_OF_LIST(),
};

const PropertyInfo qdev_prop_chr = {
    .name  = "str",
    .description = "ID of a chardev to use as a backend",
    .get   = get_chr,
    .set   = set_chr,
    .release = release_chr,
};

附录

后端的一点抽象

在 chardev 下除了每一个后端一个文件描述,还有

static void char_fd_class_init(ObjectClass *oc, void *data)
{
    ChardevClass *cc = CHARDEV_CLASS(oc);

    cc->chr_add_watch = fd_chr_add_watch;
    cc->chr_write = fd_chr_write;
    cc->chr_update_read_handler = fd_chr_update_read_handler;
}

mux

可以找到 qemu-options.hx 中关于 char mux 的介绍:

patch 的讨论 还有一些补充信息