当前位置:网站首页 > 容器化与Kubernetes > 正文

kvm虚拟化技术原理(虚拟化 kvm)



泻药。这问题恐怕是难回答。我知道的并不多所以我也答不上来,下面就强答一把胡扯一下。

只从Intel Houdini一侧说,这问题不好回答的主要原因是公开信息太少。知道答案的人多半都签了NDA不能说,而不知道答案的人能从公开途径获取的信息实在不足以分析出多少名堂。

==================================================

评论里有大大指出我的脑补臆测全部是错误的,特此引用:

@dxnil
您的猜测和脑补全都是错误的。

PS Houdini是中国团队独立开发的。项目发起,概念验证,原型以及最后的产品化都由中国团队完成。很骄傲我们有第一流的团队,和第一流的产品能力。


下面是原回答,根据

@dxnil

的评论是完全错误的,仅保留来记录我原本的猜测确实都是胡扯。

请大家帮我点反对+没有帮助来折叠了吧~

==================================================

例如说Houdini的血缘,可能带有一些跟NVIDIA的Project Denver相似的血缘——并不是说Houdini跟Denver之间有任何直接关系,而是它们俩涉及的某些技术可能有共同的来源。

<- 这个跟项目在哪里发起、哪里立项、哪里实现并没有直接关系。来源就是来源。

作为一个dynamic binary translator,资料上说Houdini是把ARM指令动态翻译为x86指令。但事实上确实如此么?Intel Atom里是否有未公开的隐藏指令集/微指令架构/执行模式,而Houdini是否利用它来更高效地实现动态二进制翻译,外界无从得知。

这种层面的信息,在没有好的分析思路指导的前提下,单靠反汇编libhoudini.so从汇编的茫茫大海中想分析出什么,希望渺茫。

Intel对Android on x86 (Atom)的支持是一整套的,不只是Houdini来模拟ARM的native指令,还有x86版的Dalvik VM与ART。如果说Atom上有这么个未公开的部分,或许Intel版Dalvik / ART也会去使用它,例如说让Dalvik VM的JIT编译器直接编译到这个上面,绕开表面的x86指令集。到底有没有(过)这样的实现呢?

还等真的知道内幕的人在可以透露的范围内说说Houdini的奥秘~

Houdini的开发工作应该有一部分是在Intel上海做的。不知道Intel上海的大大们上不上知乎呢…

我厂基于 QEMU 实现过和 Houdini 功能相同的东西,在 Android x86 系统上达到了商业上能接受的 ARM 兼容性,但是 Houdini 的性能确实领先很多。QEMU 本身的实现,对多线程的支持问题以及我们自己的实现方式问题都会导致性能的损失。

1.二进制翻译是小case。

2.处理各种各样的例外才是功夫活,qemu自己也一堆不稳定。由于被业界傻叉红帽子接手,user mode几乎被废弃了。qemu的主流变成了KVM的附庸。

3. 至于原理,user级的翻译无非就是系统调用的翻译。

4.关于优化,这个你可说到重点了。qemu慢在三个方面,一个是它兼顾跨平台,无法有效使用主机平台;二是它自己生成代码和C代码就有优化空间,三是可以针对个别的case做相应的优化。也就是废弃通用性,专注特殊性。

个人浅见,仅供参考。

不行。

题主所说的任务属于二进制翻译(Binary Translation),即将源指令集的指令翻译为目标指令集的指令。另外,题主所说的任务还要求是端到端的,即要一次性将包含源指令集指令的整个 ELF (源 ELF)翻译为包含目标指令集指令的 ELF(目标 ELF)。

二进制翻译的一般方法是将源指令集指令提升(lift)到某种中间表示(IR),再从这个中间表示生成目标指令集指令。基于这种方法,存在静态二进制翻译动态二进制翻译两种方法。qemu-tcg 就是利用动态二进制翻译,在执行时动态地将 guest 的下一个 basic block 提升至 TCG IR,然后再基于 TCG IR 生成 host 指令集指令并执行。动态二进制翻译本质上是一种 JIT,它无法完成题主所要求的端到端二进制翻译任务,原因在于:

  1. 动态二进制翻译无法翻译那些没有被执行的源指令集代码;
  2. 动态二进制翻译得到的目标指令集代码往往和 JIT 运行环境耦合,例如 qemu 翻译得到的代码可能会直接读写 qemu 进程中的数据结构,这些代码显然不能作为端到端翻译的结果;
  3. 另外,存在不少源指令集指令(例如各个架构下的特权指令和一些 CISC 指令)无法被直接翻译到目标指令集指令,这些指令需要依赖软件模拟才能在目标架构上“运行”。

因此要想实现题主所要求的任务,只能仰赖于静态二进制翻译。静态二进制翻译通过静态二进制分析将源 ELF 中的所有代码提升至 IR,然后再从 IR 生成目标 ELF。从 IR 生成目标 ELF 是简单的,因此静态二进制翻译主要的难点在于第一步,即二进制代码提升。对于这一步,事实上已经有工具可以做到,例如 mcsema、llvm-mctoll 等,在普通情况下代码提升效果也不算差,但仍然存在不少问题。

二进制代码提升的第一步是反汇编。反汇编看似简单,实则是一个比较困难的任务,它要解决的问题包括:区分出一个 ELF 中的代码和数据、恢复出代码-数据引用关系、在代码中进一步划分函数边界、识别间接调用目标等等,其中区分代码和数据、识别间接调用目标已经被证明是不可判定问题,因此不存在一种通用方法可以完全准确地进行反汇编。反汇编的结果不准确,二进制代码提升的结果也就不准确。

反汇编之后,是根据反汇编得到的源指令集指令生成 IR。这一步仍然存在某些源指令集指令无法被提升的问题。例如,如果源指令集中包含 SSE / AVX 等 SIMD 指令,那么 llvm-mctoll 无法工作。如果要求通用性,则只能像 mcsema 那样首先确定每一种源指令集指令的语义,然后根据源指令集指令的语义生成模拟这种语义的 IR。具体做法和示例可以参见如下 remill 文档。

Machine code to bitcode: the life of an instruction

即使 mcsema 会使用 LLVM 优化器对生成的 LLVM IR 进行优化,单从生成的代码质量来说,显然这种通用的二进制代码提升方法效果并不好,与软件模拟没有太大的本质区别。

以上仅仅是二进制代码提升过程中所遇到的问题的冰山一角。实际上,为了实现端到端二进制代码翻译任务,还有许多问题亟需解决。例如,源 ELF 一般都会依赖动态库,这些依赖的动态库是否也应该翻译?如何保证翻译后的代码的二进制兼容性?源指令集架构和目标指令集架构内存模型往往不同,怎么保证翻译后的代码内存操作的语义一致?随便哪个问题挑出来都是一个大号灵车,根本不敢想。

总结,目前没有成熟的方案能够实现题主所要求的端到端二进制翻译任务。可能可以工作的方案是利用已有的静态二进制代码提升工具将源 ELF 提升至 IR 后再生成目标代码,但仍然面临不够通用、生成的代码效率不佳、二进制兼容性、语义不一致等各种灵车问题。即使未来这些问题都解决了,可能最后发现还是不如 qemu 动态二进制翻译来得简单可靠。

kvm虚拟化技术实现原理 - 百度文库 (baidu.com)

KVM的虚拟化技术好像和其他的虚拟化不太一样,但是KVM的相关的虚拟化原理,可能无法解决题主的问题,但也应该能帮得上一些忙。

提及虚拟机,就不得不提EastFax USB Server,虚拟机在日常使用中呢,是越来越普及,越来越方便,但是长久使用时,也暴露出了它无法避免的问题,那是脱离硬件的虚拟机无法支持USB设备,对于一些依赖加密狗的应用软件来说就是一个大问题,对于此种问题,EastFax USB Server可以帮你解决。

EastFax USB Server是一款智能USB远程连接,集中管控的软硬件结合产品,将Ukey插在USB Server设备上,只需要在虚拟机或者云服务器中安装客户端软件就可以连接调用。可以远程调用,集中管控等。不仅支持虚拟机、云服务器,而且还可以兼容各种USB设备,更能兼容USB3.0。更可以让虚拟化与U盾加密狗再次紧密结合,精彩联动。

我尽量说的简单一点,可能不太严谨

docker容器:电脑再直接运行一个操作系统,与主机共享驱动和硬件

qemu:在电脑再用软件模拟一个电脑,在里面安装操作系统

kvm:使qemu有与主机共享硬件的能力

wine直接在其它系统上模拟windows api,可以理解为虚拟机和容器的混合体。

最近在阅读李强编著的《QEMU/KVM 源码解析与应用》这本书来学习 Linux 内核虚拟化相关知识,通过读书笔记的方式来提炼和归纳书中重要的知识点。本文主要内容是关于 QEMU 事件循环机制的介绍。

关注微信公众号:Linux 内核拾遗
文章来源: mp.weixin..com/s/-fNJ

QEMU 程序的运行是基于各类文件 fd 事件的,QEMU 在运行过程中会将自己感兴趣的文件 fd 添加到其监听列表上并定义相应的处理函数,在其主线程中,有一个循环用来处理这些文件 fd 的事件,如来自用户的输入、来自 VNC 的连接、虚拟网卡对应 tap 设备的收包等。QEMU 的事件循环机制基于 glib,glib 是一个跨平台的、用 C 语言编写的若干底层库的集合。

glib 实现了完整的事件循环分发机制,在这个机制中有一个主循环负责处理各种事件,事件通过事件源描述,事件源包括各种文件描述符(文件、管道或者 socket)、超时和 idle 事件等,每种事件源都有一个优先级,idle 事件源在没有其他高优先级的事件源时会被调度运行。

glib 使用 GMainLoop 结构体来表示一个事件循环,每一个 GMainLoop 都对应有一个主上下文 GMainContext。事件源使用 GSource 表示,每个 GSource 可以关联多个文件描述符,每个 GSource 会关联到一个 GMainContext,一个 GMainContext 可以关联多个 GSource。

glib 的一个重要特点是能够定义新的事件源类型,可以通过定义一组回调函数来将新的事件源添加到 glib 的事件循环框架中。因此应用程序可以利用 glib 的这套机制来实现自己的事件监听与分发处理。

glib 主上下文的一次循环包括 prepare、query、check、dispatch 四个过程,分别对应 glib 的 g_main_context_prepare()、g_main_context_query()、g_main_context_check()以及 g_main_context_dispatch()四个函数,其状态转换如下图所示。

  • prepare:通过 g_main_context_prepare()会调用事件对应的 prepare 回调函数,做一些准备工作,如果事件已经准备好进行监听了,返回 true。
  • query:通过 g_main_context_query()可以获得实际需要调用 poll 的文件 fd。
  • check:当 query 之后获得了需要进行监听的 fd,那么会调用 poll 对 fd 进行监听,当 poll 返回的时候,就会调用 g_main_context_check()将 poll 的结果传递给主循环,如果 fd 事件能够被分派就会返回 true。
  • dispatch:通过 g_main_context_dispatch()可以调用事件源对应事件的处理函数。

根据这个 glib 事件循环机制的处理流程,应用程序需要做的就是把新的事件源加入到这个处理流程中,glib 会负责处理事件源上注册的各种事件。

QEMU 的事件循环机制如下图所示,QEMU 在运行过程中会注册一些感兴趣的事件,设置其对应的处理函数。图示的 QEMU 主循环中添加了了来自 tap 设备、qmp 以及 VNC 等的事件源:

  • 当监听到 VNC 有连接到来时 glib 框架就会调用 vnc_client_io 函数来处理。
  • 当网卡设备的后端 tap 设备接收到网络包后 QEMU 调用 tap_send 将包路由到虚拟机网卡前端。
  • 当用户发送 qmp 命令之后 glib 会调用 tcp_chr_accept 来处理 qmp 命令。

通过如下命令启动虚拟机,并结合该虚拟机来介绍 QEMU 中的事件循环机制:

root@ubuntu:~# qemu-system-x86_64 -m 1024 -smp 4 -hda /home/test/test.img --enable-kvm -vnc :0

该命令行启动的 QEMU 程序,它包含了如图所示的 3 种/5 个事件源:

AioContext 自定义事件源:qemu_aio_context 和 iohander_ctx,前者用于处理 QEMU 中块设备相关的异步 I/O 请求通知,后者用于处理 QEMU 中各类事件通知,包括信号处理 fd、tap 设备的 fd 以及 VFIO 设备对应的中断通知等等。

  • glib 标准事件源:vnc: GSource 这两个 VNC 事件。
  • glib 内部事件 fd。

glib 中事件源可以添加多个事件 fd,每一个事件源本身都会有一个 fd,当添加一个 fd 到事件源时,整个 glib 主循环都会监听该 fd,并且任何一个 fd 准备好事件之后都可以唤醒主循环。当前例子中,QEMU 主循环总共会监听 6 个 fd,其中 5 个是事件源本身的 fd,还有一个是通过系统调用 SYS_signalfd 创建的用来处理信号的 fd。

QEMU 主循环对应的最重要的几个函数如下图所示。QEMU 的 main 函数定义在 vl.c 中,在进行好所有的初始化工作之后会调用函数 main_loop 来开始主循环。

main_loop 及其调用的 main_loop_wait 的主要代码如下:

// vl.c static void main_loop(void) { ... do { ... last_io = main_loop_wait(nonblocking); ... } while (!main_loop_should_exit()); } // main-loop.c void main_loop_wait(int nonblocking) { ... // 计算最小timeout值 timeout_ns = qemu_soonest_timeout(timeout_ns, timerlistgroup_deadline_ns( &main_loop_tlg)); ret = os_host_main_loop_wait(timeout_ns); ... } static int os_host_main_loop_wait(int64_t timeout) { int ret; static int spin_counter; // 主循环函数1 glib_poolfds_fill(&timeout); ... if (timeout) { spin_counter = 0; qemu_mutex_unlock_iothread(); } else { spin_counter++; } // 主循环函数2 ret = qemu_poll_ns((GPollFD *)gpollfds->data, gpollfds->len, timeout); if (timeout) { qemu_mutex_lock_iothread(); } // 主循环函数3 glib_pollfds_poll(); return ret; }

main_loop_wait 在调用 os_host_main_loop_wait 前,会调用 qemu_soonest_timeout 函数先计算一个最小的 timeout 值,该值是从定时器列表中获取的,表示监听事件的时候最多让主循环阻塞的时间,timeout 使得 QEMU 能够及时处理系统中的定时器到期事件

该函数的主要工作是获取所有需要进行监听的 fd,并且计算一个最小的超时时间。

// main-loop.c static void glib_pollfds_fill(int64_t *cur_timeout) { // 1. 调用准备函数 g_main_context_prepare(context, &max_priority); n = glib_n_poll_fds; do { GPollFD* pfds; glib_n_poll_fds = n; g_array_set_size(gpollfds, glib_pollfds_idx + glib_n_poll_fds); pfds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx); // 2. 获取需要监听的fd,并返回fd时间最小的timeout n = g_main_context_query(context, max_priority, &timeout, pfds, glib_n_poll_fds); } while (n != glib_n_poll_fds); ... *cur_timeout = qemu_soonest_timeout(timeout_ns, *cur_timeout); }
  1. 首先调用 g_main_context_prepare 开始为主循环的监听做准备。
  2. 接着在一个循环中调用 g_main_context_query 获取需要监听的 fd,所有 fd 保存在全局变量 gpollfds 数组中,需要监听的 fd 的数量保存在 glib_n_poll_fds 中,g_main_context_query 还会返回 fd 时间最小的 timeout,该值用来与传过来的 cur_timeout(定时器的 timeout)进行比较,选取较小的一个,表示主循环最大阻塞的时间

glib_pollfds_fill 调用完成后,此时已经有了所有需要监听的 fd 了,然后会调用 qemu_mutex_unlock_iothread 释放 QEMU 大锁(Big Qemu Lock,BQL)。

接着 os_host_main_loop_wait 函数会调用 qemu_poll_ns,它接受 3 个参数:

  1. 要监听的 fd 数组。
  2. fds 数组的长度。
  3. timeout 值,表示 g_poll 最多阻塞的时间,这是一个跨平台的 poll 函数,用来监听文件上发生的事件。如果 QEMU 配置了 CONFIG_PPOLL,那么就 qemu_poll_ns 会调用 ppoll 而不是 g_poll。
// qemu-timer.c int qemu_poll_ns(GPollFD* fds, guint nfds, int64_t timeout) { #ifdef CONFIG_POLL if (timeout < 0) { return ppoll((struct pollfd*)fds, nfds, NULL, NULL); } else { struct timespec ts; int64_t tvsec = timeout / LL; ... ts.tv_sec = tvsec; ts.tv_nsec = timeout % LL; return ppoll((struct pollfd *)fds, nfds, &ts, NULL); } #else return g_poll(fds, nfds, qemu_timeout_ns_to_ms(timeout)); #endif }

qemu_poll_ns 的调用会阻塞主线程,当该函数返回之后,要么表示有文件 fd 上发生了事件,要么表示一个超时,不管怎么样,这都将进入第三步,即调用 glib_pollfds_poll 函数。

glib_pollfds_poll 函数负责对事件进行分发处理。

// main-lool.c static void glib_pollfds_poll(void) { GMainContext* context = g_main_context_default(); GPollFD* fds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx); // 检测事件并分发事件 if (g_main_context_check(context, max_priority, pfds, glib_n_poll_fds)) { g_main_context_dispatch(context); } }

它首先调用了 glib 框架的 g_main_context_check 检测事件,然后调用 g_main_context_dispatch 进行事件的分发。

QEMU 自定义了一个新的事件源 AioContext,有两种类型的 AioContext:

  • 第一类用来监听各种各样的事件,比如 iohandler_ctx。
  • 第二类是用来处理块设备层的异步 I/O 请求,比如 QEMU 默认的 qemu_aio_context 或者模块自己创建的 AioContext。

这里只关注第一种情况,即事件相关的 AioContext。

AioContext 结构体定义如下:

// include/block/aio.h struct AioContext { GSource source; /* Used by AioContext users to protect from multi-threaded access. */ QemuRecMutex lock; /* The list of registered AIO handlers */ QLIST_HEAD(, AioHandler) aio_handlers; ... uint32_t notify_me; /* lock to protect between bh's adders and deleter */ QemuLockCnt bh_lock; /* Anchor of the list of Bottom Halves belonging to the context */ ... bool notified; EventNotifier notifier; ... /* TimerLists for calling timers - one per clock type */ QEMUTimerListGroup tlg; ... };
  • source:glib 中的 GSource,每一个自定义的事件源第一个成员都是 GSource 结构的成员。
  • lock:QEMU 中的互斥锁,用来保护多线程情况下对 AioContext 中成员的访问。
  • aio_handlers:一个链表头,其链表中的数据类型为 AioHandler,所有加入到 AioContext 事件源的文件 fd 的事件处理函数都挂到这个链表上。
  • notify_me 和 notified 都与 aio_notify 相关,主要用于在块设备层的 I/O 同步时处理 QEMU 下半部(Bottom Halvs,BH)。
  • first_bh:QEMU 下半部链表,用来连接挂到该事件源的下半部,QEMU 的 BH 默认挂在 qemu_aio_context 下。
  • notifier:事件通知对象,类型为 EventNotifier,在块设备进行同步且需要调用 BH 的时候需要用到该成员。
  • tlg:管理挂到该事件源的定时器。

剩下的结构与块设备层的 I/O 同步相关,此处略过。

AioContext 拓展了 glib 中 source 的功能,不但支持 fd 的事件处理,还模拟内核中的下半部机制,实现了 QEMU 中的下半部以及定时器的管理

接下来介绍 AioContext 的相关接口,这里只以文件 fd 的事件处理为主。

aio_context_new 用于创建一个 AioContext:

// async.c AioContext *aio_context_new(Error errp) { int ret; AioContext *ctx; ctx = (AioContext *) g_source_new(&aio_source_funcs, sizeof(AioContext)); aio_context_setup(ctx); ret = event_notifier_init(&ctx->notifier, false); ... g_source_set_can_recurse(&ctx->source, true); aio_set_event_notifier(ctx, &ctx->notifier, false, (EventNotifierHandler *) event_notifier_dummy_cb); ... timerlistgroup_init(&ctx->tlg, aio_timerlist_notifer, ctx); return ctx; }
  1. aio_context_new 函数首先创建分配了一个 AioContext 结构 ctx。
  2. 然后初始化代表该事件源的事件通知对象 ctx->notifier。
  3. 接着调用了 aio_set_event_notifier 用来设置 ctx->notifier 对应的事件通知函数。
  4. 最后初始化 ctx 中其他的成员。

