极术小姐姐 · 2023年02月06日

虚拟化科普之 CPU 篇

0. 目录

  1. 垫话
  2. 什么是虚拟化?
  3. CPU 虚拟化怎么实现?
  4. 从虚拟化定义的角度审视线程模型
  5. 从线程视角来看,一个物理 CPU 包含哪些范畴?
  6. Is that all?yes, if no one asks
  7. 如何解决 OS 级 CPU 虚拟化所面临的 “敏感指令” 问题
  8. 总结

1. 垫话

   本文所讨论的 CPU 虚拟化,是原教旨语义上的,基于硬件实现的 OS 级别的 CPU 全虚拟化。

   本文源自一次跨团队分享,旨在科普 CPU 虚拟化的基本实现思路以及兄弟团队所关心的底层基础知识。

   虽然仓促,但写完发现基本逻辑还是顺畅的,尤其是对具有基本操作系统及体系结构素养,但并无虚拟化知识背景的同学来说,本文有助于快速建立起 CPU 虚拟化的认知。拿出来润色润色,正好可以水一篇。

   此前在深圳的地铁上,无意间和同事们聊起虚拟化,在一无所知的我的反复追问下,老王试图仅凭一句 “它一执行就退出来了”,让我实现虚拟化一分钟从入门到精通。

   老王如果当时能按本文的逻辑讲,我应该不至于反而更加困惑,最终不得不离职。

2. 什么是虚拟化?

   来自笔者的非官方定义:所谓虚拟化,目的是把一个东西当多个东西用(generally speaking),方法论是分时复用。

   通俗点说,在一个物理主体中运行的多个实体,从每个实体的视角来看,其独占一个物理实体,且并不会觉得其实际上是与其他实体在共享同一个物理实体。

3. CPU 虚拟化怎么实现?

   其实最简单的 CPU 虚拟化实现,就是广大程序员朋友们所耳熟能详的线程模型。

   本文不讨论进程,进程的本质是一套独立的页表(也就是地址空间),再加上一组被其所管理的线程,本质上只是一个壳,底层运行(内核调度)实体是线程。

4. 从虚拟化定义的角度审视线程模型

   线程作为内核之上的运行实体,它是如何做到每个线程都以为自己拥有一个独立的物理 CPU 的?

   要回答上面的问题,需要先回答下面的元问题。

5. 从线程视角来看,一个物理 CPU 包含哪些范畴?

   所谓线程,就是一坨正在被执行的代码。如果把代码反汇编出来,其本质就是在不断地倒腾各种寄存器。

   对于一个线程来说,其所能感知到的 CPU 实体,其实就是 CPU 的各种寄存器,也就是本科操作系统教科书中所说的 “CPU 现场”。

   最典型的两个寄存器:

  1. SP:stack pointer,指示当前程序的栈顶在哪。
  2. IP:instruction pointer,指示当前程序在运行哪一行代码。

   所以,若要对每一个线程(kernel 之上所运行的实体)呈现出一个独立的物理 CPU,手法就是:

   给每个线程定义一个所谓的 “CPU 上下文”,一般是通过软件实现。不排除 X86 的 TSS(task state segment)设计,试图从硬件上提供任务上下文保存及切换机制,但是正统的 linux kernel 并未采用。

   在本科操作系统教科书中,这个软件实现的 “CPU 上下文” 被称为 TCB(thread control block)。

   一个 TCB 中所包含的 CPU 上下文信息,就是实现一个线程级别 CPU 虚拟化的完整闭包。

    如图 1,通过 CPU 上下文切换实现分时复用的基本手法:

1. 当 kernel 加载某个线程到物理 CPU 上运行时

   通过这个线程的 TCB,将物理 CPU 的上下文恢复成该线程 TCB 所刻画的 CPU 现场。

2. 当某个线程不再被 kernel 执行时

   将当前物理 CPU 的现场保存到该线程的 TCB 中。

image.png

图 1

   从效果上来看,一个物理 CPU 被虚拟出了三个虚拟的 CPU。通过 CPU 上下文的 save、restore,实现系统中运行着的每个线程,都以为自己在独占地、不间断地在物理 CPU 上运行。

   即使这种 "独占、不间断" 感其实是虚幻的,但是并不重要:如果一个东西看起来像鸭子,走起来像鸭子,叫起来像鸭子,那么它就是鸭子。

   有点柏拉图的理念世界、现象世界那味了。

