cpu没有虚拟化怎么用安卓软件_cpu没有虚拟化怎么用安卓软件

操作系统的虚拟化有很多内容,本文就CPU的虚拟化作一个简单介绍,希望能够讲解清楚这个概念。

首先明确一下概念,我们这里所说的操作系统对CPU的虚拟化,具体指的是什么?一般情况下,一台PC机只有一个CPU,而一个CPU在某一个确定的时间点只能执行一条指令,但我们在使用的时候却不这样觉得。比如,你可能一边在复制一个较大的文件,一边在用Word应用程序来码字。这给你的感觉是CPU好像同时在做两件事情,怎么可能?这时我们说,CPU在微观上是串行的,在宏观上是并行的。这里的CPU给你的假象就是操作系统对CPU的虚拟化技术来实现的。

一、进程

谈到对CPU的虚拟化,就不得不提到进程。操作系统为正在运行的程序提供的抽象,就是所谓的进程。拷贝文件和码字就是两个不同的进程。为了理解这个概念的构成要素,我们需要理解它的机器状态。进程的机器状态大体上有两个主要的组成部分,一个是它可以访问的内存(也叫地址空间),另一个就是CPU的相关的寄存器,比如IP,SP等。操作系统会为进程预留相关的API,比如创建(create),销毁(destroy),等待(wait)等,以使得进程可以在诸如运行,就绪,阻塞的状态之间来回切换。下表是跟踪两个进程的状态的一个例子(假设这两个进程里的指令没有涉及到I/O操作,只使用了CPU):


表1 进程的状态

时间

进程0

进程1

1

运行

就绪

2

运行

就绪

3

运行

就绪

4

运行

就绪

进程0现在完成

5

-

运行

6

-

运行

7

-

运行

8

-

运行

进程1现在完成

如果进程涉及I/O请求,那可能是下面的情况:


表2 涉及I/O的进程的状态

时间

进程0

进程1

1

运行

就绪

2

运行

就绪

进程0发起I/O

3

阻塞

运行

进程0被阻塞,所以进程1运行

4

阻塞

运行

5

就绪

运行

I/O完成

6

就绪

运行

进程1现在完成

7

运行

-

8

运行

-

进程0现在完成

即使在这个简单的例子中,操作系统也必须做出许多决定。首先,系统必须决定在进程0发出I/O时运行进程1。这样做可以通过保持CPU繁忙来提高资源利用率。其次,当I/O完成时,系统决定不切换回进程0(是否切换取决于操作系统的相关调度策略)。


二 、受限直接运行

为了虚拟化CPU,操作系统需要以某种方式让许多任务共享物理CPU,让它们看起来像是同时运行。基本思路是:运行一个进程一段时间,然后运行另一个进程,如此轮换。通过以这种方式时分共享(time sharing)CPU,就实现了虚拟化。构建这样的虚拟化机制时存在一些挑战。最主要的挑战就是控制权的问题:如何在高效地运行进程的同时保留对CPU的控制?控制权对于操作系统尤为重要,因为操作系统负责资源管理。如果没有控制权,一个进程可以简单地无限制运行并接管机器,或访问没有权限的信息。受限直接执行(limiteddirect execution, LDE)主要就是解决这个问题。先看直接执行,所谓的“直接执行”就是只需直接在CPU上运行程序即可。当操作系统希望启动程序运行时,它会在进程列表中为其创建一个进程条目,为其分配一些内存,将程序代码(从磁盘)加载到内存中,找到入口点(main()函数或类似的),跳转到那里,并开始运行用户的代码。表3展示了这种基本的直接执行协议(没有任何限制),使用正常的调用并返回跳转到程序的main(),并在稍后回到内核。表3 直接运行协议(无限制)


操作系统

程序

在进程列表上创建条目为程序分配内存将程序加载到内存中根据argc/argv设置程序栈

清除寄存器执行call main方法

执行main()从main中执行return

释放进程的内存,将进程从进程列表中移除