AioContext 的创建函数中,aio_set_event_notifer 函数调用了 aio_set_fd_handler 函数,后者用于添加或者删除 AioContext 事件源中的一个 fd,如果是添加则会设置 fd 对应的读写函数。添加事件源中 fd 监听处理的步骤如下:

// aio-posix.c void aio_set_fd_handler(AioContext *ctx, int fd, bool is_external, IOHandler* io_read, IOHandler* io_write, void* opaque) { AioHandler* node; bool is_new = false; bool is_deleted = false; node = find_aio_handler(ctx, fd); /* Are we deleting the fd handler? */ if (!io_read && !io_write) { ... } else { if (node == NULL) { /* Alloc and insert if it's not already there */ node = g_new0(AioHandler, 1); node->pfd.fd = fd; QLIST_INSERT_HEAD(&ctx->aio_handlers, node, node); g_source_add_poll(&ctx->source, &node->pfd); is_new = true; } /* Update handler with latest information */ node->io_read = io_read; node->io_write = io_write; node->opaque = opaque; node->is_external = is_external; node->pfd.events = (io_read ? G_IO_IN | G_IO_HUP | G_IO_ERR : 0); node->pfd.events |= (io_write ? G_IO_OUT | G_IO_ERR : 0); } aio_epoll_update(ctx, node, is_new); aio_notify(ctx); if (deleted) { g_free(node); } }

该函数的参数说明如下:

  • 第一个参数 ctx 表示需要添加 fd 到哪个 AioContext 事件源。
  • 第二个参数 fd 表示添加的 fd 是需要在主循环中进行监听的。
  • 第三个参数 is_external 用于块设备层,对于事件监听的 fd 都设置为 false。
  • 剩余参数中,io_read 和 io_write 都是对应 fd 的回调函数,opaque 会作为参数调用这些回调函数。

该函数的主要流程如下:

  1. aio_set_fd_handler 函数首先调用 find_aio_handler 查找当前事件源 ctx 中是否已经有了 fd。
  2. 考虑新加入的情况,这里会创建一个名为 node 的 AioHandler,使用 fd 初始化 node->pfd.fd,并将其插入到 ctx->aio_handlers 链表上,调用 glib 接口 g_source_add_poll 将该 fd 插入到了事件源监听 fd 列表中
  3. 设置 node 事件读写函数为 io_read,io_write 函数,根据 io_read 和 io_write 的有无设置 node->pfd.events,也就是要监听的事件。

aio_set_fd_handler 调用之后,新的 fd 事件就加入到了事件源的 aio_handlers 链表上了,如下图所示:

aio_set_fd_handler 函数一般被块设备相关的操作直接调用,如果仅仅是添加一个普通的事件相关的 fd 到事件源,通常会调用其封装函数 qemu_set_fd_handler,该函数将事件 fd 添加到全部变量 iohandler_ctx 事件源中。

glib 中自定义的事件源需要实现 glib 循环过程中调用的几个回调函数,QEMU 中为 AioContext 事件源定义了名为 aio_source_funcs 的 GSourceFuns 结构,这几个函数都是自定义事件源需要实现的:

// async.c static GSourceFuncs aio_source_funcs = { aio_ctx_prepare, aio_ctx_check, aio_ctx_dispatch, aio_ctx_finalize };

这里介绍一下最重要的事件处理分派函数 aio_ctx_dispatch,该函数会调用 aio_dispatch,后者要完成 3 件事:

  1. 第一是 BH 的处理。
  2. 第二是处理文件 fd 列表中有事件的 fd。
  3. 第三是调用定时器到期的函数。

其中第二步的代码逻辑如下:

// aio-posix.c bool aio_dispatch(AioContext* ctx) { AioHandler* node; bool progress = false; ... node = QLIST_FIRST(&ctx->aio_handelrs); while (node) { AioHandler* tmp; int revents; ctx->walking_handlers++; revents = node->pfd.revents & node->pfd.events; node->pfd.revents = 0; if (!node->deleted && (revents & (G_IO_IN | G_IO_HUP | G_IO_ERR)) && aio_node_check(ctx, node->is_external) && node->io_read) { node->io_read(node->opaque); /* aio_notify() does not count as progress */ if (node->opaque != &ctx->notifier) { progress = true; } } if (!node->deleted && (revents & (G_IO_OUT | G_IO_ERR)) && aio_node_check(ctx, node->is_external) && node->io_write) { node->io_write(node->opaque); progress = true; } tmp = node; node = QLIST_NEXT(node, node); ctx->walking_handlers--; if (!ctx->walking_handlers && tmp->deleted) { QLIST_REMOVE(tmp, node); g_free(tmp); } } /* Run our timers */ progress |= timerlistgroup_run_timers(&ctx->tlg); return progress; }

aio_dispatch_handlers 函数会遍历 aio_handlers,遍历监听 fd 上的事件是否发生了。

  1. fd 发生的事件存在 node->pfd.revents 中,注册时指定需要接受的事件存放在 node->pfd.events 中,revents 变量保存了 fd 接收到的事件。
  2. 对应 G_IO_IN 可读事件来说,会调用注册的 fd 的 io_read 回调,对 G_IN_OUT 可写事件来说,会调用注册的 fd 的 io_write 函数。
  3. 如果当前的 fd 已经删除了,则会删除这个节点。

以 signalfd 的处理为例介绍 QEMU 事件处理的过程。signalfd 是 Linux 的一个系统调用,可以将特定的信号与一个 fd 绑定起来,当有信号到达的时候 fd 就会产生对应的可读事件。

vl.c 中的 main 函数会调用 qemu_init_main_loop 进行 AioContext 事件源的初始化,如下:

// main-loop.c int qume_init_main_loop(Error errp) { int ret; GSource* src; Error* local_error = NULL; init_clocks(); ret = qemu_signal_init(); ... qemu_aio_context = aio_context_new(&local_error); ... qemu_notify_bh = qemu_bh_new(notify_event_cb, NULL); gpollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD)); src = aio_get_g_source(qemu_aio_context); g_source_set_name(src, "aio-context"); g_source_attach(src, NULL); g_source_unref(src); src = iohandler_get_g_source(); g_source_set_name(src, "io-handler"); g_source_attach(src, NULL); g_source_unref(src); return 0; }

qemu_init_main_loop 函数的主要逻辑如下:

  1. 首先调用 qemu_signal_init 将一个 fd 与一组信号关联起来,qemu_signal_init 调用 qemu_set_fd_handler 函数将该 signalfd 对应的可读回调函数设置为 sigfd_handler。
  2. qemu_set_fd_handler 在首次调用时会调用 iohandler_init 创建一个全局的 iohandler_ctx 事件源,这个事件源的作用是监听 QEMU 中的各类事件。
  3. 最终 qemu_signal_init 会在 iohandlers_ctx 的 aio_handlers 上挂一个 AioHandler 节点,其 fd 为这里的 signalfd,其 io_read 函数为这里的 sigfd_handler。
  4. 接着调用 aio_context_new 创建一个全局的 qemu_aio_context 事件源,这个事件源主要用于处理 BH 和块设备层的同步使用。
  5. 最后调用 aio_get_g_source 和 iohandler_get_g_source 分别获取 qemu_aio_context 和 iohandler_ctx 的 GSource,以 GSource 为参数调用 g_source_attach 两个 AioContext 加入到 glib 的主循环中去。

将信号对应的 fd 加入事件源以及将事件源加入到 glib 的主循环之后,QEMU 就会进入一个 while 循环中进行事件监听。

当使用 kill 向 QEMU 进程发送 SIGALARM 信号时,signalfd 就会有可读信号,从而导致 glib 的主循环返回调用 g_main_context_dispatch 进行事件分发,这会调用到 aio_ctx_dispatch,最终会调用到 qemu_signal_init 注册的可读处理函数 sigfd_handler。

  1. QEMU/KVM 源码解析与应用 - 李强
关注微信公众号:Linux 内核拾遗
文章来源: mp.weixin..com/s/-fNJ

距离这个虚拟化层面的漏洞公告发出已有两个多月了,漏洞详情可以查看:

简单来说是通过Cirrus VGA操作读取宿主机内存中的内容,对宿主机造成风险。


除了对qemu打Patch的修复方法外,直接使用其他模拟替换Cirrus也是可以解决这个问题的。

