第六章 QEMU系统仿真的启动参数校验和后台运行



文章目录

  • 系列文章目录
  • 第六章 QEMU系统仿真的启动参数校验和后台运行
  • 前言
  • 一、QEMU是什么?
  • 二、QEMU系统仿真的启动分析
  • 1.系统仿真的初始化代码
  • 2.配置项合法性校验
  • qemu_validate_options()
  • qemu_process_sugar_options()
  • qemu_process_early_options()
  • qemu_process_help_options()
  • qemu_maybe_daemonize()
  • 总结



前言

本文以 QEMU 8.2.2 为例,分析其作为系统仿真工具的启动过程,并为读者展示各种 QEMU 系统仿真的启动配置实例。
本文读者需要具备一定的 QEMU 系统仿真使用经验,并对 C 语言编程有一定了解。


一、QEMU是什么?

QEMU 是一个通用且开源的机器模拟器和虚拟机。
其官方主页是:https://www.qemu.org/


二、QEMU系统仿真的启动分析

1.系统仿真的初始化代码

QEMU 作为系统仿真工具,其入口代码在 system/main.c 文件中,初始化函数 qemu_init() 的实现在 system/vl.c 文件中,在完成命令行启动项配置解析后,程序进入解析后的启动项校验工作,本文继续前上一篇文章的工作,完成以下代码部分的分析。

2.配置项合法性校验

这部分代码在 system/vl.c 文件中,实现如下:

void qemu_init(int argc, char **argv)
{
...
    /*
     * Clear error location left behind by the loop.
     * Best done right after the loop.  Do not insert code here!
     */
    loc_set_none();

    qemu_validate_options(machine_opts_dict);
    qemu_process_sugar_options();

    /*
     * These options affect everything else and should be processed
     * before daemonizing.
     */
    qemu_process_early_options();

    qemu_process_help_options();
    qemu_maybe_daemonize(pid_file);
...
}

代码首先通过函数 loc_set_none() 设置错误报告位置为 none,然后调用函数 qemu_validate_options() 对机器配置参数字典进行校验。

qemu_validate_options()

此函数在 /system/vl.c 文件中,定义如下:

static void qemu_validate_options(const QDict *machine_opts)
{
    const char *kernel_filename = qdict_get_try_str(machine_opts, "kernel");
    const char *initrd_filename = qdict_get_try_str(machine_opts, "initrd");
    const char *kernel_cmdline = qdict_get_try_str(machine_opts, "append");

    if (kernel_filename == NULL) {
         if (kernel_cmdline != NULL) {
              error_report("-append only allowed with -kernel option");
              exit(1);
          }

          if (initrd_filename != NULL) {
              error_report("-initrd only allowed with -kernel option");
              exit(1);
          }
    }

    if (loadvm && incoming) {
        error_report("'incoming' and 'loadvm' options are mutually exclusive");
        exit(EXIT_FAILURE);
    }
    if (loadvm && preconfig_requested) {
        error_report("'preconfig' and 'loadvm' options are "
                     "mutually exclusive");
        exit(EXIT_FAILURE);
    }
    if (incoming && preconfig_requested && strcmp(incoming, "defer") != 0) {
        error_report("'preconfig' supports '-incoming defer' only");
        exit(EXIT_FAILURE);
    }

#ifdef CONFIG_CURSES
    if (is_daemonized() && dpy.type == DISPLAY_TYPE_CURSES) {
        error_report("curses display cannot be used with -daemonize");
        exit(1);
    }
#endif
}

函数校验了 Linux 系统启动参数的配置合法性。


qemu_process_sugar_options()

此函数在 /system/vl.c 文件中,定义如下:

static void qemu_process_sugar_options(void)
{
    if (mem_prealloc) {
        QObject *smp = qdict_get(machine_opts_dict, "smp");
        if (smp && qobject_type(smp) == QTYPE_QDICT) {
            QObject *cpus = qdict_get(qobject_to(QDict, smp), "cpus");
            if (cpus && qobject_type(cpus) == QTYPE_QSTRING) {
                const char *val = qstring_get_str(qobject_to(QString, cpus));
                object_register_sugar_prop("memory-backend", "prealloc-threads",
                                           val, false);
            }
        }
        object_register_sugar_prop("memory-backend", "prealloc", "on", false);
    }
}

函数校验了 CPU 启动参数的配置合法性。


qemu_process_early_options()

此函数在 /system/vl.c 文件中,定义如下:

static void qemu_process_early_options(void)
{
    qemu_opts_foreach(qemu_find_opts("name"),
                      parse_name, NULL, &error_fatal);

    object_option_foreach_add(object_create_pre_sandbox);

#ifdef CONFIG_SECCOMP
    QemuOptsList *olist = qemu_find_opts_err("sandbox", NULL);
    if (olist) {
        qemu_opts_foreach(olist, parse_sandbox, NULL, &error_fatal);
    }
#endif

    if (qemu_opts_foreach(qemu_find_opts("action"),
                          process_runstate_actions, NULL, &error_fatal)) {
        exit(1);
    }

#ifndef _WIN32
    qemu_opts_foreach(qemu_find_opts("add-fd"),
                      parse_add_fd, NULL, &error_fatal);

    qemu_opts_foreach(qemu_find_opts("add-fd"),
                      cleanup_add_fd, NULL, &error_fatal);
#endif

    /* Open the logfile at this point and set the log mask if necessary.  */
    {
        int mask = 0;
        if (log_mask) {
            mask = qemu_str_to_log_mask(log_mask);
            if (!mask) {
                qemu_print_log_usage(stdout);
                exit(1);
            }
        }
        qemu_set_log_filename_flags(log_file, mask, &error_fatal);
    }

    qemu_add_default_firmwarepath();
}

程序首先通过函数 parse_name() 处理 "name"类启动项,代码如下:

static int parse_name(void *opaque, QemuOpts *opts, Error **errp)
{
    const char *proc_name;

    if (qemu_opt_get(opts, "debug-threads")) {
        qemu_thread_naming(qemu_opt_get_bool(opts, "debug-threads", false));
    }
    qemu_name = qemu_opt_get(opts, "guest");

    proc_name = qemu_opt_get(opts, "process");
    if (proc_name) {
        os_set_proc_name(proc_name);
    }

    return 0;
}

接下来调用函数 object_option_foreach_add(),完成在创建 sandbox 之前的所需要的对象创建工作。

object_option_foreach_add(object_create_pre_sandbox);

函数 object_option_foreach_add() 定义如下:

static void object_option_foreach_add(bool (*type_opt_predicate)(const char *))
{
    ObjectOption *opt, *next;

    QTAILQ_FOREACH_SAFE(opt, &object_opts, next, next) {
        const char *type = ObjectType_str(opt->opts->qom_type);
        if (type_opt_predicate(type)) {
            user_creatable_add_qapi(opt->opts, &error_fatal);
            qapi_free_ObjectOptions(opt->opts);
            QTAILQ_REMOVE(&object_opts, opt, next);
            g_free(opt);
        }
    }
}

而作为上述函数参数的 object_create_pre_sandbox() 定义如下:

/*
 * Very early object creation, before the sandbox options have been activated.
 */
static bool object_create_pre_sandbox(const char *type)
{
    /*
     * Objects should in general not get initialized "too early" without
     * a reason. If you add one, state the reason in a comment!
     */

    /*
     * Reason: -sandbox on,resourcecontrol=deny disallows setting CPU
     * affinity of threads.
     */
    if (g_str_equal(type, "thread-context")) {
        return true;
    }

    return false;
}

接着,如果定义了 CONFIG_SECCOMP ,则执行以下代码,调用 parse_sandbox() 函数创建 sandbox 类对象。

#ifdef CONFIG_SECCOMP
    QemuOptsList *olist = qemu_find_opts_err("sandbox", NULL);
    if (olist) {
        qemu_opts_foreach(olist, parse_sandbox, NULL, &error_fatal);
    }
#endif

函数 parse_sandbox() 在 /system/qemu-seccomp.c 文件中,定义如下:

int parse_sandbox(void *opaque, QemuOpts *opts, Error **errp)
{
    if (qemu_opt_get_bool(opts, "enable", false)) {
        uint32_t seccomp_opts = QEMU_SECCOMP_SET_DEFAULT
                | QEMU_SECCOMP_SET_OBSOLETE;
        const char *value = NULL;

        value = qemu_opt_get(opts, "obsolete");
        if (value) {
            if (g_str_equal(value, "allow")) {
                seccomp_opts &= ~QEMU_SECCOMP_SET_OBSOLETE;
            } else if (g_str_equal(value, "deny")) {
                /* this is the default option, this if is here
                 * to provide a little bit of consistency for
                 * the command line */
            } else {
                error_setg(errp, "invalid argument for obsolete");
                return -1;
            }
        }

        value = qemu_opt_get(opts, "elevateprivileges");
        if (value) {
            if (g_str_equal(value, "deny")) {
                seccomp_opts |= QEMU_SECCOMP_SET_PRIVILEGED;
            } else if (g_str_equal(value, "children")) {
                seccomp_opts |= QEMU_SECCOMP_SET_PRIVILEGED;

                /* calling prctl directly because we're
                 * not sure if host has CAP_SYS_ADMIN set*/
                if (prctl(PR_SET_NO_NEW_PRIVS, 1)) {
                    error_setg(errp, "failed to set no_new_privs aborting");
                    return -1;
                }
            } else if (g_str_equal(value, "allow")) {
                /* default value */
            } else {
                error_setg(errp, "invalid argument for elevateprivileges");
                return -1;
            }
        }

        value = qemu_opt_get(opts, "spawn");
        if (value) {
            if (g_str_equal(value, "deny")) {
                seccomp_opts |= QEMU_SECCOMP_SET_SPAWN;
            } else if (g_str_equal(value, "allow")) {
                /* default value */
            } else {
                error_setg(errp, "invalid argument for spawn");
                return -1;
            }
        }

        value = qemu_opt_get(opts, "resourcecontrol");
        if (value) {
            if (g_str_equal(value, "deny")) {
                seccomp_opts |= QEMU_SECCOMP_SET_RESOURCECTL;
            } else if (g_str_equal(value, "allow")) {
                /* default value */
            } else {
                error_setg(errp, "invalid argument for resourcecontrol");
                return -1;
            }
        }

        if (seccomp_start(seccomp_opts, errp) < 0) {
            return -1;
        }
    }

    return 0;
}

最后调用函数 qemu_add_default_firmwarepath() 添加固件查找默认路径,定义如下:

void qemu_add_default_firmwarepath(void)
{
    static const char * const dirs[] = {
        CONFIG_QEMU_FIRMWAREPATH
        NULL
    };

    size_t i;

    /* add configured firmware directories */
    for (i = 0; dirs[i] != NULL; i++) {
        qemu_add_data_dir(get_relocated_path(dirs[i]));
    }

    /* try to find datadir relative to the executable path */
    qemu_add_data_dir(get_relocated_path(CONFIG_QEMU_DATADIR));
}

qemu_process_help_options()

接下来处理获取启动项的帮助信息的工作,此时我们已经完成命令行启动项分析,如果所带参数是显示对应启动项的帮助信息,此处就完整所需工作。函数 qemu_process_help_options() 定义如下:

static void qemu_process_help_options(void)
{
    /*
     * Check for -cpu help and -device help before we call select_machine(),
     * which will return an error if the architecture has no default machine
     * type and the user did not specify one, so that the user doesn't need
     * to say '-cpu help -machine something'.
     */
    if (cpu_option && is_help_option(cpu_option)) {
        list_cpus();
        exit(0);
    }

    if (qemu_opts_foreach(qemu_find_opts("device"),
                          device_help_func, NULL, NULL)) {
        exit(0);
    }

    /* -L help lists the data directories and exits. */
    if (list_data_dirs) {
        qemu_list_data_dirs();
        exit(0);
    }
}

qemu_maybe_daemonize()

接下来如果系统支持后台运行,则完成后台化操作,函数 qemu_maybe_daemonize() 在 /system/vl.c 文件中,定义如下:

static void qemu_maybe_daemonize(const char *pid_file)
{
    Error *err = NULL;

    os_daemonize();
    rcu_disable_atfork();

    if (pid_file) {
        char *pid_file_realpath = NULL;

        if (!qemu_write_pidfile(pid_file, &err)) {
            error_reportf_err(err, "cannot create PID file: ");
            exit(1);
        }

        pid_file_realpath = g_malloc0(PATH_MAX);
        if (!realpath(pid_file, pid_file_realpath)) {
            if (errno != ENOENT) {
                warn_report("not removing PID file on exit: cannot resolve PID "
                            "file path: %s: %s", pid_file, strerror(errno));
            }
            return;
        }

        qemu_unlink_pidfile_notifier = (struct UnlinkPidfileNotifier) {
            .notifier = {
                .notify = qemu_unlink_pidfile,
            },
            .pid_file_realpath = pid_file_realpath,
        };
        qemu_add_exit_notifier(&qemu_unlink_pidfile_notifier.notifier);
    }
}

至此,如果宿主机系统支持后台运行,则 QEMU 进程完成后台化运行的动作


总结

以上分析了 QEMU 系统仿真在启动过程中,对启动参数的合法性校验以及进入后台运行的操作,完成这些操作后,QEMU 进程可以脱离命令行终端进入后台运行。