听起来很简单,不是吗?但是,这种方法在我们虚拟化CPU时产生了一些问题。第一个问题:如果我们只运行一个程序,操作系统怎么能确保程序不做任何我们不希望它做的事,同时仍然高效地运行它?第二个问题:当我们运行一个进程时,操作系统如何让它停下来并切换到另一个进程,从而实现虚拟化CPU所需的时分共享?直接执行的明显优势是快速。但是,在CPU上运行会带来一个问题——如何防止进程执行我们不希望它执行的指令?我们采用的方法是引入一种新的处理器模式,称为用户模式(usermode)。在用户模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能发出I/O请求。这样做会导致处理器引发异常,操作系统可能会终止进程。与用户模式不同的内核模式(kernel mode),操作系统(或内核)就以这种模式运行。在此模式下,运行的代码可以做它喜欢的事,包括特权操作,如发出I/O请求和执行所有类型的受限指令。但是,我们仍然面临着一个挑战——如果用户希望执行某种特权操作(如从磁盘读取),应该怎么做?为了实现这一点,几乎所有的现代硬件都提供了用户程序执行系统调用的能力。系统调用是在Atlas等古老机器上开创的,它允许内核小心地向用户程序暴露某些关键功能,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令同时跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。还有一个重要的细节没讨论:一旦进入陷阱,陷阱怎么知道在OS内运行哪些代码?实际上,内核通过在启动时设置陷阱表(trap table)来实现。当机器启动时,它在特权(内核)模式下执行,因此可以根据需要自由配置机器硬件。操作系统做的第一件事,就是告诉硬件在发生某些异常事件时要运行哪些代码。例如,当发生硬盘中断,发生键盘中断或程序进行系统调用时,应该运行哪些代码?操作系统通常通过某种特殊的指令,通知硬件这些陷阱处理程序的位置。一旦硬件被通知,它就会记住这些处理程序的位置,直到下一次重新启动机器,并且硬件知道在发生系统调用和其他异常事件时要做什么(即跳转到哪段代码)。假设每个进程都有一个内核栈,在进入内核和离开内核时,寄存器(包括通用寄存器和程序计数器)分别被保存和恢复。受限直接运行的协议可以用表4来描述:


表4 受限直接运行协议

操作系统启动(内核模式)

硬件

初始化陷阱表

记住系统调用处理程序的地址

操作系统运行(内核模式)

硬件

程序(应用模式)

在进程列表上创建条目

为程序分配内存

将程序加载到内存中

根据argv 设置程序栈

用寄存器/程序计数器填充内核栈

从陷阱返回

从内核栈恢复寄存器

转向用户模式

调到main

运行main

……

调用系统调用

陷入操作系统

将寄存器保存到内核栈

转向内核模式

跳到陷阱处理程序

处理陷阱

做系统调用的工作

从陷阱返回

从内核栈恢复寄存器

转向用户模式

跳到陷阱之后的程序计数器

……从main返回

陷入(通过exit())

释放进程的内存,将进程从进程列表中清除

LDE协议有两个阶段。第一个阶段(在系统引导时),内核初始化陷阱表,并且CPU记住它的位置以供随后使用。内核通过特权指令来执行此操作。第二个阶段(运行进程时),在使用从陷阱返回指令开始执行进程之前,内核设置了一些内容(例如,在进程列表中分配一个节点,分配内存)。这会将CPU切换到用户模式并开始运行该进程。当进程希望发出系统调用时,它会重新陷入操作系统,然后再次通过从陷阱返回,将控制权还给进程。该进程然后完成它的工作,并从main()返回。这通常会返回到一些存根代码,它将正确退出该程序(例如,通过调用exit()系统调用,这将陷入OS中)。此时,OS清理干净,任务完成了。直接执行的下一个问题是如何实现进程之间的切换。你可能会说,切换嘛,这有什么问题吗?但实际上这有点棘手,特别是,如果一个进程正在CPU上运行,这就意味着操作系统没有运行。如果操作系统没有运行,它怎么能做事情?(提示:它不能)虽然这听起来几乎是哲学,但这是真正的问题——如果操作系统没有在CPU上运行,那么操作系统显然没有办法采取行动。因此,我们遇到了关键问题——如何重获CPU的控制权?我们假定进程的代码是不可知的,也就是它会执行任何我们不能预料到的指令,不管是正确的或是错误的。比如,它有可能执行一个无限循环,一直占用着CPU。这种情况操作系统如何获取CPU的控制权?答案在许多年前构建计算机系统的许多人都发现了:时钟中断。时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统重新获得CPU的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。表5 的例子展示了加入时钟中断的受限直接执行协议是如何切换进程的。在这个例子中,进程A正在运行,然后被中断时钟中断。硬件保存它的寄存器(在内核栈中),并进入内核(切换到内核模式)。在时钟中断处理程序中,操作系统决定从正在运行的进程A切换到进程B。此时,它调用switch()例程,该例程仔细保存当前寄存器的值(保存到A的进程结构),恢复寄存器进程B(从它的进程结构),然后切换上下文(switch context),具体来说是通过改变栈指针来使用B的内核栈(而不是A的)。最后,操作系统从陷阱返回,恢复B的寄存器并开始运行它。


