1940S的操作系统
操作系统:使程序能更好运行的系统软件
1940s额计算机程序是相对机械的数值计算
(1) 不需要也没有操作系统
(2) 操作系统中唯一的对象就是独占计算机运行的程序
1950s有了让多个程序共享一台计算机的需求
计算机非常贵,集中管理、排队使用
(1) 希望程序切换能够尽量无缝
(2) 希望提供一些程序共同需要的功能,例如读写文件
于是有了操作任务的系统
(1) 批处理系统
(2) 操作系统是一个简单的串行程序调度器和一系列库函数
(3) 操作系统中开始有设备、文件、任务,但只有一个独占计算机运行的程序
1960因为计算资源的增加,有了同时将多个程序载入内存的能力
1,操作系统需要决定将哪些程序装入内存
2,在多个隔离的程序之间切换,隔离使一个程序出bug不会crash整个系统
3,操作系统中多了“进程”对象和进程管理API
时钟中断:使程序在执行时,异步地插入函数调用
由操作系统(调度策略)决定是否要切换到另外一个程序执行
什么是操作系统
从用户的角度上看:
(1) 操作系统是一个控制软件
(2) 管理应用程序
(3) 为应用程序提供服务
(4) 杀死应用程序
对内部对象来说:CPU抽象成进程,磁盘抽象成文件,内存抽象成地址空间
(1) 资源管理
(2) 管理外设、分配资源
OS Kernel(操作系统内核)的特征
并发:1,计算机系统中同时存在多个运行的程序,需要OS管理和调度
共享:1,“同时”访问2,互斥共享
虚拟1,利用多道程序设计技术,让每个用户都觉得有一个计算机专门为他服务
异步:
(1) 程序的执行不是一贯到底,而是走走停停,向前推进的速度不可预知
(2) 但只要运行环境相同,OS需要保证程序运行的结果也要相同
为什么学习操作系统
因为操作系统本质上也是一个软件,当你熟悉操作系统的原理,以后要实现一个大型的系统或者软件也能参考,了解并发,线程,进程
第二章
操作系统的启动: POST(加电自检):寻找显卡和执行BIOS
系统调用(来源于应用程序):应用程序主动向操作系统发出服务请求
异常(来源于不良的应用程序):非法指令或其他坏的处理状态(如:内存出错)
中断(来源于外设):来自不同的硬件设备的计时器和网络的中断
程序访问主要是通过高层次的API接口而不是直接进行系统调用
系统调用之上有API供应用程序调用:Win32 API,POSIX API(UNIX LINUX Mac OS)
第三章内存管理
CPU:运算器、控制器、Register、Cache、MMU
内存管理能够实现:物理存储抽象为逻辑地址空间,保护各程序各自内存空间,程序间共享内存,虚拟化 - 暂时不用的数据移动到磁盘
程序经过编译、汇编、链接、载入对应到逻辑地址
连续内存分配、内存碎片(外碎片 - 程序之间、内碎片 - 程序之内)
物理地址空间-硬件支持的地址空间
起始地址0到地址Max sys
逻辑地址空间——一个运行程序所拥有的内存范围,起始地址0,到地址Max prog
第一匹配分配:
从0地址开始找第一块满足条件的内存块分配
优点:简单,易于产生更大空闲块,向着地址空间的结尾
缺点:外部碎片,不确定性
最优适配分配
为了分配n字节,使用最小的可用空闲块,以致块的尺寸比n大
原理:为了避免分割大空闲块
为了最小化外部碎片产生的尺寸
优点:当大部分分配是小尺寸时非常有效,比较简单
缺点:外部碎片,重分配慢,易产生很多没用的微小碎片
最差匹配分配
为了分配n字节,使用最大可用空闲块,以致块的尺寸比n 大
原理:为了避免有太多微小的碎片
优点:假如分配是中等尺寸效果最好
缺点:重分配慢,外部碎片,易于破碎大的空闲块以致大分区无法被分配
内存碎片整理:压缩式(移动空闲内存)、交换式(暂时不用的数据移动到磁盘)
第四章:非连续内存分配
优点:一个程序的物理地址空间是非连续的,更好的内存利用和管理,允许共享代码与数据(共享库),支持动态加载和动态链接
缺点:如何建立虚拟地址和物理地址之间的转换
硬件解决方法
1,分段:更好的分离和共享
分段segment:内存块大小可变内存地址表示为(segment number, offset)
一个段:一个内存块,一个逻辑地址空间
程序访问内存地址需要:一个二维的二元组(s,addr),s:段号,addr:段内偏移
段表:段号 - 基址,长度
2,分页
(1) 划分物理内存至固定大小的帧,大小是2的幂
(2) 划分逻辑空间至相同大小的页
(3) 建立方案,转换逻辑地址为物理地址:页表、MMU/TLB
页寻址机制:页映射到帧,页是连续的的虚拟内存,帧是非连续的物理内存,不是所有的页都有对应的帧
页和帧内的偏移量是一样的,所以页表所对应是页号映射帧号
分页机制的性能问题:
1,访问一个内存单元需要2次内存访问,一次用于获取页表项,一次用于访问数据
2,页表可能非常大,例如:一个64位机器如果每页1024字节,那么一个页表的大小会是2^54
解决方法:因为页表过大,CPU缓存放不下,将页表放入内存中
TLB(Translation Lookaside Buffe),缓存页表经常访问的映射关系,先访问TLB,如果有,很快就可以得到,没有,就去访问页表,在缓存到TLB中去
多级页表,反向页表(帧→页)
反向页表的实现:页寄存器、关联内存、hash table
第五章:虚拟内存
为了解决内存不足
(1) 覆盖技术(overlay),常用程序模块独占常驻内存,不常用程序模块分时共享内存,但需要程序员设计覆盖关系,增加了程序设计复杂度,并且有从硬盘读写模块的开销,粒度是程序模块
(2) 交换技术:将不运行的程序送到外存,从而获得空闲内存空间
OS把一个进程的整个地址空间保存到外存中(换出),而将外存中的某个进程的地址空间读入内存中(换入)。换入换出内容的大小为整个程序的地址空间
(3) 虚存技术
目标:像覆盖技术那样,不是把程序的所有内容放在内存中。因由OS来自动完成,像交换技术那样,能够实现进程在内存与外存之间的交换
特征:大的用户空间,部分交换,不连续性
(4) 页表表项
①驻留位表示该页是在内存还是外存,1为在内存中,表示可用;0位外存,如果访问该页,将导致缺页中断
②保护位:表示允许对该页做何种类型的访问,如,只读、可读写、可执行等
③修改位:表明此页在内存中是否被修改过。当系统回收该物理页面时,根据此位来决定是否把它的内容写回外存
④访问位:如果该页面被访问过(包括读操作或写操作),则设置此位。用于页面置换算法
(5) 算法,在页表中查找所需数据的物理地址,如存在则直接读取;如不存在,先判断是否有空余内存,如无,先换出内存上的数据(未被修改直接free,被修改过则写回硬盘),再从硬盘读取数据
第六章:页面置换算法
功能:当缺页中断发生,需要调入新的页面而内存已满时,选择内存当中哪个物理页面被置换
目标:尽可能地减少页面的换进换出次数(即缺页中断的次数)。具体来说,把未来不再使用的或短期内较少使用的页面换出,通常只能在局部性原理指导下依据过去的统计数据来进行预测
页面锁定:用于描述必须常驻内存的操作系统的关键部分或时间关键的应用进程。实现的方法是:在页表中添加锁定标志位
局部页面置换算法:
(1) 最优页面置换算法(OPT)
思路:当一个缺页中断发生时,对于保存在内存当中的每一个逻辑页面,计算在它 的下一次访问之前,还需等待多长时间,从中选择等待时间最长的那个,作为被置换的页面
缺点:只是理想情况,在实际系统中无法知道一个页面要等待多长时间才会被访问
(2) 先进先出算法(FIFO)
思路:选择在内存中驻留时间最长的页面并淘汰
缺点:性能较差,调出的页面有可能是经常要访问的页面,并且有Belady现象
(3) 最近最久未使用算法(Least Recently Used,LRU)
思路:当一个缺页中断发生时,选择最久未使用的那个页面,并淘汰
缺点:需要记录各个页面使用时间的先后顺序,开销比较大
(4) 时钟页面置换算法
思路:
①需要用到页表当中的访问位,当一个页面被装入内存时,把该位初始化位0.然后如果这个页面被访问(读/写),则把该位置为1;
②把各个页面组织成环形链表(类似钟表面),把指针指向最老的页面(最先进来)
③当发生一个缺页中断时,考察指针所指向的最老页面,若它的访问位为0,立即淘汰;若访问位为1,则把该位置为0,然后指针往下移动一个。如此下去,直到找到被淘汰的页面,然后把指针移动到它的下一格。
(5) 二次机会法
同时使用脏位和使用位来指导置换,(读/写)
00直接被淘汰,01和10变成00指针指向下一项,11变成01指针指向下一项
(6) 最不常用算法(Least Frequency Used,LFU)
思路:当一个缺页中断发生时,选择访问次数最少的那个页面,并淘汰
LRU和LFU的区别:LRU考察的是多久未访问,时间越短越好;而LFU考察的是访问的次数或频度,访问次数越多越好
(7) Belady现象:在采用FIFO算法时,有时会出现分配的物理页面数增加,缺页率反而提高的异常现象
局部算法的问题
局部算法都是基于一个前提,即程序的局部性原理,如果局部性不成立,各种页面置换算法没有什么分别,例如:访问:1,2,3,4,5,6…,即单调递增,不管采用何种算法,每次的页面访问都会产生中断。
如果局部性原理成立,如何证明,如何对它进行定量分析?这就是工作集模型
工作集:一个进程当前正在使用的逻辑页面集合
可以用一个二元函数W(t,△)来表示:
(1) t是当前的执行时刻
(2) △称为工作集窗口(working-set window),即一个定长的页面访问的时间窗口
(3) W(t,△)=在当前时刻t之前的△时间窗口当中的所有页面所组成的集合(随着t的变化,该集合也在不断地变化)
(4) |W(t,△)| 指工作集的大小,即页面数目
常驻集:指在当前时刻,进程实际驻留在内存当中的页面集合
(1) 工作集是进程在运行过程中固有的性质,而常驻集取决于系统分配给进程的物理页面数目,以及所采用的页面置换算法;
(2) 如果一个进程的整个工作集都在内存当中,即常驻集等于工作集,那么进程将很顺利地运行,而不会造成太多的缺页中断(直到工作集发生剧烈变动,从而过渡到另一个状态)
(3) 当进程常驻集的大小达到某个数目之后,再给它分配更多的物理页面,缺页率也不会明显下降
全局置换算法
(1) 工作集页面置换算法:追踪之前n个的引用,在之前n个内存访问额页引用是工作集,n被称为窗口大小,不在这个窗口之内的页面也会被淘汰
(2) 缺页率页面置换算法
可变分配策略:常驻集大小可变。例如:每个进程在刚开始运行的时候,先根据程序大小给它分配一定数目的物理页面,然后在进程运行过程中年,再动态地调整常驻集的大小。
(1) 可采用全局页面置换的方法,当发生一个缺页中断时,被置换的页面可以是在其它进程当中,各个并发进程竞争地使用物理页面
(2) 优缺点:性能较好,但增加了系统开销
(3) 具体实现:可以使用缺页率算法(PFF,page fault frequency)来动态调整常驻集的大小
(4) 缺页率:表示“缺页次数/内存访问次数”
(5) 当缺页率高的时候增加工作集,当缺页率低时减少工作集
(6) 当当前发生缺页时间减去上一个缺页时间大于某一个值时,代表这个程序运行很好,缺页率较低,从工作集中移除没有在[上一个缺页时间,当前缺页时间]被引用的页面,小于某一个值时,仅增加缺失页到工作集中
(3)抖动问题:如果分配一个进程的物理页面太少,不能包含整个的工作集,即常驻集小于工作集,那么进程将会造成很多的缺页中断,需要频繁地在内存和外存之间替换页面,从而使进程的运行速度变得很慢,我们把这种状态称为“抖动”
第7章进程
定义:一个具有一定独立功能的程序在一个数据集合上的一个动态执行过程
组成:
(1) 程序的代码
(2) 程序处理的数据
(3) 程序计数器中的值,指示下一条将运行的指令
(4) 一组通用的寄存器的当前值,堆,栈
(5) 一组系统资源(如打开的文件)
总之,进程包含了正在运行的一个程序的所有状态信息
进程与程序的联系
(1) 程序是产生进程的基础
(2) 程序的每次运行构成不同的进程
(3) 进程是程序功能的体现
(4) 通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包含多个程序
进程与程序的区别
(1) 进程是动态的,程序是静态的:程序是有序代码的集合;进程是程序的执行,进程有核心态/用户态
(2) 进程是暂时的,程序是永久的:进程是一个状态变化的过程,程序可长久保存
(3) 组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)
(4) 类比:食谱=程序;人=CPU;原料=数据;做蛋糕=进程
进程的特点:
(1) 动态性:可动态地创建、结束进程
(2) 并发性:进程可以被独立调度并占用处理机运行;并发并行
(3) 独立性:不同进程的工作不相互影响(分配不同页表)
(4) 制约性:因访问共享数据/资源或进程间同步而产生制约
进程控制结构
进程控制块(PCB):操作系统管理控制进程运行所用的信息集合。操作系统用PCB来描述进程的基本情况以及运行变化的过程,PCB是进程存在的唯一标志。
PCB具有以下三大类信息
(1) 进程标识信息。如本进程的标识,本进程的产生者标识(父进程标识);用户标识。
(2) 处理机状态信息保存去。保存进程的运行现场信息:
用户可见寄存器,用户程序可以使用的数据,地址等寄存器
控制和状态寄存器,如程序计数器(PC),程序状态字(PSW)
栈指针,过程调用/系统调用/中断处理和返回时需要用到它
(3) 进程控制信息(调度、通信、存储、IO、树形结构)
进程的生命周期管理
创建(源自系统、用户、进程),
运行(OS调度),
等待(阻塞,等待数据就绪、其他进程完成),
唤醒(由OS或其他进程完成,回到就绪态),
结束
进程的状态:创建状态(New),运行状态(running),就绪状态(Ready),等待状态(又称为阻塞状态Blocked),结束状态(Exit)
进程挂起:为了合理且充分地利用系统资源
进程在挂起状态时,意味着进程没有占用内存空间。处在挂起状态的进程映像在磁盘上
阻塞挂起状态:进程在外存并等待某事件的出现
就绪挂起状态:进程在外存,但只要进入内存,即可运行
线程:进程当中的一条执行流程
从两个方面来重新理解进程
(1) 从资源组合的角度:进程把一组相关的资源组合起来,构成了一个资源平台(环境),包括地址空间(代码段、数据段)、打开的文件等各种资源;
(2) 从运行的角度:代码在这个资源平台上的一条执行流程(线程)
线程的优点:
(1) 一个进程中可以同时存在多个线程
(2) 各个线程之间可以并发地执行
(3) 各个线程之间可以共享地址空间和文件等资源
线程的缺点:一个线程崩溃,会导致其所属进程的所有线程崩溃
进程与线程的比较
(1) 进程是资源分配单位,线程是CPU调度单位、
(2) 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈
(3) 线程同样具有就绪、阻塞和执行三种基本状态,同样具有状态之间的转换关系
(4) 线程能减少并发执行的时间和空间开销
(1) 线程的创建时间比进程短
(2) 线程的终止时间比进程短
(3) 同一进程内的线程切换时间比进程短
(4) 由于同一进程的各线程间共享内存和文件资源,可直接进行不通过内核的通信
线程机制提高并发性能,但数据共享导致线程间容易发生互相干扰,安全性差。需要性能时使用线程(如科学计算),需要安全时使用进程(如浏览器)
线程的实现:
(1) 用户线程:在用户空间实现,由用户线程库管理调度,自定义调度算法,OS只管理进程层面;POSIX Pthreads,Mach C-threads,Solaris threads
(2) 内核线程,在内核中实现,由OS管理,粒度小,但产生用户态到内核态切换的开销;Windows,Solaris,Linux
(3) 轻量级进程:在内核中实现,支持用户线程,每个进程拥有多个轻量级进程,各自对应一个内核线程 ;Solaris
用户进程的缺点:
(1) 阻塞性的系统调用如何实现?如果一个线程发起系统调用而阻塞,则整个进程在等待
(2) 当一个线程开始运行后,除非它主动地交出CPU的使用权,否则它所在的进程当中的其他线程将无法运行
(3) 由于时间片分配给进程,故与其它进程比,在多线程执行时,每个线程得到的时间片较少,执行会较慢
、
上下文切换
停止当前运行进程(从运行状态改变成其他状态)并且调度其他进程(转变成运行状态)
(1) 必须在切换之前存储许多部分的进程上下文
(2) 必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过
(3) 必须快速(上下文转换时非常频繁的)
需要存储什么上下文
寄存器(PC,SP,。。。),CPU状态。。。
一 些时候可能会费时,所以我们应该尽可能避免
操作系统将进程控制块放置在一个合适的队列中
● 就绪队列
● 等待IO队列(每个设备的队列)
● 僵尸队列
创建进程
(1) Windows进程创建API:CreateProcess(filename)
(2) Unix进程创建系统调用:fork/exec
fork()把一个进程复制成二个进程,parent(old PID),child(new PID)
exec()用新程序来重写当前进程,PID没有改变
用fork和exec创建进程的示例
int pid = fork();
if(pid == 0){//子进程在这里继续
exec(“program”,argc,argv0,argv1,…);
}else if(pid>0){
//父进程在这个运行
}
fork()创建一个继承的子进程
(1) 复制父进程的所有变量和内存
(2) 复制父进程的所有CPU寄存器(有一个寄存器例外)
fork()的返回值
(1) 子进程的fork()返回0
(2) 父进程的fork()返回子进程标识符
(3) fork()返回值可方便后续使用,子进程可使用getpid()获取PID
进程的加载和执行:
(1) Exec()调用允许一个进程“加载”一个不同的程序并且在main开始执行(事实上 _start)
(2) 它允许一个进程指定参数的数量(argc)和它字符串参数数组(argv)
(3) 如果调用成功
(4) 代码,stack(栈)&heap(堆)重写
进程的等待和终止
wait()系统调用是被父进程用来等待子进程的结束
(1) 一个子进程向父进程返回一个值,所以父进程必须接受这个值并处理
(2) wait()系统调用担任这个要求
(1) 它使父进程去睡眠来等待子进程的结果
(2) 当一个子进程调用exit()的时候,操作系统解锁父进程,并且将通过exit()传递得到的返回值作为wait调用的一个结果(连同子进程的pid一起),如果这里没有子进程存活,wait()立刻返 回
(3) 当然,如果这里有为父进程的僵尸等待,wait()立即返回其中一个值(并且解决僵尸状态)
● 进程结束执行之后,它调用exit()
● 这个系统调用:将这程序的"结果"作为一个参数
○ 关闭所有打开的文件,连接等等
○ 释放内存
○ 释放大部分支持进程的操作系统结构
○ 检查是否父进程是存活着的:如果是的话,它保留结果的值直到父进程需要它;在这种情况里,进程没有真正死亡,但是它进入了僵尸状态
■ 如果没有,它释放所有的数据结构,这个进程死亡
○ 清理所有等待的僵尸进程
● 进程终止是最终的垃圾收集(资源回收)
第八章 调度算法
背景:
上下文切换:
● 切换CPU的当前任务,从一个进程/线程到另一个
● 保存当前进程/线程在PCB/TCP中的执行上下文(CPU状态)
● 读取下一个进程/线程的上下文
CPU调度
从就绪队列中挑选一个进程/线程作为CPU将要运行的下一个进程/线程
调度程序:挑选进程/线程的内核函数
内核运行调度程序的条件(满足一条即可)
(1) 一个程序从运行状态切换到等待状态
(2) 一个进程被终结了
不可抢占:调度程序必须等待事件结束
可以抢占
(1) 调度程序在中断被响应后执行
(2) 当前的进程从运行切换到就绪,或者一个进程从等待切换到就绪
(3) 当前运行的进程可以被换出
调度原则
● 调度策略
人们通常都需要"更快"的服务
什么是更快?
○ 传输文件时的高带宽
○ 玩游戏时的低延迟
○ 这两个因素是独立的
和水管类比
○ 低延迟: 喝水的时候想要一打开水龙头水就流出来
○ 高带宽: 给游泳池充水时希望从水龙头里同时流出大量的水,并且不介意是否存在延迟
我们的目标:
○ 减少响应时间: 及时处理用户的输出并且尽快将输出提供给用户
○ 减少平均响应时间的波动: 在交互系统中,可预测性比高差异性低平均更重要
○ 增加吞吐量: 减少开销(操作系统开销,上下文切换);系统资源的高效率用(CPU,IO设备)
○ 减少等待时间: 减少每个进程的等待时间
● 程序执行模型
执行模型 : 程序在CPU突发和IO中交替
○ 每个调度决定都是关于在下一个CPU突发时将哪个工作交给CPU
○ 在时间分片机制下,线程可能在结束当前CPU突发前被迫放弃CPU
● 评价指标
CPU使用率:CPU处于忙状态所占时间的百分比
吞吐量:在单位时间内完成的进程数量
周转时间:一个进程从初始化到结束,包括所有等待时间所花费的时间
等待时间:进程在就绪队列 中的总时间
响应时间:从一个请求被提交到产生第一次相应所花费的总时间
调用算法:
(1) FCFS(先来先服务) First come,First Served
规定:如果进程在执行中阻塞,队列中的下一个会得到CPU
优点:简单
缺点
(1) 平均等待时间波动较大
(2) 花费时间少的任务可能排在花费时间长的任务后面
(3) 可能导致I/O和CPU之间的重叠处理
(2) SPN(SJF) SRT短进程优先(短作业优先) 短剩余时间优先
规定:按照预测的完成时间将任务入队,可以是可抢占的或者不可抢占的
问题:可能导致饥饿,连续的短任务流会使长任务饥饿,都安人物可用时的任何长任务的CPU时间都会增加平均等待时间
需要预知未来:怎么预估下一个CPU的突发的持续时间,简单的解决方法询问用户,如果用户欺骗就杀死进程
(3) HRRN 最高响应比优先
(4) Round Robin (轮循)
使用时间切片和抢占来轮流执行任务
在叫做量子(或者时间切片)的离散单元中分配处理器
时间片结束时,切换到下一个准备好的进程
(5) Multilevel Feedback Queues (多级反馈队列)
优先级队列中的轮循
就绪队列被划分成独立的队列: 比如前台(交互),后台(批处理)
每个队列拥有自己的调度策略: 比如前台(RR),后台(FCFS)
调度必须在队列间进行:
● 固定优先级: 先处理前台,然后处理后台;可能导致饥饿
● 时间切片: 每个队列都得到一个确定的能够调度其进程的CPU总时间;比如80%使用RR的前台,20%使用FCFS的后台
一个进程可以在不同的队列中移动
例如,n级优先级-优先级调度在所有级别中,RR在每个级别中
● 时间量子大小随优先级级别增加而增加
● 如果任务在当前的时间量子中没有完成,则降到下一个优先级
优点: CPU密集型任务的优先级下降很快;IO密集型任务停留在高优先级
(6) Fair Share Scheduling(公平共享调度)
FSS控制用户对系统资源的访问
● 一些用户组比其他用户组更重要
● 保证不重要的组无法垄断资源
● 未使用的资源按照每个组所分配的资源的比例来分配
● 没有达到资源使用率目标的组获得更高的优先级
实时调度
定义:正确性依赖于时间和功能两方面的一种操作系统
性能指标
(1) 时间约束的及时性
(2) 速度和平均性能相对不重要
主要特性:时间约束的可预测性
强实时系统:需要保证的时间内完成重要的任务,必须完成
弱实时系统:要求重要的进程的优先级更高,尽量完成,并非必须
任务:一次计算,一次文件读取,一次信息传递等等
属性:取得进展所需要的资源,定时参数
多处理调度
多处理器的CPU调度更加复杂
(1) 多个相同的单处理器组成一个多处理器
(2) 优点;负载共享
对称多处理器(SMP)
(1) 每个处理器运行自己的调度程序
(2) 需要在调度程序中同步
优先级反转
可以发生在任务基于优先级的可抢占的调度机制中
当系统内的环境强制使高优先级任务等待低优先级任务时发生
第九章同步
独立线程:
(1) 不和其他线程共享资源或状态
(2) 确定性→输入状态决定结果
(3) 可重现→能够重现起始条件,I/O
(4) 调度顺序不重要
合作线程
(1) 在多个线程中共享状态
(2) 不确定性
(3) 不可重现
不确定性和不可重现意味着bug可能是间歇性发生的
进程/线程,计算机/设备需要合作
优点:共享资源,加速(I/O操作和计算可以重叠,多处理器将程序分成多个部分并行执行),模块化(将大程序分解成小程序)
无论多个线程的指令序列怎样交替执行,程序都必须正常工作
● 多线程程序具有不确定性和不可重现的特点
● 不经过专门设计,调试难度很高
不确定性要求并行程序的正确性
● 先思考清楚问题,把程序的行为设计清楚
● 切忌给予着手编写代码,碰到问题再调试
一些概念
Rece Condition(竞态条件)
(1) 系统缺陷:结果依赖于并发执行或者事件的顺序/时间(不确定性,不可重现)
(2) 怎样避免竞态:让指令不被打断
Atomic Operation(原子操作)
(1) 原子操作是指一次不存在任何中断或者失败的执行
该执行成功结束
或者根本没有执行
并且不应该发现任何部分执行的状态
(2) 实际上操作往往不是原子的
有些看上去是原子操作,实际上不是
连x++这样的简单语句,实际上是由3条指令组成的
有时候甚至连单条机器指令都不是原子的
(Critical section)临界区
临界区是指进程中的一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会被执行的代码区域
Mutual exclusion(互斥)
当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并且访问任何相同的共享资源
Dead lock(死锁)
两个或以上的进程,在相互等待完成特定任务,而最终没法将自身任务进行下去
Starvation(饥饿)
一个可执行的进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行
假设我们有一些锁的实现
(1) Lock.Acquire() – 在锁被释放前一直等待,然后获取锁
(2) Lock.Release() – 解锁并唤醒任何等待中的进程
(3) 这些一定是原子操作,如果两个线程都在等待同一个锁,并且同时发现锁被释放了,那么只有一个能够获得锁
互斥:同一时间临界区中最多存在一个线程
Progress:如果一个线程想要进入临界区,那么它最终会成功
有限等待:如果一个线程i处于入口区,那么在i的请求被接受之前,其他线程进入临界区的时间是有限制的
无忙等待(可选):如果一个进程在等待进入临界区,那么在它可以进入之前会被挂起
方法一:禁用硬件中断
(1) 没有中断,就没有上下文切换,因此没有并发
硬件将中断处理延迟到中断被启用之后
大多数现代计算机体系结构都提供指令来完成
(2) 进入临界区,禁用中断
(3) 离开临界区,开启中断
(4) 一旦中断被禁用,线程就无法被停止
整个系统都会为你停下来
可能导致其他线程处于饥饿状态
(5) 要是临界区可以任意长的话,无法限制响应中断所需时间
方法2:基于软件的解决方法
满足进程Pi和Pj之间互斥的经典的基于软件的解决方法(1981年)
使用两个共享数据项
● int turn; //指示该谁进入临界区
● bool flag[]; //指示进程是否准备好进入临界区
进入临界区:
flag[i] = true;
turn = j;
while(flag[j] && turn == j);
退出临界区
flag[i] = false;
Bakery 算法(N个进程的临界区)
● 进入临界区之前,进程接收一个数字
● 得到的数字最小的进入临界区
● 如果进程Pi和Pj收到相同的数字,那么如果i<j,Pi先进入临界区,否则Pj先进入临界区
● 编号方案总是按照枚举的增加顺序生成数字
方法3:更高级的抽象
硬件提供了一些原语
● 像中断禁用, 原子操作指令等
● 大多数现代体系结构都这样
操作系统提供更高级的编程抽象来简化并行编程
● 例如,锁,信号量
● 从硬件原语中构建
锁是一个抽象的数据结构
● 一个二进制状态(锁定,解锁),两种方法
● Lock::Acquire() 锁被释放前一直等待,然后得到锁
● Lock::Release() 锁释放,唤醒任何等待的进程
大多数现代体系结构都提供特殊的原子操作指令
● 通过特殊的内存访问电路
● 针对单处理器和多处理器
Test-and-Set 测试和置位
● 从内存中读取值
● 测试该值是否为1(然后返回真或假)
● 内存值设置为1
bool TestandSet(bool *target){
bool rv = *target;
*target = true;
return rv; }
交换
● 交换内存中的两个值
void Exchange(bool *a, bool *b){
bool tmp = *a;
*a = *b;
*b = tmp; }
总结
锁是更高等级的编程抽象
○ 互斥可以使用锁来实现
○ 通常需要一定等级的硬件支持
常用的三种实现方法
○ 禁用中断(仅限于单处理器)
○ 软件方法(复杂)
○ 原子操作指令(单处理器或多处理器均可)
可选的实现内容:
○ 有忙等待
○ 无忙等待
第十章;信号量和管程
信号量的抽象数据类型
● 一个整形(sem),具有两个原子操作
● P(): sem减一,如果sem<0,等待,否则继续
● V(): sem加一,如果sem≤0,唤醒一个等待的P
信号量是整数
信号量是被保护的变量
(1) 初始化完成后,唯一改变一个信号量的值的办法是通过P()和V()
(2) 操作必须是原子
P()能够阻塞,V()不会阻塞
我们假定信号量是公平的
● 没有线程被阻塞在P()仍然堵塞如果V()被无限频繁调用(在同一个信号量)
● 在实践中,FIFO经常被使用
两个类型信号量
● 二进制信号量: 可以是0或1
● 计数信号量: 可以取任何非负数
● 两者相互表现(给定一个可以实现另一个)
信号量可以用在2个方面
● 互斥
● 条件同步(调度约束——一个线程等待另一个线程的事情发生)
一个线程等待另一个线程处理事情
比如生产东西或消费东西(生产者消费者模式),互斥(锁机制)是不够的
有界缓冲区的生产者-消费者问题
● 一个或者多个生产者产生数据将数据放在一个缓冲区里
● 单个消费者每次从缓冲区取出数据
● 在任何一个时间只有一个生产者或消费者可以访问该缓冲区
正确性要求
● 在任何一个时间只能有一个线程操作缓冲区(互斥)
● 当缓冲区为空时,消费者必须等待生产者(调度,同步约束)
● 当缓存区满,生产者必须等待消费者(调度,同步约束)
每个约束用一个单独的信号量
● 二进制信号量互斥
● 一般信号量 fullBuffers
● 一般信号了 emptyBuffers
class BoundedBuffer{
mutex = new Semaphore(1);
fullBuffers = new Semaphore(0);
//说明缓冲区初始为空
emptyBuffers = new Semaphore(n);
//同时可以有n个生产者来生产 };
BoundedBuffer::Deposit©{
emptyBuffers->P();
mutex->P();
Add c to the buffer;
mutex->V();
fullBuffers->V(); }
BoundedBuffer::Remove©{
fullBuffers->P();
mutex->P();
Remove c from buffer;
mutex->V();
emptyBuffers->V(); }
信号量的双用途
● 互斥和条件同步
● 但等待条件是独立的互斥
读,开发代码比较困难
● 程序员必须非常精通信号量
容易出错
● 使用的信号量已经被另一个线程占用
● 忘记释放信号量
不能够处理死锁问题
管程
目的: 分离互斥和条件同步的关注
什么是管程
● 一个锁: 指定临界区
● 0或者多个条件变量: 等待,通知信号量用于管程并发访问共享数据
一般方法
● 收集在对象,模块中的相关共享数据
● 定义方法来访问共享数据
Lock
● Lock::Acquire() 等待直到锁可用,然后抢占锁
● Lock::Release() 释放锁,唤醒等待者如果有
Condition Variable
● 允许等待状态进入临界区允许处于等待(睡眠)的线程进入临界区
○ 某个时刻原子释放锁进入睡眠
● Wait() operation释放锁,睡眠,重新获得锁放回
● Signal() operation(or broadcast() operation)唤醒等待者(或者所有等待者),如果有
实现
● 需要维持每个条件队列
● 线程等待的条件等待signal()
实现
● 需要维持每个条件队列
● 线程等待的条件等待signal()
class Condition{
int numWaiting = 0;
WaitQueue q; };
Condition::Wait(lock){
numWaiting++; Add this thread t to q;
release(lock); //释放之前获得的锁
schedule(); //need mutex 计划执行
require(lock);
}
Condition::Signal(){
if(numWaiting > 0){
Remove a thread t from q;
wakeup(t); //need mutex
numWaiting–; } }
管程消费者生产者问题
class BoundedBuffer{
Lock lock;
int count = 0; //buffer 为空
Condition notFull, notEmpty; };
BoundedBuffer::Deposit©{
lock->Acquire(); //管程的定义:只有一个线程能够进入管程
while(count == n)
notFull.Wait(&lock); //释放前面的锁
Add c to the buffer;
count++;
notEmpty.Signal();
lock->Release(); }
BoundedBuffer::Remove©{
lock->Acquire();
while(count == 0)
notEmpty.Wait(&lock);
Remove c from buffer;
count–;
notFull.Signal();
lock->Release(); }
开发,调试并行程序很难
● 非确定性的交叉指令
同步结构
● 锁: 互斥
● 条件变量: 有条件的同步
● 其他原语: 信号量
怎么样有效地使用这些结构
● 制定并遵循严格的程序设计风格,策略
第十一章:死锁问题
一组阻塞的进程持有一种资源等待获取另一个进程所占有的一个资源
示例:
● 系统有2个磁带驱动器
● P1和P2各有一个,都需要另外一个
资源类型R1,R2,…,Rm(CPU, memory space, IO devices)
每个资源类型Ri有Wi个实例.
每个进程使用资源如下:
● require,get ← free resource
● use,hold ← requested,used resource
● release ← free resource
可重复使用的资源
● 在一个时间只能有一个进程使用且不能被删除
● 进程获得资源,后来释放由其他进程重用
● 处理器,IO通道,主和副存储器,设备和数据结构,如文件,数据库和信号量
● 如果每个进程拥有一个资源并请求其他资源,死锁可能发生
使用资源
● 创建和销毁
● 在IO缓存区的中断,信号,消息,信息
● 如果接收消息阻塞可能会发生死锁
● 可能少见的组合事件会引起死锁
资源分配图
一组顶点V和边E的集合
● V有两种类型:P={P1,P2,…,Pn},集合包括系统中的所有进程
○ R={R1,R2,…,Rm},集合包括系统中的所有资源类型
● requesting,claiming edge - directed edge Pi → Rj
● assignment,holding edge - directed edge Rj → Pi
基本情况
如果图中不包含循环:
● 没有死锁
如果图中包含循环:
● 如果每个资源类只有一个实例,那么死锁
● 如果每个资源类有几个实例,可能死锁
死锁特征
死锁出现一定会出现以下四个条件,但是出现以下四个条件不一定死锁:
● 互斥: 在一个时间只能有一个进程使用资源
● 持有并等待: 进程保持至少一个资源正在等待获取其他进程持有的额外资源
● 无抢占: 一个资源只能被进程资源释放,进程已经完成了它的任务之后
● 循环等待: 存在等待进程集合{P0,P1,…,Pn},P0正在等待P1所占用的资源,P1正在等待P2占用的资源…Pn-1在等待Pn的资源,Pn正在等待P0所占用的资源
死锁处理方法
常见方法
● 确保系统永远不会进入死锁状态
● 运行系统进入死锁状态,然后恢复.
● 忽略这个问题,假装系统中从来没有发生死锁,用于大多数操作系统,包括UNIX
死锁预防
Deadlock Prevention 预防
限制申请方式
● 互斥 - 共享资源不是必须的,必须占用非共享资源
● 占用并等待 - 必须保证当一个进程请求的资源,它不持有任何其他资源需要进程请求并分配其所有资源,它开始执行之前或允许进程请求资源仅当进程没有资源
○ 资源利用率低,可能发生饥饿
● 无抢占 -如果进程占有某些资源,并请求其他不能被立即分配的资源,则释放当前正占有的资源
○ 被抢占资源添加到资源列表中
○ 只有当它能够获得旧的资源以及它请求新的资源,进程可以得到执行
● 循环等待 - 对所有资源类型进行排序,并要求每个进程按照资源的顺序进行申请
Deadlock Avoidance 避免
需要系统具有一些额外的先验信息提供
最简单和最有效的模式是要求每个进程声明它可能需要的每个类型资源的最大数目
资源的分配状态是通过限定提供与分配的资源数量,和进程的最大需求
死锁避免算法动态检查的资源分配状态,以确保永远不会有一个环形等待状态
当一个进程请求可用资源,系统必须判断立即分配是否能使系统处于安全状态
系统处于安全状态指: 针对所有进程,存在安全序列
序列<P1,P2,…,Pn>是安全的: 针对每个Pi,Pi要求的资源能够由当前可用的资源+所有的Pj持有的资源来满足,其中j<i.如果Pi资源的需求不是立即可用,那么Pi可以等到所有Pj完成
当Pi完成后,Pi+1可以得到所需要的资源,执行,返回所分配的资源,并终止.
用同样的方法,Pi+2,Pi+3和Pn能获得其所需的资源.
如果系统处于安全状态→无死锁
如果系统处于不安全状态→可能死锁
避免死锁: 确保系统永远不会进入不安全状态
Deadlock Detection 检测
每个资源类型单一实例
Maintain wait-for graph
节点是进程
Pi→Pj: Pi等待Pj
定期调用检测算法来搜索图中是否存在循环
算法需要n^2次操作,n是图中顶点的数目
数据结构:
Available: 长度为M的向量表示每种类型可用资源的数量
Allocation: 一个nxm矩阵定义了当前分配给各个进程每种类型资源的数量,如果Alocation[i, j] = k, 进程Pi拥有资源Rj的k个实例
Request: 一个nxm矩阵表示各进程的当前请求.如果Request[i, j] = k,表示进程Pi请求k个资源Pj的实例
检查算法使用
何时,使用什么样的频率来检测依赖于:
死锁多久可能会发生?
多少进程需要被回滚? one for each disjoint cycle
如果检测算法多次被调用,有可能是资源图有多个循环,所以我们无法分辨出多个可能死锁进程中的哪些"造成"死锁
Recovery from Deadlock 恢复
终止所有的死锁进程
在一个时间内终止一个进程直到死锁消除
终止进程的顺序应该是:
● 进程的优先级
● 进程运行了多久以及需要多少时间才能完成
● 进程占用的资源
● 进程完成需要的资源
● 多少进程需要被终止
● 进程是交互还是批处理
选择一个受孩子 - 最小的成本
回滚 - 返回到一些安全状态,重启进程到安全状态
饥饿 - 同一进程可能一直被选作受害者,包括回滚的数量
IPC(Inter Process Communication)
概述
进程通信的机制及同步
不使用共享变量的进程通信
IPC facility 提供2个操作:
send(message) - 消息大小固定或者可变
receive(message)
如果P和Q想通信,需要:
在它们之间建立通信链路
通过send/recevie交换消息
通信链路的实现
物理(例如,共享内存,硬件总线)
逻辑(例如,逻辑属性)
直接通信
进程必须正确的命名对方:
send(P, message) - 发送消息到进程P
receive(Q, message) - 从进程Q接收信息
通信链路的属性
自动建立链路
一条链路恰好对应一对通信进程
每对进程之间只有一个链路存在
链路可以是单向的,但通常是双向的
间接通信
定向从消息队列接收消息
● 每个消息对垒都有一个唯一的ID
● 只有它们共享了一个消息队列,进程才能够通信
通信链路的属性
● 只有进程共享一个共同的消息队列,才建立链路
● 链接可以与许多进程相关联
● 每对进程可以共享多个通信链路
● 链接可以是单向或者双向
操作
(1) 创建一个新的消息队列
(2) 通过消息队列发送和接收消息
(3) 销毁消息队列
原语的定义如下:
send(A,message) --发送消息到队列A
receive(A,message)–从队列A接受消息
● 通信链路缓冲
通信链路缓存大小:
○ 0容量 - 0 message : 发送方必须等待接收方
○ 有限容量 - n messages的有限长度 : 发送方必须等待,如果队列满
○ 无限容量 - 无限长度 : 发送方不需要等待
信号
信号Signal
● 软件中断通知事件处理
● Examples: SIGFPE, SIGKILL, SIGUSRI, SIGSTOP, SIGCONT
接收到信号时会发生什么?
● catch: 指定信号处理函数被调用
● ignore: 依靠操作系统的默认操作(abort, memory dump, suspend or resume process)
● mask: 闭塞信号因此不会传送(可能是暂时的,当处理同样类型的信号)
不足:
● 不能传输要交换的任何数据
管道
数据交换
子进程从父进程继承文件描述符(0 stdin, 1 stdout, 2 stderr)
进程不知道(或不关心)从键盘,文件,程序读取或写入到终端,文件,程序.
例如: $ ls | more (两个进程, 管道是缓存,对于ls来说是stdout,对于more来说是stdin )
消息队列
消息队列按FIFO来管理消息
● message: 作为一个字节序列存储
● message queues: 消息数组
● FIFO & FILO configuration
共享内存
进程
● 每个进程都有私有地址空间
● 在每个地址空间内,明确地设置了共享内存段
优点:快速,方便地共享数据
不足:必须同步数据访问
最快的方法
一个进程写另一个进程立即可见
没有系统调用干预
没有数据复制
不提供同步
● Socket
第十二章:文件系统
文件系统:一种用于持久性存储的系统抽象
- 在存储上: 组织,控制,导航,访问和检索数据
- 在大多数计算机系统包含文件系统
- 个人电脑,服务器,笔记本电脑
- ipod,tivo,机顶盒,手机,电脑
- google可能也是由一个文件系统构成的
文件:文件系统中一个单元的相关数据在操作系统中的抽象、
文件系统的功能:
分配文件磁盘空间
(1) 管理文件块(哪一块属于哪一个文件)
(2) 管理空闲空间(哪一块是空闲的)
(3) 分配算法(策略)
管理文件集合
(1) 定位文件及其内容
(2) 命名:通过名字找到文件的接口
(3) 最常见:分层文件系统
(4) 文件系统类型(组织文件的不同方式)
提供的便利及特征
(1) 保护:分层来保护数据安全
(2) 可靠性/持久性:保持文件的持久即使发生崩溃、媒体错误、攻击等
文件和块
文件属性:名称、类型、位置、大小、保护、创建者、创建时间、最近修改时间…
文件头
(1) 在存储元数据中保存了每个文件的信息
(2) 保存文件的属性
(3) 跟踪哪一块存储块属于逻辑上文件结构的哪个偏移量
文件描述符
文件使用模式:
使用程序必须在使用前先"打开"文件
f = open(name, flag);
…
… = read(f, …);
… close(f);
内核跟踪每个进程打开的文件:
● 操作系统为每个进程维护一个打开文件表
● 一个打开文件描述符是这个表中的索引
需要元数据来管理打开文件:
文件指针: 指向最近的一次读写位置,每个打开了这个文件的进程都这个指针
文件打开计数: 记录文件打开的次数 - 当最后一个进程关闭了文件时,允许将其从打开文件表中移除
文件磁盘位置: 缓存数据访问信息
访问权限: 每个程序访问模式信息
用户视图: 持久的数据结构
系统访问接口:
(1)字节的集合(UNIX)
(2)系统不会关心你想存储在磁盘上的任何的数据结构
操作系统内部视角:
(1)块的集合(块是逻辑转换单元,而扇区是物理转换单元)
(2)块大小<> 扇区大小: 在UNIX中, 块的大小是 4KB
当用户说: 给我2-12字节空间时会发生什么?
(1)获取字节所在的块
(2)返回块内对应部分
如果要写2-12字节?
(1)获取块
(2)修改块内对应部分
写回块
在文件系统中的所有操作都是在整个块空间上进行的: getc()putc() 即使每次只访问1字节的数据,也会缓存目标数据4096字节(一个磁盘块)
用户怎么访问文件: 在系统层面需要知道用户的访问模式
顺序访问: 按字节依次读取(几乎所有的访问都是这种方式)
随机访问: 从中间读写(不常用,但是仍然重要,如: 虚拟内存支持文件,内存页存储在文件中;更加快速,不希望获取文件中间的内容的时候也必须先获取块内所有字节)
内容访问: 通过特征
文件内部结构:
无结构: 单词,比特的队列
简单记录结构: 列,固定长度,可变长度
复杂结构: 格式化的文档(word, PDF), 可执行文件, …
多用户系统中的文件共享是很必要的
访问控制:
谁能够获得哪些文件的哪些访问权限
访问模式: 读,写,执行,删除,列举等
文件访问控制列表(ACL):
<文件实体, 权限>
UNIX模式:
<用户|组|所有人,读|写|可执行>
用户ID识别用户,表明每个用户所允许的权限及保护模式
组ID允许用户组成组,并指定了组访问权限
指定多用户,客户如何同时访问共享文件:
和过程同步算法相似
因磁盘IO和网络延迟而设计简单
UNIX文件系统(UFS)语义:
对打开文件的写入内容立即对其他打开同一文件的其他用户可见
共享文件指针允许多用户同时读取和写入文件
会话语义:
写入内容只有当文件关闭时可见
锁:
一些操作系统和文件系统提供该功能
目录:
文件以目录的方式组织起来
目录是一类特殊的文件: 每个目录都包含了一张表<name, pointer to file header>
目录和文件的树形结构: 早期的文件系统是扁平的(只有一层目录)
一个文件系统需要先挂载才能被访问
一个未挂载的文件系统被挂载在挂载点上
文件别名:
两个或多个名关联同一文件
硬链接:多个文件项指向一个文件
软连接:以“快捷方式”指向其他文件
通过存储真实文件的逻辑名称来实现
如果删除一个有别名的文件会如何呢? : 这个别名将成为一个悬空指针
Backpointers 方案:
每个文件有一个包含多个backpointers的列表,所以删除所有的Backpointers
backpointers使用菊花链管理
添加一个间接层: 目录项数据结构
链接: 已存在文件的另外一个名字(指针)
链接处理: 跟随指针来定位文件
我们如何保证没有循环呢?
只允许到文件的链接, 不允许在子目录的链接
每增加一个新的链接都用循环检测算法确定是否合理
限制路径可遍历文件目录的数量
文件系统种类
磁盘文件系统: 文件存储在数据存储设备上,如磁盘; 例如: FAT,NTFS,ext2,3,ISO9660等
数据库文件系统: 文件根据其特征是可被寻址的; 例如: WinFS
日志文件系统: 记录文件系统的修改,事件; 例如: journaling file system
网络,分布式文件系统: 例如: NFS,SMB,AFS,GFS
特殊,虚拟文件系统
虚拟文件系统
分层结构:
(1)顶层: 文件,文件系统API
(2)上层: 虚拟(逻辑)文件系统 (将所有设备IO,网络IO全抽象成为文件,使得接口一致)
(3)底层: 特定文件系统模块
目的: 对所有不同文件系统的抽象
功能:
(1)提供相同的文件和文件系统接口
(2)管理所有文件和文件系统关联的数据结构
(3)高效查询例程,遍历文件系统
与特定文件系统模块的交互
数据结构:
卷[第四声]控制块(UNIX: “superblock”)
(1)每个文件系统一个
(2)文件系统详细信息
(3)块,块大小,空余块,计数,指针等
文件控制块(UNIX: “vnode” or “inode”)
(1)每个文件一个
(2)文件详细信息
(3)许可,拥有者,大小,数据库位置等
目录节点(Linux: “dentry”)
(1)每个目录项一个(目录和文件)
(2)将目录项数据结构及树形布局编码成树形数据结构
(3)指向文件控制块,父节点,项目列表等
其中: 卷控制块(每个文件系统一个),文件控制块(每个文件一个),目录节点(每个目录项一个)
持续存储在二级存储中: 在分配在存储设备中的数据块中
当需要时加载进内存:
卷控制块: 当文件系统挂载时进入内存
文件控制块: 当文件被访问时进入内存
目录节点: 在遍历一个文件路径时进入内存
数据缓存
数据块按需读入内存:
(1)提供 read() 操作
(2)预读: 预先读取后面的数据块
数据块使用后被缓存:
(1)假设数据将会再次被使用
(2)写操作可能被缓存和延迟写入
两种数据块缓存方式:
(1)普通缓冲区缓存
(2)页缓存: 同一缓存数据块和内存页
分页要求: 当需要一个页时才将其载入内存
支持存储: 一个页(在虚拟地址空间中)可以被映射到一个本地文件中(在二级存储中)