1. CPU虚拟化
操作系统也被叫做资源管理器。
将单个cpu转换为看似无限数量的cpu、从而让许多程序看似同时运行、这就是所谓的虚拟化cpu。
时分共享CPU技术: 通过让一个进程只运行一个时间片,然后切换到其他进程中,操作系统提供了存在多个虚拟CPU的假象
1.1 虚拟化内存
内存就是一个字节数组、要读取内存、必须制定一个地址、才可以访问到内存中的数据、程序运行时、要一直访问内存、将所有的数据结构保存在内存中,并通过各种指令来访问他们,程序的每个指令也在内存中、因此每次读取指令也需要访问内存。
每个进程访问自己的私有虚拟地址空间、与其他运行的进程互不干扰。
1.2 寄存器
进程的机器状态的另一部分时寄存器,许多指令明确的读取或更新寄存器,有一些特殊的寄存器例如:
程序计数器(指令指针):主要告诉我们程序即将执行哪个命令,
栈指针、帧指针用于管理函数参数栈、局部变量及返回地址
1.3 进程
1.3.1 进程创建
程序如何转换为进程?
- 操作系统做的第一件事就是将代码和所有的静态资源加载到内存中,加载到内存的地址空间中,(现代计算机懒加载:在需要运行时才会加载到内存)
- 分配一些内存给程序的运行时栈(存放局部变量、函数参数和返回地址)
- 分配一些内存给堆(比如对象或者链表 、树等数据结构)
- 其他的特别任务,如输入/输出(I/O)相关任务。
1.3.2 进程状态

