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。
- 有效位:如栈和堆在空间中是两端、中间部分为无效。
- 保护位:是否可执行访问
- 存在位:表示该页是在物理存储器上还是磁盘上。
- 参考位:追踪页是否被访问。
- 脏位:表明页面被带入内存后是否被修改过
未完待续