6. Is that all?yes, if no one asks

   是否线程模型就是理想的原教旨 CPU 虚拟化实现的方案呢?很可惜并不是。

   线程模型下对 CPU 的虚拟化,是建立在 “实体是运行在同一个 OS 中” 的共同认知前提下的。每个线程都默认(共识)它们有着共同的大家长,也就是 kernel。

   而我们所谓原教旨的 CPU 虚拟化,是提供支持运行 OS 级别的实体。每个实体中跑的不是基于一个 OS 共识的线程,而是不同的 OS。

   这就是线程模型的 CPU 虚拟化,与我们所谓的原教旨 CPU 虚拟化之间存在的语义 gap。

   搞清楚这个语义 gap 是搞清楚原教旨 “CPU 虚拟化” 的重要前提。

   那么就要回答,有这样的共识和没有这样的共识,会带来什么不同?

   不同太多,只拿中断向量表做例子。

   现代操作系统中,中断向量都是由 OS(严格意义上讲应该是 kernel)来维护的,所谓中断向量表,就是中断处理函数的表(保护模式下实际上是中断描述符表,这里不展开)。

   X86 体系结构下通过 LIDT(load interrupt descriptor table)指令设置中断向量表基地址,kernel 启动时初始化好中断向量表(本质上是在内存中的数组),并将这个数组的基地址通过 LIDT 指令设置到物理 CPU 的 IDTR(interrupt descriptor register)中。

   问题在于,中断向量表的基地址是不能乱设置的。

   如果系统中运行着 N 个 OS 实体,每个 OS 的 kernel 都来 LIDT 一把(而且大概率 lidtr 的目标地址对应不同的物理地址),那么 Host 自己 kernel 的中断向量表也会被搞乱,最直接的后果就是宿主机自己先挂了。

   像 LIDT 这种会对整个系统级别资源(硬件)产生影响的操作指令,被称为 “敏感指令”。

   有人会问:那线程模型的 CPU 虚拟化,不会有这种 “敏感指令” 的问题吗?

   还真没有,因为现代操作系都有严格的特权级设计,线程(用户态)代码是无法(实际上也不需要)执行这些 “敏感指令” 的,因为 “敏感指令” 必然为特权指令(反之不然),只有 kernel 有执行这些指令的权限,且只有 kernel 会去执行这些指令。

   而在线程模型下,是有 “共同 os(kernel)” 的语义前提的。

   图 2 是在我们建立起 “敏感指令” 的概念后,重新审视图 1 的结果:

   在线程运行的过程中,除了 TCB 里面所包含的 CPU 寄存器,其实还有很多寄存器(图 2 中黑色部分的寄存器)是不被 TCB 所包括的,这些寄存器只能被 kernel 访问。

image.png

图 2

7. 如何解决 OS 级 CPU 虚拟化所面临的 “敏感指令” 问题

   文本不讨论半虚拟化、二进制翻译等手法实现的 CPU 虚拟化,这类手法虽然也可以解决 “敏感指令” 问题,但其本质上是通过牺牲软件架构的可扩展性或者是性能,绕过了 “敏感指令” 所带来的的原生性问题。

   在基于硬件实现的全虚拟化方案下,要解决 “敏感指令” 问题只能通过体系结构本身,也就是通过 CPU 自身的设计来支持。

   X86 下,Intel 的方案称为 VTX(virtualization technology extension)技术。

   本文不打算全面展开 VTX 技术,那将是一个 long long story,只从本文所讨论的逻辑范畴,拆解 VTX 所提供的相关能力:

  • 提供将 underlying registers 也封装进一个 “CPU 上下文” 中的能力

    VTX 下一个 OS 级别 CPU 的上下文称为 VMCS(virtual machine control structure)。

    结合图 2,简单理解为 VMCS = TCB + underlying registers(实际上 VMCS 所包含的范畴比这个等式所表达的要广泛的多的多,但本文没必要展开)。

    一个 VMCS 中所包含的 CPU 上下文信息,就是实现一个 OS 级别 CPU  虚拟化的完整闭包。

    如图 3,OS 级别 CPU 虚拟化的实现,其底层手法依然是分时复用:

image.png

图 3

  • 截获实体中对敏感指令的执行的能力

    仅提供 VMCS 这样的 OS 级别 CPU 上下文还不够,因为仍然没有解决敏感指令的问题(会把系统搞乱)。

    VTX 提供了 root、non-root 两种处理器模式,Host(VMM)运行在 root 模式下,Guest(虚拟机)运行在 non-root 模式下,当 Guest 中执行 LIDT 指令时,CPU 将自动捕获这条对敏感指令的执行,进而引发 CPU 从 non-root 模式退出到 root 模式,也就是从 Guest 中退出(VMEXIT)到 Host(VMM)中,并且 Guest 的 VMCS 中会带有本次从 non-root 下退出来的原因(reason),VMM 会根据这个 reason 执行相应的模拟逻辑。

    具体来说,如果 Guest 是因为执行 LIDT 指令而退出,那么 VMM 并不会真的将 Guest 所指定的中断向量表地址写入实际的物理 CPU 寄存器中,而是写入 Guest 所对应的 VMCS 中。等到下次该 Guest 再次被调度执行时,VMRESUME 指令会直接将 VMCS 所刻画的 CPU 现场恢复到 non-root 模式下的 CPU 上下文中,其中就包括 non-root 模式下的 IDTR 寄存器。

    如此实现了每个 Guest 都有各自独立的 IDTR(中断向量表),又不会把宿主机搞挂。

8. 总结

 OS 级别的 CPU 虚拟化,其核心是解决敏感指令问题,基本方法论还是分时复用。

作者: 戴胜冬
文章来源:窗有老梅

推荐阅读

欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
2882
内容数
267
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息