1.3.3 两个进程间的状态交换 有I/O
| 时间 | ProcessO | PROCESS1 | 注 |
|---|---|---|---|
| 1 | 运行 | 就绪 | |
| 2 | 运行 | 就绪 | |
| 3 | 运行 | 就绪 | PROCESS0发起I/O |
| 4 | 阻塞 | 运行 | PROCESS0阻塞 |
| 5 | 阻塞 | 运行 | PROCESS1运行 |
| 6 | 阻塞 | 运行 | |
| 7 | 就绪 | 运行 | I/O完成 |
| 8 | 就绪 | 运行 | PROCESS1完成 |
| 9 | 运行 | – | |
| 10 | 运行 | – | PROCESS0完成 |
2 CPU虚拟化机制
2.1 受限直接执行
为了实现虚拟化,操作系统需要以某种方式让许多任务共享物理CPU,让他们看起来像在同时运行。
2.1.1 直接运行协议(无限制)
| 操作系统 | 程序 |
|---|---|
| 在进程列表上创建条目 为程序分配内存 将程序加载到内存中 根据argc/argv设置程序栈 | |
| 清除寄存器 执行main方法 | |
| 执行main方法 从main中执行return | |
| 释放进程的内存将进程从进程列表中删除 |
2.1.2 受限制的操作
2.2.1中存在一些问题 如果进程希望执行某些受限制操作(如磁盘发出I/O请求或申请资源)怎么办?
2.1.2.1 用户模式
为了解决上述问题 操作系统出了2种模式。在用户模式下运行的代码会受到限制如发出I/O指令、操作系统会引发异常、可能会终止进程。
2.1.2.2 内核模式
在内核模式下、运行的代码可以做他喜欢做的事情。包括特权操作。
2.1.2 模式间的切换
操作系统允许内核模式中的权限小力度的透给用户模式使用。
要执行这些指令、程序必须执行特殊的陷阱指令。该指令可以让程序跳入内核并将权限提升到内核模式。完成后操作系统调用特殊的陷阱返回指令。降低权限回到用户指令。
陷阱指令如何知道在os中运行那些代码?内核模式通过在启动中设置陷阱表来实现。
2.1.2 直接运行协议(受限)
| 操作系统@内核模式 | 硬件 | 程序 |
|---|---|---|
| 初始化陷阱表 | ||
| 记住系统调用处理程序的地址 | ||
| 在进程列表上创建条目 为程序分配内存 将程序加载到内存中 根据argc/argv设置程序栈 用寄存器/程序计数器填充内核栈 从陷阱返回 | ||
| 从内核栈恢复寄存器 转向用户模式 跳到main | ||
| 运行main 调用系统调用、陷入操作系统 | ||
| 将寄存器保存内核栈 转向内核模式 跳到陷阱处理程序 | ||
| 处理陷阱 系统调用 陷阱返回 | ||
| 从内核栈恢复寄存器 转向用户模式 跳到陷阱之后的程序计数器 | ||
| …从main返回 exit() | ||
| 释放进程的内存将进程从进程列表中清除 |
2.2 协作方式和时钟中断
协作方式:等系统自动调用并获得CPU控制权
时钟中断:时钟设备可以编程为每隔几毫秒产生一次中断、中断时正在运行的程序终止,操作系统预先配置的中断处理程序会开始运行、操作系统重新获得CPU控制权,并安排后续的执行等。
| 操作系统 | 硬件 | 备注 |
|---|---|---|
| 初始化陷阱表 | . | |
| 记录地址:系统调用处理程序、时钟处理程序 | . | |
| 启动中断时钟 | . | |
| 启动时钟、每隔x ms中断CPU | . | |
| .进程A | ||
| 时钟中断、将寄存器A保存到内核栈A 转向内核模式、跳到陷阱处理程序 | . | |
| 处理陷阱 将寄存器A保存到进程结构A、将进程结构B恢复到寄存器B 从陷阱返回(进入B) | . | |
| 从内核栈B恢复寄存器B、转向用户模式、跳到B的程序计数器 | . | |
| .进程B |
3. 进程调度
调度指标:
T周转时间= T完成时间-T到达时间
T响应时间=T首次运行-T到达时间
3.1 先进先出FIFO
按照先进先出调度、假设A、B、C同时到达系统、且A比B早一点点、B比C早一点点 他们的运行时间都是10m 那么平均周转时间就是T周转时间 = (10+20+30)/3 = 20 (因为程序串行执行需要等上一个进程执行完成)。如果A要运行100秒、其他的运行10秒呢?
平均周期:(100+110+120)/3
3.2 最短任务优先SJF
最短时间的优先执行、次短的其次执行。
在用上面的例子,平均周期(10+20+120)/3。
虽然平均周期得到了提升但假如任务不是同时到达而是分别到达呢、比如A在t=0时到达、BC在t=10到达?
这样BC会被迫等待A执行完成在进行调度
3.3 最短完成时间优先(STCF)
上述的两种调度由于都是非抢断式的、需要等上一个任务执行完成才会执行下一个。但是我们可以通过前面说的时钟中断方式重新获得CPU控制权并将当前执行程序保存至上下文中、进行后续调用。即向SJF添加抢占、称为最短完成时间优先。
3.4 轮转
根据时钟中断不停切换运行的进程(RR时间片必须是时钟中断的倍数)直到完成。
摊销:设置足够长的时间片用于降低上下文切换成本比如时间片100m 上下文切换1m这样比例为1%。
4. 调度
4.1 多级反馈队列 MLFQ
4.1.1 基本原则
MLFQ中有许多独立的队列、每隔队列有不同的优先级、任何时刻、一个任务只能存在于一个队列中。MLFQ会优先执行较高优先级的工作。当每个队列中存在多个任务时,该队列采用轮转调度。MLFQ通过任务的历史执行情况来预测他未来的行为。
- 规则1:如果A优先级>B,运行A。
- 规则2:如果A、B优先级相等,轮转执行。
- 规则3:工作进入系统时、放在最高优先级。
- ~~ 规则4a:工作用完整个时间片后、降低其优先级 下移一位。~~
- ~~ 规则4b:如果工作在时间片内主动释放CPU、优先级不变。~~
- 规则4:一旦程序用完时间配额、无论主动放弃多少次、都会降级
- 规则5:每经过一段时间、系统就会将所有任务重新加入最高优先级
4.2 比例份额
也称公平份额调度程序、现代使用的典型例子彩票调度:
每隔一段时间、都会进行一次彩票选举,以确定接下来要执行的程序,越是应该频繁运行的程序、越是应该拥有更多赢得彩票的机会。
4.2.1 基本概念
- 彩票调度:为每个程序设置票数、在时间片执行完成后、进行随机选举确定下一个执行程序。
例:A 100 B 50 C 40 随机数为120 则运行B - 步长调度:认为ABC开始都是0,根据票数设置步长。根据最小布长进行切换
| A 步长=100 | B 步长=200 | C 步长=40 | 谁执行 |
|---|---|---|---|
| 0 | 0 | . 0 | . A执行 |
| 100 | 0 | .0 | .B |
| 100 | 200 | .0 | .C |
| 100 | 200 | 40. | C. |
| 100 | 200 | 80. | .C |
| 100 | 200 | .120 | .A |
| 200 | 200 | .160 | .C |
4. 虚拟化内存
4.1 机制:地址转换
直接受限访问的补充,硬件每次内存访问进行处理(指令获取、数据读取、写入)、将指令中的虚拟地址转换为数据实际存储的物理地址。每次访问,将内存引用重定位到内存中实际位置。营造出每个进程都有自己独立的内存空间
4.1.1 动态重定向
如何让物理地址不是从0开始而是从指定位置开始 :
cpu需要2个硬件寄存器:基址寄存器和界限寄存器(也被称为限制寄存器),这两个寄存器可以让我们的地址空间放在屋里内存任意位置,有能确保只能访问自己的空间。
基址寄存器: 用于保存物理地址开始位置。
界限寄存器: 用于确保只能访问自己的空间。
保护位: 用于判断进程进操是否有特定访问权限
通过基地寄存器和界限寄存器实现的虚拟内存会导致非常多的浪费、那如何支持大地址空间实例呢?
4.1.2 分段
在MMU(内存管理单元)中引入不止一个基址和界限寄存器,给每个地址空间内的逻辑段一对,一个段是地址空间内一个连续定长的区域,如代码、堆、栈。
如何快速定位到虚拟地址对应的物理空间真实位置?
显式方式,用虚拟地址开头的几位标识不同的段。如下图、有3个段、所以取前两位作为标记,用14位虚拟地址来标识,如下