事实上,cirrus vga是90年代早期的设备,存在各种bug和安全问题。详细可以参考qemu vga的维护者Gerd Hoffmann的这篇文章qemu:using cirrus considered harmful(https://www.kraxel.org/blog/2014/10/qemu-using-cirrus-considered-harmful/)。在qemu的upstream中,已经准备放弃cirrus显卡模拟。 

调研了几家公有云和有项目交集的私有云厂商,仍有大部分没有解决这个问题,私有云尤为严重。

不过,已经开启的云主机/虚拟机 想要更换VGA设备比较复杂,有停机同时意味着停服务的风险,所以大量运行中的实例仍保留有Cirrus:

↑ 国内top3的公有云


基于OpenStack的私有云厂商可以参考以下两种方式较简单的修复:

1. 指定镜像属性修复


只需要在image-upload时或直接image-update 指定这个porperty即可

glance image-upload --property hw_video_model=vga …… 
glance image-update --property hw_video_model=vga [image_uuid]

2. 修改nova代码修复

在 libvirt driver 代码中

支持的图形设备有 vga, cirrus, vmvga, xen, qxl

判断逻辑最后,如果没有对镜像显示指定 hw_video_model,则会使用 vedio.type

我们再看看这个video.type

确实为cirrus , 所以我们对此处修改即可:

nova/virt/libvirt/config.py ... self.type = 'cirrus' 改为 self.type = 'vga' ...

最新的Ocata版本可以适用。

一句话patch :

sed -i "s/self\.type = 'cirrus'/self\.type = 'vga'/g" /usr/lib/python2.7/site-packages/nova/virt/libvirt/config.py 

  • 由于 OpenStack 快照也是image形式,第一种方法在 创建云主机快照 -- 通过快照创建云主机 时,也需指定快照文件的 hw_video_model
  • 第二种方法没有上述要求,但在nova代码更新时需要重新修复


微信:


这儿分享云计算相关的技术实践和项目经验,还有各种在云、云安全方面搞出的大新闻、趣事儿。

北京时间6月11-12日,由腾讯安全发起,腾讯安全科恩实验室与腾讯安全平台部联合主办,腾讯安全学院协办的2019腾讯安全国际技术峰会(TenSec 2019)在上海西岸艺术中心召开。

腾讯安全科恩实验室两位安全研究员Marco Grassi和陈星宇透过对VirtualBox的架构设计及攻击面和虚拟机逃逸漏洞利用过程的分析,总结并首度发布了云计算和桌面虚拟化技术的最新漏洞发现,以下是漏洞分析详情。

背景介绍

QEMU(Quick Emulator)是一款免费的开源模拟器,可以用来执行硬件虚拟化。

它通过动态二进制转换模拟机器的处理器,并为机器提供一组不同的硬件和设备模型, 使其能够运行于各种客户操作系统。 它还可以与KVM一起使用,以接近本机的速度运行虚拟机(通过利用Intel VT-x 等硬件扩展)。 QEMU还可以对用户级进程进行仿真,允许某个架构编译的应用程序在另一个架构上运行。

SLiRP模块主要模拟了网络应用层协议,其中包括IP协议(v4和v6)、DHCP协议、ARP协议等,在sourceforge上有 一个很古老的版本源码,QEMU源码中的slirp代码和这里的十分相似。引人注意的是,slirp模块很久未做修改,但是他是QEMU中默认的网络模块,所以其安全性很值得研究。

漏洞成因与细节

在模拟tcp协议时,slirp中对几个特别的端口进行了特殊处理,其中包括端口113(Identification protocol),21(ftp), 544(kshell),6667 6668(IRC)……在处理这些特殊的端口时,需要对用户数据进行操作,一不小心就会出现问题。 CVE-2019-6778就是slirp处理113端口的tcp请求时,未验证buffer剩余空间是否足够,直接拷贝用户数据导致的堆溢出。

slirp模块中有两个重要的数据结构,一个是mbuf,一个是sbuf,mbuf是存储用户从ip层传入的数据的结构,而sbuf是存储tcp层中数据的结构体。他们的定义分别如下:

可以看到在模拟ident协议时,程序拷贝了mbuf中的用户data至sbuf中,同时将sb_wptr和sb_rptr向后加上拷贝的字节数,但是这里程序并未对sb_cc进行任何的操作,在上一层的函数的验证,

在调用tcp_emu之前,会验证sbuf中的剩余空间是否足够,但是由于在模拟ident协议时拷贝了数据却并未加上相应的长度进sb_cc,这样使得sbspace计算出来的空间并不是sbuf实际的剩余空间。

所以如果用户一直向113端口发送数据的话,那就会造成在sbuf中的溢出。

poc如下:

在host中运行 sudo nc -lvv 113 ,再在guest中运行poc中即可。注意这里不一定要连接host,只要任何guest可以连接的IP都可以。

漏洞利用

由于溢出发生处是在一块纯buffer,前后的数据在实际运行中都是不稳定的,所以需要一个适当的手段来控制堆。

Malloc Primitive

IP分片(IP fragmentation)

IP fragmentation is an Internet Protocol (IP) process that breaks packets into smaller pieces (fragments), so that the resulting pieces can pass through a link with a smaller maximum transmission unit(MTU) than the original packet size. The fragments are reassembled by the receiving host.

在IPv4中,IP分片存在于两个mtu不一样的网络之间传输数据,如果一个较大的packet想传输到一个mtu较小的网络中,那么就需要将这个packet分片后再发送,在IP头中就有专门的字段来满足这一需求。

·Zero (1 bit),为0,不使用。

·Do not fragment flag (1 bit),表示这个packet是否为分片的。

·More fragments following flag (1 bit),表示这是后续还有没有包,即此包是否为分片序列中的最后一个。

·Fragmentation offset (13 bits),表示此包数据在重组时的偏移。

在试图重组ip包时,如果重组函数返回NULL,这表示当前的分片序列并没有结束,这样这个包就不会被接下来的流程处理,而会直接return!

这意味着我们可以在内存中任意分配IP包(也就是mbuf),这将是一个非常好的malloc原语(primitive)。

Infoleak

想要任意地址写的前提是我们需要一个leak。好消息是由于溢出的字节数是我们可以控制,因此我们可以修改地址的低位。leak的计划就将是:

1. 溢出修改m_data的低位,在堆的前面写入一个伪造的ICMP包头;

2. 发送一个ICMP请求,将MF bit置位(1);

3. 第二次溢出修改第二步的m_data的低位至伪造的包头地址;

4. 发送MF bit为0的包结束ICMP请求;

5. 接收来自host的ICMP返回包。

这样完成了infoleak,我们可以得到qemu-system的基址以及slirp所使用的堆基址。

Control PC

现在的问题转化为,在已知基址的情况下,如何利用任意地址写对程序执行流的控制?

最终,在全局段上找到了我们的目标对象:QEMUTime。

在QEMUTimer中,expire_time时间到了以后,将会执行cb(opaque)。

漏洞危害
漏洞修复

漏洞详情可见Redhat安全社区:

bugzilla.redhat.com/sho

漏洞修复如图所示,在拷贝数据前验证sbuf中剩余空间是否足够,目前QEMU最新版已修复该漏洞。

这两篇的内容:

  • 杂谈 (就是现在看到的这一篇)
  • Qemu使用心得(翻新版)

(以下内容还是挺乱的,基本上还是想到啥说啥。)

  • 引子
运行在Windows上的Qemu 4.2

去年(2019年)发布的Qemu 4.x有了更完善的PowerPC Mac的模拟。Qemu 4.0版本把Sungem网卡合并到了主线,更早一些的3.0版把PMU(电源管理单元)的模拟也合并到了主线版本。现在还差Screamer声卡的模拟没有合并到主线了,毕竟现在还差点火候。目前Screamer只是在Mac OS 9下表现良好,而Mac OS X下还是会有爆音等问题。当然,一些老问题,比如鼠标指针有时乱飘,(Windows下)键盘布局有时会乱掉等等,都得到了解决或改善。Windows版本的Qemu中的磁盘IO性能低下的问题也有很大的改观,不像以前版本的Qemu,磁盘性能只有Power Mac G3 300的一半不到。

2018年底还有人开了个新的fork,尝试模拟ATI Rage 128 Pro。目前这个fork已经在4.1版就并入主线了。当然,目前Rage 128的模拟肯定还是在非常初始的阶段,2D模拟都还不完善,3D模拟更是一片空白。要这个fork版中模拟Rage 128,还需把Revision 136版本的显卡ROM放进磁盘的分区里,然后在OpenBIOS里用命令加载这个显卡ROM。

注:Qemu 4.1正式版就已经有Rage 128的最基本支持了(没有任何图形加速),设备名为ati-vga,不需要额外在OpenBIOS中加载ROM。但同样不推荐使用这个设备。

参考帖子 https://www.emaculation.com/forum/viewtopic.php?f=34&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;t=7047&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;start=2025#p62215
Mac版ATI Rage 128 Pro 16M,曾用于Power Mac G4 AGP Graphics, Gigabit Ethernet, Cube, Digital Audio型号

这个ATI Rage 128 Pro的模拟让我想到了之前有人在某个PC模拟器上模拟NVIDIA Riva 128, TNT和TNT2的尝试,至今也还停留在VBE的实现上。 算了,先不谈这个遥遥无期且随时可能被放弃的fork了。目前如果需要3D,还不如模拟3dfx Voodoo更实在些(Voodoo当年也有Mac版),至少文档丰富,而且有现成的代码,不像NVIDIA和ATI基本没有文档。

  • 看看现在Qemu 4.2的表现

(以Screamer分支的Windows版本为例)

Mac OS 9.2.2的模拟基本算完整,运行速度也十分接近我的iBook G3 800MHz(当然浮点性能除外)。目前还没有2D加速,这点稍逊SheepShaver,因为至少SheepShaver有QuickDraw加速,当然要说3D加速,目前两者都没有。当然,文件交换也不如SheepShaver方便。不过Qemu的硬件模拟更完整,至少有内存管理单元(MMU)和电源管理单元(PMU),连ADB接口(苹果早期的鼠标键盘接口)甚至USB都支持,这点Qemu扳回一局。 目前,稳定性方面也是Qemu更强,不像SheepShaver经常莫名其妙崩溃。

先放一张MacBench 5的跑分图。 图上黄柱的qemu项就是本机的跑分。 主机配置是AMD Ryzen 3600X(基准频率锁定在4.25GHz),16G DDR4 3200内存,不过Qemu程序和系统镜像是放在了老硬盘上。

可以看出,在这台主机上,Qemu模拟的CPU 性能是Power Macintosh G3 300的287.9%, 但FPU性能只有它的70.9%。现在Windows版的Qemu磁盘性能已经不再是瓶颈,跑出了450.6%的成绩。这个成绩稍微比我之前那台i3 7100 3.9GHz上的好一些,毕竟模拟器还是很吃CPU单核性能的。当然,图中还有别人的i5 2520M电脑在macOS Mojave中运行Qemu(很可能是老版本)和SheepShaver的跑分参考。从这个参考中还可以发现,SheepShaver的浮点性能还是很强的,不过整数性能一般般,与Qemu刚好相反。

(更新 2020-02-17 )补充一个MacBench的成绩作为对照,主机CPU i5 4300M。

可以看出, 主机CPU的性能对Qemu模拟器的性能影响非常大。

Mac OS 9.2.2 多媒体方面

MOV影片播放流畅(虽然显示的帧率在14-15之间跳动),Sorenson 3编码(SVQ3),480x360,1041kbps
QuickTime VR影片,播放和操作都流畅。
采用QuickTime技术的多媒体光盘 The Little Prince(可在Macintosh Garden网站找到), 运行流畅

Mac OS X Tiger 10.4

Mac OS X下同样没有显卡加速,GUI动画比以前流畅些,但还是有肉眼可见的掉帧,Dock放大图标的动画还是卡顿。

在Mac OS X中,Screamer还不怎么能用。有时候会爆音,有时候播放的提示音会一直重复,不过播放MP3时还是流畅的。

其他系统

Mac OS X Server 1.2v3(Rhapsody 5.6)能安装和运行。

第一次启动Blue Box,正在准备磁盘镜像。
Blue Box中的Mac OS 9.0

Mac OS X 10.0 DP2能运行,连BlueBox(Mac OS.app)都能启动。暂且可以把BlueBox看成是一个全屏的Mac OS虚拟机。但Rhapsody和Mac OS X DP2本身没有声音,在BlueBox中的Mac OS 9却有声音。

  • 回到标题,为什么今朝还看Qemu?

之前的文章提到过老前辈PearPC和SheepShaver,一个是模拟Mac OS X 10.1-10.4,一个模拟System 7.1.2 - Mac OS 9.0.4。不过它们都还有自己的短板,并且它们的开发已经不再活跃了。之前PearPC似乎诈尸过两次,一次是在2011年,时隔六年发布0.5版;还有一次是在2015年把代码搬到了GitHub,还开了0.6pre版的坑,只是一直没有下文。 看更新日志,2011年的PearPC 0.5主要是增加了AMD64版的JIT,而2015年的0.6pre版则是打算修bugs和翻新代码,顺便改善AMD64平台的支持。不过0.6pre还一直没有下文,目前git版的代码也存在一些bugs。

SheepShaver就更惨了,作者在2003年(或者更早)就弃坑了,把代码交给社区来维护。SheepShaver虽然也在更新,但都是一些bug修复,而且一直没有64位版本,编译器也不能用GCC 4.4以上的版本。后来SheepShaver在Mac OS X平台上栽了跟头,一是老版本没法运行,二是当时Mac也考虑放弃32位支持了,这样一来, SheepShaver的古董代码要是不现代化那就没法适应当前的软件环境了。所幸,2018年开始,有第三方的爱好者开了fork,给SheepShaver翻新代码,同时让SheepShaver也支持64位。(其实2015年就有人尝试创建了一个支持GCC5的fork。)

可以说,SheepShaver目前的状态只是代码维护,不会有新功能的增加(所以不要期待MMU和OS 9.2的模拟了);PearPC可能会不定期诈尸,开发者没有明确表态是否弃坑。(当然,本文也可能不定期诈尸。)而Qemu的PowerPC模拟就不一样了,它还在积极开发中,fork的功能成熟后很可能会合并到主线。

用一句话说,SheepShaver和PearPC代表过去,Qemu代表未来。

  • 最后再回(zhi)顾(jing)一下Qemu模拟Mac OS和Mac OS X的历程。

Qemu很早就支持PowerPC的模拟了,当时也有Power Mac机型的模拟,只不过当时还是个运行OpenBIOS的普通机型,只能运行一些支持PowerPC的Linux发行版。后来在2011年,有个叫Natalia Portillo的开发者在Qemu的GSoC 2011项目上开了个坑,想让Qemu模拟的Power Mac能启动8.5版本以上的Mac OS,具体的讨论可以见E-Maculation社区的帖子。当时虽然没能启动Classic Mac OS,不过到2013年,用Qemu 1.6模拟Mac OS X 10.2-10.4是没问题了,只是速度还不如PearPC快。

开发者Programmingkid的截图

当然,开发者并不满足于只能运行Mac OS X的状况,于是在GSoC 2015项目上又开坑,尝试运行Mac OS 9。到了2015年8月, Mac OS 9光盘能加载了。只是,启动时会遭遇崩溃。

原帖 https://www.emaculation.com/forum/viewtopic.php?f=34&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;t=7047&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;start=125#p51037

但无论怎么说,这都是个里程碑。Mac OS是个奇葩的系统,高度依赖硬件和ROM,要完整地模拟它,难度可想而知。后来开发者用上了MacsBug调试器等工具,开发也逐步进入了正轨。 2015年11月,在把所有功能扩展和控制板移除后,可以成功启动到桌面(用安装盘的系统文件夹)。

原贴 https://www.emaculation.com/forum/viewtopic.php?f=34&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;t=7047&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;start=275#p51638

当时我也尝试过他们的git版,尝试把运行一个最简化的中文系统,但是最终还是没法运行。不过当时至少收获到了一个小技巧,把光盘版的System手提箱中的“xboo”资源删掉,就能在硬盘上启动而不报错了。 当然,开发者们肯定没有这么闲,还在用MacsBug抓取错误详情,并改进Power Mac机型的模拟,完善Qemu和对应的OpenBIOS代码。同时,他们也在测试其他几个版本的启动情况,在当时至少9.1版能启动(当然也会崩溃),更低版本则根本没法加载。

到了2016年2月,另一个好消息传来——Mac OS X的Classic环境可以使用了,这是在PearPC中做不到的。

2016年6月份,开发者mcayland找到了Mac OS 9.2崩溃的问题所在,Qemu的模拟的一些CPU指令还有DMA都存在一些问题。同时开发者Programmingkid也发现Mac OS 9.0.4也能启动了。 2016年7月,Mac OS 9的模拟取得了较大的进展,用patched过的OpenBIOS以及Qemu 2.7pre版本可以启动Mac OS 9.1/9.2的光盘了,还有人成功安装上了Mac OS。同时,Mac OS X 10.0和10.1也能启动和运行了。

最后,Qemu 2.7正式版终于可以模拟Mac OS 9.1/9.2了,又一个里程碑。当然,开发者也没有停下脚步,他们还在完善Mac OS 9的模拟,同时尝试运行Mac OS X Server 1.x(Rhapsody),Mac OS X 10.0 Developer Preview/Public Preview和Mac OS X 10.5以及其他一些操作系统。

后来Qemu 2.8/2.9版本身没有太多Power Mac方面的更新,但开发者也在(可更改分辨率的)VGA驱动,Sungem网卡,还有Screamer声卡还有PMU(电源管理单元)等方面努力。这些改进大多也合并到了Qemu主线(除了Screamer)。

后来,再后来,就是现在的Qemu了……

前途是光明的,但道路是曲折的。Qemu的开发者应该不会这么轻易就满足现状。Qemu 4.2同时增加了Macintosh Quadra 800机型的模拟,不过目前还是运行OpenBIOS的基本机器,只能运行Linux发行版,还不能加载Mac ROM,距离模拟System 7和Mac OS 8还有很长的路要走。

2月23日 更新: qemu-system-m68k模拟Macintosh Quadra 800需要MacROM.bin文件,应该就是Quadra 800机型的ROM。但目前还只能启动Linux。如果不指定内核和initrd来启动Linux,直接启动会白屏。


谈不上翻新之前的Qemu使用心得,更确切说应该是更新与补充吧。

能看懂英文并且有一定是使用经验的读者应该直接看Qemu的官方文档,而不是听我在这瞎侃。Here you go. PowerPC emulation guide, Networking guide, and another useful doc ( pdf / html )

当然,翻新版会探讨新版本相关的一些配置,网络的配置,还有共享文件的一些方法。

内容导航:

  • 杂谈 (上篇)
  • Qemu使用心得(翻新版)(现在看到的这一篇)

本篇内容是对之前Qemu使用心得的更新和补充。 前一篇写于2018年,当时还是针对Qemu 3.0pre版本来写的。如今的Qemu 4.2版本已经将许多功能合并到主线了,因此与前篇稍有区别。

官方版Qemu可在官网下载页找到。不过去官网基本下载的是源码。如果需要官方的git版本, GitHub上也有Qemu的git镜像。

Windows用户可以在QEMU for Windows站点找到Qemu的Windows版本,其中w32文件夹是32位版本,w64文件夹是64位版本。如果需要带Screamer的实验性版本,可以在E-Maculation论坛的这篇帖子上找到。(定位到10-02-2020: Qemu 4.2 with screamer sound support)

macOS用户,如果安装了Homebrew或者MacPorts,可以用它们来安装Qemu,具体命令见上面Qemu官网下载页的说明。如果没有Homebrew或者MacPorts,或者需要带Screamer的实验性版本,可以在E-Maculation论坛的这篇帖子上找到。编译后的官方版4.2的文件名Qemu-4.2-OSX-Catalina-21-12-2019.zip, 带Screamer声卡的版本分Catalina和非Catalina版,根据自己运行的系统版本来选择。

Linux或BSD用户,各发行版的源里一般都有Qemu,可以按需安装。但是确保一定要装上含有qemu-system-ppc的软件包。如果发行版软件源中的Qemu版本太低,可以自行从源码编译Qemu。如果需要带Screamer声卡的版本,可以在GitHub上找到这个版本的源码。具体的编译步骤和过程,这里不再详细介绍。

Android用户,目前有个叫Limbo的软件,使用的就是Qemu来进行模拟。自带图形化的配置界面。可以在下载页面中找到模拟ppc的版本。不过由于Limbo是图形界面的,文章下面的内容没太大作用,只是部分命令可以作为参考。

通过命令来启动Qemu模拟器。如果是从E-Maculation论坛下载的Qemu,那压缩包中会有一个写好的启动脚本。Qemu.command(macOS)或qemu.bat(Windows),使用文本编辑器可以打开进行编辑。修改这个文件之前,建议先备份,至少可以当成模版使用。

例如该文件内容可能是(适用于Linux和Mac):

https://www.zhihu.com/topic//qemu-system-ppc -L pc-bios -boot d -M mac99,via=pmu -m 512 \ -prom-env "auto-boot?=true" -prom-env "boot-args=-v" -prom-env "vga-ndrv?=true" \ -drive file=MacOS9.2.iso,format=raw,media=cdrom \ -drive file=MacOS9.2.img,format=raw,media=disk \ -sdl \ -netdev user,id=network01 -device sungem,netdev=network01 \ -device VGA,edid=on \

可以将这些内容保存为一个文本文档,例如保存为qemu.sh,然后设置可执行的权限。

Windows的话,可以将这些内容保存在一个bat格式的批处理文件中,比如qemu.bat,放在qemu可执行程序的目录下。

qemu-system-ppc.exe -L pc-bios -boot d -M mac99,via=pmu -m 512 ^ -prom-env "auto-boot?=true" -prom-env "boot-args=-v" -prom-env "vga-ndrv?=true" ^ -drive file=MacOS9.2.iso,format=raw,media=cdrom ^ -drive file=MacOS9.2.img,format=raw,media=disk ^ -sdl ^ -netdev user,id=network01 -device sungem,netdev=network01 ^ -device VGA,edid=on ^

Qemu的参数与之前那篇文章介绍的大同小异。如果参数太多,一行显示不下,可以在一个参数写完后换行。Linux和Mac下的换行需要在行末输入空格反斜杠"\"再回车, Windows下是空格 "^"符号 再回车。具体可以参考上面两段。

https://www.zhihu.com/topic//qemu-system-ppc 

Qemu程序名称和路径,即当前目录下的qemu-system-ppc程序。如果是自行在Linux和Mac编译安装的Qemu,需要把前面的 "https://www.zhihu.com/topic//"去掉,不然很可能提示command not found(找不到这个命令)。如果是Linux下,已经通过发行版的软件源安装了Qemu,自行编译Qemu后,自己编译的Qemu很可能位于/usr/local/bin目录中

-L pc-bios 

指定ROM文件所在的目录是pc-bios文件夹。如果是自行在Linux和Mac上编译安装的,需要把这个参数删除,以免Qemu找不到所需的ROM。

-boot d

-boot d是从光盘启动。如果需要从硬盘启动,则将d改为c,即-boot c。

-M mac99,via=pmu

-M 指定的是机型,mac99机型指的是Power Mac G4 AGP Graphics型号(即代号为Sawtooth的G4机型),via=pmu指的是带PMU单元的模拟, mac99与via=pmu之间以英文逗号隔开。可以把pmu改成pmu-adb来带ADB总线的Mac机型,也可以直接把 " ,via=pmu "(注意via前有英文逗号)直接删掉。 除了mac99机型,还有g3beige机型,-M g3beige,是米黄色机箱的Power Macintosh G3,默认就是ADB总线的机型。

-m 512 

-m 设置内存大小,单位为MB。模拟Mac OS 9建议在1024M以下,1G内存时无法开启虚拟内存,这样Screamer会不发声(这是已知的bug),当然也不要在Mac OS 9里手动关闭虚拟内存。

-cpu G4 

-cpu,当然这个在刚才是示例文件中不存在,如果需要手动改CPU型号时可以加上去。 一般有-cpu G4或者-cpu G3可选。 如果需要列出支持的CPU, 可以运行 qemu-system-ppc -cpu help命令。(但是最好不要改CPU类型,因为很多型号Qemu不一定能完全模拟,有些型号可能无法让系统正常启动。)

-prom-env "auto-boot?=true" -prom-env "boot-args=-v" -prom-env "vga-ndrv?=true" 

-prom-v定义的是NVRAM的环境变量。"auto-boot?=true"指的是开启模拟器后尝试自动从一个驱动器启动,不需要的话可以把true改成false。"boot-args=-v"可以指定Mac OS X的启动参数,-v指的是输出启动详情(而不是显示为苹果logo),不需要的话也可以去掉-v。 "vga-ndrv?=true"参数能让Mac OS X客户机更改分辨率,如果需要模拟ATI Rage显卡,必须改成"vga-ndrv?=false"。

-drive file=MacOS9.2.iso,format=raw,media=cdrom -drive file=MacOS9.2.img,format=raw,media=disk 

-drive定义的是驱动器,后面的参数有file=,format=,media=(,index=),参数之间用英文逗号隔开。 file=后接文件名和路径,比如file=C:\MacOS9.2.2.iso,~/Downloads/Mac\ OS\ 9.2.2.iso 等,注意Windows中路径和文件名不能包含空格和逗号,而Linux和Mac中的文件名可以有空格,但需要在空格前加上反斜杠"\"。 format=参数是定义镜像文件格式,比如img和iso格式是raw,除了这些,还有qcow、qcow2和qed等等格式的磁盘镜像,根据镜像文件类型来确定。media=参数是定义媒介类型,比如cdrom指光驱,disk指硬盘。而还有一个可选参数index=,指的是驱动器序号,从0开始写,用于指定接口IDE0/1和主/从关系,这个参数是可选的。

如果不想用-drive定义驱动器,可以试试老版本Qemu提供的 -hda , -hdb, -cdrom, -fda, -fdb等来定义,后面直接加文件名。如:

-hda /path/to/file.img -cdrom /path/to/file.iso

-hda, -cdrom比较方便,但缺点是不能自定义格式,而是自动检测。也不能像-drive一样,定义磁盘缓存的方式(直写还是回写)等。

-sdl

-sdl指的是使用SDL2来显示用户界面。如果不指定此参数,那默认使用GTK界面。

-netdev user,id=network01 -device sungem,netdev=network01 

这两个选项用于定义网络。 -netdev定义的是网络类型, user指的是使用NAT(类似路由器)方式联网,id指的是这个网络的名字。除了user,还可以使用tap,vde等方式,但这些比较高级,不建议初学者使用。 -device则是指定一个设备及其参数,这里指定了sungem网卡,netdev参数指的是让这块网卡连接哪一个网络,这里定义的是之前那个参数创建的network01网络。

-device VGA,edid=on 

这行是使用-device选项来定义VGA设备(一块最基本的显卡),并加上了edid=on的参数,意思是注入显示器EDID,从而让客户机操作系统可以选择分辨率。

-device除了定义VGA以外,还可以定义usb-mouse(USB鼠标),usb-kbd(USB键盘),usb-audio(USB声卡),usb-net(USB网卡),adb-mouse(ADB鼠标),adb-keyboard(ADB键盘),rtl8139(“万能”的Realtek RTL8139网卡),ati-vga(尚未完全支持的ATI Rage 128 Pro显卡)等等。要列出可以-device支持的设备,加上-device help参数即可。

更多参数及用法,可以参考Qemu的官方文档(英文),或者这本手册(英文)。

了解了这些参数,大可将开头的那份参数模板复制下来,然后粘贴到一个文本文件中,再根据实际情况加以调整。文件保存后,Linux和Mac下可以用chmod +x命令设置可执行权限,以后就能直接运行这个脚本文件。在Windows中则是将这个文件的扩展名改为.bat或.cmd,Windows也就能直接执行了。

如果需要使用Qemu运行多个系统,那么可以继续新建文本文件,将上面的模板抄下来,以别的名字保存。到时候想运行哪个“虚拟机”就打开相应的脚本文件,是不是很像VMware和VirtualBox的虚拟机管理器?

这里再吐槽几点,Qemu曾有过几个第三方的图形前端,可以像配置VMware一样创建不同的虚拟机。但Qemu版本更迭速度较快,而且Qemu官方没有给出图形前端。老版本的图形前端搭配新版本的Qemu程序虽然能用,但无法用上所有的新功能,而且有时新版本Qemu改了一两个参数的用法,那用原来的命令无法启动了。 于是,这些第三方图形前端的作者后来大多都弃坑了。 这种事情还是官方来做最好,但官方现在并不想写GUI配置程序,要知道一些自由软件的作者都是有自己脾气的,一副“爱用不用”的态度,“反正源码在这,有本事自己改啊”。

可以使用qemu-img来创建磁盘镜像。但参数较多,如果要图方便,不如直接用bochs项目的bximage程序创建raw镜像。

qemu-img用法示例:

qemu-img create -f raw macos.img 200M

基本用法 qemu-img create -f 格式 文件名及路径 大小

其中-f指的是镜像格式,常用格式有qcow2和raw等格式。最后的文件大小可以是M,G,T等等。qcow2格式支持动态扩展,而raw只能是固定大小。但是raw基本可以被Mac和Linux主机系统挂载,qcow2不行。根据需求选择。

如果是macOS版本,那就比较简单。 直接在Machine菜单中选择Change ide1-cd0选项,然后在弹出的打开文件对话框中找到要更换的光盘镜像即可。

Linux和Windows版本也能更换,但是稍微麻烦一些。在Qemu窗口中按下Ctrl-Alt-2键,会弹出QEMU的控制台窗口。 输入命令:

change ide1-cd0 iso文件路径和名称

这样就能更换了镜像了。 如果光驱设备不是ide1-cd0,那么可以用“info block”来查看当前的存储设备,找到带 cd 字样的设备,那基本就是光驱了。

如果提示Error: Device 'ide1-cd0' is locked ,可以在客户机系统中弹出光盘后再试。

如果客户机中不能弹出光盘,可以用 eject -f ide1-cd0 命令来强制弹出光盘。(如果只是一般弹出而不是强制弹出,可以去掉-f参数。)

change还能更换其他设备的镜像,比如软盘。change用法(方括号内为可选项):

change 设备 镜像路径和名称 [格式] [read-only或read-write]

更多Qemu控制台相关命令和用法,可以参见此文档。

刚刚模板里的网络设置,默认是user类型(即NAT)。相当于主机模拟了一个路由器,将主机自己插入WAN口,将客户机插入LAN口。这种模式下,主机无法访问客户机里的网络资源,比如ping不通客户机IP 10.0.2.15。但客户机可以访问主机的网络资源,比如客户机可以ping通主机的IP 192.168.1.101。

默认的网络设置如下:

IP: 10.0.2.15
子网掩码(网络): .255.0
网关(路由器):10.0.2.2
DNS服务器:10.0.2.3
SMB服务器(可选):10.0.2.4



但一般情况下就足够使用了,至少客户机系统可以上网。 user模式下可以指定一个目录,用虚拟一台SMB服务器(即Windows的共享共享方式)来共享这个目录。

注:一般情况下不建议用TAP方式联网,一是太麻烦,二是很多情况下没必要用。 适合有一定局域网经验和有这方面需求的用户。

Windows下的TAP网络设置

如果需要实现桥接的方式,需要借助TAP网卡。这要求主机系统安装TAP驱动。Windows的TAP驱动比较好找,而且配置起来比较简单。 比如这个tap驱动,这个驱动捆绑在了一个叫OpenXXX的软件(这个软件的名字里有一个不合时宜的词)中。

可以在Windows的设备管理器的“操作”菜单,“添加过时硬件”选项中,使用向导来一步一步安装。

具体安装步骤就不再详细介绍。 安装之后,网络和共享中心的“更改适配器选项”中就会出现一个新的网络连接,将这个网络连接重命名为“tap”或者其他的英文名字(这个名字在Qemu中会用到)。再同时选中这个tap网卡和主机的有线网卡,右键点击,选择“桥接”选项。至于没有有线网卡接口的笔记本,或者无法使用有线网卡的情况,自求多福吧……(或者老老实实用回user模式。)

连接完之后,还需要修改Qemu启动参数的这一行

-netdev user,id=network01 -device sungem,netdev=network01 

把user改为tap,同时在id=network01后面加上",ifname=tap",修改后如下:

-netdev tap,id=network01,ifname=tap -device sungem,netdev=network01 

这个ifname指的是TAP网卡的名字,就是刚刚给网络连接重命名时的名字,如果叫“以太网2”,恐怕就算是写了,Qemu也不认吧。

这时启动Qemu,在客户机系统里进行IP地址的设置。把客户机系统设置在主机的网段。这样就把Qemu和主机接在了同一个网络。

Mac和Linux下的TAP网络

Mac下也有TunTap OSX的tap驱动,于2015年更新,但是在macOS Mojave下,Qemu无法使用/dev/tap0设备。因此也就无法测试了。

Linux下则自带tap设备,需要用# modprobe tun tap来启用tun和tap设备。然后参照Qemu文档的TAP设置说明进行设置,比较复杂。

Qemu文件共享没有SheepShaver那么方便,SheepShaver可以直接将主机的磁盘映射到模拟器,但Qemu不行。 不过还是有其他方法用来共享文件。

最通用的方法 制作ISO文件

这种方法比较简单,Windows和Linux下都有工具可以制作ISO格式的光盘镜像文件。 Mac的磁盘工具也可以直接把文件夹制作成镜像,此时选择保存格式应该是“CD/DVD主镜像”,默认扩展名.cdr,但可以直接改为.iso,使用Roxio Toast也可以制作ISO。

制作完镜像文件,修改Qemu的启动参数,把-drive中的file=文件名改了就行。相当于主机刻录了一张光盘交给模拟器使用。

这种方法的缺点是,只能主机分享文件给模拟器。而且分享新文件时需要重新编辑镜像,这点比较麻烦。

通过网络共享文件

当然,这里的通过网络不是指通过网盘传输。之前介绍了如何联网,用TAP方式可以将主机和模拟器放在同一个网络中。但即使是user方式,客户机系统也可以访问主机的资源。因此可以在主机上搭建一个FTP服务,在客户机中就能够访问了,主机地址是10.0.2.2。可以用Mac OS 9的“网络浏览器”访问,或者Mac OS X的Finder的“连接到服务器”(Command - K快捷键)。

挂载磁盘镜像

这个方法不适用于Windows,因为不支持HFS+文件系统。此外,如果要直接挂载镜像,则必须使用raw格式(扩展名一般是img)。macOS直接可以通过双击img镜像的方式来挂载镜像,如果是raw格式但用了别的扩展名,直接改扩展名即可(改完扩展名别忘了把启动脚本里的镜像文件名也改了)。拷贝完文件再推出磁盘(Command - E)。

macOS下最方便,直接双击即可挂载

Linux下也可以用mount命令来挂载。

# mount -o loop,offset=xxxxx os9.img /mnt/

只是这个offset值麻烦一些,要在支持APM分区表的fdisk中列出磁盘分区,找到磁盘的开始扇区的序号,再用这个扇区号乘以512,得到的值就是offset值了。具体方法不多讲。

当然,Windows要读写镜像还有一种曲线方法…… 就是借助SheepShaver。 在SheepShaver中添加这个磁盘镜像,然后就能在SheepShaver中读写了。

更新:Windows下如果使用的是RAW格式的磁盘镜像(.img等),可以使用TransMac软件进行读写。但是TransMac只支持写HFS+格式的镜像,而HFS是只读的。Windows下的其他HFS/HFS+工具或许也有类似功能。

暂时介绍这么多用法。如有疑惑,请翻Qemu的文档。虽然目前文档大多还是英文,多数没有本地化。


附:

Qemu启动参数配置模版

Windows

cd /d %~dp0 qemu-system-ppc.exe ^ -L pc-bios ^ -M mac99,via=pmu ^ -cpu G4 ^ -m 512 ^ -prom-env "auto-boot?=true" ^ -prom-env "boot-args=-v" ^ -prom-env "vga-ndrv?=true" ^ -hda X:\images\mac.img ^ -cdrom X:\images\mac.iso ^ -sdl ^ -usb ^ -netdev user,id=network01 -device sungem,netdev=network01 ^ -device VGA,edid=on ^ -boot d REM 将本文件放在与Qemu程序相同的目录下。镜像资源建议也放在该目录下。 REM 如果不需要自定义硬件,则只需要把-hda后的路径改为硬盘镜像的路径,-cdrom后的路径改为光盘镜像的路径。

本文是写这个章节的一些边角料:source/认识鲲鹏920:一个服务器SoC/设备和设备总线.rst · Kenneth-Lee-2012/从鲲鹏920了解现代服务器实现和应用_公开 - 码云 Gitee.com,由于没法放到那个文档的主逻辑中,我放在这里。(顺便感慨说一句,你看我在那里写一句话,可能我在背后要确认两三天。正经写点东西还是很难的)

本文用3.1.50源代码版本作为参考。

qemu可以虚拟机模拟一个虚拟的PCIe的总线系统(关于PCIe的基本原理可以看前面的这个链接)。具体模拟成的样子在具体模拟的那个机器上上定义,比如我在PC上用这个参数启动虚拟机:

~/work/qemu-run-arm64/qemu/aarch64-softmmu/qemu-system-aarch64 \  -s -cpu cortex-a57 -machine virt \  -nographic -smp 2 -m 1024m -kernel arch/arm64/boot/Image \  -append "console=ttyAMA0"

mainchine是ARM平台virt,它的机器定义在就在hw/arm/virt.c中。所以它会有一个默认的PCI结构。默认是这样的:

那么如果你要在其中插入一个PCIe设备,方法就是用这个参数:

-device <dev>[,bus=pcie.0]...

不同的dev类型会有不同参数,这个可以看对应设备的详细说明,部分qemu的文档中有,但文档不一定同步,你可以通过这样的命令让qemu直接打印出来:

-device help

不同的设备有不同的属性,这个在源代码中是通过object_property_add()或者在设备class里加的,你可以从qemu源代码的hw目录中找,每种设备类型都有类似的定义的,比如vfio-pic的定义是这样的:

static Property vfio_pci_dev_properties[] = { DEFINE_PROP_PCI_HOST_DEVADDR("host", VFIOPCIDevice, host), DEFINE_PROP_STRING("sysfsdev", VFIOPCIDevice, vbasedev.sysfsdev), DEFINE_PROP_ON_OFF_AUTO("display", VFIOPCIDevice, display, ON_OFF_AUTO_OFF), DEFINE_PROP_UINT32("xres", VFIOPCIDevice, display_xres, 0), DEFINE_PROP_UINT32("yres", VFIOPCIDevice, display_yres, 0), DEFINE_PROP_UINT32("x-intx-mmap-timeout-ms", VFIOPCIDevice, intx.mmap_timeout, 1100), DEFINE_PROP_BIT("x-vga", VFIOPCIDevice, features, VFIO_FEATURE_ENABLE_VGA_BIT, false), DEFINE_PROP_BIT("x-req", VFIOPCIDevice, features, VFIO_FEATURE_ENABLE_REQ_BIT, true), DEFINE_PROP_BIT("x-igd-opregion", VFIOPCIDevice, features, VFIO_FEATURE_ENABLE_IGD_OPREGION_BIT, false), DEFINE_PROP_BOOL("x-no-mmap", VFIOPCIDevice, vbasedev.no_mmap, false), DEFINE_PROP_BOOL("x-balloon-allowed", VFIOPCIDevice, vbasedev.balloon_allowed, false), DEFINE_PROP_BOOL("x-no-kvm-intx", VFIOPCIDevice, no_kvm_intx, false), DEFINE_PROP_BOOL("x-no-kvm-msi", VFIOPCIDevice, no_kvm_msi, false), DEFINE_PROP_BOOL("x-no-kvm-msix", VFIOPCIDevice, no_kvm_msix, false), DEFINE_PROP_BOOL("x-no-geforce-quirks", VFIOPCIDevice, no_geforce_quirks, false), DEFINE_PROP_BOOL("x-no-kvm-ioeventfd", VFIOPCIDevice, no_kvm_ioeventfd, false), DEFINE_PROP_BOOL("x-no-vfio-ioeventfd", VFIOPCIDevice, no_vfio_ioeventfd, false), DEFINE_PROP_UINT32("x-pci-vendor-id", VFIOPCIDevice, vendor_id, PCI_ANY_ID), DEFINE_PROP_UINT32("x-pci-device-id", VFIOPCIDevice, device_id, PCI_ANY_ID), DEFINE_PROP_UINT32("x-pci-sub-vendor-id", VFIOPCIDevice, sub_vendor_id, PCI_ANY_ID), DEFINE_PROP_UINT32("x-pci-sub-device-id", VFIOPCIDevice, sub_device_id, PCI_ANY_ID), DEFINE_PROP_UINT32("x-igd-gms", VFIOPCIDevice, igd_gms, 0), DEFINE_PROP_UNSIGNED_NODEFAULT("x-nv-gpudirect-clique", VFIOPCIDevice, nv_gpudirect_clique, qdev_prop_nv_gpudirect_clique, uint8_t), DEFINE_PROP_OFF_AUTO_PCIBAR("x-msix-relocation", VFIOPCIDevice, msix_relo, OFF_AUTOPCIBAR_OFF), /*  * TODO - support passed fds... is this necessary?  * DEFINE_PROP_STRING("vfiofd", VFIOPCIDevice, vfiofd_name),  * DEFINE_PROP_STRING("vfiogroupfd, VFIOPCIDevice, vfiogroupfd_name),  */ DEFINE_PROP_END_OF_LIST(), };

但这个还是很烦,因为部分参数是从父类继承过来的。更好的办法和前面用help参数一样,可以这样让它打印:

-device vfio-pic,?

缺点是通常没有解释,如果你需要知道详细的意思,还是需要看代码。

下面给出一些比较常用的参数:

  • id,设备标识,用于指定的一个字符串,用于其他配置引用这个设备
  • host,host本地的设备标识,比如vfio-pci设备的bdf
  • bus,总线代号,比如这里的pcie.0,如果你创建更多的总线,就可以是那个总线的id
  • addr,总线上的设备地址,也就是bdf中的d或者df,不能和其他地址冲突。比如pcie.0上默认已经有0和1了,你指定这个地址就会失败
  • slot和chassis,这个概念我不知道为了什么引入的,这两个都是硬件的概念,是指PCI的插槽和扩展器。反正你保证两者的组合不会和别人重复就可以了。

下面插入两张virtio的网卡到pcie.0中:

~/work/qemu-run-arm64/qemu/aarch64-softmmu/qemu-system-aarch64 \  -s -cpu cortex-a57 -machine virt \  -nographic -smp 2 -m 1024m -kernel arch/arm64/boot/Image \  -netdev type=user,id=net0,hostfwd=tcp::5555-:22 \  -netdev type=user,id=net1 \  -device virtio-net-pci,bus=pcie.0,netdev=net0,addr=6.0 \  -device virtio-net-pci,bus=pcie.0,netdev=net1,addr=7.0 \  -append "console=ttyAMA0"

-netdev创建了两个本地设备,-device用这两个本地设备制造了两张网卡,我们给定了addr,整个拓扑就是这样的:

我们再增加两条根桥和一个virtio网卡:

~/work/qemu-run-arm64/qemu/aarch64-softmmu/qemu-system-aarch64 \ -s -cpu cortex-a57 -machine virt \ -nographic -smp 2 -m 1024m -kernel arch/arm64/boot/Image \ -netdev type=user,id=net0,hostfwd=tcp::5555-:22 \ -netdev type=user,id=net1 \ -netdev type=user,id=net2 \ -device pcie-root-port,id=pcie.1,bus=pcie.0,port=1,chassis=1,slot=0 \ -device pcie-root-port,id=pcie.2,bus=pcie.0,port=2,chassis=2,slot=0 \ -device virtio-net-pci,bus=pcie.0,netdev=net0,addr=5.0 \ -device virtio-net-pci,bus=pcie.1,netdev=net1,addr=0.0 \ -device virtio-net-pci,bus=pcie.2,netdev=net2,addr=0.0 \ -append "console=ttyAMA0"

整个系统变成这样:

这里这个chassis不能省略,qemu里现在没有自动分配这个id,你不给它就直接互相冲突,保证你至少给定chassis或者slot就可以规避(保证这个组合对每个设备都是唯一的)。我感觉这个设计是多余的,也许我体会不够深。

这里的根桥id组织不优美,因为理论上根桥应该是挂在RC下面而不是第一条总线下面,但qemu现在就做成这样了,也只好忍着了。

用这种方法快速了解Linux的PCIe发现流程是个挺好的体验。

虽说使用Qemu体验MIPS架构的Windows NT 4.0已经不是什么新鲜事了,但本文还是介绍一下使用qemu-system-mips64el来安装和体验Windows NT 3.51中文版的方法。毕竟Windows NT 4.0中文版的光盘里除了i386就只有Alpha和PPC了,没有MIPS架构。目前只找到了英文NT4的MIPS版,3.51的中文版(简体和繁体)都还有MIPS的。

目前Qemu模拟的MIPS Magnum R4000机型配有ARC固件时可以运行Windows NT。早在1991年,MIPS的R4000处理器就已经是64位的了。

Windows NT 3.51 中文版镜像,并确认光盘里有mips目录,本文以Server版为例。(这个镜像是网友xkai花了大价钱买来一套未拆封的正版NT 3.51 Server才dump出来的。)

Qemu 2.9以上的版本,低于2.9的Qemu 2.x版本在模拟时会发生内存泄漏,可以使用目前最新的5.1版。安装后确保有qemu-system-mips64el

ARC ROM文件,位于setup.zip中。将NTPROM.RAW重命名为mipsel_bios.bin并放入相应目录内,如Windows下的Qemu安装目录内或*nix的/usr/share的相应位置。不重命名的话,也可以直接在qemu启动参数中加入-bios /path/to/NTPROM.RAW来启动。

在命令提示符中浏览到Qemu安装目录,运行qemu-img来创建镜像。建议使用img格式的原始磁盘镜像而不是qcow2格式,因为img镜像更容易被挂载和编辑,至少Windows下的WinImage可以直接打开,macOS也可以直接双击挂载。在当前目录下创建一个名为winnt351.img的2GB镜像的命令如下:

qemu-img create -f raw winnt351.img 2G

然后启动qemu,命令示例:

qemu-system-mips64el -sdl -M magnum -m 128 -net nic -net user -hda "winnt351.img" -cdrom "Windows NT 3.51 Server 简体中文版无繁体输入法.iso" -global ds1225y.filename=nvram -global ds1225y.size=8200

镜像路径按实际填写。“-global ds1225y.filename=nvram -global ds1225y.size=8200”指定了nvram文件的名称和位置,以及文件大小,该文件用于存储固件中的设置和参数。如果因qemu没有SDL支持而不能启动请去掉-sdl参数。

首次启动会提示没有初始化环境,需要初始化。

进入启动管理器界面后选择“Run Setup”进行设置。

然后在Initialize system中选择Set default configuration。

Select monitor resolution中建议选择1024x768或800x600,其余的默认按回车即可。

返回初始化菜单后,再选择Set default environment。在界面中选择Scsi Hard Disk,输入SCSI ID为0,partition为1(默认值)。这一步必须进行,否则可能在安装系统后无法引导,提示“Press any key to continue”。

完成后选择Exit退出,此时本来会重启模拟器,但是Qemu 5.1并不会自动重启而是卡在当前界面不动,需要手动关闭并重开。重开模拟器后可以继续在初始化界面设置时间和网卡地址。

在主界面中选择Run a program,并输入程序路径

cd:\mips\arcinst

其中cd:\代表光驱盘符,mips\arcinst指的是这个程序的位置和名称。

按提示创建5M的系统分区,如果询问是否格式化为系统分区则按y确定。

(其实只创建一个2GB的系统分区也是可行的,系统也安装在这个分区中只是此时只能是FAT文件系统。)

完成后退出到主界面,再选择Run a program,这回运行setupldr来启动安装程序。

cd:\mips\setupldr

启动安装程序后和在x86机型上安装差不多了。记得创建一个新的分区来安装系统,可以是FAT也可以是NTFS,但更推荐FAT文件系统。Windows 2000之前系统的NTFS版本较低,如果让现在的系统(2000/XP以上)挂载了,那NT3.51/NT4无法访问了。

完成文本界面的安装后会提示重启计算机,但此时也同样需要手动关闭并重开模拟器。之后会看到启动管理器中出现了Windows NT 3.51的启动项,运行即可。

正常安装即可,但注意不要安装网络和打印机。安装网络会卡在配置网络的阶段。

安装完毕后,重新开启模拟器,就能体验Windows NT 3.51了。

如果丢失了nvram,需要重新配置模拟器的固件环境,此时启动项也会丢失。初始化了启动环境(如SCSI和Partition)也需要重新创建启动项。此时在启动管理器的主界面中选择Run setup中的Manage startup。再选择Add a boot selection来创建引导项目。

选择默认的system partition,然后输入osloader directory,把默认的 \os\nt\osloader.exe 改为 \os\winnt351\osloader.exe ,即填写系统实际的osloader路径。嫌麻烦的话可以手动挂载这个磁盘镜像,例如用WinImage,然后把启动分区的\os\winnt351目录改名为\os\nt,这样每次按回车就行了。

询问系统与osloader是否在同一个分区时选no,再选择分区位置中的Scsi hard disk,默认的ID 0,分区为2,因为系统安装在了第二个分区中。(如果安装在了同一个分区中,这一步直接选yes就可以了。)询问operating system root directory时,默认路径是\winnt,但系统安装在了\winnt35中,因此需要填写\winnt35。

再然后是填写这个启动项的名称,按默认的Windows NT也行。最后一步是选择是否启用调试器,默认no即可。完成后返回主菜单即可。

gunkies.org/wiki/Instal

最近,我们在内部虚拟化平台测试的时候,意外发现了一台安装了 Windows Server 的虚拟机在热迁移之后发生了 Blue Screen of Death(BSoD,俗称蓝屏),错误码是 “CRITICAL STRUCTURE CORRUPTION”。起初没有将这个问题和热迁移挂钩,但是由于在相同环境下这个问题总会在热迁移之后发生,于是打算调查了下该问题。通过测试发现了一些奇怪的现象:配置一些低代数的 CPU model (例如 Core 2 Duo) 的虚拟机不会出现这种问题,并且在 Guest 为 Linux 时也不会发生类似于 Crash 的现象。后来经过调查,发现问题来源出在了该环境正在使用的 RHEL kernel-3.10.0-327 (历史原因比较老),此问题也已经在后续的 kernel 版本中被修复,但鉴于该问题涉及到了不少组件和知识点,便整理了一下分享出来。

注:我们的虚拟化平台底层使用的是 KVM + QEMU 组合。

BSoD 对于大多数 Windows 用户来说不是一个稀奇的现象,Windows 内部实现了一种被称为 Kernel Patch Protection (KPP) 的机制,这是为了防止一些对内核 Hook 操作 -- 例如将 System Service Descriptor Table (SSDT) 这张包含各种系统调用 handler 的表篡改,导致内核误执行恶意代码。KPP 会检测出一系列重要元素的非法改动,如果检测出来,那么就会发生错误代码为 CRITICAL STRUCTURE CORRUPTION 的 BSoD 崩溃。

Windows 在发生 BSoD 之后,都会在系统目录下生成几个 .dmp 为后缀的文件,我们称为 Dump 文件,这种文件可以有效地帮助我们找到发生 BSoD 时操作系统的一些重要状态信息,主要是两个文件:minidump 和 memory.dmp。memory.dmp 记录了崩溃时最完整的内存状态,因此该文件大小会随着使用内存的增加而增加。而 minidump 则是 memory.dmp 的缩小版,也提供了精简但有用的信息。其中,内存中的数据是被直接写入到了 memory.dmp 文件中。于是我们找到了我们问题现场中的 memory.dump 文件,从该文件提供的信息中,找到了有效的线索。

memory.dmp 文件无法直接打开,需要使用特定的 dump 文件解析工具 windbg,安装方式可参考该链接:docs.microsoft.com/en-u ,打开之后,执行 “!analyze -v” 即可将 dump 信息翻译成可阅读的错误信息。以下是我们的 Windows 虚拟机 BSoD 产生的关键 Dump 信息:

CRITICAL_STRUCTURE_CORRUPTION (109) This bugcheck is generated when the kernel detects that critical kernel code or data have been corrupted. There are generally three causes for a corruption: 1) A driver has inadvertently or deliberately modified critical kernel code or data. See http://www.microsoft.com/whdc/driver/kernel/64bitPatching.mspx 2) A developer attempted to set a normal kernel breakpoint using a kernel debugger that was not attached when the system was booted. Normal breakpoints, "bp", can only be set if the debugger is attached at boot time. Hardware breakpoints, "ba", can be set at any time. 3) A hardware corruption occurred, e.g. failing RAM holding kernel code or data. Arguments: Arg1: a39fcc5fef3b7531, Reserved Arg2: b3b6d8e641bc8534, Reserved Arg3: 00000001c0000103, Failure type dependent information Arg4: 0000000000000007, Type of corrupted region, can be ... 7 : Critical MSR modification ... 