表5 受限直接执行协议(时钟中断)

操作系统启动(内核模式)

硬件

初始化陷阱表

记住以下地址:

系统调用处理程序

时钟处理程序

启动中断时钟

启动时钟,每隔x ms中断cpu

操作系统运行(内核模式)

硬件

程序(应用模式)

进程A……

时钟中断

将寄存器(A)保存到内核栈(A)

转向内核模式

跳到陷阱处理程序

处理陷阱

调用swith()例程

将寄存器(A)保存到进程结构(A)

将进程结构(B)恢复到寄存器寄存器(B)

从陷阱返回(进入B)

从内核栈(B)恢复寄存器(B)

转向用户模式

跳到B的程序计数器

进程B……

三 进程调度

至此,我们有了虚拟化CPU的基本机制。但还有一个问题没有答案:在特定时间,我们应该运行哪个进程?调度程序必须回答这个问题,进程调度程序解决了何时进行进程切换(注意,并不是每一个时钟中断产生之后都要进行进程切换,否则就太可怕了),以及每个进程运行多长时间。它还解决了如何快速响应用户的交互请求的问题。为了解决这些问题,许多聪明又努力的人在过去提出了很多相关的算法,比如先进先出(First In First Out,FIFO)算法,是最基本的算法,也最容易实现,有时候也被称之为先到先服务。还有最短任务优先(Shortest Job First,SJF): 先运行最短的任务,然后是次短的任务,如此下去。最短完成时间优先(Shortest Time-to-Completion First,STCF): 每当新工作进入系统时,它就会确定剩余工作和新工作中,谁的剩余时间最少,然后调度该工作。一种比较著名的调度算法是多级反馈队列(Multi-level FeedbackQueue,MLFQ)算法,多级反馈队列需要解决两方面的问题。首先,它要优化周转时间。其次,MLFQ希望给交互用户(如用户坐在屏幕前,等着进程结束)很好的交互体验,因此需要降低响应时间。这里的问题是:通常我们对进程一无所知,应该如何构建调度程序来实现这些目标?调度程序如何在运行过程中学习进程的特征,从而做出更好的调度决策?MLFQ中有许多独立的队列,每个队列有不同的优先级。任何时刻,一个工作只能存在于一个队列中。MLFQ总是优先执行较高优先级的工作(即在较高级队列中的工作)。当然,每个队列中可能会有多个工作,因此具有同样的优先级。在这种情况下,我们就对这些工作采用轮转调度。因此,MLFQ调度策略的关键在于如何设置优先级。MLFQ没有为每个工作指定不变的优先级,而是根据观察到的行为调整它的优先级。例如,如果一个工作不断放弃CPU去等待键盘输入,这是交互型进程的可能行为,MLFQ因此会让它保持高优先级。相反,如果一个工作长时间地占用CPU,MLFQ会降低其优先级。通过这种方式,MLFQ在进程运行过程中学习其行为,从而利用工作的历史来预测它未来的行为。有关进程调度的内容相对比较复杂,这里不多深入探讨,感兴趣的同学请自行查找相关资料。 以上,我们了解到似乎操作系统相当偏执。它希望确保控制机器。虽然它希望程序能够尽可能高效地运行 [因此也是受限直接执行(limited directexecution)背后的全部逻辑],但操作系统也希望能够对错误或恶意的程序说“啊!别那么快,我的朋友”。偏执狂全天控制,并且确保操作系统控制机器。也许这就是我们将操作系统视为资源管理器的原因。