原书链接:Operating Systems: Three Easy Pieces
介绍
1.学生与老师的对话
这一段主要讲学生应该如何学习。
听过->忘了
看过->记得
做过->理解
2.介绍操作系统
本书是给知道操作系统怎么运行的人阅读的。
当程序运行起来的时候,发生了什么?
程序跑起来的时候只是做一件非常简单的事情:它执行指令。
每秒几百万次的执行,处理器从内存中取指令,解码这条指令,执行这条指令。
执行完当前这条指令后,处理器移动到下一条指令,直到程序最终完成
听起来很简单,但是我们会学习当一个程序跑起来时,围绕着我们最初的目的,让系统容易用,会有很多其他狂野的事情会发生。
操作系统让你的程序跑起来很简单,甚至允许你看上去有很多程序同时在运行,允许程序分享内存,允许程序通过设备去交流,还有很多有意思的事情。
操作系统这个软件负责确保操作系统运行正确且高效,而且还要容易用。
操作系统做的第一件通用技术是虚拟化,操作系统通过虚拟化技术把物理资源转化为更通用,更猛,更容易用的虚拟状态。
因此我们有时用虚拟机作为操作系统的参考。
为了更好用,操作系统提供一些接口供我们调用。一个典型的操作系统,一般来说会提供几百个系统调用
因为系统提供这些调用去跑程序,访问内存,访问设备,以及其他相关操作,我们有时候说系统提供一个标准库给应用。
最后,因为虚拟化允许很多程序运行,很多程序并发的访问他们自己的指令和数据,以及很多程序访问设备,系统有时候被认为是资源管理器,
处理器,内存,硬盘是系统的资源。
也正因如此,操作系统的角色是去管理这些资源,要做到效率,公平。
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <assert.h> #include "common.h" int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "usage: cpu <string>\n"); exit(1); } char *str = argv[1]; while (1) { Spin(1);//自旋 printf("%s\n", str); } return 0; }
虚拟化CPU
这个程序没做太多事情,自旋1s,打印一句话,这个str是用户一开始在cmdline中输入的。
系统开始跑程序,该程序重复校验时间,直到1s过去。
过去1s后,打印一个用户输入的字符串。
当我们把上面的程序跑4个,输出的结果就非常复杂了。
事情变得有趣起来,尽管我们只有一个CPU,但是我们4个程序似乎一起跑了起来,在同一时间。
这种魔法一般的事情到底是怎么发生的?
这其实是操作系统在硬件的帮助下产生了这种错觉。
系统有很多虚拟的CPU,把一个CPU变成好像是无限个CPU,因此允许很多程序看似立刻运行起来,我们称之为虚拟化CPU,这是本书首先主要关注点部分。
当然,运行程序,停止运行,另外告诉CPU哪个程序去运行,这需要一些API帮助你跟操作系统去沟通你的需求,我们也会讲这些API通过这本书。事实上,大部分人都是通过API来跟操作系统沟通的。
你也许注意到了,运行多个程序在同一时间这个能力,引发了一系列的问题。例如,如果2个程序想要运行在一个特定时间,谁应该运行?一条系统方针回答了这个问题,系统方针(policy)在很多地方去回答这种类型的问题。所以我们会慢慢学习这些知识,通过不断的学习操作系统实现的基本机制。可以把操作系统的角色看成是资源管理者。
虚拟化内存
现在,让我们考虑一下内存的问题吧。
现代机器呈现的物理内存模型非常简单,内存只是一个字节数组
想要去读内存,需要一个地址,然后要能有权限访问存在这里面的数据
想要去写内存,你还需要一个指定的数据,写到内存里。
当一个程序运行时,内存都是可访问的,一个程序保存它所有的数据结构在内存中,并且通过各种指令去访问他们,比如加载,保存,或者其他明确的指令去访问他们。
不要忘记程序的指令也在内存中,因此在每次取指令时访问内存。
通过malloc去分配内存。
当我们去跑一个程序的多个实例时,发现分配了相同的地址,但是却能正常独立运行,
这就好像每个程序都有私有内存,但是却共享相同的物理内存一样。
事实上,正是如此,这就是操作系统的虚拟内存。每一个进程访问自己的私有虚拟地址空间,系统通过某种方式映射到机器的物理内存。
一个运行程序的内存引用,不会影响其他进程的地址空间
运行程序只关心自己获得的那点物理内存。
然而,实际情况是物理内存是一种共享资源,由操作系统管理。所有这一切究竟是如何完成的也是本书第一部分的主题,关于虚拟化的主题
并发
这本书另外一个主题是并发,我们使用这个概念术语来指代在同一个程序中同时(即同时)处理许多事情时出现并且必须解决的许多问题。并发问题首先出现在操作系统本身;正如您在上述虚拟化示例中所见,操作系统同时处理许多事情,首先运行一个进程,然后运行另一个进程,依此类推。事实证明,这样做会导致一些深刻而有趣的问题。
不幸的是,并发的问题不再仅限于系统本身,现代多线程程序展示了同样的问题。
程序运行次数多,counter++算出来的值就不一样。
这问题的原因在于执行指令的顺序。
执行++这个指令,先要load,然后increase,最后store,但是这些操作都不是原子性的,在本书的第二部分,有很多细节方面的描述。
持久化
持久化的硬件常常以输入输出设备的形式出现,HDD是常用的仓库,尽管SSD也很好。
文件系统是管理硬盘的,它负责可靠,高效的存放文件。
系统不会创建虚拟的,私人的硬盘空间。
open , write , close 这些系统调用会被路由到文件系统中,该系统负责处理请求,或者返回错误码给用户。
系统有点像一个标准库。
日志和写时复制来应对crash的问题。
症结:
文件系统是操作系统的一部分,用来管理持久数据,什么技术才能让这件事做得这么准确?
什么机制,策略需要去做到高性能,怎么才能更可靠,当面对硬件和软件的错误时。
第三部分,会讲通用IO,硬盘,RAIDs 和文件系统的相关细节。
设计目标
找到好的trade-off是成功构建系统的关键。
如何抽象,是让系统方便去用的关键,在计算机科学中,抽象是所有东西的基础。
抽象可以让我们写一个大系统拆分为非常小的,方便理解的小片段。
写C不用想汇编,写汇编不用想逻辑门
虚拟化让系统变得很好用,但这并不是没有代价的。
我们必须努力的去提供虚拟化以及其他系统特性但没有额外的开销。
这些额外的开销,可能是额外的运行时间,额外的空间,
我们寻找一种办法可以最小化其中一个,或者both,但这往往不能总是达到,有时候,我们要学着去注意和忍受。
另外一个目标是 为 应用与应用, 系统与应用 之间提供保护,使得他们互相之间不会影响。
隔离是一个重要的原则,
系统必须要不停的运行,否则运行在其上面的应用就会停止。
系统通常提供高度的可靠性。
随着系统长得越来越复杂,构造一个操作系统是非常具有挑战性的事情。
很多正在研究的课题聚焦在这个问题。
我们还有其他的目标:能效,安全,可移动设备,
###
一些故事
操作系统一开始就是一个lib,用于给开发者调用,以避免去写一些底层代码。
这个阶段主要是批处理
解释系统调用和程序调用的区别:系统调用开启的是硬件许可等级,而程序调用是用户层面的许可,这就意味着程序调用是受到约束的。
一个特别的硬件指令称之为trap
硬件传递控制去一个trap handler
提升为内核模式
系统完成系统调用,会把控制权交还给用户。return-from-trap
系统会加载很多工作到内存,然后
虚拟化
3.学生与老师的对话
physical <-> virtual
4.抽象:进程
本章节,讨论系统提供给用户的最基础的抽象:进程
进程的定义:正在运行的程序
程序是没有生命的东西:它就放在硬盘中,一堆指令等待开始行动。是操作系统获得这些字节,让他们运行起来,将程序变成有用的东西。
当同时想运行多个程序时,这么做是为了让系统更容易去运行,每一个进程都不需要关系CPU是不是可以用。
问题的关键:
怎么去提供有很多个CPU的错觉?尽管只有很少的物理CPU可以用,我们怎么才能够让程序以为我们有几乎无限多的CPU提供呢?
系统通过虚拟化CPU来制造这种假象,通过运行一个程序,然后停止运行去跑另外一个程序,更进一步的,系统可以增强这种错觉有很多虚拟的CPU。
这种基本的技术,叫做time sharing 的CPU(CPU时间分片?),允许用户去跑尽可能多的并发程序,潜在的成本是性能,如果CPU要被分享,那么每个进程会跑得更慢一些。
去实现CPU的虚拟化,而且要很好的实现,系统需要一些低级的机器和一些高级的智能。我们称之为低级机器机制。
下面我们会讲述如何进行 语境切换 (context switch ,也叫做上下文切换)
提示:使用时间分片和空间分片。
时间分片是一种基本技术,在OS中用来共享资源。
通过允许某个实体( entity ) 使用资源一小段时间,然后切换给别的实体使用。
相对于分时复用的是空间复用,例如
上下文切换赋予OS停止跑一个程序然后去跑另外一个程序的能力。
这种分时服用机制被所有现代的操作系统使用。
在这些机制之上,以策略的形式存在操作系统中的一些智能
策略是在系统中做决定的算法。
给定一堆可用的程序跑在操作系统中,那个程序应该运行呢?
这是调度策略决定的,一般使用历史信息(比如上一分钟谁跑的时间长一些),工作量知识(这是什么类型的程序),性能指标(系统是不是为交互性能,吞吐量进行过优化),来做决定。
4.1抽象:进程
进程是系统跑程序的抽象。
正如我们上面所说,进程只是一个正在运行的程序;在任何时刻,我们都可以通过清点它在执行过程中访问或影响的系统的不同部分来总结一个进程
要去明白什么构成一个进程,我们需要理解状态机。
程序的什么部分是重要的?当他在运行的时候?
进程的一个重要组件是内存,指令存在内存中,运行程序的数据读写也在内存中,
进程可以寻址的内存(地址空间),是进程的一部分。
寄存器也是进程状态机的一部分
一些特殊寄存器构成状态机
比如:
(PC , program counter , 也叫做 IP,instruction pointer),这个特殊寄存器会告诉我们下一步执行的指令是什么,
SP,stack pointer and FP ,frame pointer 是用来管理栈,函数参数,本地变量,返回地址。
最后还有关于IO操作的。
也就是说,进程包括:进程运行的地址空间,进程的状态机,进程用到的IO操作。
4.2进程API
进程必须实现的API
-
create
-
destroy
-
wait
-
miscellaneous control , 各种各样的控制,比如挂起suspend
-
status , 查看进程运行状态
4.3进程创建:一点细节
程序怎么转换为进程?
-
load , 将静态数据和代码加载进内存,到进程的地址空间,
-
create and initializing a stack , 创建和初始化栈
-
do other work as related to I/O setup 做一些与输入输出相关的事情。
早期系统会把程序一次过加载到内存,现在的程序只加载当前需要的,想知道怎么一点点加载进内存需要知道分页和交换的机制。
当我们讨论内存虚拟化,时再详细讨论,现在我们只要知道在允许任何东西之前,系统一点要做一些事情去获得重要的程序比特,将其从硬盘加载到内存。
当代码和静态数据加载到内存中后,系统需要做一些事情。必须为程序运行时的栈(stack)分配一定的空间。
C语言用stack栈 ,存放本地参数,函数参数,返回地址。
系统分配这些内存给到进程。
他们会填充main 函数的argc,argv数组。
系统会分配给程序的堆(heap)
在C语言中,堆用来分配动态分配的数据,程序请求分配内存函数malloc
,free
来释放
堆被很多数据结构所需要:链表,哈希表,树,还有其他有趣的数据结构。
系统还会做其他初始化的任务,特别是与输入输出相关的。
4.4 进程状态
-
running , 程序在允许。
-
Ready , 程序可以跑,但还不跑
-
block , 缺乏条件无法继续跑 , IO请求时,让出CPU给其他进程。
4.5 数据结构
有一些关键的数据结构去追踪相关的信息片段。
追踪每个进程的状态,例如,追踪进程列表。
xv6系统中,有一个寄存器上下文会被记录,用来切换进程,这是后面要讨论的上下文切换。
// the registers xv6 will save and restore // to stop and subsequently restart a process struct context { int eip; int esp; int ebx; int ecx; int edx; int esi; int edi; int ebp; }; // the different states a process can be in enum proc_state { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE }; // the information xv6 tracks about each process // including its register context and state struct proc { char *mem; // Start of process memory uint sz; // Size of process memory char *kstack; // Bottom of kernel stack // for this process enum proc_state state; // Process state int pid; // Process ID struct proc *parent; // Parent process void *chan; // If !zero, sleeping on chan int killed; // If !zero, has been killed struct file *ofile[NOFILE]; // Open files struct inode *cwd; // Current directory struct context context; // Switch here to run process struct trapframe *tf; // Trap frame for the // current interrupt };
数据结构:进程列表
进程列表通常也称为任务列表。
有时候人们会倾向于独立的数据结构去保存进程信息,进程控制块,它是一个C的数据结构包含所有进程的信息,我们有时候也称之为进程描述器。
4.6总结
我们已经了解了系统的最基本抽象:进程。有了这个抽象在意识中,我们可以实事求是:我们需要底层机制去实现进程,也需要高层策略去智能化的调度这种进程。有了这些概念,我们可以知道系统如何虚拟化CPU。
5.插曲:进程api
在这个插曲,我们讨论UNIX如何创建进程。
UNIX提供了一个非常耐人寻味的方式去创建一个新的进程,使用一对系统指令:fork,exec
5.1 fork,系统调用
fork系统调用是用来创建一个新进程,但是请预先警告:这肯定是您调用过的最奇怪的例程。
5.2 wait,系统调用
wait,可以让某个进程先阻塞,使得程序能按我们预期的顺序运行。
5.3 exec,系统调用
exec把原先的进程复制一遍,重写的当前的堆栈,包括静态数据等,就好像被复制的进程没有执行过一样。
5.4 为什么我们要造API
fork和exec的分开可以让shell做一系列复杂的事情,
5.5 进程控制和使用者
kill,可以发signals 给一个进程,信号机制给我们提供了丰富的
6. 机制:有限的直接执行
分时服用CPU已经可以解决我们先运行一个程序,再运行另外一个程序的要求。但是如何建立这么一个虚拟化的机制,是充满了挑战的。
-
性能:我们怎么才能做到不增加额外的开销
-
控制:我们要怎么保证进程运行高效而保留控制权
在保持控制的同时获得高性能是构建操作系统的核心挑战之一。
要做到保留控制权的前提 还能高效的完成虚拟化,需要硬件和系统的支持。
系统会使用恰如其分的硬件支持然后高效的完成这件事。
6.1 基本技术:有限的直接执行
系统跟程序的交互
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
好像很简单,但这其实引发了其他的问题:
-
怎么保证程序不会影响其他程序,同时还高效运行。
-
怎么切换不同的进程。
6.2 问题1:受限的操作
受限的操作如何让程序完成在cpu中跑的要求呢?
系统调用其实就是函数调用,只是里面隐藏着著名的trap指令。
为了执行一个系统调用,程序必须执行一个特殊的trap指令,这个trap指令跟让用户进入系统模式没有太多区别。
上电时,系统初始化,系统会告诉硬件,当发生硬件中断时,应该去执行什么代码,当发生键盘输入时,应该执行什么事情,等等规定。
系统会告诉硬件一系列的trap handles(类似钩子函数?)
通常也伴随着一些特殊的指令。
尽管我们花费了很大的力气去保护操作系统,但是我们还要注意用户输入值的边界,
去识别具体的系统调用,系统调用会标记系统调用号。
OS @ boot Hardware (kernel mode) initialize trap table remember address of... syscall handler OS @ run Hardware Program (kernel mode) (user mode) Create entry for process list Allocate memory for program Load program into memory Setup user stack with argv Fill kernel stack with reg/PC return-from-trap restore regs (from kernel stack) move to user mode jump to main Run main() ... Call system call trap into OS save regs (to kernel stack) move to kernel mode jump to trap handler Handle trap Do work of syscall return-from-trap restore regs (from kernel stack) move to user mode jump to PC after trap ... return from main trap (via exit()) Free memory of process Remove from process list
6.3进程间切换
这是个非常严重的问题,其本质是,我们怎么从新获得CPU的控制权。
-
合作的方式:等系统调用
这种方式假定每个程序都周期的让出CPU资源,但是如果有个程序一直卡在死循环中,系统就无法进行切换了。
-
不合作的方式:由系统来控制
通过时钟中断来实现,当时钟中断来时,当前运行的进程停止,预先配置好的时钟中断handler运行,在此时系统重获CPU控制权。
当一个中断发生,硬件有责任为当前运行的程序 保留 足够多的状态,当从trap返回时怎么继续运行。这种中断后要执行的行动,跟执行一个明确的系统调用,其实是很像的,都需要保持寄存器的状态等,方便从trap返回时的运行。
保存和重载上下文
调度器负责决定是否切换当前进程。
如果是要切换进程,系统会执行上下文切换的底层代码。
所有的操作系统只需要保存一些当前运行程序的寄存器值,把它们存到内核的栈,重载一些寄存器的值。
保存通用寄存器,PC,SP寄存器的值。
重载寄存器的值。
内核栈切换到准备运行的进程。
通过切换栈,通过切换堆栈,内核在一个进程的上下文中进入对切换代码的调用,返回这个准备运行的进程的上下文,当系统终于真正运行return-from-trap指令,准备运行的进程就成为当前运行的进程。
reboot 很好,可以重置状态机
时钟中断让系统获得控制权,这非常棒。
OS @ boot Hardware (kernel mode) initialize trap table remember addresses of... syscall handler timer handler start interrupt timer start timer interrupt CPU in X ms OS @ run Hardware Program (kernel mode) (user mode) Process A ... timer interrupt save regs(A) → k-stack(A) move to kernel mode jump to trap handler Handle the trap Call switch() routine save regs(A) → proc t(A) restore regs(B) ← proc t(B) switch to k-stack(B) return-from-trap (into B) restore regs(B) ← k-stack(B) move to user mode jump to B’s PC Process B
用户的寄存器由硬件存储
内核寄存器由系统存储,
lmbench 这个工具可以测量上下文切换要花多长时间。
内存带宽的限制也可能使得你CPU的性能无法全部发挥。
7.调度介绍
早期的调度器来自人类社会实践经验,比如流水线怎么运转等。
7.1 工作负载假设
对工作负载越了解,你就能更细致的调整自己的调度策略。
fully-operational scheduling discipline , 全面运作的调度策略。
进程的假设,有时候进程也被称为job
-
每个工作跑相同的时间
-
所有工作在同一时间到达
-
一旦开始,工作就一直到到结束
-
所有的工作只用到CPU
-
所有工作的运行时间都是知道的。
7.2 调度指标
Scheduling Metrics
目前先采用只有周转时间这么一个指标。
周转时间是一个性能指标
另外一个指标是公平性。
性能跟公平性经常是矛盾的,
7.3 FIFO
FIFO : 有很多很好的特性,简单,容易实现,鉴于我们的假设,它运行得很好。
但是FIFO无法保证Shortest Job First , SJF ,
调度设计需要遵从最短任务优先。
7.4 SJF.最短的工作有限,shortest job first
如果说所有任务都同时到达,那当然,我们的SJF调度策略就是最佳策略。
7.5 最短时间完成优先 STCF ,shortest Time-to-completion First
释放条件3,一个程序开始就要跑完
通过时钟中断和上下文切换,当某个程序抢占CPU一定时间后,进入时钟中断,然后切换到别的进程。
Preemptive Shortest Job First (PSJF) 抢占最短工作优先
7.6 新的度量:响应时间
要解决一个终端没有响应的问题,如果只是以周转时间为度量指标,那么系统会先跑A, 跑完再跑B,再跑C
但是用户坐在终端面前,等待C的反馈,这样用户体验就不好了,互动性很差。
所以我们要建造一个对响应时间敏感的调度器。
7.7 循环 Round Robin
循环调度(RR),可以解决响应不及时的问题。
循环调度不是跑完整个任务,而是去跑一个时间片,time slice , 有时候也被叫做调度量(这个英文有点意思,叫做scheduling quantum , quantum 在粤语里面叫 kouta , 比如说计划生育,我不生,能不能把这个kouta让给别人呀?)
每过一个时间片,就切换下个进程来跑,直到跑完,所以有时候说一个循环也叫做一个时间片。
一个时间片必须是时钟中断周期的倍数
时间片越短,看似响应时间越快,但是这也会导致上下文开销的问题。
值得注意的是,上下文切换的成本,不仅仅只有保存和恢复几个寄存器的操作系统操作。
程序在跑的时候,还会建立大量的状态,在CPU缓存中,在分支预测,转译后备缓冲器等一些列的芯片硬件中。
随意的切换任务,会导致这些状态被flush
TLB : translation lookaside buffer : is a memory cache that is used to reduce the time taken to access a user memory location.[1] It is a part of the chip's memory-management unit (MMU). The TLB stores the recent translations of virtual memory to physical memory and can be called an address-translation cache.
转译后备缓冲器:用于改进虚拟地址到物理地址的转译速度,这是CPU中的一种缓存,存放虚拟地址映射到物理离职的标签页表条目。
如果访问的虚拟内存在TLB中,在硬件上,可以完成快速的转换,很快给出一个匹配结果。
如果不在TLB中,就要使用标签页表进行虚拟地址转换,但访问速度跟TLB比起来就很慢。
当我们提供响应时间的表现时,周转时间就非常的糟糕了。
其实任何公平的调度政策(对CPU资源分配按时间片来分配),在周转时间上都是表现不佳的,事实上,这是一种固有的权衡。
如果你不想公平,可以让运行时间短的程序先跑完,这样响应时间就慢了。
如果你想要公平,响应时间短,那你的周转时间就长。
在可能的情况下,重叠操作以最大限度地提高系统的利用率 , 比如在硬盘IO,发送消息给远程机器,
7.8 考虑IO
上面都没有提到IO操作,这里试图将IO操作引入讨论,如果程序还要进行IO操作呢?怎么办?
initiates 发起,
当IO请求发给了硬盘驱动,进程会阻塞几个毫秒或者更长。
当CPU使用和disk使用同时发生,叫做overlap,
因此,我们看到了调度程序如何合并 I/O。通过将每个 CPU 突发视为一项作业,调度程序可确保“交互式”进程频繁运行。当这些交互式作业执行 I/O 时,其他 CPU 密集型作业运行,从而更好地利用处理器
7.9 不再有甲骨文
7.10 总结
两个指标: 周转时间,响应时间。
multi-level feedback queue 多级反馈队列。
8.多级反馈队列,MLFQ
Multics 多信道
MLFQ 从历史中学习,然后去预测未来。
8.1MLFQ基本原则
-
如果优先级A>B,A跑,B不跑
-
如果优先级相等,则遵循RR原则。
MLFQ 有很多不同的队列,这些队列有不同的优先级。
高优先级的工作先运行
这种调度器的关键在于如何去设定调度优先级,优先级根据job被观察的行为进行调节。
relinquishes 放弃
如果一个进程经常放弃CPU资源,等待用户输入,那么他的优先级会提高
如果一个CPU密集型进程在跑了很长一段时间,那么他的优先级会降低。
8.2尝试1:怎么改变优先级?
-
当一个job进入系统,把优先级调到最高。
-
如果这个时间片都用完,那么这个job的优先级下降。如果没用完这个时间片就放弃CPU,他就继续保持当前的优先级。
调度器必须能在攻击中被安全保护。
如果一个进程在CPU时间片快用完的时候,放弃CPU,那么他就不会被降级,这样的话对其他进程就不公平。
8.3 尝试2:优先级提高
-
经过一定的周期,将所有jobs移动到最高优先级的队列。
怎么去设定这个周期,被称为巫毒常数,过快,过慢的提升优先级都不太好。
8.4 尝试3:更好的会计
-
重写规则4,当job使用完当前等级的时间配额,则降级
8.5 调整MLFQ和其他问题
一个关键问题是如何参数化:parameterize
几个队列?每个队列要多少时间片?优先级应该多久提升一次?
这就需要系统调优了。
避免巫毒常数 , Ousterhout’s Law
使用默认值来规避一些特殊参数错误导致的问题。
常见可调整参数:
-
不同队列的时间片参数
-
每个时间片的长度,多久优先级提升一次。
nice指令可以给系统提建议,提升或者降低某一个进程的优先级。
9.比例份额调度器
通常也被称为彩票调度器:
其实非常的简单,抽一张彩票去决定谁来运行,哪个进程更应该进行就给他更高的运行概率。
9.1 基本概念:票据代表你的份额
随机有3个好处:
-
避免边缘情况
-
轻量级
-
快
9.2 票据机制
-
票据汇率,所有进程在抽奖运行时,都会映射到系统给他的票据来进行。
-
票据交换,一个进程可以主动把自己的票据给其他进程。
-
票据通胀,程序互相信任的时候,可以有一个大哥,把一个小弟进程票据变得很多,让他能用更多的CPU资源。
9.5 怎么去分配
票据的分配要求使用者非常清楚自己的系统中,哪些程序应该使用更多的CPU时间。
9.6 步幅调度
这也是一种直接了当的调度方法,新票号=当前票号+步幅
特点是每个进程都按照其票据完全公平的分配了CPU运行时间。
彩票形式的调度策略比步幅的调度策略多了一个好处:没有全局状态。这样对新加入的进程比较友好。
9.7 Linux系统的完全公平调度
高效,可拓展。CFS目标是花很少的时间去做调度决策,通过其固有的设计和对数据结构的巧妙使用,非常适合该任务
尽管经过积极优化,调度器还是会占用5%的CPU时间。
该调度器遵循一个基本原则:公平的分配CPU,尽管是竞争的进程
virtual runtime (vruntime). 虚拟运行时间
如何平衡公平和效率?
CFS用多个控制参数进行:
-
sched latency , 调度在准备切换之前要运行多长时间
保证公平的方法,sched latency/进程数 = 每个进程可以分到的时间片,每个进程都跑这么多个时间片。
-
min granularity, 最小粒度,典型值6ms,
如果作业的时间不是理想的时间片的倍数,那么CFS会追踪vruntime的值,随着时间的延长,最终还是会公平。
-
权重,根据权重去分配sched latency,也就是进程的运行时间。同时要适配vruntime要映射回标准时间。
-
使用红黑树,更快的查找,保存进程到一棵红黑树,时间复杂度为O(logn)
处理IO,睡眠进程。
对于睡眠进程,如果一个进程睡眠了,那么他将不会再运行的jobs的红黑树中,那么这是唤醒时,我们将最小的值给到他,让他享受CPU资源,但是又不至于占用太久。但这对于一些只睡眠一小段时间的进程来说,是不公平的,因为这个机制,他们没有分享到其应有的CPU资源。
9.8 总结
没有什么调度器是灵丹妙药,每种调度器都有其存在的缺陷。
10.多CPU调度
多线程程序才能发挥多核心处理器的性能。
10.1 背景:多处理器架构
重点关注硬件缓存,
在单CPU时,有多级缓存制度,
缓存因此基于局部性的概念,其中有两种:时间局部性和空间局部性
缓存一致性问题,cache coherence
总线窥探:bus snooping
通过监控内存的访问,如果知道了有CPU拷贝了这段内存,当我们去读值的时候,就要放弃读,或者用缓存的数据先更新这个值。
10.2 不要忘记同步
当程序访问共享数据时,他们需要关心很多事。
上锁可以解决同步问题,但是如果核心数越多,上锁会导致性能下降就越大。
10.3 缓存关联Cache Affinity
尽可能保持一个进程在同一个CPU,