操作系统导论-虚拟化-总结

313 阅读11分钟

书籍《操作系统导论》的总结

历史

早期的操作系统

只是一些函数库,让程序员省去一些需要写的通用代码,然后程序的执行由操作员去分批执行,当时操作系统没有交互式界面,因为实现的成本太高,所以这时的计算机有大部分实际是在闲置,然后很浪费资源

超越库,提供一些保护

这时候就出现了系统调用的概念,硬件层面支持了一些成为陷阱(trap)的东西, 当程序调用(系统调用)的时候会陷入到陷阱,从而让操作系统变更为内核模式,从而可以对系统做到保护作用

多道程序时代

这时候是小型机的时代,都希望于利用计算机资源(比如当一个程序陷入了io操作,为啥不让另一个程序执行,等io做完再让程序恢复运行),多道程序,所以这时候出现了内存保护等问题,然后程序中出现并发问题,这时候就出现了unix操作系统

摩登时代

这时候出现了个人pc,当时刚出现的时候其实是一种倒退,因为个人pc上的操作系统对内存保护的并不好,容易出现一大堆问题,随着时间发展一些小型计算机上的功能也逐渐的搬运到了个人计算机上

抽象,进程

进程的定义就是: 运行中的程序

进程有几种机器状态,第一个明显的部分是内存,因为指令存储再内存中,进程可以进行读取和写入内存,进程访问的内存空间也叫做地址空间 ,另一个部分是寄存器,许多指令明确读取或更新寄存器,有一些特殊的寄存器构成了机器状态的一部分,比如程序计数器,栈指针和帧指针,然后程序可能访问io等设备,此类io信息也是一部分

进程api

现在操作系统基本都要提供创建进程的api, 然后基本形式如下

  • 创建
  • 销毁
  • 等待
  • 其他控制(暂停或恢复)
  • 状态 有一些接口来返回进程的状态信息

进程创建的一些细节

进程创建毫无疑问就是要把程序加载进内存,早期的操作系统,这种加载是今早完成的,也就是全部加载进去,而现在操作系统是懒加载的模式,程序执行期间执行需要加载的部分代码和数据片段,然后代码的静态数据加载到内存后,操作系统还需要为程序分配栈空间和堆空间,然后系统可以根据传入参数来初始化栈(其实就是main函数的arge和argv), 操作系统还会执行一些其他初始化任务,比如unix系统会给每个程序初始化的时候分配3个打开的文件描述符,分别是标准输入输出和错误,所有东西都准备齐活后,最后就是执行入口函数,也就是跳到main函数,然后os将cpu执行权限转移到新的进程

进程状态

  • 运行 正在处理器上运行
  • 就绪 已经准备好了运行,但是os没有运行它
  • 阻塞 某些操作比如io操作,阻塞完成后进入就绪状态

进程api

  • fork() 调用的时候创建一个一摸一样的进程,然后都在调用出返回,子函数返回值是0,而父函数返回对应的子函数的pid
  • wait() 延迟自己的执行,知道子函数执行完毕才会回到父函数
  • exec() 从可执行程序中加载代码和静态数据,并覆写自己的代码段,栈,堆然后操作系统程序执行该程序,因此它没有创建新进程而是直接将当前进程替换为不同的运行程序

为什么这样设计api

第一次用这个api的时候会感觉很怪异,但是问题是这样实现的东西是最好用的,将很多api合起来可以实现很多复杂的功能,而不是其他花里胡哨的方案,做对事是最重要,抽象和简化并不能代替做对事情

虚拟化cpu

受限直接执行

无限制

假如操作系统给程序初始化好了东西后,开始执行程序,这样程序直接再内存中执行,但是结果是操作程序无法进行控制和保护,一定要等到程序执行过后才能回到内核中

受限制操作

操作系统提供很多系统调用,然后利用硬件提供陷阱(trap), 使用LED协议,在系统启动的时候初始化这些陷阱表,然后让硬件知道调用处理的程序在那,当系统调用的时候会把寄存器存储到内核栈,当内核模式返回到用户模式的时候又会从内核栈中返回到寄存器,然后硬件从程序计数器的位置进行返回执行

进程切换的问题

当进程在执行的时候也就意味着系统没有在运行,它没有在运行又如何进行进程切换呢

等待系统调用

每次当进程系统调用的时候,也就是进入了系统执行阶段,进行程序切换,但是这是不现实的,因为程序可能写错永远的死循环,或者程序永远都不进行系统调用,这就导致系统不可能被调用

操作系统进行控制

这时候就需要硬件层面进行执行,之前是因为系统调用触发了硬件上的陷阱,这时候硬件支持了一种叫时钟的东西,时钟设备可以没过多久进行一次时钟中断的事件,然后操作系统通过设置时钟中断程序(陷阱)来接管机器