从 Arguments 中的 Arg4 来看,是由于检测到了重要的 MSR 被非法篡改。

在深挖这个错误之前,先介绍一下 MSR。MSR (Model Specific Register) 是一种 x86 架构下的寄存器类型,用途比较广泛,既可以作为某个特性的开关,也可以存放某个指令结果,总的来说,它的引入主要是为了给不同的 CPU 特性提供帮助。CPU 特性代表着 CPU 支持的特殊功能,这是 CPU 的重要属性之一,不同代数的 CPU 特性会不同,在新代数的 CPU 上会支持芯片厂商提供的新特性,例如,AVX (Advanced Vector Extensions) 这个支持单指令多数据流的指令是在 Intel Sandy Bridge 处理器之后才支持的。x86 CPU 上有一个非常重要的指令 CPUID,它不仅可以查到该 CPU 的厂家 ID,还可以查到该 CPU 所支持的所有特性集合。在执行了 CPUID 指令后,EDX 和 ECX 的每个 bit 分别都存放着不同特性的 feature flag,表示这该特性的支持与否。

因此,MSR 这类寄存器也是根据不同的处理器代数而变化的,而随着有一部分 MSR 在很多代处理器中都被广泛使用到,这些 MSR 已经被固定在了 x86 处理器中,意味着在未来的处理器版本中也会一直存在,它们被成为 Architectural MSR。

