转载自 link
操作系统导论
[图片上传失败...(image-13203a-1630146185361)]
第4章 进程
运行的程序就是进程,操作系统有很多关于进程的API,
·创建(create):操作系统必须包含一些创建新进程的方法。在shell中键入命令或双击应用程序图标时,会调用操作系统来创建新进程,运行指定的程序。
·销毁(destroy):由于存在创建进程的接口,因此系统还提供了一个强制销毁进程的接口。当然,很多进程会在运行完成后自行退出。但是,如果它们不退出,用户可能希望终止它们,因此停止失控进程的接口非常有用。
·等待(wait):有时等待进程停止运行是有用的,因此经常提供某种等待接口。
·其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他控制。例如,大多数操作系统提供某种方法来暂停进程(停止运行一段时间),然后恢复(继续运行)。
·状态(statu):通常也有一些接口可以获得有关进程的状态信息,例如运行了多长时间,或者处于什么状态。
把程序和静态数据加载到内存,然后执行他就成了进程,现代操作系统将加载的过程lazily懒惰化了,需要用的时候才加载
进程有三个状态,运行、就绪、阻塞
第5章 进程API
fork , 复制当前的进程,父进程返回子进程pid,子进程返回0
wait,父进程等待子进程执行完毕
exec,加载某个程序到内存中运行,覆盖当前进程
shell也是一个用户程序[插图],它首先显示一个提示符(prompt),然后等待用户输入。你可以向它输入一个命令(一个可执行程序的名称及需要的参数),大多数情况下,shell可以在文件系统中找到这个可执行程序,调用fork()创建新进程,并调用exec()的某个变体来执行这个可执行程序,调用wait()等待该命令完成。子进程执行结束后,shell从wait()返回并再次输出一个提示符,等待用户输入下一条命令。
第6章 机制:受限直接执行
OS不可能说,创建了一个进程,把所有权限完全交给进程,然后把自己挂起,那坏蛋写一个死循环,我们就只能重启计算机了,OS会把CPU交给进程,但是他如何拿回来呢?
答案很简单,许多年前构建计算机系统的许多人都发现了:时钟中断(timer interrupt)[M+63]。时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统重新获得CPU的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。
<span style="color:red">第8章 调度:多级反馈队列</span>
·规则1:如果A的优先级 > B的优先级,运行A(不运行B)。
·规则2:如果A的优先级 = B的优先级,轮转运行A和B。
·规则3:工作进入系统时,放在最高优先级(最上层队列)。
·规则4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。
·规则5:经过一段时间S,就将系统中所有工作重新加入最高优先级队列。
规则4是反馈
规则5可避免饥饿
第9章 调度: 比例份额
彩票调度
很多人都买过彩票,但是很少有人中奖,我们假设有100张彩票,有AB两个人分别买了75张和25张,然后我们进行随机开奖,开到一张票,这张票在谁手上谁就中奖。
同理,我们假设AB是两个进程,则中奖的那位就能获得CPU控制权。
彩票货币
如果进程可以将自己的彩票分配给自己的线程,则实现了彩票货币化。
彩票转让
一个进程可以吧自己的彩票分给其他进程,例如客户端把彩票分给服务端以加快服务端处理速度。
彩票通胀
最后,彩票通胀(ticket inflation)有时也很有用。利用通胀,一个进程可以临时提升或降低自己拥有的彩票数量。当然在竞争环境中,进程之间互相不信任,这种机制就没什么意义。
算法实现
我们只需要记录每个进程拥有多少彩票,并把他们排好,然后OS生产随机数,从第一个进程开始,对彩票数进行累加,当累和第一次超过随机数时,这个进程中奖。
我们还可以对进程按照彩票数从大到小排好序,这样可以加快遍历速度。
步长调度
A、B、C这3个工作的票数分别是100、50和250,我们通过用一个大数分别除以他们的票数来获得每个进程的步长。比如用10000除以这些票数值,得到了3个进程的步长分别为100、200和40。我们称这个值为每个进程的步长(stride)。每次进程运行后,我们会让它的计数器 [称为行程(pass)值] 增加它的步长,记录它的总体进展。
当进行调度的时候,取出行程最小的进程进行调度,如果有多个,则从中随机选择任意一个。
第10章 多处理器调度(高级)
问题
- 缓存一致性: CPU1修改了自己的缓存,但是CPU2没有同步修改怎么办?
硬件提供了这个问题的基本解决方案:通过监控内存访问,硬件可以保证获得正确的数据,并保证共享内存的唯一性。在基于总线的系统中,一种方式是使用总线窥探(bus snooping)[G83]。每个缓存都通过监听链接所有缓存和内存的总线,来发现内存访问。如果CPU发现对它放在缓存中的数据的更新,会作废(invalidate)本地副本(从缓存中移除),或更新(update)它(修改为新值)。回写缓存,如上面提到的,让事情更复杂(由于对内存的写入稍后才会看到),你可以想想基本方案如何工作
- 并行(发)问题:
如果线程1执行第一行,会将head的当前值存入它的tmp变量。如果线程2接着也执行第一行,它也会将同样的head值存入它自己的私有tmp变量(tmp在栈上分配,因此每个线程都有自己的私有存储)。因此,两个线程会尝试删除同一个链表头,而不是每个线程移除一个元素,
void rease(){
Node *tmp = head;
// ...
free(tmp);
}
- 缓存亲和度: 一个线程在某个CPU上运行,他会有一些缓存,如果他切换到其他CPU上,则缓存需要重新加载。
单队列多处理器调度
(Single QueueMultiprocessor Scheduling,SQMS) 一个队列,处理器需要执行时,锁住队列,然后从队列取出任务,最后释放锁,但是他不好解决亲和度问题和锁的问题。
多队列多处理器调度
(Multi-QueueMultiprocessor Scheduling,MQMS)多个队列,一般而言,一个处理器一个队列,如果任务数量能整除CPU数量,这很好分配,无法整除的时候,就涉及到了负载不均的问题。
最明显的答案是让工作移动,这种技术我们称为迁移(migration)。通过工作的跨CPU迁移,可以真正实现负载均衡。
注意我们要让任务轮流迁移,否则就不太公平
另一个方法是窃取工作量少的队列不定期偷看其他队列,如果发现他比自己多,则偷取一个任务。
如果太频繁地检查其他队列,就会带来较高的开销,可扩展性不好,而这是多队列调度最初的全部目标!相反,如果检查间隔太长,又可能会带来严重的负载不均。
第15章 机制:地址转换
动态(基于硬件)重定位
即基址加界限机制,所有的程序都认为自己的地址空间从0开始,但是当他执行的时候,操作系统会决定其在物理内存中的实际加载地址,并将起始地址记录在基址寄存器中。
第16章 分段
问题:栈和堆之间,有一大块“空闲”空间,造成了浪费
在硬件中使用三对寄存器来分别记录代码段,堆和栈
问题: 如何判断一个地址是代码段,堆还是栈?
虚拟地址前两位表示
问题: 如何共享?
操作系统为段记录额外信息,
[图片上传失败...(image-e2b0a1-1630146185361)]
第17章 空闲空间管理
要满足变长的分配请求,应该如何管理空闲空间?什么策略可以让碎片最小化?不同方法的时间和空间开销如何?
OS如何知道free的内存有多大?
在C中,每当我们使用malloc的时候,OS会给我们分配一块内存,当我们使用free的时候,这块内存会被释放,然而这个时候OS如何知道这块内存有多大,他该如何来释放呢?
[图片上传失败...(image-e694b8-1630146185361)]
其实malloc返回给我们的内存是小于OS给我们的内存的,可以看到OS在这块内存的前面加了一些信息,包括内存的大小和一个校验部分。
OS如何组织空闲内存?
每块内存的开头都有一个部分,包括这块内存的大小以及下一块内存的地址
[图片上传失败...(image-fa9087-1630146185361)]
内存分配和释放
OS分配内存的时候,会在他的内存链表中找到一块合适的,将其切割并移除链表分配给应用程序,当OS回收的时候,会将其放回链表,但是这个办法经历了长时间的内存管理以后,OS的内存将会一团糟,这个时候操作系统又会合并相邻块,之后内存又成为了一个整体。
如何选择链表中的内存块?
匹配算法 | 备注 | |
---|---|---|
最优匹配 | 在所有可以分割的节点中,选择内存最小的那个节点 | |
最差匹配 | 在所有可以分割的节点中,选择内存最大的那个节点 | |
首次匹配 | 寻找过程中,发现的第一个能匹配的节点 | |
下次匹配 | 在首次匹配的基础上,每次查询的链表起点就是上一次匹配的节点 |
有趣的匹配算法
分离空闲列表:
如果某个应用程序经常申请一种(或几种)大小的内存空间,那就用一个独立的列表,只管理这样大小的对象。其他大小的请求都交给更通用的内存分配程序。
伙伴系统:
[图片上传失败...(image-57eee8-1630146185361)]
第18章 分页:介绍
页表是什么,在哪里?
用一张表,储存虚拟地址到物理地址的映射,往往虚拟地址和物理地址的前几位是不同的,这些不同的位被称为页,而剩下的成为偏移量。从虚拟地址映射到物理地址只需要将前几位进行转换即可,这张表就叫做页表。
页表储存在内存中,每个进程都拥有一份自己的页表。
页表中有什么
页表往往是一个数组映射,所以没有物理叶帧(数组下标)
代号 | 意义 | 备注 |
---|---|---|
P | 存在位 | 表示该页表是否被进程申请使用 |
R/W | 读写权限位 | 表示该页面权限是可读还是可写 |
U/S | 访问是否需要特权级 | |
PWT | ||
PCD | ||
A | 访问位 | 近期是否访问过该页 |
D | 脏位 | 该页是否被写 |
PAT | ||
G | ||
? | 操作系统自定义使用 | |
PFN | 叶帧 |
[图片上传失败...(image-2cf011-1630146185361)]
分页很慢
分页机制导致与内存发生的交互次数翻倍了,程序更慢了。
第19章 分页:快速地址转换(TLB)
高速页表缓冲区:地址转换旁路缓冲存储器(translation-lookasidebuffer)
这是一个硬件,他能缓存页表地址转化,速度远高于内存读取
TLB未命中
TLB和其他缓存相似,前提是在一般情况下,转换映射会在缓存中(即命中)。如果是这样,只增加了很少的开销,因为TLB处理器核心附近,设计的访问速度很快。如果TLB未命中,就会带来很大的分页开销。必须访问页表来查找转换映射,导致一次额外的内存引用(或者更多,如果页表更复杂)。如果这经常发生,程序的运行就会显著变慢。相对于大多数CPU指令,内存访问开销很大,TLB未命中导致更多内存访问。因此,我们希望尽可能避免TLB未命中。
一般是操作系统来处理未命中,发生未命中,硬件抛异常,进入内核,陷入操作系统,操作系统更新TLB,返回之前的未命中代码进行重试
重点细节:
- 返回进入陷阱的那条代码重试。
- OS千万不能在处理TLB时又触发未命中,这将导致无限递归。
进程上下文切换
页表是进程私有的,如果发生了进程上下文切换,则TLB全部变为无效。
上下文切换的时候清空TLB,这是一个可行的解决方案,进程不会再读到错误的地址映射。但是,有一定开销:每次进程运行,当它访问数据和代码页时,都会触发TLB未命中。如果操作系统频繁地切换进程,这种开销会很高。
为了减少这种开销,一些系统增加了硬件支持,实现跨上下文切换的TLB共享。比如有的系统在TLB中添加了一个地址空间标识符(Address Space Identifier,ASID)。可以把ASID看作是进程标识符(Process Identifier,PID),但通常比PID位数少(PID一般32位,ASID一般是8位)。
TLB项中有什么
TLB是并行硬件,他可以并行查找
代号 | 意义 | 备注 |
---|---|---|
VPN | ||
R/W | 全局位 | 是否全局共享 |
PWT | 进程空间 | 属于哪个进程 |
PFN | ||
C | 一致位 | 决定硬件如何缓存 |
D | 脏位 | 是否被写入新数据 |
V | 有效位 | 该映射是否有效(注意区别页表的存在位) |
[图片上传失败...(image-fecf73-1630146185361)]
第20章 分页:较小的表
页表太大,因此消耗的内存太多。
假设一个32位地址空间(232字节),4KB(212字节)的页和一个4字节的页表项。一个地址空间中大约有一百万个虚拟页面(232/212)。乘以页表项的大小,你会发现页表大小为4MB。
使用更大的页
这种方法的主要问题在于,大内存页会导致每页内的浪费,这被称为内部碎片(internalfragmentation)问题
结合分段
我们可能有3个页表,地址空间的代码、堆和栈部分各有一个
杂合方案的关键区别在于,每个分段都有界限寄存器,每个界限寄存器保存了段中最大有效页的值。
分段并不像我们需要的那样灵活,因为它假定地址空间有一定的使用模式。例如,如果有一个大而稀疏的堆,仍然可能导致大量的页表浪费。
多级页表
分配更多级的页表,这是使用时间换空间
反向页表
记录物理页映射到虚拟页,而不是虚拟页映射到物理页
第21章 超越物理内存:机制
我们的内存始终是有限的,如何超越这个限制,为应用程序提供无限空间的假象呢?
操作系统如何利用大而慢的设备,透明地提供巨大虚拟地址空间的假象?
操作系统利用大而慢的磁盘和内存中的数据进行交换,借此实现了超越物理内存的目的。
页错误
如果一个页不存在,它已被交换到硬盘,在处理页错误的时候,操作系统需要将该页交换到内存中。那么,问题来了:操作系统如何知道所需的页在哪儿?在许多系统中,页表是存储这些信息最自然的地方。因此,操作系统可以用PTE中的某些位来存储硬盘地址,这些位通常用来存储像页的PFN这样的数据。当操作系统接收到页错误时,它会在PTE中查找地址,并将请求发送到硬盘,将页读取到内存中。
交换何时发生?
OS不会等到内存完全满了才进行交换,OS往往会预留BUF。
为了保证有少量的空闲内存,大多数操作系统会设置高水位线(High Watermark,HW)和低水位线(Low Watermark,LW),来帮助决定何时从内存中清除页。原理是这样:当操作系统发现有少于LW个页可用时,后台负责释放内存的线程会开始运行,直到有HW个可用的物理页。这个后台线程有时称为交换守护进程(swap daemon)或页守护进程(page daemon)[插图],它然后会很开心地进入休眠状态,因为它毕竟为操作系统释放了一些内存
第22章 超越物理内存:策略
最优替换策略
只存在于理论上的策略,他的作用是评估其他算法
FIFO
先进先出
补充:Belady的异常Belady(最优策略发明者)及其同事发现了一个有意思的引用序列[BNS69]。内存引用顺序是:1,2,3,4,1,2,5,1,2,3,4,5。他们正在研究的替换策略是FIFO。有趣的问题:当缓存大小从3变成4时,缓存命中率如何变化?
一般来说,当缓存变大时,缓存命中率是会提高的(变好)。但在这个例子,采用FIFO,命中率反而下降了!你可以自己计算一下缓存命中和未命中次数。这种奇怪的现象被称为Belady的异常(Belady’s Anomaly)。
其他一些策略,比如LRU,不会遇到这个问题。可以猜猜为什么?事实证明,LRU具有所谓的栈特性(stack property)[M+70]。对于具有这个性质的算法,大小为N + 1的缓存自然包括大小为N的缓存的内容。因此,当增加缓存大小时,缓存命中率至少保证不变,有可能提高。先进先出(FIFO)和随机(Random)等显然没有栈特性,因此容易出现异常行为。