保存和恢复上下文

操作系统进行切换进程时候,应该要保证进程能够恢复原来的状态,这时候有个最基本的功能就是上下文切换的功能,os在进行进程切换的时候,会执行一些底层汇编代码,来保存通用寄存器,程序计算器,以及当时的运行进程的内核指针

进程调度

首先通过5个假设,来逐渐的进行进程调度的设计

  1. 每个工作的运行时间相同
  2. 所有工作同时到达
  3. 一旦开始,程序就要执行到完成
  4. 所有工作只用cpu
  5. 每个工作的运行时间是已知的

一个指标用来描述性能 周转时间 = 完成时间 - 到达时间, 因为假设到达时间相同,所以周转时间 = 完成时间

FIFO先进先出调度

  1. A,B,C都是运行10分钟,最终平均周转时间是(10+20+30) / 3 = 20分钟
  2. 假设A 100分钟,B,C都是10分钟,但是A先达, 这就导致(100 + 110 + 120)/3 = 110
    结论,可以看出这种调度肯定是不行的,当某些特殊情况性能直线下降

SJF最短任务优先

这个策略其实就是实现最短的任务最先运行,这样虽然可能优化,但是前提假设是任务同时到达,当依旧是A 100先到达,然后B,C后到达,这时候还是B,C等待A运行,性能还是直线下降

STCF最短完成时间优先

这其实是在SJF上优化,就是当完成时间短的任务进来也就是b,c, 当完成时间短的进来系统时钟停止后优先调度,这样b,c可以先执行,这样看起来好像就ok了,但是有个重要的问题就是,当一直来短时间任务那么长时间任务就会被饿死

响应时间

之前的任务都是后台执行的形式,现在有种任务需要及时响应的(比如桌面系统),此时引入和一个新的度量 响应时间 = 首次运行 - 到达时间

轮转

实现一种机制,也就是没过多少时间任务就进行一次轮转执行,这样就可以给每个任务都有机会进行执行,但是这里有个问题,当轮转的速度过快,上下文进行切换的成本会消耗整体性能,但是机器又不会及时的去响应,轮转这种机制在周转时间来看是比较差的,因为轮转不关心作业何时完成

结合io

当io操作的时候不会用到cpu, 所以这时候最好的方式是当有io操作的时候,系统对进程进行切换,这样就可以更好的利用资源

多级反馈队列MLFQ

设定有多个队列,然后每个队列有不同优先级,任何时候一个进程只能存在于一个队列种,当具有相同优先级的进程采取轮转的机制,MLFQ的关键是如何设置进程的优先级

尝试1

  • 工作进入系统,先放在最高优先级上
  • 用完一个时间片后,降低其优先级
  • 如果工作在其时间内主动释放,就保存当前的优先级

当有一个长工作的时候,通过上面的规则自然会被排在低优先级种,而刚进来的进程不管其是长短都会先被调度下,从而判定其是长任务还是短任务,当有io操作的时候因为时间片没用完所以会保留其优先级,然后低优先级会在io之间运行,当io完成,又会调度高优先级的进程

一些问题

会有饥饿的问题,当不断有交互性的任务进入的时候,低优先级的任务几乎无法获得调度,其次用户写的程序可以糊弄系统,不定时的放弃调度 从而保留其高优先级

尝试2

  • 周期性的提升所有工作的优先级

这时候有个问题,如何配置这个周期性的问题,设置的太长,还是会出现饿死的现象,如果太短,交互形的工作又得不到合适的cpu

尝试3

  • 调度程序记录进程在每层的消耗总时间,一旦工作用完了再某层的配合就进行降级操作,这样防止了那些多次io操作的进程垄断cpu的操作

MLFQ调优和其他问题

设置多少个队列,时间配额设置多少,多久提升一次优先级,这些都是问题所在

比例份额

使用一种彩票制度来进行调度,给每个进程都分配彩票,然后随机抽取彩票来执行,然后还有彩票货币制度,每个彩票的用户可以拥有自己的货币,根据货币的数量来对应彩票的兑换制度,从而让用户内部工作也可以进行统一的调度,然后还有彩票转让,让一个进程可以临时将自己的彩票转让给另一个进程,然后彩票可以通胀,一个进程可以临时的提升或降低自己的彩票

问题

如何去分配彩票,因为程序执行时间没有标准答案,如果让用户自行设置好型依旧没啥用

步长制度

用一个比较大的数来除以各自工作的彩票数,从而得到每个彩票的步长,行程长短会被记录,每次挑行程最短的那个进程来执行,从而最终大家形成都是一样长的,对于交互式应用可以给多的彩票从而行程就短,调度的次数就多,每次进来的进程设置行程就是大家的平均行程就行了