举个例子介绍一下,相信 TSC (Time Stamp Counter) 这个 x86 特性对于很多人不陌生,该特性可以帮助我们通过 rdtsc 指令获取到当前 CPU 的运行时间。在 TSC 中,IA32_TIME_STAMP_COUNTER MSR (也称作 MSR_IA32_TSC,IA32 表示这是一个 Architectural MSR) 就是被使用来作为一个计数寄存器,每过一个 Clock Time,该寄存器的值就会递增加一。而 rdtsc 指令就是从 IA32_TIME_STAMP_COUNTER 的 64 位时间戳值读进 EAX 和 EDX 寄存器(分别存放高 32 位和低 32 位)。在 TSC 特性中,更新和获取时间戳都涉及到了 MSR 的读取和更新,这就需要说到 rdmsr 和 wrmsr 这两个来分别对某个 MSR 进行读和写的指令。x86 处理器定义了 4 个等级的特权,分别为 Privilege 0 - 3,每个等级有自己可以访问的资源范围,例如内存区域和指令集等等,从 0 到 3 特权依次降低,通常内核都是直接运行在 Privilege 0,用户空间是 Privilege 3,一般对 MSR 的读写只能在 Privilege 0 进行。对于没有实现的 MSR 进行读写,处理器会报出 general protection error。

例子:利用 rdmsr & wrmsr 来对上文提到的 MSR_IA32_TSC 进行写和读:

uint32_t hi = 0; uint32_t lo = 0xb; # Write to TSC MSR asm volatile("mov %0,%%eax"::"r"(lo)); asm volatile("mov %0,%%edx"::"r"(hi)); # Set TSC MSR Address to ECX register asm volatile("mov $0x10,%ecx"); asm volatile("wrmsr"); # Read from TSC MSR asm volatile("mov $0x10,%ecx"); asm volatile("rdmsr":"=a"(lo),"=d"(hi));

在上面的 dump 文件信息中,错误信息 Arguments 中的 Arg3 (Failure type dependent information) 提供的可能是更具体的 MSR 地址,在 KVM 代码中定义的 MSR 发现,这个值的低 32 位和一个叫做 Auxiliary TSC MSR 地址是一致的,这个发现成为了怀疑这个 MSR 的关键证据。通过这个 MSR 我们也找到了 KVM 的 commit 中找到了一个关键的 Patch (patchwork.kernel.org/pr),看上去这个问题确实是由 KVM 没有将 Auxiliary TSC MSR 暴露给用户空间而间接导致的,但是这个问题为什么被热迁移影响到,而且又为什么仅仅会发生在 Windows 和一些特定 CPU model 上,并为何不会影响到 Linux Guest,还需要更多背景知识将这些行为给串联起来。

Auxiliary TSC MSR 是由 rdtscp 这个 CPU 特性引入的,和上文介绍的 rdtsc 类似,rdtscp 也是 x86 上用于获取 64 位时间戳的功能,但相比较之下,rdtscp 会等到所有 CPU 所有指令执行完毕后才会从时间计数器上读取时间,这样可以解决指令乱序的问题(因为 intel CPU 实质上并不会真正按顺序执行每条指令);但是在哪里使用了 Auxiliary TSC MSR 呢?这就要提到 rdtscp 另外一个特点,那就是在获取到时间戳的同时还可以知道这个时间戳是在哪个 CPU core 获取的,对应的 processor id 就是从 Auxiliary TSC 寄存器中读取的。

在我们的虚拟化底层架构中,KVM 负责了虚拟机的 vCPU 和内存管理的模拟,OS 视野下的寄存器地址同样也是 KVM 提供的。

KVM 模拟出来的 vCPU 拥有自己的 feature flags,在上文提到,很多 MSR 寄存器是为了帮助某些 CPU 特性而引入的,因此对于一些 MSR,KVM 在初始化的时候通常也会通过 CPUID 查询该 vCPU 是否支持对应的特性来决定是否为 Guest 来暴露这些 MSR,Auxiliary TSC MSR 和 rdtscp 就是处于这种关系。

KVM 定义了一类特殊的 MSR,那就是 shared MSR。CPU 虚拟化机制中,Guest kernel 和主机 kernel 处于轮流使用 CPU 这样一个关系,那么这必然涉及到每次交接的时候寄存器的状态都需要切换成接棒者上次使用完时的状态,我们称作 vm-exit (送棒) 和 vm-entry (接棒),这就涉及到一系列寄存器的切换,而有些和系统调用有关的 MSR 寄存器的切换是开销比较大的,实际上 Linux kernel 是不会使用到一些 MSR 寄存器的,那么就说明,如果使用这些 MSR 的只有 Guest,这些 MSR 实际上没有在每次 vm-exit/entry 都切换的必要了,于是这类寄存器就被定义成了 shared MSR,起源可追溯到该 patch(lore.kernel.org/patchwo) 。这是一个比较重要的概念,因为 Auxiliary TSC MSR 就是这其中一员。

KVM 定义了一套 API 来让用户空间对 vCPU 服务进行调用,通过 ioctl,用户可通过 KVM fd (文件描述符) 调用到相应的 API。KVM 对 vcpu 的 MSR 访问,定义了以下 API:

KVM_GET_MSR_INDEX_LIST:获取到该 vcpu 所支持的 MSR 列表;

KVM_GET_MSRS:读取返回所有 KVM_GET_MSR_INDEX_LIST 支持的 MSR;

KVM_SET_MSRS:写入所有 KVM_GET_MSR_INDEX_LIST 支持的 MSR,该 API 并不能保证原子性,在写入某个 MSR 失败后则会停止并返回已写入成功的 MSR 数量。

上述的 API 中的 KVM_GET_MSR_INDEX_LIST 实际上并不是会原封不动地返回所有的 MSR,KVM 在内部定义了两个 MSR 列表,分别叫 msrs_to_save 和 emulated_msrs,msrs_to_save 该 list 中定义了一些列 MSR,在 KVM 启动的时候,如果主机 CPU 不支持这个列表中的某一个 MSR 则会将其从列表中移除;emulated_msrs 则是专门用于 KVM 自身功能的 MSR,这两个列表包含了所有可被上三个 API 暴露给用户空间的 MSR。因此如果你想查询或者改动的 MSR 不在这个列表里,尽管你创建的 vCPU 支持该 MSR,你也无法通过 KVM 来访问到这个 MSR。

在我们使用的 KVM 版本中,因选择的 vCPU 模型支持 rdtscp 特性,Auxiliary TSC MSR 被暴露给了 Guest OS,但是该 Auxiliary TSC MSR 并未定义在上一段提到的 msrs_to_save 列表中,因此这个 MSR 是不会暴露给用户空间的。在我们的场景中,用户空间就是指的 QEMU 进程,这样导致了一个局面,当我的 vCPU 支持 rdtscp 特性时,Guest 是可以知道 Auxiliary TSC MSR 的存在,但 QEMU 无法知道,因为 QEMU 获取到所有 vCPU MSR 的方式就是通过 KVM_GET_MSR_INDEX_LIST API。

上一个部分结尾处指出了 QEMU 和 Guest 的视角对于 Auxiliary TSC MSR 存在性不一致的问题,至于为什么这个不一致在碰上了热迁移的时候就会导致 Auxiliary TSC 寄存器被破坏,涉及到了 QEMU 的热迁移行为。

在 KVM 和 QEMU 的关系中,KVM 只是负责 vCPU 模拟和内存管理的工作,而 QEMU 负责了申请虚拟机所需内存、处理 I/O 请求以及一些增强功能等等。跨主机热迁移就是一个由 QEMU 来实现的功能,该功能可允许虚拟机在几乎没有离线的情况下迁移至不同的主机,热迁移实质要做的就是将当虚拟机的存储、内存以及处理器的状态都原封不动地复制到目标主机上,下图梳理了一下 QEMU 热迁移的整体流程:

该流程建立在源端和目标端 QEMU 是共享存储的情况下,其中源端 QEMU 和目标端 QEMU 都是分别在源主机和目标主机上以进程的形式存在。内存的迭代传输这里就不做详细介绍了,我们更关心的是 vCPU 状态的同步,也就是图中最后一步 “Get vCPU state” & “Set vCPU state” -- 将源 vCPU state 转移到目标端的 vCPU 中。

QEMU 中定义了一个 CPUState 的结构体,存放了该虚拟机每个 vCPU 的动态数据来映射一个 vCPU 的状态,例如地址空间、核心数等等,而 CPUState.env_ptr 则涵盖了不同架构下的 CPU 属性的扩展,例如在 x86 下有自己特有的一系列 MSR 寄存器。

在热迁移末尾的 Get vCPU State 中, QEMU 会调用 KVM KVM_GET_MSRS API 来尝试获取到所有 MSR 寄存器的值,在目标端通过 KVM_SET_MSRS 来将这些值设置到目标端的 vCPU 中,对于不支持的 MSR 自然就不会做这个操作,而 QEMU 在初始化 vCPU 的时候会通过KVM_GET_MSR_INDEX_LIST API 来获取到所支持的 MSR 列表。前文中提到在我们的 KVM 版本中,Auxiliary TSC MSR 并未被暴露给 QEMU,因此 QEMU 误认为这个寄存器是不被支持的,从而热迁移的时候也不会将这个寄存器的值搬运到目标端。

这种情况下,如果在热迁移之前 Auxiliary TSC 寄存器被写入,热迁移后这个寄存器就会被改变。Windows 内核可能使用了 rdtscp 和 Auxiliary TSC,导致了热迁移之后 KPP 检测到了该寄存器被 “非法破坏”,从而触发了 BSoD 保护。

总的来说,除了Guest 运行着 Windows 这个条件之外,KVM 未暴露 Auxiliary TSC MSR 给用户空间、虚拟机 CPU model 包含了 rdtscp 特性共同促使了这个问题的发生。这也可以解释为什么某些低代数 CPU model 的虚拟机不会有这个问题,因为 rdtscp 指令是在 Intel Nehalem 处理器之后才被引入的。而因为在上文提到 Auxiliary TSC 属于 shared msrs,Linux 内核没有使用到该寄存器,所以这个 MSR 的丢失也不会对 Linux Guest Kernel 产生影响。后续的解决方式就是在 KVM 层面将 Auxiliary TSC MSR 暴露给用户空间。

KVM 针对这个问题的相关修复 Patch:

  1. patchwork.kernel.org/pr
  2. patchwork.kernel.org/pr
  3. patchwork.kernel.org/pr

虚拟机热迁移是 QEMU 的一项重要功能,也是现在大多数主流虚拟化平台的必备功能,在大量的场景中被使用。然而在为我们提供运维方便的同时,这也是一块非常容易踩坑的区域,很多时候会受 Hypervisor 本身、Guest OS, 甚至宿主机 CPU 的影响产生一些不可预计的后果。因此我们在优化热迁移工作效率的同时,也会继续深挖这个过程中潜在的问题。

How to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures:intel.com/content/dam/w

Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3B: System Programming Guide: intel.com/content/dam/w

Windows Internals:amazon.com/Windows-Inte

KVM API: kernel.org/doc/html/lat

对虚拟化、容器技术感兴趣的小伙伴欢迎加入我们。

Tianren,SmartX 研发工程师。SmartX 拥有国内最顶尖的分布式存储和超融合架构研发团队,是国内超融合领域的技术领导者。

本代码参考自社区的经典Demo:

kvmtest.c [LWN.net]

代码去掉了错误验证,只保留了最最核心的代码, 并进行了详细的注释.

其中相关CPU虚拟化知识可以学习以下两本书:

陈海波老师的书,可以作为虚拟化知识的入门,是我读过的最好的虚拟化入门教材, 当然也是学习操作系统好教材, 强烈推荐.

戚正伟老师对虚拟化背景进行了全面的总结和分析. 然后分章节解析CPU, 内存, IO虚拟化以及Qemu/KVM的源码实现, 难度较大, 非常详细!!!! 我正在细读.

两书中都提到了本文的代码!

此外我和两位老师都见过面, 哈哈哈哈哈.

标题中10行, 其实是核心代码. 哈哈哈哈哈哈哈

#include <fcntl.h> #include <linux/kvm.h> #include <stdint.h> #include <stdio.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h>  int main(void) { /  * kvm 用于承接打开的设备文件dev/kvm的文件描述符  * vmfd 用于承接创建的VM的文件描述符  * vcpufd 用于承接创建的vCPU的文件描述符  *  * mem 指向一块4KB内存空间,用于存放虚拟机执行的代码  *  * sregs 用于初始化段寄存器状态  *  * mmap_size用于记录vCPU的共享内存空间大小  * run 指向vCPU的共享内存映射  *  * */ int kvm, vmfd, vcpufd; uint8_t *mem; struct kvm_sregs sregs; size_t mmap_size; struct kvm_run *run; /// 以下汇编代码,是依次将"HELLO KVM\n"通过in/out指令写入0x3f8端口  /// 每次执行in/out都会触发VM-Exit,KVM将处理VM-Exit,如果是IO导致的,则KVM将继续向上提交  /// 即,交由QEMU等用户程序将继续处理  const uint8_t code[] = { 0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */ 0xb0, 'H', /* mov $'H', %al */ 0xee, /* out %al, (%dx) */ 0xb0, 'E', /* mov $'E', %al */ 0xee, /* out %al, (%dx) */ 0xb0, 'L', /* mov $'L', %al */ 0xee, /* out %al, (%dx) */ 0xb0, 'L', /* mov $'L', %al */ 0xee, /* out %al, (%dx) */ 0xb0, 'O', /* mov $'O', %al */ 0xee, /* out %al, (%dx) */ 0xb0, ' ', /* mov $' ', %al */ 0xee, /* out %al, (%dx) */ 0xb0, 'K', /* mov $'K', %al */ 0xee, /* out %al, (%dx) */ 0xb0, 'V', /* mov $'V', %al */ 0xee, /* out %al, (%dx) */ 0xb0, 'M', /* mov $'M', %al */ 0xee, /* out %al, (%dx) */ 0xb0, '\n', /* mov $'\n', %al */ 0xee, /* out %al, (%dx) */ 0xf4, /* hlt */ }; /// 打开KVM模块设备文件  kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC); /// 创建虚拟机, 并获取其文件描述符  vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long) 0); /// 创建4KB大小的内存空间(一页匿名页),用于存放VM执行的代码  mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); /// 将上述代码拷贝到匿名页中  memcpy(mem, code, sizeof(code)); /// 将代码映射到VM物理内存(GPA)的第二个页框处,因为第一个页框被实模式保留,用于存放IDT(中断向量表)  struct kvm_userspace_memory_region region = { .slot = 0, // 内存卡槽  .guest_phys_addr = 0x1000, // GPA的起始映射地址,即第二个页框  .memory_size = 0x1000, // 映射内存的大小,4KB  .userspace_addr = (uint64_t) mem, // 映射内存的起始地址  }; ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region); /// 创建vCPU并获取其文件描述符  vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long) 0); /// 映射vCPU的共享内存到run中,使我们可以访问其kvm_run结构  /// 当VM_Exit时,kvm_run结构中将保留exit_reason,我们将基于此知悉退出原因,从而进行相应的处理  mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL); run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0); /// 设置代码段寄存器和代码段基址, 这是内存的分段管理  /// 此时我们并没有开启分页管理,因此虚拟地址直接等于物理地址  /// 而虚拟地址是根据线性地址+代码段基址计算的  /// 代码段基址是根据GDT(全局描述符表)和CS(代码段,选择子)确定的  ioctl(vcpufd, KVM_GET_SREGS, &sregs); sregs.cs.base = 0; sregs.cs.selector = 0; ioctl(vcpufd, KVM_SET_SREGS, &sregs); /// 关于kvm_sregs和kvm_regs(并不严格):  /// kvm_regs 用于通用寄存器的配置  /// kvm_sregs 用于段计算器的配置  /// 下用于设置rip寄存器, cs.base + rip 既是物理地址(没开启分页,虚拟地址等于物理地址,rip则表示线性地址),  struct kvm_regs regs = { .rip = 0x1000, }; ioctl(vcpufd, KVM_SET_REGS, &regs); /// 开启一个循环,执行VM的代码,并处理IO事件  while (1) { /// 开启vCPU  ioctl(vcpufd, KVM_RUN, NULL); /// 执行到这里说明发生了VM_Exit  switch (run->exit_reason) { case KVM_EXIT_HLT: /// VM关机  puts("KVM_EXIT_HLT"); return 0; case KVM_EXIT_IO: /// IO事件  /// 在这里,输出写入0x3f8的字符  if (run->io.direction == KVM_EXIT_IO_OUT && run->io.size == 1 && run->io.port == 0x3f8 && run->io.count == 1) putchar(*(((char *) run) + run->io.data_offset)); break; } } }

本文章应用于 iOS UTM 安装使用教程及问题解决

UTM 虚拟机的共享目录是通过 SPICE 的 SPICE WebDavd 实现的,通过运行 WebDav Server 实现文件共享

Spice User Manual

SPICE WebDavd 对于虚拟机系统来说需要安装配置后,才能正常使用。


对于 Windows 系统,首先需要安装 SPICE GUEST 套件,再进行 SPICE WebDavd 的安装。

UTM QEMU 分支:GitHub

下载 UTM 项目提供的 spice-guset-tools。

Releases: github.com/utmapp/qemu/

得到 spice-guest-tools 的 iso 文件。

将 iso 文件导入 UTM 虚拟机 “驱动器”。

导入部分不再加以叙述,若对导入部分存在疑惑,请重新阅读 iOS UTM虚拟机安装使用教程

启动系统后,查看 “DVD/CD 驱动器” ,iso 内文件结构如下。

运行 spice-guest-tools.exe,完成安装 SPICE Tools.

安装 SPICE Tools 需要使用稍长的时间,等待即可。

出现以下返回即为安装成功。

至此可以使用“剪贴板共享”。


从 SPICE 项目网站下载适用于 Windows 的 SPICE WebDavd 客户端。

链接:Index of /download/windows/spice-webdavd
  • 对于 64 位 Windows 下载 spice-webdavd-x64-latest.msi
  • 对于 32 位 Windows 下载 spice-webdavd-x86-latest.msi

对于 Windows ,请注意下载文件的扩展名是否正确。

运行 msi 文件等待安装完成,关闭虚拟机完成接下来的配置。


虚拟机内 Windows 系统已经准备就绪,接下来设置 UTM 的共享目录即可使用。

开启 “目录共享” 选项,“只读” 选项根据需求自行选择。

回到虚拟机主页,选择 "文件共享",在选择页面中,自行选择除 iCloud 和云同步软件的本机目录,作为共享目录。

再次启动虚拟机即可使用 UTM 共享目录。


完成虚拟机内 SPICE WebDavd 的安装和 UTM 共享目录的正确设置后,虚拟机内将多一个网络位置。

虚拟机内 WebDav 网址 : localhost:9843/

一般来说,虚拟机内 Windows 操作系统将自动将 SPICE 的 WebDav 挂载到资源管理器。

也可以通过 "网络浏览器" 访问,或是手动挂载网络位置。