堆判断
14位虚拟地址:堆地址在虚拟地址上为41024 = 4096,4096换算2进制14位为 01000001101000,所以如果为01、硬件就知道该地址是堆。

以虚拟地址4200为例(由于堆位置在虚拟地址4KB位置应该为4096),所以偏移量就是4200-4096=104,加上基址寄存器存的物理地址34KB,真实物理地址为341024+104 = 34920,这就是真实地址位置。
栈判断
栈与其他存储不同、栈是反向扩展、比如上图栈初始位置在28KB,延伸到26KB,对应的虚拟地址则是16-14KB。
硬件为了支持该反向判断,须新增是否正向增长标记、1代表向下、0代表线下。
如:
15KB虚拟地址二进制为:11110000000000,前两位为指定段、然后我们要处理偏移量3KB
4.1.3 空闲队列

分割:当程序请求内存时,通过空闲列表找到大小足够的内存块,如上图如果申请大小为1的空间,内存会变成1-10 空闲,10-20 使用,20-21使用,21-30空闲。
合并:当程序调用free()归还空间时,程序会判断与其相邻的队列是否空闲进行合并处理。
4.1.3.1 追踪已分配内存空间的大小
为了方便快速定位内存位置,我们需要在头块(header)中保存一些其他信息,如内存大小,和一个幻数用于完整性检查,当操作系统分配内存时并非找到需要的内存大小、而是需要找到内存大小N+头块大小的空间列
4.1.3.2 让堆增长
当堆被用光后无法在继续分配空间,会在向操作系统申请更大的空间,将他们映射到请求进程的地址空间中去,并返回新的堆的末尾地址。就得到了更大的堆。
4.1.4 基本策略
最优匹配
遍历整个空闲列表,找到和请求大小一样或更大空间的空闲块、最后选择候选者中最小的一块返回。
最差匹配
找到最大的块并进行分割满足用户需求,再将剩余的块加入空闲列表。
首次匹配
找到第一个足够大的块,将请求的空间返回给用户,同样将剩余的空闲空间留给后续请求。优势不需要遍历所有的空闲块,但可能会让空闲列表开头有很多的小块。
下次匹配
不同于首次匹配、下次匹配多维护了一个指针,指向上一次结束的位置
分离空闲列表
基本想法 如果某个程序经常申请一种或几种大小的内存空间,那就用一个独立的列表来单独管理,其他大小的请求交给通用的内存分配程序。
如超级工程师jeff设计的厚块分配程序,在内核启动时,为可能频繁请求的内核程序创建一些对象缓存,并分离出一些特定大小的空闲队列。
4.1.5 分页
有人说,操作系统有2种方法来解决空间管理问题、第一种是将分割成不同长度大小的分片,就像虚拟内存中的分段、这种方法的弊端就是会产生大量的空间碎片,第二种就是将空间分割成固定长度的分片,这种思想也叫做分页。
页表

为了记录地址空间每个虚拟页放在物理内存中的位置,主要作用是为地址空间的每个虚拟页 保存地址转换。如上图页表具有四个条目:虚拟页0→物理帧3。
- 有效位:如栈和堆在空间中是两端、中间部分为无效。
- 保护位:是否可执行访问
- 存在位:表示该页是在物理存储器上还是磁盘上。
- 参考位:追踪页是否被访问。
- 脏位:表明页面被带入内存后是否被修改过
未完待续
本文详细探讨了CPU虚拟化原理、内存管理机制(包括虚拟地址转换、分段和页表)、进程创建与状态、进程调度算法(如FIFO、SJF、STCF和MLFQ),以及内存分配策略。涵盖了操作系统如何利用时钟中断实现进程抢占和内存空间的有效管理。
1331





