5.14.2.8. 其他小设施

5.14.2.8.1. BQL:Big Qemu Lock

BQL是一个简化Qemu IO调度模型的锁机制。Qemu中主流的线程包括:

  1. 进程的主线程,这个线程完成初始化后,剩下的时间全部用于IO调度。调度通过glib的MainLoop机制完成。也就是说,所有的IO都转化为文件fd,注册成MainLoop的一个Source,内部的通知也通过eventfd和signalfd这些机制注册,之后只要用poll这组fd,然后一个事件一个事件串行处理就可以了。

  2. vcpu线程,每个vcpu一个,用于处理翻译,执行和异常处理。

  3. 通过-object iothread,id=my_id额外创建的io线程。

其中第三个是后来加的功能,在这个功能之前,第一个线程才是iothread,这个名字在代码现在还有痕迹。我们这里为了区分,把第一个叫main iothread,第三个叫extra iothread。

BQL就是一个简单的mutex,通过qemu_mutex_lock/unlock_iothread()调用。和大多数锁不同,那些锁是在少数关键区域才上锁的,而这个锁在大部分时候都是锁上的,只在小部分地方才放开,比如:

  1. vcpu翻译和执行的时候

  2. main iothread做polling的时候

除此以外,所有时间都是有锁的。这保证了qemu那些传统的io代码,比如设备模拟,vcpu的中断和异常处理,都是独占的。

在这个基础上,Qemu提供start/end_exclusive(),这个两个函数创建一个互斥区,等待所有cpu都进入BQL的lock状态,这样在这个范围内的操作就是在所有CPU间互斥的。

Extra iothread是另一个独立的体系,它的原理和main iothread相近,代码都有部分共享,有自己独立的时钟和bottom half等所有辅助机制。但它没有BQL,互斥使用aio_context_acquire/release(),也是个mutex。它的存在主要是为了帮某些子系统(主要是块设备)挂在它上面的事件处理独立运行,如果需要发回主线程处理,就只能通过发消息回去main iothread中来完成了。

5.14.2.8.2. RCU

qemu也支持类似内核的RCU机制(从liburcu移植过来的),接口是这样的:

  1. 用到这个机制的线程都要调用rcu_register_thread()设置相关线程变量

  2. 读方用rcu_read_lock/unlock()保护,或者直接放一个RCU_READ_LOCK_GUARD进行区域自动保护。

  3. 用原子指令替换变量,释放旧数据的有两种模式:

    1. 修改完替换指针,调用sychonized_rcu()等所有reader都退出访问了,再释放旧数据。

    2. 用call_rcu1(head, func)设定一个释放函数,等reader退出自动释放。启动head通常是放在数据中的一个成员,类型是struct rcu_head。call_rcu(head, func, field)和g_free_rcu(obj, field)是call_rcu1的封装。

5.14.2.8.3. Monitor

Qemu的Monitor是Qemu的控制界面,它可以占据当前的控制台,也可以通过其他tty控制台进行访问。Qemu的Monitor当前在概念空间上有两种:

QMP

Qemu Message Protocol,这是通过json消息对运行中的Qemu进行控制。通过Qemu参数-qmp启动。启动后可以用telnet一类的中断登录上去控制。

HMP

Human Message Protocol,这直接就是命令行接口了,这在Qemu启动后通过热键进入(默认是ctl-a c)。

QMP是Qemu的核心逻辑,HMP最终都是解释为QMP的实现完成相应的功能的。比如hmp_info_version查qemu的版本,实际调用的是qmp_query_version()。

5.14.2.8.4. Error

Qemu的报错机制做得有点怪,这里总结一下。首先,它采用了POSIX errno类似的机制来处理多级调用的错误问题,比如:

// 这只是表示调用关系,不是C语法
a() {
  Error *err = NULL;
  b(&err) {
     c(err);
  }
}

当a调用b的时候,有些错误可能是b本身产生的,有些可能是它调用的c产生的。a通过传入一个err变量来获得这个错误返回值。如果返回NULL的时候,就是没有错误,否则就是某种错误。

报错的一层用这些函数报告错误::

error_setg(error, ...);         // 设置错误
error_append_hint(error, ...);  // 补充错误提示

error_setg()可以生成这个Error对象,如果输入进来是个NULL,那么它就不产生,这说明调用者也不关心这个错误,所以浪费时间生成它也不值得。

调用方如果关心这个返回是否成功,检查err是不是NULL就可以了,如果发现不是。有两种可能,一种err是它自己的,它可以用这种方法传递过去::

error_propagate(error, ...);    // 传播错误

如果err是上一层传进来的,那么让它直接返回,用原来的err传播就行。

这里要注意的是:a一层定义的err是Error的指针,调用b的时候是指针的指针,b在调用c的时候也是指针的指针,b如果要判断c有没有产生错误,要判断的是Error的指针有没有值在其中就行了。实际上Error不是调用者分配的,而是报错的人分配的。

这个方案和errno最大的不同是errno是全局变量,而这个变量是一层层传递进去的。好处是你可以选择在什么范围中传播,但实际上你是可以用全局参数的::

Error *error_abort;
Error *error_fatal;
Error *error_warn;

所以如果你传进去的是这些全局变量,error_setg()和error_propagate()会自动根据类型报错和退出。

所以,大部分时候你只看到调用者就是简单用这些全局变量发起各种请求,这最终的结果就是无论被调用者发生了什么错误,整个qemu都会abort(),exit()或者简单打印一个错误信息。

5.14.2.8.5. 事件通知

Qemu的事件通知用于两个线程间进行消息同步,在Linux下主要是对eventfd(2)和signalfd(2)的封装,在Windows下是对CreateEvent()的封装。它主要是封装这样一对接口::

event_notifier_set(EventNotifier);
event_notifier_test_and_clear(EventNotifier);

前者发起通知,后者测试通知。

5.14.2.8.6. 编译系统

Qemu使用meson作为基础的编译系统,但它也提供一个基础的./configure文件作为配置命令入口,只是这个配置命令不靠auto-tool工具生成。

meson的主配置文件是根目录下的meson.build,qemu的这个基本文件定义了所有下属子目录用的子meson.build,在这些meson.build文件中,你只需要把你的文件加到对应的xxx_ss文件集中,就可以参与编译。所以每个子目录的行为还是很简单的。

5.14.2.8.7. 命令行参数

qemu的命令行参数在主程序system/vl.c中解释,但因为参数众多,它也做成一个框架了,在解释前通过qemu_add_opts()或者qemu_add_drive_opts()这一类的调用注册新的参数进去。然后在后面用循环去独处其中的参数,再设置给对应的模块。每个参数的自参数可以用qemu_opt_get...()系列函数分类型读出。

更通用的参数可以通过qemu-options.hx直接生成,这基本是一个生成qemu_add_opts()的参数表。