如果在 UTM 配置 "共享目录" 环节开启 "只读" 选项,虚拟机内操作系统将不能使用 WebDav 将文件传输到共享目录。


原因:Windows XP SP2 中引入的安全更改会影响 WebDav 的文件传输。 此安全更改确保未经授权的服务器无法强制客户端计算机遭受拒绝服务攻击。 如果尝试下载大于 50MB 的文件,客户端计算机会将其解释为拒绝服务攻击。 因此,下载过程会停止。

通过安装补丁或修改注册表解决该问题。

下载链接:MicrosoftEasyFix 55026.msi

安装完成后,重启虚拟机内操作系统,即可解决该问题。

每个 Linux 发行版的包管理器可能不一致,且 spice-webdavd 包可能不在发行版仓库或包名不一致。

需要安装 spice-vdagent spice-webdavd

Debian / Ubuntu

# apt update # apt install spice-vdagent spice-webdavd

CentOS Stream / Rocky Linux / Fedora

# dnf update # dnf install spice-vdagent spice-webdavd

Arch Linux

# pacman -Syyu spice-vdagent phodav

安装完成后,关闭虚拟机完成接下来的配置。


开启 “目录共享” 选项,“只读” 选项根据需求自行选择。

回到虚拟机主页,选择 "文件共享",在选择页面中,自行选择除 iCloud 和云同步软件的本机目录,作为共享目录。

再次启动虚拟机即可使用 UTM 共享目录。


安装 spice-webdavd 或 phodav 包后,会自动添加 “spice-webdavd.service” 。

# 使用 systemd 启动 spice-webdavd 服务 # systemctl enable --now spice-webdavd

也可以通过执行命令启动 spice-webdavd

# 启动 spice-webdavd,端口可以自行选择 # sudo spice-webdavd -p 9843

成功运行 spice-webdavd 后,通过访问 http://localhost:9843 使用 WebDav 共享目录。

http://localhost/$port 根据 spice-webdavd 服务端口自行修改

接下来可以通过 HTTP 访问共享目录或通过挂载 WebDav 来访问共享目录。


需要安装 davfs2

Debian / Ubuntu

# apt update # apt install davfs2

CentOS Stream / Rocky Linux / Fedora

# dnf update # dnf install davfs2

Arch Linux

# pacman -Syyu davfs2

安装完成后,使用 mount 指令挂载 WebDav。

普通用户也可以挂载 davfs

# 将 Port 改为 spice-webdavd 设置的端口 $ export $port=Port # 创建挂载点,根据自己需求选择 $ mkdir /home/gdzzc/webdav $ export $mount_path=/home/gdzzc/webdav # 挂载 WebDav $ mount -t davfs http://localhost:$port $mount_path # 访问挂载点即可使用共享目录 $ cd /home/gdzzc/webdav

两年前,大约在2021年七八月份,Qemu有个分支能在模拟的Quadra 800机型中运行Mac OS系统。当时的Quadra 800模拟还有很大问题,比如播放声音时会卡住,经常容易崩溃等等。如今这个分支基本上达到了能用的水平,比之前完善了许多,也快要被合并到主线了。根据Emulation上的讨论,目前这个Qemu分支还有些FPU问题,运行BSD也还有些问题,因而还没融入主线。运行后也发现framebuffer也有一些问题,例如有时选中一个图标后反选会有花屏背景,有时移动窗口也有花屏背景。

拖动窗口时花屏,以及被识别为128MHz的68040

Quadra 800是Quadra系列中较高端的机型,配备33MHz 68040处理器,板载8M内存,最大支持136M内存,板载512K显存(可以扩展到1M)。

Macintosh Quadra 800,图源:Wikipedia

Qemu能模拟这台Mac,自然也因为这台机器在68040机器中十分有代表性。

关于如何运行,Emaculation论坛编译了这个分支的Qemu(macOS和Windows)。这个版本可以运行Mac OS、A/UX、NetBSD。

当然也可以自己编译该分支的最新git版本:

git clone -b q800.upstream3 https://www.github.com/mcayland/qemu

教程文档在Emaculation上也有

Windows:emaculation.com/doku.ph

macOS:Running qemu-system-m68k in macOS

下载完二进制,里面一般附带了一个启动脚本。需要做的是生成nvram的镜像,然后修改脚本,修改硬盘、光盘镜像的路径。

如果需要启动盘和ROM,也可以直接使用Cockatrice III模拟器的资源。

System 7.1启动光盘(含磁盘工具等):sourceforge.net/project

安装后的系统硬盘镜像:sourceforge.net/project

Cockatrice III模拟器Windows二进制(含Quadra 800 ROM):sourceforge.net/project


在Mac版本的Qemu 7中,建议使用cocoa界面而不是SDL,即添加-display cocoa参数。

cocoa界面可以使用系统的菜单栏进行一些设置,如切换光盘镜像,设置模拟器速度等。

此外选用cocoa界面后,按下Command-W、Command-Q等Mac的快捷键不会触发系统的关闭窗口、退出程序命令,避免误操作关闭了Qemu。但是Command-Option-Esc快捷键依然是主机系统响应,如果要在Qemu模拟器中强制退出当前程序,只能是进入monitor界面(View菜单中进入),然后使用sendkey alt-meta_l-esc命令发送快捷键,meta_l表示左Meta键(Meta键就是Windows或Command键),meta_r表示右Meta键。


目前Qemu模拟的Quadra 800还不支持软驱,安装A/UX时还得通过SCSI来引导启动盘。

另外,模拟的Quadra 800没有136M内存限制以及不支持24位真彩色的限制。模拟器的MMU也能支持虚拟内存。目前显示模式支持:

Available modes: 640x480x1 640x480x2 640x480x4 640x480x8 640x480x24 800x600x1 800x600x2 800x600x4 800x600x8 800x600x24 1152x870x1 1152x870x2 1152x870x4 1152x870x8

其中640x480和800x600模式支持24位色,缺失1024x768一档分辨率,最高的1152x870分辨率最高只支持8位色(256色)。不过800x600x24位色显然已经超出1M显存所支持的范围了。

尝试运行MacBench 4.0,能获取系统信息但是一测试就出错。


尝试双屏版fork,具体见帖子:

emaculation.com/forum/v

需要用此fork版本,和这个rom, 不过还是没有成功启动系统。

  • 《云计算发展编年史 1725-2023(第二版)》
  • 《虚拟化技术 — 硬件辅助的虚拟化技术》
  • KVM
  • QEMU
  • QEMU-KVM
    • 集成软件架构
    • CPU 虚拟化实现
    • Memory 虚拟化实现
    • I/O 虚拟化实现
  • QEMU-KVM 虚拟机的本质
    • VM 的 vCPU 两级调度
    • VM 的 vCPU 多核拓扑
    • VM 的 vCPU 模型
    • VM 的磁盘设备
    • VM 的网络设备

KVM(Kernel-based Virtual Machine,基于内核的虚拟机)是一种 TYPE1 Hypervisor(裸金属类型)虚拟化技术,VMM 和 HostOS 一体化,直接运行 Host Hardware 之上,实现硬件和虚拟机完全管控。具有以下 3 个典型特点是:

  1. 依赖 CPU 硬件辅助的虚拟化技术(e.g. Intel VT-x / AMD-V);
  2. VMM 和 HostOS 一体化;
  3. 运行效率高。

所以,要理解 KVM 的前提是需要对硬件辅助虚拟化技术有一个清晰的了解,强烈推荐先查看前文列表。

所以,KVM 的本质就是一个 Linux Kernel Module,命名为 kvm.ko(kvm-intel.ko / kvm-AMD.ko),在利用了 Kernel 所提供的部分操作系统能力(e.g. 内存管理、进程管理、硬件设备管理)的基础之上,再加入了 CPU 和 Memory 虚拟化的能力,使得 Linux Kernel 得以具备成为一个完备 VMM 的 3 个条件:

  1. 资源控制(Resource Control):VMM 必须能够管理所有的系统资源。
  2. 等价性(Equivalence):在 VMM 管理下运行的 HostOS 和 GuestOS,除了 CPU 时序和硬件资源可用性之外的行为应该完全保持一致。
  3. 效率性(Efficiency):绝大多数的 GuestOS 指令应该由 Host Hardware 直接执行而无需 VMM 的参与(二进制翻译)。

KVM 最早于 2007 年 2 月 5 日被集成到 Linux Kernel 2.6.20 中,现在最新的 KVM 已经具备以下功能清单:

  • 支持 SMP(Symmetric Multi-Processing,对称多处理)多核处理器架构。
  • 支持 NUMA (Non-Uniform Memory Access,非一致存储访问)多核处理器架构。
  • 支持 CPU 亲和性。
  • 支持 CPU 和 Memory 超分(Overcommit)。
  • 支持 VirtIO 设备和驱动。
  • 支持 PCI 设备直通(Pass-through)和 SR-IOV(单根 I/O 虚拟化)。
  • 支持热插拔 CPU、Disk、NIC 等设备。
  • 支持 Live Migration(实时迁移)。
  • 支持 KSM (Kernel Shared Memory,内核内存共享技术)。

当启动 Linux 操作系统并加载 kvm.ko 时,会完成以下工作:

  1. 首先,初始化 kvm.ko 的数据结构;
  2. 然后,kvm.ko 检测当前的 CPU 体系结构,读写 CR4 寄存器的虚拟化模式开关,再执行 Intel VT-x 的 VMXON 指令,将 VMM 设置为运行在 Root Mode 之上;
  3. 最后,kvm.ko 创建设备接口文件 /dev/kvm 暴露给 User Application(e.g. QEMU)。

需要注意的是,KVM 运行在 Kernel space 且本身不具备任何设备模拟的能力。所以,KVM 还必须借助于一个运行在 User space 用户态的 Application(e.g. QEMU)来模拟 “组装“ 出一台完整 VM 所需要的各种虚拟设备(e.g. 网卡、显卡、存储控制器和硬盘)。

QEMU(Quick Emulator)最早于 2001 由天才程序员 Fabrice Bellard 发布,是一款开源的、采用了动态二进制翻译技术的 TYPE2 Hypervisor(寄居式类型)VMM 软件。

在经过多年发展后的 2019 年,QEMU 4.0.0 发布并对外宣称支持模拟 x86、x86_64、ARM、MIPS、SPARC、PowerPC 等多种 CPU 架构,同时也几乎可以模拟所有的物理设备,这简直就是一个奇迹般的软件。

但由于原生的 QEMU 主要采用了软件 “捕获-模拟“ 的实现方式,所以也存在性能低下的问题。

上述提到,KVM 和 QEMU 各有千秋、互为补充,并且都具有开源底色,所以后来在 KVM 开发者社区对 QEMU 进行稍加改造之后,就推出了 QEMU-KVM 分支发行版(一个特殊的 QEMU 版本)。

KVM 社区提供的软件分发包中包含了以下 4 个文件内容:

  1. KVM 内核模块
  2. QEMU
  3. QEMU-KVM
  4. virtio 驱动程序

其中,QEMU-KVM 就是专门针对 KVM 的 QEMU 分支,现已经被广泛的集成(二次开发)到各种著名的商业产品中,包括:AWS、阿里云等等。

虽然,在后来的 QEMU 1.3 版本中,开发者社区又将 QEMU 和 QEMU-KVM 这两个分支合并了,但为了清晰的区分两者,所以还是习惯性的在 KVM 语境中将其称之为 QEMU-KVM。

如下图所示,kvm.ko 运行在 Kernel space,并通过 /dev/kvm 文件向 User space 暴露了交互接口,同时也提供了 libkvm 函数库给 QEMU 进行 include。

QEMU 通过 open() / close() 来打开/ 关闭 /dev/kvm 设备接口文件,并通过设备 I/O 接口 ioctl() 来调用 kvm.ko 提供的接口函数,以此来应用 KVM 基于硬件辅助虚拟化技术实现的 CPU 虚拟化、Memory 虚拟化、I/O 虚拟化等功能。此外的,VM 配置管理、VM 生命周期管理、VM 虚拟外设管理、以及一些特定的虚拟机技术(e.g. 动态迁移)等,则都由 QEMU 自己来实现。

open("/dev/kvm", O_RDWR|O_LARGEFILE) = 3 ioctl(3, KVM_GET_API_VERSION, 0) = 12 ioctl(3, KVM_CHECK_EXTENSION, 0x19) = 0 ioctl(3, KVM_CREATE_VM, 0) = 4 ioctl(3, KVM_CHECK_EXTENSION, 0x4) = 1 ioctl(3, KVM_CHECK_EXTENSION, 0x4) = 1 ioctl(4, KVM_SET_TSS_ADDR, 0xfffbd000) = 0 ioctl(3, KVM_CHECK_EXTENSION, 0x25) = 0 ioctl(3, KVM_CHECK_EXTENSION, 0xb) = 1 ioctl(4, KVM_CREATE_PIT, 0xb) = 0 ioctl(3, KVM_CHECK_EXTENSION, 0xf) = 2 ioctl(3, KVM_CHECK_EXTENSION, 0x3) = 1 ioctl(3, KVM_CHECK_EXTENSION, 0) = 1 ioctl(4, KVM_CREATE_IRQCHIP, 0) = 0 ioctl(3, KVM_CHECK_EXTENSION, 0x1a) = 0 

下面以创建一个 VM 为例,介绍 QEMU-KVM 调用 KVM 内核模块启动 VM 的流程概要:

  1. 打开 /dev/kvm 设备接口文件,返回一个 kvmfd(文件描述符)句柄。针对这个句柄执行 ioctl 调用即可完成对 VM 执行相应的管理。
kvmfd = open("/dev/kvm", O_RDWR); 
  1. 调用 kvm.ko 的 KVM_CREATE_VM 接口创建 VM,并返回一个 vmfd 句柄。
vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0); 
  1. 为 VM 映射 HVA/HPA、PCI 设备、以及信号处理。
ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem); 
  1. 将 VM 的 QCOW2 镜像文件的数据映射到 User Process 的虚拟地址空间。相当于 Host 的 boot 过程,把操作系统内核映射到内存。
  2. 创建 vCPU,并根据 NUMA Topo 配置为 vCPU 分配 vMemory。KVM_CREATE_VCPU 时,KVM 为每一个 vCPU 生成对应的 fd,对其执行相应的 ioctl 调用,就可以对 vCPU 进行管理。
ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid); vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0); 
  1. 创建 vCPU 个数的 User Threads 并运行 GuestOS 代码。
ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0); 
  1. Main Thread 进入循环,监听并捕获 VM Exit 的原因,做相应的处理。VM Exit 是一个 Intel VT-x 指令,当 GuestOS 执行 I/O 操作时、访问硬件设备时,缺页中断时等等情况,都会执行 VM Exit,将 CPU 交还给 VMM。
open("/dev/kvm") ioctl(KVM_CREATE_VM) ioctl(KVM_CREATE_VCPU) for (;;) { ioctl(KVM_RUN) switch (exit_reason) { /* 分析退出原因,并执行相应操作 */ case KVM_EXIT_IO: /* ... */ case KVM_EXIT_HLT: /* ... */ } } 

Kernel 在加载 kvm.ko 成为 VMM 之后,就具备了 3 种不同的运行模式,分别对应了 Intel VT-x 的 2 种特权模式:

  1. User Mode(User space):运行 QEMU(User Process)代码。
  2. Kernel Mode(Kernel space,CPU Root Mode):运行 kvm.ko 代码。
  3. Guest Mode(Kernel space,CPU Non-root Mode):运行 GuestOS 代码。

Kernel Mode 作为 User Mode 和 Guest Mode 之间沟通的桥梁。在 User Mode 中,QEMU 通过 ioctl() 来操作 VM。然后 Kernel Mode 收到 ioctl() 请求,首先完成一些准备工作(e.g. 将 vCPU 上下文加载到 VMCS 等),然后 CPU 执行 VM Entry 指令,进入到 Non-Root Mode,CPU 开始执行 GuestOS 的代码。

KVM 同样通过 /dev/kvm 向 QEMU 提供了 Memory 虚拟化的功能。

QEMU 调用了 KVM_CREATE_VM 接口后得到了一个 vmfd,针对这个句柄执行 ioctl 调用就可以为 VM 创建 GPA,并自动维护 GPA 和 HVA、HPA 之间的映射关系,底层依旧是通过 Intel VT-x 提供的 EPT 技术来实现。

在操作系统层面表现为,/dev 目录树下的 Devices 对于所有 User Process 或 Kernel Thread 而言都是一致且通用的,但是对于所有打开了 /dev/kvm 设备的 VM 进程或线程,其所能使用到的都是唯一且各不相同的地址映射(实现了 GuestOS 间的隔离)。

前面提到,I/O 设备的模拟和 I/O 虚拟化功能的实现主要由 QEMU 来完成。

例如:在执行 GuestOS 代码的过程中,GuestOS 发出了一个 I/O 请求,该事件会被 VMM 捕获,然后执行 VM Exit 执行,CPU 会自动将 GuestOS 的上下文加载到 VMCS Guest State Area 中,挂起 GuestOS,并从 VMCS Host State Area 中加载 VMM 的通用事件处理函数的入口地址,开始执行 VMM 的代码。VMM 根据 I/O 事件类型,将 I/O 请求交由 QEMU 最终完成处理。

综合上述内容,再回头看 QEMU-KVM 虚拟机的本质。

QEMU-KVM 虚拟机,简称 VM,由 vCPU、vMemory、虚拟 I/O 设备以及 GuestOS 组成:

  • 一个 VM 就是一个 User Process,包含了下列几种 User Threads:
  1. vCPU 线程:用于运行 GuestOS 的代码。
  2. I/O 线程:用于运行 QEMU 模拟 I/O 设备的代码。
  3. 其它线程:比如处理 event loop,offloaded tasks 等的线程。
  • VM 的一个 vCPU 就是一个 User Process 内的一个 User Thread。
  • VM 的 vMemory 就是分配给 User Process 的虚拟地址空间中的一块内存。
  • VM 可以继承 HostOS 中的 NUMA 和大页内存特性。
  • VM 在 HostOS 的 FIle System 层面体现为一个 XML 文件和一个 QCOW2 文件。前者描述了 GuestOS 的特征信息,后者储存了 GuestOS 的数据。

在操作系统的调度层面,GuestOS 与 VMM(HostOS)共同构成了 vCPU 的两级调度系统。vCPU threads、QEMU Process threads、Linux Scheduler、pCPU 之间的二级调度模型如下图所示:

  • GuestOS 负责第 2 级调度:将运行在 GuestOS 之上的 User Application 调度到 vCPU 上。
  • VMM(HostOS)负责第 1 级调度:将 QEMU Process threads、vCPU threads 调度到 pCPU 上。

两级调度模型之间的调度策略和调度器类型互不相关。当 vCPU 的数量大于 pCPU 时,vCPU 可能会在多个 pCPU 之间 “偏移“(pCPU 分时复用或空间复用)。在 VMM 层面,也可以根据 NUMA 亲和性、CPU 绑定等策略将 vCPU 绑定到指定的 pCPU 上,用空间换时间的方式获得更高的性能。

KVM 支持 SMP 和 NUMA 等多核处理器架构,可以自定义 VM 的 vCPU 拓扑。

  • 对 SMP 类型的客户机,使用:
qemu-kvm -smp <n>[,cores=<ncores>][,threads=<nthreads>][,sockets=<nsocks>][,maxcpus=<maxcpus>] 
  • 对 NUMA 类型的客户机,使用:
qemu-kvm -numa <nodes>[,mem=<size>][,cpus=<cpu[-cpu>]][,nodeid=<node>] 

KVM 支持自定义 CPU 模型 (models),CPU 模型定义了哪些宿主机的 CPU 功能(features)会暴露给 Guest OS。为了让 VM 能够在具有不同 CPU 功能的 Hosts 之间做安全迁移,往往不会将所有的 Host CPU 功能都暴露给 VM,而是会取得 Server 集群的交集,以保证 VM 迁移的安全性。

可以通过以下指令获取 Host CPU 模型清单:

$ kvm -cpu ? x86 Opteron_G5 AMD Opteron 63xx class CPU x86 Opteron_G4 AMD Opteron 62xx class CPU x86 Opteron_G3 AMD Opteron 23xx (Gen 3 Class Opteron) x86 Opteron_G2 AMD Opteron 22xx (Gen 2 Class Opteron) x86 Opteron_G1 AMD Opteron 240 (Gen 1 Class Opteron) x86 Haswell Intel Core Processor (Haswell) x86 SandyBridge Intel Xeon E312xx (Sandy Bridge) x86 Westmere Westmere E56xx/L56xx/X56xx (Nehalem-C) x86 Nehalem Intel Core i7 9xx (Nehalem Class Core i7) x86 Penryn Intel Core 2 Duo P9xxx (Penryn Class Core 2) x86 Conroe Intel Celeron_4x0 (Conroe/Merom Class Core 2) x86 cpu64-rhel5 QEMU Virtual CPU version (cpu64-rhel5) x86 cpu64-rhel6 QEMU Virtual CPU version (cpu64-rhel6) x86 n270 Intel(R) Atom(TM) CPU N270 @ 1.60GHz x86 athlon QEMU Virtual CPU version 0.12.1 x86 pentium3 x86 pentium2 x86 pentium x86 486 x86 coreduo Genuine Intel(R) CPU T2600 @ 2.16GHz x86 qemu32 QEMU Virtual CPU version 0.12.1 x86 kvm64 Common KVM processor x86 core2duo Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz x86 phenom AMD Phenom(tm) 9550 Quad-Core Processor x86 qemu64 QEMU Virtual CPU version 0.12.1 Recognized CPUID flags: f_edx: pbe ia64 tm ht ss sse2 sse fxsr mmx acpi ds clflush pn pse36 pat cmov mca pge mtrr sep apic cx8 mce pae msr tsc pse de vme fpu f_ecx: hypervisor rdrand f16c avx osxsave xsave aes tsc-deadline popcnt movbe x2apic sse4.2|sse4_2 sse4.1|sse4_1 dca pcid pdcm xtpr cx16 fma cid ssse3 tm2 est smx vmx ds_cpl monitor dtes64 pclmulqdq|pclmuldq pni|sse3 extf_edx: 3dnow 3dnowext lm|i64 rdtscp pdpe1gb fxsr_opt|ffxsr fxsr mmx mmxext nx|xd pse36 pat cmov mca pge mtrr syscall apic cx8 mce pae msr tsc pse de vme fpu extf_ecx: perfctr_nb perfctr_core topoext tbm nodeid_msr tce fma4 lwp wdt skinit xop ibs osvw 3dnowprefetch misalignsse sse4a abm cr8legacy extapic svm cmp_legacy lahf_lm 

自定义 VM 的 CPU 模型,-cpu 选项除了可以指定 GuestOS 的 CPU 模型,还可以指定附加的 CPU 特性。并且 -cpu 会将指定的 CPU 模型的所有功能全部暴露给 GuestOS,即使某些特性在实际的 Host 上并不支持,此时 QEMU-KVM 就会通过软件模拟的方式来支持这些特性,因此,也会消耗一定的性能。

qemu-kvm -cpu <models> # -cpu host 表示 Guest OS 使用和 Host OS 相同的 CPU model。 

QEMU-KVM 定义一个磁盘设备的可用选项有很多。

qemu-kvm -drive option[,option[,option[,...]]] # 硬件映像文件路径; file=/path/to/somefile # 指定硬盘设备所连接的接口类型,即控制器类型,如:ide、scsi、sd、mtd、floppy、pflash 及 virtio 等; if=interface # 设定同一种控制器类型中不同设备的索引号,即标识号; index=index # 定义介质类型为硬盘(disk)还是光盘(cdrom); media=media # 指定映像文件的格式,具体格式可参见 qemu-img 命令; format=format 

定义 VM 的 boot 磁盘设备,默认为 -boot order=dc,once=d。每种设备使用一个字符表示,不同的 CPU 架构所支持的设备及其表示字符也不尽相同,例如:在 x86 架构上,a、b 表示软驱、c 表示第一块硬盘,d 表示第一个光驱设备,n-p 表示网络适配器;

qemu-kvm -boot [order=drives][,once=drives][,menu=on|off] 

QEMU 支持模拟多个类型的网卡设备。可以使用 qemu-kvm -net nic,model=? 来获取当前 CPU 架构支持模拟的 NIC 类型,例如:x86 架构上默认的 NIC 为 e1000。

  • 创建一个新的 NIC 设备并连接至 VLAN n 中:
    • macaddr 用于为其指定 MAC 地址;
    • name 用于指定一个在监控时显示的网上设备名称。
qemu-kvm -net nic[,vlan=n][,macaddr=mac][,model=type][,name=name][,addr=addr][,vectors=v] 
  • 通过物理机的 Tap 网络接口连接至 VLAN n 中:
    • 使用 script=file 指定的脚本(默认为 /etc/qemu-ifup)来配置当前网络接口;
    • 使用 downscript=file 指定的脚本(默认为 /etc/qemu-ifdown)来撤消接口配置;
    • 使用 script=no 和 downscript=no 可分别用来禁止执行脚本;
qemu-kvm -net tap[,vlan=n][,name=name][,fd=h][,ifname=name][,script=file][,downscript=dfile] 
  • 在 user(用户模式)下创建网络栈,不依赖 root 权限;有效选项有:
    • vlan=n:连接至 VLAN n,默认 n=0;
    • name=name:指定接口的显示名称,常用于监控模式中;
    • net=addr[/mask]:设定 GuestOS 可见的 IP 网络,掩码可选,默认为 10.0.2.0/8;
    • host=addr:指定 GuestOS 中看到的物理机的 IP 地址,默认为指定网络中的第二个,即:x.x.x.2;
    • dhcpstart=addr:指定 DHCP 服务地址池中 16 个地址的起始 IP,默认为第 16 个至第 31 个,即:x.x.x.16-x.x.x.31;
    • dns=addr:指定 GuestOS 可见的 DNS 服务器地址;默认为 GuestOS 网络中的第三个地址,即:x.x.x.3;
    • tftp=dir:激活内置的 TFTP 服务器,并使用指定的 dir 作为 TFTP 服务器的默认根目录;
    • bootfile=file:BOOTP 文件名称,用于实现网络引导 GuestOS;如:qemu -hda linux.img -boot n -net user,tftp=/tftpserver/pub,bootfile=/pxelinux.0
qemu-kvm -net user[,option][,option][,...] 

- END -

关于 “云物互联” 微信公众号:

欢迎关注 “云物互联” 微信公众号,我们专注于云计算、云原生、SDN/NFV、边缘计算及 5G 网络技术的发展及应用。热爱开源,拥抱开源!

技术即沟通

化云为雨,落地成林

其实这是一个很久就看过的文章,但是由于是在微信上看到的,下来一直没有机会试一下,久而久之就遗忘了,最近在知乎上又看到这篇(当然是剽窃的,知乎最近这种抄别人劳作的风气越來越重了,本来想举报一下,结果又要侵权本人提供证据,被侵权的文章也不是我写的,我也就没法提供证据举报了)

原文链接(感谢作者提供了一个非常好的例子):

300来行代码带你实现一个能跑的最小Linux文件系统

由于原作者的示例是基于一个比较老的linux 内核版本,在新版本上有较多编译报错,大部分是结构体的变化,比较大一点的是tinyfs_readdir 接口的变化

(这一版本只是处理了编译报错相关的问题,主体部分未做修改)

tinyfs.h

#define MAXLEN 8 #define MAX_FILES 32 #define MAX_BLOCKSIZE 512 //define the dir struct dir_entry { char filename[MAXLEN]; uint8_t idx; }; //define the file block struct file_blk { uint8_t busy; mode_t mode; uint8_t idx; union { uint8_t file_size; uint8_t dir_children; }; char data[0]; };

tinyfs.c

#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include "tinyfs.h" struct file_blk block[MAX_FILES+1]; int curr_count = 0; static int get_block(void) { int i; for( i = 2; i < MAX_FILES; i++) { if (!block[i].busy) { block[i].busy = 1; return i; } } return -1; } static struct inode_operations tinyfs_inode_ops; static int tinyfs_readdir(struct file *filp, struct dir_context *ctx) { loff_t pos; struct file_blk *blk; struct dir_entry *entry; struct dentry *dentry = filp->f_path.dentry; int i; if (!dir_emit_dots(filp, ctx)) return 0; pos = filp->f_pos; if (pos) return 0; blk = (struct file_blk*)dentry->d_inode->i_private; if (!S_ISDIR(blk->mode)) { return -ENOTDIR; } //loop get one dir included file name entry = (struct dir_entry *)&blk->data[0]; for (i = 0; i < blk->dir_children; i++) { if (!dir_emit(ctx, entry[i].filename, strlen(entry[i].filename), entry[i].idx, DT_UNKNOWN)) break; filp->f_pos += sizeof(struct dir_entry); pos += sizeof(struct dir_entry); } return 0; } ssize_t tinyfs_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos) { struct file_blk *blk; char *buffer; blk = (struct file_blk*)filp->f_path.dentry->d_inode->i_private; if (*ppos >= blk->file_size) return 0; buffer = (char *)&blk->data[0]; len = min((size_t)blk->file_size, len); if (copy_to_user(buf, buffer, len)) { return -EFAULT; } *ppos += len; return len; } ssize_t tinyfs_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos) { struct file_blk *blk; char *buffer; blk = filp->f_path.dentry->d_inode->i_private; buffer = (char *)&blk->data[0]; buffer += *ppos; if (copy_from_user(buffer, buf, len)) { return -EFAULT; } *ppos += len; blk->file_size = *ppos; return len; } const struct file_operations tinyfs_file_operations = { .read = tinyfs_read, .write = tinyfs_write, }; static const struct file_operations tinyfs_dir_operations = { .owner = THIS_MODULE, .read = generic_read_dir, .iterate_shared = tinyfs_readdir, }; //create file static int tinyfs_do_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode) { struct inode *inode; struct super_block *sb; struct dir_entry *entry; struct file_blk *blk, *pblk; int idx; sb = dir->i_sb; if (curr_count >= MAX_FILES) { return -ENOSPC; } if (!S_ISDIR(mode) && !S_ISREG(mode)) { return -EINVAL; } inode = new_inode(sb); if (!inode) { return -ENOMEM; } inode->i_sb = sb; inode->i_op = &tinyfs_inode_ops; inode->i_atime = inode->i_mtime = inode_set_ctime_current(inode); idx = get_block(); //get free block save new file blk = &block[idx]; inode->i_ino = idx; blk->mode = mode; curr_count ++; if (S_ISDIR(mode)) { blk->dir_children = 0; inode->i_fop = &tinyfs_dir_operations; } else if (S_ISREG(mode)) { blk->file_size = 0; inode->i_fop = &tinyfs_file_operations; } inode->i_private = blk; pblk = (struct file_blk *)dir->i_private; entry = (struct dir_entry *)&pblk->data[0]; entry += pblk->dir_children; pblk->dir_children ++; entry->idx = idx; strcpy(entry->filename, dentry->d_name.name); inode_init_owner(&nop_mnt_idmap, inode, dir, mode); d_add(dentry, inode); return 0; } static int tinyfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode) { return tinyfs_do_create(&nop_mnt_idmap, dir, dentry, S_IFDIR | mode); } static int tinyfs_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, bool excl) { return tinyfs_do_create(&nop_mnt_idmap, dir, dentry, mode); } static struct inode * tinyfs_iget(struct super_block *sb, int idx) { struct inode *inode; struct file_blk *blk; inode = new_inode(sb); inode->i_ino = idx; inode->i_sb = sb; inode->i_op = &tinyfs_inode_ops; blk = &block[idx]; if (S_ISDIR(blk->mode)) inode->i_fop = &tinyfs_dir_operations; else if (S_ISREG(blk->mode)) inode->i_fop = &tinyfs_file_operations; inode->i_atime = inode->i_mtime = inode_set_ctime_current(inode); inode->i_private = blk; return inode; } struct dentry *tinyfs_lookup(struct inode *parent_inode, struct dentry *child_dentry, unsigned int flags) { struct super_block *sb = parent_inode->i_sb; struct file_blk *blk; struct dir_entry *entry; int i; blk = (struct file_blk *)parent_inode->i_private; entry = (struct dir_entry *)&blk->data[0]; for (i = 0; i < blk->dir_children; i++) { if (!strcmp(entry[i].filename, child_dentry->d_name.name)) { struct inode *inode = tinyfs_iget(sb, entry[i].idx); struct file_blk *inner = (struct file_blk*)inode->i_private; inode_init_owner(&nop_mnt_idmap, inode, parent_inode, inner->mode); d_add(child_dentry, inode); return NULL; } } return NULL; } int tinyfs_rmdir(struct inode *dir, struct dentry *dentry) { struct inode *inode = dentry->d_inode; struct file_blk *blk = (struct file_blk*) inode->i_private; blk->busy = 0; return simple_rmdir(dir, dentry); } int tinyfs_unlink(struct inode *dir, struct dentry *dentry) { int i; struct inode *inode = dentry->d_inode; struct file_blk *blk = (struct file_blk *)inode->i_private; struct file_blk *pblk = (struct file_blk *)dir->i_private; struct dir_entry *entry; entry = (struct dir_entry*)&pblk->data[0]; for (i = 0; i < pblk->dir_children; i++) { if (!strcmp(entry[i].filename, dentry->d_name.name)) { int j; for (j = i; j < pblk->dir_children - 1; j++) { memcpy(&entry[j], &entry[j+1], sizeof(struct dir_entry)); } pblk->dir_children --; break; } } blk->busy = 0; return simple_unlink(dir, dentry); } static struct inode_operations tinyfs_inode_ops = { .create = tinyfs_create, .lookup = tinyfs_lookup, .mkdir = tinyfs_mkdir, .rmdir = tinyfs_rmdir, .unlink = tinyfs_unlink, }; int tinyfs_fill_super(struct super_block *sb, void *data, int silent) { struct inode *root_inode; umode_t mode = S_IFDIR; root_inode = new_inode(sb); root_inode->i_ino = 1; inode_init_owner(&nop_mnt_idmap, root_inode, NULL, mode); root_inode->i_sb = sb; root_inode->i_op = &tinyfs_inode_ops; root_inode->i_fop = &tinyfs_dir_operations; root_inode->i_atime = root_inode->i_mtime = inode_set_ctime_current(root_inode); block[1].mode = mode; block[1].dir_children = 0; block[1].idx = 1; block[1].busy = 1; root_inode->i_private = &block[1]; sb->s_root = d_make_root(root_inode); curr_count ++; return 0; } static struct dentry *tinyfs_mount(struct file_system_type *fs_type, int flags, const char* dev_name, void *data) { return mount_nodev(fs_type, flags, data, tinyfs_fill_super); } static void tinyfs_kill_superblock(struct super_block *sb) { kill_anon_super(sb); } struct file_system_type tinyfs_fs_type = { .owner = THIS_MODULE, .name = "tinyfs", .mount = tinyfs_mount, .kill_sb = tinyfs_kill_superblock, }; static int tinyfs_init(void) { int ret; memset(block, 0, sizeof(block)); ret = register_filesystem(&tinyfs_fs_type); if (ret) pr_info("register tinyfs filesystem failed\n"); return ret; } static void tinyfs_exit(void) { unregister_filesystem(&tinyfs_fs_type); } module_init(tinyfs_init); module_exit(tinyfs_exit); MODULE_LICENSE("GPL");
/driver # insmod tinyfs.ko /driver # mkdir tinydir /driver # mount -t tinyfs none tinydir/ 

3、创建长名字的文件ls -al 显示怪异,不符合预期

/driver/tinydir # touch 1.txt /driver/tinydir # ls 1.txt /driver/tinydir # touch 2.txt /driver/tinydir # ls 1.txt 2.txt /driver/tinydir # touch 45666.txt /driver/tinydir # ls 1.txt 2.txt? 45666.txt /driver/tinydir # ls -al total 4 d--------- 1 0 0 0 Dec 9 13:12 . drwxr-xr-x 3 0 0 4096 Dec 9 12:46 .. -rw-r--r-- 1 0 0 0 Dec 9 13:12 1.txt ?r--rwSr-- 1 0 0 0 Dec 9 13:12 2.txt? -rw-r--r-- 1 0 0 0 Dec 9 13:12 45666.txt /driver/tinydir # touch 34455.txt /driver/tinydir # ls 1.txt 34455.txt 2.txt? 45666.txt34455.txt

最后创建一个34455.txt,最后显示的部分都和45666.txt连到一起了

还是debug,不清楚怎么debug ko的可以参考我之前的文章:无人知晓:qemu 单步调试linux driver

打断点在tinyfs_readdir 函数中,发现问题(idx应该是block的索引,正常按创建文件的顺序一次增加,从2~32, 但是实际上这里看到都已经越界覆盖了)

原因:struct file_blk 结构体中, 字段char data[0] 是当一个指针在使用,但是又没有分配空间,无论在文件内容存储,还是存储dir的filename信息都会向后覆盖block,导致后面存放的文件属性block 被踩坏

文件名字异常的的bug原因:

结构体定义:

struct dir_entry {

char filename[MAXLEN]; //maxlen为8, 创建文件名时不能超过;

uint8_t idx;

};

使用的地方在

tinyfs_do_create

-->strcpy(entry->filename, dentry->d_name.name);//换成strncpy就好了

针对前面的struct file_blk中data[0]的问题有两种方式,一种是在使用前分配,删除时释放,另外一种就是我现在用的增加一个结构体数组,做了一些长度限制(一个目录下只能存在4个文件或目录项)。

tinyfs.h

#define MAXLEN 8 #define MAX_FILES 32 #define MAX_BLOCKSIZE 512 #define MAX_SUBDIR_FILES 4 //one dir can include max file size //define the dir node info struct dir_entry { char filename[MAXLEN]; uint8_t idx; }; //just same count char as dir node size #define FILE_BUF_SIZ (sizeof(struct dir_entry) * MAX_SUBDIR_FILES) //define the file block struct file_blk { uint8_t busy; mode_t mode; uint8_t idx; union { uint8_t file_size; uint8_t dir_children; }; union {//用作目录时,最大记录的条目数,不超过4个; 用作文件时,这里就是文件的buffer大小 struct dir_entry dir_data[MAX_SUBDIR_FILES]; //one dir can exit 4 file char file_data[FILE_BUF_SIZ]; }; };

tinyfs.c

