kernel/user mode 的切换细节和设计考量

我们知道,在 OS(operating system)中有一个 kernel mode, user mode 的概念,其用处在于限定 instruction 的执行权限。处于 user mode 的 instruction,不可以直接执行访问 hardware 等敏感操作;而处于 kernel mode 的 instruction 则可以。

如果不深究细节,似乎 user/kernel mode 是非常显然的模式,不就类似于调用某个 HTTP API 嘛,没啥了不起。但如果深究细节,问一下 user/kernel mode 的切换到底发生了什么事情,为什么要如此设计这样的切换流程,那么,很多东西就变得不再平凡。

看到 kernel/user mode,可能最直观的想法就是:OS 提供了一堆可供 user 使用的 kernel 函数,如 funcKernel() ,这些函数可以被 user mode 的任何方法 funcUser() 调用。而这些 funcKernel() 函数的实现,是用处于 kernel mode 的方法来实现的。

但,这样的叙述是有问题的,因为没有精准推敲所使用的术语。当我们讨论 user/kernel mode 的执行时,我们讨论的是 instruction,而不是方法。方法是可以被拆分为多条 instruction 的,而 instruction 可以被拆分吗?

也即是,按照上述符号, funcKernel 和 funcUser 不再是一个个的方法了,而是一条条 atomic 的 instruction(让我们将其对应的符号切换为 instrKernel 和 instrUser ),请问,可以让一条 instruction 的执行被拆分为多条其它 instruction 的执行吗?

显然,从 CPU 的角度讲,instruction 已经是最小执行单位了,是不可以被拆分的。

此时,我们终于看到了 devil in the detail:如何让不可拆分的 instruction instrKernel 和 instrUser 来实现类似于 funcKernel 和 funcUser 之间的嵌套调用?

一种最直观而粗糙的想法,当然就是强行模仿 method 之间的调用方式,即:再引入一层虚拟的 instruction set,类似于 virtual machine。任何 instrUser 都是 virtual instruction,而 instrKernel 才是真正的 hardware instruction。这可以算是 virtual machine 的解决思路,我们暂时不予讨论。

那如果不使用这样的 virtual instruction set 层,又该如何解决呢?此时,无论是 OS kernel 的 instruction,还是 user application 所使用的 instruction,对于 CPU 来讲都是无差别对待的 hardware instruction。例如,你不能说 user application 的 mov/add 操作,就不同于 OS kernel 的 mov/add 操作。它们都是 CPU 的真实 instruction,当然是无差别同等对待。

那 CPU 应该如何区分 instruction 的「出处」呢?如何知道在执行一条 instruction 的时候,是来自于 instrKernel 还是来自于 instrUser 呢?

一个直观的解决方案,当然就是引入 state 来区分 instruction。CPU ring 即是这样的 state variable,只不过它不是被放于 instruction 中的,而是放于 CPU 中。即:不是在每一条 instruction 中放入一个 state 来说明自己的出处,而是直接将权限 state 放于 CPU 中,让 CPU 根据自己的这个 ring variable 来判断执行权限。

紧随而来的问题是如何改变这个 state variable 呢?即:如何切换 kernel/user mode?

如果直接将这个「改变操作」放于一条常规的 instruction 中,那么执行这条 instruction 时,CPU ring 应该是什么状态呢?

  • 如果是 user mode,那岂不是 user application 可以随时改变 CPU 的权限,然后让 user application 继续自己指定的、后续任意 instruction 的执行?此时,权限控制不就没有意义了吗?

  • 如果是 kernel mode,那么,user mode 调用它本来就是为了改变 CPU ring 的,现在连改变的 instruction 也是没有权限执行的了,那它还怎么改变自己的权限呢?

如此,将这个「改变操作」直接放于一条常规的 instruction 中肯定是不合适的。那么,对于 CPU 来讲还有什么异常的 instruction 吗?那就只剩 exception 了。

当 user mode 调用 system call 时,其对应的操作是抛出一个 exception(称为 trap)。抛出的 exception,会有事先注册到 trap table 的 exception handler 来捕获、处理这个 exception。

此时,这个 trap handler 就可以改变 CPU ring,并执行这个 trap handler 所指定的后续 instructions。等到 trap handler 所指定的 instructions 全部被执行完后,它再将 CPU ring 改回 user mode 状态。

乍一看,整个过程似乎有些神奇和晦涩?但仔细来看的话,其实这样的通过「exception + trap handler」的方式所实现的,正是最开始我们提到的类似于 funcKernel 和 funcUser 之间的嵌套调用!

如上所述,虽然 instrUser 不能被拆分,但通过抛出 exception 的方式,这个 instruction 的执行流程被强制性转换到了 trap handler,这不就是函数中调用子函数的执行流程跳转吗?!也即是:通过抛出 exception 来实现 instruction 级别的流程控制。而所有的这些 system call 的 trap handler,不就等价于一开始的 instrKernel 吗?

此时,我们终于可以松一口气,引入 CPU ring、system call 对应 exception handler、trap handler 改变 CPU ring 这一些列晦涩的骚操作,不过是为了实现 instruction 级别的「子函数调用」罢了,通过抛出 exception 的方式来实现 instruction 级别的流程跳转,并没有什么玄幻的黑魔法。接下来,在子函数中来控制权限、改变 CPU ring,不过是自然而然的操作。

讨论至此,文章似乎应该直接结束。但真正的思考者,总是不会满足于对问题的解决,还会去重新回顾整个问题的来龙去脉,梳理其中的元问题和元认知。从我们得到的答案来看,似乎这些想法和操作都相当自然、直观啊,那为什么一开始的时候会困惑呢?一开始的时候,为什么不会按照如此“自然”的思路来理解 kernel/user mode 的细节呢?

我想,这是因为我们太习惯于待在 high level 了,以至于不会很自然地切换到 low level 的视角来思考问题。通过「暴露有限的 API 来控制权限」是一种常用的手段,但习惯于 high level 视角的我们会习惯于「调用 API 就意味着 caller 本身可以被拆分为多步」,而如果 caller function 本身不能被拆分了,那么这个经典的解决方案就不起作用了。而以 low level 的视角来讲,「是否能调用 function」并不是等价于「function 是否能被拆分」,而是等价于「流程控制」,即:「执行流」能否从一个地方被转移到另一个地方。

所以,为什么我们无法一开始就理解这样显然的解决方案?因为解决方案的思路虽然是平凡的,但解决方案的实现却是以 low level 的视角来实现的。CPU 虽然无法做到 high level 视角下的 caller function 拆分,但却可以通过抛出 exception 来做流程控制,从而实现子函数调用。其实本来,所谓函数调用,不就是流程控制的切换吗?

当然,我们还可以再进一步追问,如果是流程控制,为什么不能直接使用 jump 操作,为什么要使用如此曲折的抛出 exception 的方式呢?事实上,exception handler 本身也就是 jump 的一种实现方式,只不过,通过注册到 trap table 来管理各种 handler 的结构要更为清晰一些。并且,对于那些无效的 exception,可直接根据无法查询到相应的 trap handler 来放弃做处理。

绚烂至极,归于平淡,绕了一圈的骚操作,终于可以以最平凡的步骤来理解了。

 

 

近期回顾

备忘录:数学、玄学与科学
简评:并发问题的牛鼻子
能力圈:可以不焦躁且专注于自我世界的基础

 

如果你喜欢我的文章或分享,请长按下面的二维码关注我的微信公众号,谢谢!

图片

 

 
posted @ 2021-01-27 20:59  kid551  阅读(220)  评论(0编辑  收藏  举报