#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include "tinyfs.h" #define tinyfs_dbg pr_debug #define tinyfs_err pr_err struct file_blk block[MAX_FILES+1]; int curr_count = 0; static int get_block(void) { int i; for( i = 2; i < MAX_FILES; i++) { if (!block[i].busy) { block[i].busy = 1; return i; } } return -1; } static struct inode_operations tinyfs_inode_ops; /* * read the entries from a directory */ static int tinyfs_readdir(struct file *filp, struct dir_context *ctx) { loff_t pos; struct file_blk *blk; struct dir_entry *entry; struct dentry *dentry = filp->f_path.dentry; int i; if (!dir_emit_dots(filp, ctx)) return 0; pos = filp->f_pos; if (pos) return 0; blk = (struct file_blk*)dentry->d_inode->i_private; if (!S_ISDIR(blk->mode)) { return -ENOTDIR; } //loop get one dir included file name entry = (struct dir_entry *)blk->dir_data; for (i = 0; i < blk->dir_children; i++) { if (!dir_emit(ctx, entry[i].filename, strlen(entry[i].filename), entry[i].idx, DT_UNKNOWN)) break; filp->f_pos += sizeof(struct dir_entry); pos += sizeof(struct dir_entry); } return 0; } ssize_t tinyfs_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos) { struct file_blk *blk; char *buffer; blk = (struct file_blk*)filp->f_path.dentry->d_inode->i_private; if (*ppos >= blk->file_size) return 0; buffer = (char *)blk->file_data; len = min((size_t)blk->file_size, len); if (copy_to_user(buf, buffer, len)) { return -EFAULT; } *ppos += len; return len; } ssize_t tinyfs_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos) { struct file_blk *blk; char *buffer; blk = filp->f_path.dentry->d_inode->i_private; buffer = (char *)blk->file_data; buffer += *ppos; if (copy_from_user(buffer, buf, len)) { return -EFAULT; } *ppos += len; blk->file_size = *ppos; return len; } const struct file_operations tinyfs_file_operations = { .read = tinyfs_read, .write = tinyfs_write, }; static const struct file_operations tinyfs_dir_operations = { .owner = THIS_MODULE, .read = generic_read_dir, .iterate_shared = tinyfs_readdir, }; static int tinyfs_do_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode) { struct inode *inode; struct super_block *sb; struct dir_entry *entry; struct file_blk *blk, *pblk; int idx; sb = dir->i_sb; if (curr_count >= MAX_FILES) { return -ENOSPC; } if (!S_ISDIR(mode) && !S_ISREG(mode)) { return -EINVAL; } pblk = (struct file_blk *)dir->i_private; //dir include file|dir count can't over MAX_SUBDIR_FILES if (pblk->dir_children >= MAX_SUBDIR_FILES) { tinyfs_err("one dir max include file cnt should less than %d\n", MAX_SUBDIR_FILES); return -ENOSPC; } inode = new_inode(sb); if (!inode) { return -ENOMEM; } inode->i_sb = sb; inode->i_op = &tinyfs_inode_ops; inode->i_atime = inode->i_mtime = inode_set_ctime_current(inode); idx = get_block(); //get free block save new file blk = &block[idx]; inode->i_ino = idx; blk->mode = mode; curr_count ++; if (S_ISDIR(mode)) { blk->dir_children = 0; inode->i_fop = &tinyfs_dir_operations; } else if (S_ISREG(mode)) { blk->file_size = 0; inode->i_fop = &tinyfs_file_operations; } inode->i_private = blk; //need check the dir inode is what? tinyfs_dbg("%s dir inode %llx", __func__, dir); entry = &pblk->dir_data[pblk->dir_children]; pblk->dir_children ++; entry->idx = idx; strncpy(entry->filename, dentry->d_name.name, MAXLEN-1); entry->filename[MAXLEN-1] = 0; inode_init_owner(&nop_mnt_idmap, inode, dir, mode); d_add(dentry, inode); return 0; } static int tinyfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode) { return tinyfs_do_create(&nop_mnt_idmap, dir, dentry, S_IFDIR | mode); } static int tinyfs_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, bool excl) { return tinyfs_do_create(&nop_mnt_idmap, dir, dentry, mode); } static struct inode * tinyfs_iget(struct super_block *sb, int idx) { struct inode *inode; struct file_blk *blk; inode = new_inode(sb); inode->i_ino = idx; inode->i_sb = sb; inode->i_op = &tinyfs_inode_ops; blk = &block[idx]; if (S_ISDIR(blk->mode)) inode->i_fop = &tinyfs_dir_operations; else if (S_ISREG(blk->mode)) inode->i_fop = &tinyfs_file_operations; inode->i_atime = inode->i_mtime = inode_set_ctime_current(inode); inode->i_private = blk; return inode; } struct dentry *tinyfs_lookup(struct inode *parent_inode, struct dentry *child_dentry, unsigned int flags) { struct super_block *sb = parent_inode->i_sb; struct file_blk *blk; struct dir_entry *entry; int i; blk = (struct file_blk *)parent_inode->i_private; entry = (struct dir_entry *)blk->dir_data; for (i = 0; i < blk->dir_children; i++) { if (!strcmp(entry[i].filename, child_dentry->d_name.name)) { struct inode *inode = tinyfs_iget(sb, entry[i].idx); struct file_blk *inner = (struct file_blk*)inode->i_private; inode_init_owner(&nop_mnt_idmap, inode, parent_inode, inner->mode); d_add(child_dentry, inode); return NULL; } } return NULL; } int tinyfs_rmdir(struct inode *dir, struct dentry *dentry) { struct inode *inode = dentry->d_inode; struct file_blk *blk = (struct file_blk*) inode->i_private; blk->busy = 0; return simple_rmdir(dir, dentry); } int tinyfs_unlink(struct inode *dir, struct dentry *dentry) { int i; struct inode *inode = dentry->d_inode; struct file_blk *blk = (struct file_blk *)inode->i_private; struct file_blk *pblk = (struct file_blk *)dir->i_private; struct dir_entry *entry; entry = (struct dir_entry*)pblk->dir_data; for (i = 0; i < pblk->dir_children; i++) { if (!strcmp(entry[i].filename, dentry->d_name.name)) { int j; for (j = i; j < pblk->dir_children - 1; j++) { memcpy(&entry[j], &entry[j+1], sizeof(struct dir_entry)); } pblk->dir_children --; break; } } blk->busy = 0; return simple_unlink(dir, dentry); } static struct inode_operations tinyfs_inode_ops = { .create = tinyfs_create, .lookup = tinyfs_lookup, .mkdir = tinyfs_mkdir, .rmdir = tinyfs_rmdir, .unlink = tinyfs_unlink, }; int tinyfs_fill_super(struct super_block *sb, void *data, int silent) { struct inode *root_inode; umode_t mode = S_IFDIR; root_inode = new_inode(sb); root_inode->i_ino = 1; inode_init_owner(&nop_mnt_idmap, root_inode, NULL, mode); root_inode->i_sb = sb; root_inode->i_op = &tinyfs_inode_ops; root_inode->i_fop = &tinyfs_dir_operations; root_inode->i_atime = root_inode->i_mtime = inode_set_ctime_current(root_inode); block[1].mode = mode; block[1].dir_children = 0; block[1].idx = 1; block[1].busy = 1; root_inode->i_private = &block[1]; tinyfs_dbg("%s root inode %llx", __func__, root_inode); sb->s_root = d_make_root(root_inode); curr_count ++; return 0; } static struct dentry *tinyfs_mount(struct file_system_type *fs_type, int flags, const char* dev_name, void *data) { return mount_nodev(fs_type, flags, data, tinyfs_fill_super); } static void tinyfs_kill_superblock(struct super_block *sb) { kill_anon_super(sb); } struct file_system_type tinyfs_fs_type = { .owner = THIS_MODULE, .name = "tinyfs", .mount = tinyfs_mount, .kill_sb = tinyfs_kill_superblock, }; static int tinyfs_init(void) { int ret; memset(block, 0, sizeof(block)); ret = register_filesystem(&tinyfs_fs_type); if (ret) pr_info("register tinyfs filesystem failed\n"); return ret; } static void tinyfs_exit(void) { unregister_filesystem(&tinyfs_fs_type); } module_init(tinyfs_init); module_exit(tinyfs_exit); MODULE_LICENSE("GPL");

执行效果

/driver/tinydir # touch 1.txt /driver/tinydir # touch 2.txt /driver/tinydir # mkdir tinysub /driver/tinydir # touch 4.txt /driver/tinydir # ls 1.txt 2.txt tinysub 4.txt /driver/tinydir # touch 5.txt ////当超出数组长度限制时,提示 [ 76.] one dir max include file cnt should less than 4 touch: 5.txt: No space left on device /driver/tinydir # ls 1.txt 2.txt tinysub 4.txt /driver/tinydir # cd tinysub/ /driver/tinydir/tinysub # touch 5.txt /driver/tinydir/tinysub # cd .. /driver/tinydir # tree . . ├── 1.txt ├── 2.txt ├── 4.txt └── tinysub └── 5.txt

上面实验的最小文件系统的结构图,dir和file都是存在block结构体中:

当然这个文件系统还有很多bug,比如文件写入内容的大小控制,删除文件再创建文件的处理(前面tinyfs_do_create下处理entry有点小问题)... ,这些留给感兴趣的同学自己分析处理(主要是我要溜娃了)。

参考:

300来行代码带你实现一个能跑的最小Linux文件系统

本篇文章聊聊如何在 Docker 里运行 Windows 操作系统, Windows in Docker Container(WinD)。

我日常使用 macOS 和 Ubuntu 来学习和工作,但是时不时会有 Windows 使用的场景,不论是运行某个指定的软件,还是要做一些跨平台软件的功能验证。

在去年开源 soulteary/docker-chatgpt[1] 之前,还折腾过将 Chrome 容器化,提供有界面服务能力容器的事情,如果当时有这个方案,或许折腾过程能更简单一些。

我们依旧是先从环境准备开始。想要使用这个方案,我们需要准备的东西有三个:安装了 Docker 的操作系统(我使用 Ubuntu)、Windows 操作系统的安装光盘(从 WinXP ~ Win11 都行)、开源项目 dockur/windows[2] 的 Docker 镜像。

这套方案中采用了 KVM 加速,所以体验最好的方案是使用或者安装一个 Linux 环境,如果你本身就在使用 Ubuntu 之类的支持 KVM 非常方便的操作系统的话,那么只需要安装 Docker 就好啦。

如果你确实需要在容器中运行 Windows,想从零开始,可以参考之前的文章《在笔记本上搭建高性价比的 Linux 学习环境:基础篇[3]》的方法来进行实践。安装 Ubuntu 的流程和以往并没有太大不同,依旧是老生常谈的三步曲:下载镜像、制作启动盘、安装系统。

如果你已经有了可以使用的 Linux 环境,可以参考上面文章中的 “更简单的 Docker 安装” 章节,完成基础环境的准备。

完成操作系统和 Docker 的准备后,我们还需要检查操作系统是否支持 KVM,需要先安装 cpu-checker

sudo apt install cpu-checker -y

然后,执行 kvm-ok,顺利的话,将能够看到类似下面的日志输出:

# sudo kvm-ok INFO: /dev/kvm exists KVM acceleration can be used

虽然开源项目 dockur/windows[4] 会根据用户指令,从 dl.bobpony.comarchive.org,以及微软官网自动下载合适的英文版系统镜像,但如果你想更快的完成系统的安装,或者想快速的启动多个 Windows Docker 容器,那么手动下载 Windows 光盘还是非常有必要的。

开源项目包含了一些自动安装的预设配置,不过,这需要使用英文版的操作系统,你可以从这里下载[5]

当然,如果你需要使用中文版的操作系统,同样可以从官方下载[6],在初始化操作系统的时候,相比英文操作系统你需要额外点一些“下一步”。

获取在 Docker 中运行 Windows 的容器镜像很简单:

docker pull dockurr/windows

当然,如果不能够直接下载,也可以选择本地构建:

git clone https://github.com/dockur/windows.git cd windows docker build -t dockurr/windows .

这个镜像主要依赖了几项技术:

  • qemus/qemu-docker[7],在容器中使用 QEMU,能够提供接近本机速度的虚拟机的网络、IO 速度等。
  • christgau/wsdd[8],让容器中的 Windows 能够出现在局域网中的其他设备的共享设备中。(Windows 10 的 1511 版本后,默认开始禁用 SMBv1,NetBIOS 设备发现功能失效,导致其他设备不能对其进行服务发现)。
  • qemus/virtiso[9],精简到 27MB 的 KVM/QEMU Virtio 驱动程序,能够让 Windows 在 Docker 环境中正常使用。
  • krallin/tini[10],正确启动 Docker 中 QEMU,以及确保进程异常能够被正确处理,或正确的终止容器进程。

好了,准备工作就绪后,我们就可以开始使用这个有趣的技术方案啦。

我们先聊聊最简单的使用方案,启动一个“无状态”的临时的 Windows 操作系统,容器会自动下载我们所需要的镜像:

version: "3" services:  windows:  image: dockurr/windows  container_name: windows  devices:  - /dev/kvm  cap_add:  - NET_ADMIN  ports:  - 8006:8006  - 3389:3389/tcp  - 3389:3389/udp  stop_grace_period: 2m  restart: on-failure

将上面的配置保存为 docker-compose.yml,然后使用 docker compose updocker compose -d 启动服务。

因为我们没有指定本地的镜像,所以如果你的网络环境访问微软 CDN 不够快的话,启动过程需要等待一些时间。

# docker compose up  [+] Running 2/1 ✔ Network win_default Created 0.1s ✔ Container windows Created 0.1s Attaching to windows windows | ❯ Starting Windows for Docker v2.04... windows | ❯ For support visit https://github.com/dockur/windows windows | windows | windows | ❯ Downloading Windows 11... windows | [i] Downloading Windows media from official Microsoft servers... windows | [i] Downloading Windows 11... windows | [+] Got latest ISO download link (valid for 24 hours): https://software.download.prss.microsoft.com/dbazure/Win11_23H2_English_x64v2.iso?t=c603adeb-c6d7-4bb9-b084-875f3beabfc2&P1=&P2=601&P3=2&P4=ynPQkgNxZoZxQkmfORJRE5yaf94m7ONuLVngMtHmDfsYTooFKSXiAdWXTKJ8dpoF2WuDkUZ4fkP1u%2bhwAh%2brAdghU%2f1ssngioKg2aLDe2UXOG3ESUAGTyRk1q515ONoXIvyJby2xPoKBVoj%2bsNp6ECqosBjx9HllmF3saRvQFPQox6v8kuhtMxyuNiXT%2fYgKppSZOifx34t6YQb0Hpo6gTkLjxlxiFBF42jLt%2blVhf1HW7ELEtvVUW7eAn9UGfs9HF6yC3p1ep7ouKYNrY0Ek0fo%2bn2v%2by3bTGbqg8lHfXjxb6bPHGE6HWP3sSZDZw4JmPt53hr1uQl%2fmjT50p504Q%3 windows | #=#=#  windows | #=#=#  0.0% 0.1% 0.2% 0.3% ... # 99.7% # 99.8% # 100.0%  100.0% windows | windows | [+] Successfully downloaded Windows image! windows | windows | ❯ Extracting Windows 11 image... windows | ❯ Adding XML file for automatic installation... windows | ❯ Building Windows 11 image... windows | ❯ Creating a 64G growable disk image in raw format... windows | ❯ Booting Windows using QEMU emulator version 8.2.1 ... windows | ...

当一切就绪后,我们可以使用两个方式来访问这个运行在 Docker 中的 Windows。

第一种方法,是使用浏览器访问容器所在主机的 IP地址:8006

在容器中自动部署的 Windows

容器启动后,会自动下载、部署 Windows,稍等片刻,就能够在浏览器中正常使用它啦:

在浏览器中访问 Windows

第二种方法,是使用支持 RDP 远程访问功能的软件,在软件服务器地址和端口内容中分别填写 IP地址3389,在用户名栏填写 docker,密码保持空白即可。

在 RDP 客户端中访问 Windows
默认情况,每次启动都需要见到它

当然,如果你的网络环境不是那么好,或者你不想每次启动容器都要等待很久,可以使用下面的方法。

让部署使用加速,主要和两个细节有关:是否进行了容器内容的持久化,是否提供了高性能的安装镜像下载方式。

比如,我们在上面的准备工作中,我们预先下载好 Windows 的安装镜像,然后将文件重命名为 win11x64.iso,接着将文件放置在目录的 https://www.zhihu.com/topic//iso 子目录中。那么,借助 Nginx,可以让整个安装部署过程变的飞快。

version: "3" services:  windows:  image: dockurr/windows  container_name: windows  devices:  - /dev/kvm  cap_add:  - NET_ADMIN  ports:  - 8006:8006  - 3389:3389/tcp  - 3389:3389/udp  stop_grace_period: 2m  restart: on-failure  environment:  VERSION: "http://winiso/win11x64.iso"  MANUAL: "N"  volumes:  - https://www.zhihu.com/topic//win:/storage  depends_on:  - winiso    winiso:  image: nginx:alpine  container_name: winiso  restart: on-failure  volumes:  - https://www.zhihu.com/topic/19636090/iso:/usr/share/nginx/html

在上面的配置中,我们增加了一个用来将本地的 Windows 安装文件转换为 dockurr/windows 快速可安装的在线地址的容器。

将配置文件保存为 docker-compose.yml,然后使用 docker compose up 或者 docker compose up -d 启动配置,我们将看到类似下面的日志:

windows | . windows | . winiso | 172.20.2.3 - - [11/Mar/2024:03:54:47 +0000] "GET /win11x64.iso HTTP/1.1" 200  "-" "Wget/1.21.4" "-" windows | . 99% 1.59G 0s windows | windows | K . windows | windows | 100% 1.95G windows | =3.7s windows | windows | windows | ❯ Extracting downloaded ISO image... windows | ❯ Detecting Windows version from ISO image... windows | ❯ Detected: Windows 11 windows | ❯ Adding XML file for automatic installation... windows | ❯ Building Windows 11 image... windows | ❯ Creating a 64G growable disk image in raw format... windows | ❯ Booting Windows using QEMU emulator version 8.2.1 ...

下载镜像的速度马上从几MB、几十MB增加到了接近每秒 2GB,不到 4s 就能完成镜像的下载和处理。

因为在配置中增加了 volumes 卷的持久化(- https://www.zhihu.com/topic//win:/storage),所以我们可以放心的停止或者重新启动容器,而不必担心每次都要重新初始化“一台”新的 Windows Docker 容器。

聊聊其他的使用技巧。

如果你的网络环境非常棒,不需要提前下载安装镜像,或者直接使用云主机进行项目部署,那么可以考虑直接调整配置文件中的内容为合适的数值:

environment:  VERSION: "win11"

支持我们调整使用的值包含:win11win10ltsc10win81win7vistawinxp20222019201620122008

默认情况下,这个 Windows 容器会使用 vCPU x2、4GB 内存、64G 的磁盘空间,来满足 Win11 的最低安装需求。我们可以根据自己的实际需求,来动态的调整容器的硬件资源限制。

environment:  RAM_SIZE: "8G"  CPU_CORES: "4"  DISK_SIZE: "256G"

比如,在上面的配置中,我们调整 CPU 核心数到 4,内存到 8GB,磁盘到 256GB。

默认情况下,Docker 会共享宿主机的 IP,如果我们想要让容器拥有独立的 IP 地址,需要先创建一个 macvlan 网络:

docker network create -d macvlan \  --subnet=192.168.0.0/24 \  --gateway=192.168.0.1 \  --ip-range=192.168.0.100/28 \  -o parent=eth0 vlan

创建完网卡后,调整上面使用的容器配置,根据自己的需求指定容器 IP 即可:

services:  windows:  container_name: windows  ..<snip>..  networks:  vlan:  ipv4_address: 192.168.0.100  networks:  vlan:  external: true

如果你的主机上有多块磁盘,或者想将某一块磁盘完整的分配给 Windows,可以采用下面的方法,其中 DEVICE 将作为你的主磁盘:

environment:  DEVICE: "/dev/sda"  DEVICE2: "/dev/sdb" devices:  - /dev/sda  - /dev/sdb

我们首先需要使用 lsusb 来获取 USB 设备的 VendorIDProductID ,然后将这些信息添加到配置中:

environment:  ARGUMENTS: "-device usb-host,vendorid=0x1234,productid=0x1234" devices:  - /dev/bus/usb

本篇文章先聊到这里,下一篇文章见。

--EOF


我们有一个小小的折腾群,里面聚集了一些喜欢折腾、彼此坦诚相待的小伙伴。

我们在里面会一起聊聊软硬件、HomeLab、编程上、生活里以及职场中的一些问题,偶尔也在群里不定期的分享一些技术资料。

关于交友的标准,请参考下面的文章:

苏洋:致新朋友:为生活投票,不断寻找更好的朋友

当然,通过下面这篇文章添加好友时,请备注实名和公司或学校、注明来源和目的,珍惜彼此的时间 :D

苏洋:关于折腾群入群的那些事


[1] soulteary/docker-chatgpt: github.com/soulteary/do
[2] dockur/windows: github.com/dockur/windo
[3] 在笔记本上搭建高性价比的 Linux 学习环境:基础篇: soulteary.com/2022/06/2
[4] dockur/windows: github.com/dockur/windo
[5] 这里下载: microsoft.com/en-us/sof
[6] 官方下载: microsoft.com/zh-cn/sof
[7] qemus/qemu-docker: github.com/qemus/qemu-d
[8] christgau/wsdd: github.com/christgau/ws
[9] qemus/virtiso: github.com/qemus/virtis
[10] krallin/tini: github.com/krallin/tini









如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。

如果你想更快的看到后续内容的更新,请戳 “点赞”、“分享”、“喜欢” ,这些免费的鼓励将会影响后续有关内容的更新速度。


本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2024年03月11日 统计字数: 7281字 阅读时间: 15分钟阅读 本文链接: soulteary.com/2024/03/1

到此这篇kvm虚拟化技术原理(虚拟化 kvm)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!







版权声明


相关文章:

  • docker启动容器命令解释(docker 启动容器失败)2025-07-06 10:54:07
  • 启动docker容器命令(启动docker的命令)2025-07-06 10:54:07
  • kvm虚拟化分为哪三层(kvm虚拟化技术实战与原理解析 pdf)2025-07-06 10:54:07
  • docker 开机启动容器内程序(docker启动容器命令解释)2025-07-06 10:54:07
  • ddpm模型(ddpm模型做化学反应)2025-07-06 10:54:07
  • pointnet++复现可视化(networkx 可视化)2025-07-06 10:54:07
  • kvm虚拟化原理(简述kvm虚拟化功能特性及优缺点?)2025-07-06 10:54:07
  • kubernetes的作用(kubernetes的主要功能)2025-07-06 10:54:07
  • 启动docker-compose容器命令(docker启动容器命令解释)2025-07-06 10:54:07
  • 启动docker容器命令(启动 docker)2025-07-06 10:54:07
  • 全屏图片