BIOS
存在内存的特定地址,启动执行,检查计算机硬件
加载位于硬盘特定地址的BootLoader,管理权交给BootLoader
BootLoader加载os,管理权交给os
系统调用
应用程序主动向操作系统发起请求指令(syscall)。同步或异步
异常
应用程序执行中被动触发操作系统执行指令。应用程序意想不到的行为。除以零、访问未知地址等。同步。应用可以感知
中断
外设向操作系统发出请求,需要操作系统提供支持。异步。对应用透明
陷阱
陷阱指的是当异常或者中断发生时,处理器捕捉到一个执行线程,并且将控制权转移到操作系统中某一个固定地址的机制。现代操作系统是由中断驱动的,中断分为硬件中断和软件中断。而陷阱属于一种软件中断。如果计算机没有进程要执行,没有用户响应请求,操作系统将等待某个事件的发生。而事件总是由中断或者陷阱引起的
双重模式操作
操作系统为保证系统正常运行,会提供两种操作模式:
- 用户模式:用户进程执行的模式,模式位1
- 系统模式:系统调用的模式,模式位0
两种模式不会互相干扰。操作系统需要硬件支持来提供双重模式操作:当用户进程进行非法操作(非法指令或非法地址访问)的时候,硬件会向操作系统发出陷阱信号,导致控制权转移到操作系统,操作系统结束用户进程并给出错误信息
进程
运行中的程序。分为用户进程和系统进程
内存管理
内存是cpu和io设备可以共同访问的数据仓库。如果cpu需要访问磁盘数据,首先生成io调用将数据加载进内存,之后才能直接访问
虚拟地址
编译器会应用程序生成的地址
物理地址
实际内存条(主存)和硬盘提供的可用地址
cpu访问内存数据的过程
- cpu中ALU(算术逻辑单元)在运算的时候需要获取某个虚拟地址的数据
- 虚拟地址到物理地址的映射关系会保存在MMU(内存管理单元)和内存中。MMU首先查找自己的映射表中虚拟地址对应的物理地址,如果找不到就去内存查找,直到找到物理地址
- cpu向总线发起访问物理地址的请求
- 内存访问物理地址的数据并通过总线传递给cpu
操作系统在其中扮演者预先建立虚拟地址到物理地址映射关系的角色,并且还限制了每个应用访问虚拟地址的空间。如果cpu执行某个应用程序时,访问了不属于它的空间,就会产生一个内存访问异常,操作系统会接管并执行相应的异常处理代码
内存碎片
内存中空闲的,未被使用的空间
外碎片
分配单元之间,没被使用的空闲空间
内碎片
已分配给该应用,但是该应用未利用到的空闲空间
内存管理
合理的内存管理就是减少这些内存碎片
- 操作系统为了加载应用程序,会提前分配一段连续的内存空间
- 应用程序为了存储、访问一些数据,需要向操作系统发起请求,操作系统为它分配一段连续的内存空间
简单的连续的内存分配算法
- 首次适配:找到第一个足够大小的空闲块就立马分配
- 最优适配:找到(空间块 - 需求块)非负数并且最小的空闲块。需要排序,容易导致很多小碎片
- 最差适配:找到(空闲块 - 需求块)最大的空闲块。需要按空闲大小倒序排序,容易导致后面申请的大块都无法分配
碎片整理
- 压缩式碎片整理:将已使用的内存块进行移动,压缩成连续的空间,最后留出大的空闲块。需要在应用暂停执行的时候才能移动,否则应用访问内存数据出错。涉及反复拷贝操作,开销较大
- 交换式碎片整理:如果通过压缩也没有足够内存块使用,可以将不活跃应用申请的内存块换出到硬盘,留出足够的空间分配给活跃应用
非连续内存管理
- 优点:一个程序的物理内存是非连续的,这样可以提高内存利用率,减少碎片。并且将程序分成多个模块分别进行管理,让有的模块可以被多个程序共享,还可以对这些模块采用不同的管理方式。
- 缺点:需要建立虚拟地址到物理地址的映射关系,采用软件的方式建立性能开销大,需要依赖硬件方式建立
两种硬件方案:分段、分页
分段
将应用分成多个段:堆、运行栈、程序数据、程序text段。在虚拟地址是连续的,但是映射到物理地址是分离的
分段寻址机制:在分段机制中,要将虚拟地址映射到物理地址,虚拟地址被划分成两个部分,第一段为segment number,第二段为segment offset。cpu根据segment number去segment table中定位到该number对应物理内存的base地址,及该segment长度limit。然后通过检查offset是否小于等于limit,如果不是就触发异常,如果是该虚拟地址就成功映射到物理地址base+offset
segment table是由操作系统预先建立,并存入硬件
分页
分段在cpu中使用较少,分页使用较多
分页类似分段,也是将内存分成多个部分
在虚拟内存中,这些部分称为page(页),在物理内存中,这些部分称为frame(帧)。虚拟地址由page number和page offset定位,物理地址由frame number和frame offset定位,page number与frame number大小可以不一致,但是两者的offset必须一致,并且page size和frame size大小一致。
硬件寻址的时候根据虚拟地址划分出page number和page offset,并通过page table和该table的base地址查询page number到frame number的映射关系,最后定位到frame number,由于page offset与frame offset一致,所以最终物理地址就是frame number + page offset
分段与分页唯一不同的地方在于每个segment的大小不同,而每个page大小相同(1k、4k等)。所以segment table需要记录[segment number -> 物理内存base地址,segment长度limit]映射关系。而page table只需要记录[page number -> frame number]映射关系
通过分段或分页中,虚拟内存是连续的,但是定位到物理内存可以是非连续的,这样提高了内存利用率,减少了碎片
page table由操作系统建立
页表
假设page size为1k(2^10),64位操作系统需要建立的page table大小达到2^(64-10)=2^54,这么大的page table cpu和内存都无法存放,这导致空间开销大。即使内存可以存下,每次寻址都要先查page table再查物理内存,涉及到两次内存查询,这导致时间开销大
- 时间:通过TLB解决
- 空间:二级/多级页表、反向页表
TLB
每次cpu的MMU(内存管理单元)寻址的时候都会查询page table,这样导致时间开销很大。TLB是MMU中包含的一个缓存,缓存了page table的数据,空间很小速度很快
为了减少TLB查询miss,应用需要保证访问局部性
二级/多级页表
以二级页表说明
将虚拟地址分成三部分:
- p1:一级页表编号
- p2:二级页表编号
- page offset:页表offset
内存中建立两个page table,一级page table中存储二级page table的base地址,二级page table存储frame number
寻址方式:
- MMU已知一级page table的起始地址,加上p1,定位到一级page table中存储的二级page table的base地址
- 根据该base加上p2,定位到frame number
- 最终加上offset,定位到物理地址
这种方式之所以能减少空间,可以通过64位系统、1k page size情况来说明:
只用一个page table会导致需要建立大小为2^54的page table,根据这个page table中的信息判断虚拟地址是否有对应的物理地址
采用二级page table会让一级page table成为二级page table的索引,如果索引不到二级page table无需建立,索引到才需要建立。当虚拟地址远远大于物理地址的时候,会极大减少二级page table的item
结合上述结果,可得如果更多级page table会大大减少所需空间,不过也会带来另外一个问题:多次访问内存page table致使时间开销大。这个可以通过TLB解决
需要为每个进程分配多级页表,以保证进程隔离,进程不会越界访问其他进程的虚拟地址。隔离之后,每个进程看到的虚拟地址可能一致,但映射的物理地址不会相同,这一层通过操作系统保证
反向页表
多级页表会导致管理页表更麻烦,并且大小受虚拟地址影响,而且多级页表的情况下需要为每个进程分配页表,所以空间开销依然不小
反向页表不同,它根据物理地址索引虚拟地址,大小受物理地址限制,并且所有进程共享同一张页表,所以空间开销很小。需要区分每个物理内存对应的应用,就需要引入pid(进程编号)
由于cpu只能拿到虚拟地址,利用反向页表的时候,需要遍历页表,匹配指定pid和虚拟地址,最终确定物理地址。相当于遍历数组的值,确定索引,时间开销较大
优化方案
基于hash函数方案:可以通过传递pid和虚拟地址,执行一段hash函数,定位到索引,即物理地址。缺点是设计高效少碰撞的hash算法难度较大,并且访问hash表会导致多出一次内存访问,这个可以通过TLB缓解
虚存技术
内存不够的情况下的解决方案之一。将应用使用的内存分成多个页管理,当内存不够用的时候,将应用没有使用到的页交换到磁盘,当内存足够的时候,将应用需要的页交换到内存。通过操作系统内核和MMU完成
内存置换的时候,需要一些信息,这些信息存在页表项中
页表由页表项组成,每个页表项包含
- 虚拟页编号
- 访问位:表示该页是否被访问过(读写)。1表示是,0表示否。置换算法优先置换没有被访问过的页
- 修改位:表示该页在内存中是否被修改过。1表示是,0表示否。如果被修改过,交换该页的时候,需要重新写到磁盘。如果没被修改过,直接释放该页即可,磁盘已经保存该页内容
- 保护位:表示该页的访问权限。可读、可读写、可执行等
- 驻留位:该页在内存还是磁盘。1表示该页在内存;0表示该页在磁盘,访问到该项将导致缺页中断。
- 物理页编号
程序局部性
虚存技术将内存置换交给了操作系统,但同样提出了对应用程序的要求。它希望程序能够保证局部性
- 时间局部性:一个数据的一次访问与下一次访问间隔时间很短
- 空间局部性:一条指令与下一条指令访问的数据集中在一个很小的区域
当程序达到时间局部性和空间局部性,就说程序的局部性很好,它能让内存置换次数更少,使得访问虚存就像访问内存一样简单、快速
缺页中断
当访问虚拟内存无法映射到物理内存时,会产生缺页中断,流程为:
- 若还有空闲物理页面,则分配物理页帧f,则进入步骤4;若没有空闲物理页面,则进入步骤2
- 通过页面置换算法,选出物理页f与其对应的虚拟页q,如果该页的修改位为1,则将该页写回磁盘
- 修改q对应的页表项(将驻留位改为0)
- 将硬盘中的数据读到物理页帧f,并修改对应的页表项(将驻留位改为1,将物理页号改为f)
- 重新运行被中断的指令
最优置换算法
局部页面置换算法
当出现缺页中断并且没有足够的物理页面时,需要置换算法保证将一部分页面写到磁盘,并腾出相应的空间。
最优置换算法希望保证将要置换的页面在后面很长一段时间都不会访问到
不过这种情况较理想,因为无法预测未来应用会访问到的页面,所以这种方式无法实现。不过这种算法可以作为一个理想评价标准
FIFO置换算法
局部页面置换算法
操作系统维护一个链表,维护使用过的页信息。链表采用先进先出原则,在缺页中断发生时,链表表头的页会被淘汰。效果较差
LRU置换算法
局部页面置换算法
最久未被使用的页优先被淘汰
采用栈或链表实现:每次访问页面,如果之前访问过,则替换到表头或栈顶,并淘汰表尾或栈底的元素。每次访问页面都要遍历一次链表或者栈,时间开销较大。如果采用hash表,空间开销大
CLOCK置换算法
局部页面置换算法
将页组织成一个环形链表,当出现缺页中断时,指针沿着环形链表不断向后转动,如果发现访问位为1的则将它置为0,如果碰到访问位为0的则淘汰该页
访问位是在cpu访问该页之后置为1的,代表该页最近被访问过。这个算法相当于给了两次机会,当需要置换时,会检查该页,如果为1,则置为0,表示只有最后一次机会留在内存。如果一个页经常被访问,操作系统置换检查时,该位应该总是1
增强CLOCL置换算法
局部页面置换算法
上述CLOCK算法没有考虑需要置换的页面是否被修改过。
- 如果被修改过,则置换该页成本较大,需要将页数据写出
- 如果没被修改过,说明该页内容已经存在于磁盘,置换成本很小,直接释放该页即可
增强CLOCK算法思想是优先淘汰没有访问并且没有写的页。它引入修改位,将访问位与修改位视为二元组,在环形链表中:
- 如果指针指向页的二元组为11,则置为01(访问为置为0)
- 如果指针指向页的二元组为10或01,则置为00
- 如果指针指向页的二元组为00,则直接淘汰
LFO置换算法
局部页面置换算法
淘汰使用最少的页
这种置换算法有两个问题:
- 需要一个计数器来统计使用次数,这个计数器不断增长,容易变得很大,需要专门的硬件(内存或寄存器)来存储,开销大
- 有些应用初始化操作会频繁加载某些页,但是之后不再使用。导致这些页面计数很多,但是以后不会再用
- 比较计数大小的时间开销较大
第二个问题的解决方案是,定时将计数寄存器右移,保证一直未被使用的数据,最后会指数级衰减
belady现象
给每个应用分配的物理页面越多,缺页率反而越高的异常现象
工作集
一个进程当前正使用的逻辑页面集合。由二元组W(t,△)
- t表示当前时间
- △表示一个时间窗口
- W(t,△)表示当前时间t之前的△时间窗口中的所有页面集合
- |W(t,△)|表示工作集大小,即页面数
|W(t,△)|可以量化应用局部性,如果页面数越大,局部性越差
常驻集
当前时刻,进程实际驻留在内存中的页面集合,大小等于操作系统分配给该进程的物理页数,内容取决于操作系统采用的页面置换算法。
工作集与常驻集:进程不断访问内存页,形成工作集集合,访问页的时候会查询常驻集中页,如果不存在则引发缺页中断
局部与全局页面置换算法
之前介绍的FIFO,LRU,CLOCk,增强CLOCK都是局部页面置换算法,只考虑某个进程内页面置换的情况。全局页面置换算法会考虑所有进程进行页置换的情况。
为什么需要全局置换算法?
每个进程工作集不断变化,程序局部性也在不断变化,操作系统可以根据这些信息动态调整常驻集大小,避免浪费或者资源不够。所以,全局页面置换算法相比局部页面置换算法,更加合理
为什么介绍局部置换算法?
既然全局置换算法更合理,为什么介绍局部置换算法呢?因为有的局部算法如FIFO,LRU,也可以用在全局;有的系统更加依赖局部置换算法来保证部分程序出现抖动不会影响全局系统
工作集页置换算法
全局页面置换算法
常驻集只保存工作集中的页面,所有不在工作集中的页面都要置换出去。每次应用访问内存页的时候,都会进行置换算法
缺页率置换算法
全局页面置换算法
思想是根据缺页率动态调整常驻集大小。当缺页率过高时,增大常驻集(物理页帧数);当缺页率过小时,减小常驻集。保证缺页率在一个合适的范围内
实现:
- t1表示当前缺页中断时间,t2表示上次缺页中断时间,T表示缺页率阈值,高于该阈值表示过高,低于等于该阈值表示过低
- 当t2 - t1 > T,从常驻集中移除不在[t1,t2]时间内的页
- 当t2 - t1 <= T,在常驻集中添加缺失的页
相比较工作集页置换算法,缺页率置换算法以缺页率为参考依据,工作集置换算法需要在每个时刻执行算法,缺页率算法只是在缺页的时候才会执行算法
抖动
当进程数越来越多,分配给每个进程的物理页数越来越少,导致常驻集永远无法包含工作集,致使大量缺页中断,操作系统频繁进行内存置换,cpu利用率大大降低。这种现象称为抖动
抖动出现代表操作系统的负载过高,这种负载往往是过多的进程数或不合理的物理页数导致的。为了缓解这种情况,操作系统需要保证:
- 所有进程的工作集总和 = 物理内存大小。如果前者大于后者,会导致物理内存不够用,经常会出现缺页中断;如果前者小于等于后者,说明物理内存足够空闲,每个进程能分配到足够的工作集。
- 平均缺页间隔时间(MTBF) = 缺页中断处理时间(PFST)。如果MTBF >= PFST,表示缺页间隔时间较长,不算很频繁,有足够的时间处理缺页中断;如果MTBF < PFST,表示缺页间隔较短,很频繁,没有足够的时间处理缺页中断
实际情况,第一个方案较难实现,往往采取第二个方案
进程与程序
关联:
- 进程是正在运行的程序。它拥有独立的堆、栈、代码段、数据段
- 程序是可执行文件,只有被操作系统加载进内存之后才能成为进程
- 一个程序可以衍生出多个进程,每个进程使用的数据内容都不相同。一个进程可能由多个程序组成。两者是多对多映射关系
区别:
- 进程是动态的,程序是静态的。程序是有序代码的集合;而进程是运行的程序,它拥有用户态和系统态之分
- 进程是暂时的,程序是永久的。进程结束之后,它就不存在了;只要硬盘不坏,它存储的程序就一直存在
- 进程和程序的组成不同。进程不光包含程序,还包含运行中的输入输出的数据,以及进程的状态信息
进程组成
进程包含如下信息:
- 代码
- 数据
- PC寄存器,存储下一条执行指令
- 一组通用寄存器
- 一组系统资源,如内存、文件系统、网络等资源
进程控制块
操作系统用来代表每个进程的数据结构,简称PCB。PCB可以描述进程的基本情况和状态变化的过程,是每个进程的唯一标识
包含三类信息:
- 进程标识信息
- 状态信息的保存区。每个进程都会利用cpu对通用寄存器、PC寄存器、状态寄存器、栈指针寄存器做一些修改,这些修改在进程切换之前需要保存下来,以便恢复使用。PCB必须能存储这些状态信息
- 进程控制信息。 进程调度情况、进程间通信信息、进程对内存的使用信息、进程使用的资源、进程与其他进程的父子关系都存在这里
通用操作系统会采用链表而不是数组来组织PCB,因为进程会不断创建和销毁,所以需要不断添加和删除操作,采用链表开销更小
进程的生命周期
进程生命周期可以分为:
- 进程创建。进程创建的三种情况:操作系统初始化、用户发起创建进程的请求、进程发起创建进程的系统调用
- 进程就绪。一旦进程创建完成,并且初始化完成,它就处于就绪状态,表示可以被执行
- 进程运行。操作系统会从多个就绪状态的进程中选择一个给cpu执行
- 进程等待。进程需要等待一些事件完成,会阻塞自己
- 进程唤醒。当处于等待状态的进程所需要的事件完成时,就会被(其他进程或操作系统)唤醒。一旦唤醒,进程进入就绪状态
- 进程结束。进程会因为这几种情况结束:正常退出(自愿)、异常退出(自愿)、致命退出(强制:访问非法地址)、被其他进程杀死(强制:kill)
进程状态变化
其中需要说明的是Running与Ready之间的状态转换:内存中有多个就绪的进程需要执行,当进程A执行时间片用完之后,操作系统会切换到进程B,让进程A进入就绪状态,进程B进入运行状态
进程挂起
进程挂起就是当内存不够用的时候,将进程换出到磁盘。挂起分为两种状态:
- 阻塞挂起:进程在磁盘并且等待事件的发生
- 就绪挂起:进程在磁盘,一旦进入内存就是就绪状态
挂起涉及到的状态转换:
- 阻塞 -> 阻塞挂起:进程需要更多内存资源时,优先将阻塞状态的进程挂起,变成阻塞挂起状态
- 就绪 -> 就绪挂起:在高优先级阻塞进程和低优先级就绪进程中,优先挂起后者,变成就绪挂起状态
- 运行 -> 就绪挂起:当出现高优先级阻塞进程进入就绪状态,操作系统可能会挂起运行进程,变成就绪挂起状态
- 阻塞挂起 -> 就绪挂起:当阻塞挂起的进程等待的事件出现时,操作系统会将它转变为就绪挂起状态
进程激活
进程激活就是将进程从磁盘换入到内存,涉及到的状态转换:
- 就绪挂起 -> 就绪:没有就绪进程,或者被就绪挂起的进程优先级较高时,会执行这种转换
- 阻塞挂起 -> 阻塞:当一个进程释放了足够的内存,操作系统会将优先级较高的阻塞挂起进程激活,变成阻塞状态
进程状态队列
操作系统根据进程状态,来分开管理进程:
- 就绪队列管理就绪状态的进程
- 阻塞队列管理阻塞状态的进程
操作系统根据进程的状态,决定将它插入哪个队列中。当进程状态变化时,操作系统会从原队列取出,插入新队列
- 根据优先级将就绪队列分成多种,优先级高的队列优先被处理
- 根据等待的事件类型,将阻塞队列分成多种,每个队列对应一个事件
线程
轻量级进程,是进程中的一条执行流程。一个进程中的不同线程,共享相同的数据区、代码区和打开的系统资源,但是每个线程有自己独立的PC、栈、通用寄存器,以便维护各自的执行状态。进程是资源分配的单位,线程是cpu调度的单位
操作系统采用数据结构TCB来表示线程,线程与进程一样,也拥有就绪、阻塞、运行三种状态,以及状态之间的转换关系
优点:
- 轻量级,打开、释放、切换更快
- 共享数据模型,比进程间消息传递更快
- 共享相同资源,更加节省空间
缺点:
- 安全性差,同一进程不同线程会对共享的数据产生污染
- 稳定性差,同一进程多个线程,其中一个奔溃会引起其他所有线程崩溃
例子:
- 高性能计算往往采用线程
- 浏览器为了稳定性和安全性,往往每个标签页一个进程(chrome)
线程所需的资源:
用户线程
由库函数实现的线程是用户线程。采用库来支持线程的创建、销毁、调度,操作系统并不知道用户线程的存在
优点:
- 线程的操作都在用户态,开销小
缺点:
- 线程执行阻塞操作会block整个进程,因为操作系统只知道进程而不知道线程
- 进程中一个线程只要不让出cpu,其他线程都无法执行,因为操作系统无法对这些线程做调度
- 操作系统只能以进程为单位分配时间片,最终分到每个线程的时间片就非常有限
内核线程
由内核维护PCB和TCB
优点:
- 不会因为个别线程阻塞其他线程,因为操作系统会执行线程切换
- 操作系统能够以线程为单位分配时间片,更灵活
缺点:
- 每次线程操作涉及系统调用,需要从用户态切换到内核态,开销大
案例:
- windows
轻量级线程
内核支持的用户线程,结合了用户线程管理开销小和内核线程稳定性高的优点。
案例:
- Linux方案:每个用户线程对应一个轻量级线程,每个轻量级线程对应一个内核线程,最终用户线程与内核线程一对一。这种方案简单,高效,使用更多
- Solaris方案:用户线程和轻量级线程多对多,轻量级线程和内核线程多对一,最终用户线程和内核线程多对多。这种方案复杂度高,更灵活,但是收益较小
- 其他方案:用户线程和轻量级线程一对一,轻量级线程和内核线程多对一,最终用户线程和内核线程多对一
用户线程与内核线程映射关系:
进程切换
进程切换涉及的几个关键操作:
- 保存进程上下文
- 恢复进程上下文
- 快速切换。为保证速度,一般采用汇编实现
其中上下文主要指进程使用的寄存器,如PC寄存器、栈寄存器、通用寄存器。这些信息在切换之前必须保存,以便之后恢复执行
操作系统为存储这些进程,提供了多种队列:
- 就绪队列
- 阻塞队列
- 僵尸队列
进程创建
- Windows进程创建:CreateProcess
- Unix/Linux进程创建:fork/exec,通过fork复制出一个子进程,并在程序中划分为不同的处理流程,在子进程流程中执行exec重写当前进程,但是pid不变
int pid = fork();//创建一个进程,并返回子进程id
if(pid == 0){
//子进程返回值为0
exec("program",argc,argv0,argv1,...)
}else if(pid > 0){
//父进程返回值为子进程id
...
}else{
//错误
...
wait(pid);//等待子进程结束
}
在fork复制进程的时候,会复制所有进程信息,只有其中各自子进程id不同,使得fork返回值不同
进程加载
Unix/Linux采用exec加载进程,绝大部分情况下fork后会使用exec,保证拷贝的子进程能够加载新程序。如上一节代码,exec会将当前进程所有信息替换为program进程信息
- 简单的fork实现就是拷贝父进程所有数据,但是当子进程执行exec时,这些信息会被替换掉,之前所有拷贝都失去意义,而且开销大
- 高效的fork实现会考虑exec带来的影响。一般只用拷贝所需的基本元信息和页表项,而非全量拷贝,保证子进程在读操作的时候不受影响,并且开销很小。之后exec操作替换所有信息也不会感到可惜。如果子进程没有执行exec,也没有单纯只读,而是做了写操作,操作系统会采用copy on write技术,在写之前全量拷贝,保证父子进程有独立的数据空间
进程等待
wait()用于父进程等待子进程结束
- 子进程exit()返回一个值给父进程
- 父进程wait()等待子进程返回结果
不同次序会有不同结果:
- 1先2后:子进程成为僵尸进程,父进程wait之后直接返回
- 2先1后:父进程阻塞,等着子进程完成exit,并唤醒父进程,将exit code返回给父进程
进程退出
exit()用户进程资源回收
- 它会返回结果给父进程
- 释放资源
- 如果父进程还存在,当前进程进入僵尸状态
- 清理僵尸进程
其中exec的时候可能出现两种情况:
- 进程处于Running
- 可能需要加载硬盘上的程序,进程会从Running转为Blocked
进程调度
从就绪进程中挑选一个占用cpu的过程,如果有多cpu,还需要提前选出一个可用的cpu
进程调度涉及到上下文切换
进程调度时机,首先需要满足这两个条件其中一个才能产生调度:
- 进程由运行状态切换到等待状态
- 进程结束
其次,还需要分情况讨论具体调度时机:
-
非抢占系统:
- 进程主动让出cpu
-
可抢占系统:
- 进程时间分片用完
- 进程由等待切换到就绪状态(表示马上会抢占cpu)
长进程:执行时间较长的进程
短进程:执行时间较短的进程
先来先服务算法
进程调度算法之一。优先服务先入队列的进程
优点:
- 简单
缺点:
- 如果长进程在就绪队列前排位置,容易导致平均等待时间过长
- 如果cpu密集型排在io密集型之前,容易导致io长时间得不到利用
短进程优先算法
进程调度算法之一。优先服务短进程。涉及到两个技术点:
- 排序
- 预测进程执行时长:类似牛顿法求根号的方式,不断预测并矫正,保证最后逼近真实值
优点:
- 平均等待时间较短
缺点:
- 容易导致进程饥饿(长进程一直无法执行)
最高响应比优先算法
进程调度算法之一。根据就绪队列中进程列出如下公式:
R=(w+s)/s
- w:进程等待时间
- s:进程执行时间
- R:响应比
等待时间越长,R越大;执行时间时间越长,R越小。该算法优先服务R最大的进程。可见,它相当于短进程优先算法的改进版
优点:
- 平均等待时间较短
- 防止个别进程无限期等待
缺点:
- 不支持抢占
时间片轮转算法
调度算法之一。指定时间片大小为N,按先后顺序取出就绪队列中的进程执行,时长不超过N,执行完成之后,取下一个进程继续执行。取进程的顺序与先来先服务一致,但是每个进程执行时间不得超过N
优点:
- 保证每个进程都能执行
缺点:
- 如果时间片设置太小,会有大量上下文切换,开销大,并且平均等待时间较长
- 如果时间片设置太大,会退化成先来先服务算法
经验规则:通常设置成10ms,可以保证上下文切换开销在1%以内
多级队列算法
调度算法之一。将就绪队列分为多级子队列,根据每个队列具体情况的不同,采用不同的算法,是一种综合使用所有算法的一种算法
多个子队列之间采用时间片轮转算法来调度,每个队列保证一定的cpu执行时间。
如:两个队列,前台交互式,占80%时间,后台批处理,占20%时间。则每个执行周期会执行4次前台进程和1次后台进程
多级反馈队列算法
多级队列算法改进版。多级队列算法没有考虑到实际队列中进程执行时间,有的较长但放在了前台队列,导致真正响应快速的进程没有得到更高的优先级
将多级子队列按优先级从高到低划分,优先级越低,分得的时间片越大,适合执行批处理任务,优先级越高,分得的时间片越小,适合执行交互式任务。执行顺序从高到底,高优先级队列执行完了才执行低优先级队列,在规定时间片内每执行完的进程,被下放到低一级的队列,但获取到了更多时间片
优点:
- 优先执行了交互式任务
- 高优先级的总时间较小,不会导致低优先级队列饥饿
- 灵活,自动调整队列中的进程优先级,每个队列可以采用适合自己的算法
缺点:
- 实现复杂度较高
这是实际系统会采用的算法
公平共享调度
按用户组重要程度,给他们使用的系统资源划分优先级,没用完的资源用比例划分
不做详细介绍
实时调度
有些系统不仅要保证功能性,更要保证实时性,希望能再可预测的时间之内完成某个功能。它对吞吐量要求并不高,但是对实时响应要求非常高。根据实时程度,分为两种:
- 硬实时:必须在指定时间内完成功能
- 软实时:尽量在指定时间内完成功能
一个实时任务中涉及的重要概念
系统往往由多个实时任务组成,多个实时任务通常是周期性请求,如图周期为5
其中假设周期为5,最大执行时间为1,则使用率为1/5
为保证调度系统实时可控,需要确立众多任务调度顺序,有两种方法
- 静态优先级调度:事先确定调度优先级
- 动态优先级调度:动态调整调度优先级
速率单调调度
任务周期越短(速率越快)的优先级越高
因为任务的周期(速率)是调度之前就能确定好的,所以它是静态优先级调度算法
最早截止时间调度
请求的任务中,截止时间越早,优先级越高
任务不同,截止时间也不同,所以只有处理任务请求的时候才能知道截止时间,所以它是动态优先级调度算法
非对称多处理器调度
选取其中一个cpu作为主,进行调度和资源访问,无需同步,但是无法利用多处理器
对称多处理器调度
简称SMP,每个cpu运行自己的调度程序,访问共享资源的时候会需要同步,同步开销较大,但是更加通用。多处理器调度其实是考虑如何将进程分配给cpu,有两种方案:
-
静态进程分配:为每个cpu维护一个就绪队列,进程一旦分配到这些队列,会一直属于该队列,进程一旦执行就不会被切换,相当于将进程绑定给cpu
- 优点:调度开销小
- 缺点:进程执行时间不同会导致cpu负载不均衡
-
动态进程分配:为所有cpu维护一个共享就绪队列,进程会分配给空闲可用的cpu执行,中途会被切换
- 优点:cpu负载均衡
- 缺点:调度开销(同步、上下文切换)
优先级反置
低优先级进程阻塞高优先级进程的现象
如三个进程A,B,C,优先级A<B<C
- A执行,需要访问资源,加锁
- 切换到C执行,需要访问资源,等待A释放锁,进入阻塞
- B开始执行,优先级比A高,抢占cpu执行,并且长时间执行导致A无法释放锁,C也无法获取锁
这样B就阻塞了C的执行
优先级继承
优先级反置的解决方案之一
当低优先级占有资源并且高优先级进程申请资源时,一旦低优先级进程被阻塞(如A被B阻塞),就让它的优先级继承高优先级进程(让A继承C的优先级),这样就能解除阻塞顺利抢占cpu并执行
优先级天花板协议
优先级反置的解决方案之一
当进程占用资源时,将它的优先级提升为M(M=所有可能占有该资源进程的最高优先级),保证让该进程不会被阻塞
缺点:该方案的缺点明显,它的滥用优先级提高的功能,当所有进程优先级一样的时候,这种方案也没用了
进程并发
优点:
- 资源共享,节约成本
- 速度提升
- 提高资源利用率
- 程序模块化,一个进程可以分成多个模块,并发执行
缺点:
- 并发会导致数据正确性、一致性受到影响
原子操作
之所以进程并发会出现问题,是因为很多进程操作都不是原子操作,导致执行一半被中断,并切换到另一个进程,使得数据正确性和一致性受到影响
原子操作是一组不可中断的操作,里面可能涉及多个步骤,要么全部成功,要么全部失败,不可能出现部分成功部分失败的情况
进程交互
进程之间有三种交互情况:
-
进程隔离:各进程资源独立,并发不会导致问题
-
共享资源:访问共享资源,会带来三个问题
- 互斥:一个进程访问共享资源,另一个进程就不能访问
- 死锁:每个进程拥有一部分资源,需要另一部分资源才能执行后续的操作,导致互相等待,而无法继续
- 饥饿:部分进程轮流占用资源,导致其他进程无法占用
操作系统要满足共享资源的同时又能提高效率,就必须提供一种高效的同步互斥机制
-
通信协作:通过互相通信来传递信息
临界区
将访问共享资源的一片代码称为临界区
进入区
临界区
退出区
剩余区
临界区访问规则:
- 空闲则入:没有其他进程进入临界区的时候,可以直接进入
- 忙则等待:临界区被其他进程访问的时候,等待
- 有限等待:不会一直等待,可以设置超时时间
- 让权等待:无法进入临界区而等待的时候,需要放弃cpu使用权(sleep和while空转会一直占用cpu,wait不会)
禁用硬件中断
通过禁用硬件中断可以很好的实现对临界区访问的同步,它可以保证进程不会被中断,也没有进程上下文切换,也没有并发执行。它的思想是通过限制并发、并行,保证进程执行的正确性和一致性
优点:
- 基于硬件方案,实现简单
缺点:
- 系统低效
- 临界区执行时间长会导致后续的进程饥饿
- 没有中断,也就无法及时响应进程的紧急事件
- 只适用于单个cpu。可以保证单个cpu的情况下进程不会并发执行,但是无法保证多个cpu的情况下进程并行执行
现代操作系统提供了禁用中断及恢复中断的指令,但是很少使用
Peterson算法
基于软件方式的实现两个线程同步的经典算法
思想是提供两个共享变量,并组合使用,来代表是否可以访问临界区。可以很好的协调多个线程访问,并且符合临界区访问规则
优点:
- 不依赖硬件,完全基于软件方法实现。虽然涉及多个共享变量的访问并非原子操作,但是依然能覆盖多线程访问的各种可能性
缺点:
- 实现复杂,需要考虑多个共享变量被并发使用的各种可能性,容易出错
- 只适用于两个线程同步,更多线程同步需要将代码改造成更加复杂的模式
锁
通过编程抽象的一种同步机制,底层依赖于硬件支持,保证一个时间点只能有一个线程访问共享资源。涉及到一个变量和两个操作
- value:锁占用状态,1为占用,0为空闲
- 获取锁:获取锁的进程才能访问共享资源
- 释放锁:释放锁后,其他进程才能继续获取锁
- 等待队列:自旋锁不需要等待队列,引入等待队列可以防止cpu空转
锁操作必须保证是原子的。cpu提供了两个原子性操作,库函数通过使用这两个原子操作,来实现锁语义
cpu提供的原子操作
- testAndSet(value):简称ts。返回value的原值,并将value设成1
- exchange(a,b):交换a,b内存中的值
class Lock{
int value=0;
WaitQueue q;
void lock(){
while(testAndSet(value))
block(currentThread);//将当前线程放入阻塞队列,放弃cpu。同时调度其他线程;如果不做任何操作,就是忙等待
}
void unlock(){
value=0;
wakeup(otherThread);//将阻塞队列中的线程唤醒到就绪队列
}
}
优点:
- 适用单cpu或多cpu
- 适用任意多线程
- 可以对多个资源进行同步控制
- 简单易用
缺点:
- 非公平,容易导致线程饥饿。将线程唤醒到就绪队列,操作系统会按线程优先级来调度,而非等待时间,会致使部分低优先级线程一直无法执行
- 有死锁风险。因为会有阻塞操作,当多个线程有多个资源占用时,可能出现死锁风险
基于锁方案的同步机制更加常用
信号量
通过编程抽象的一种同步机制。由一个整数、两个方法、一个队列组成
- sem:整数。代表资源数量
- P():获取资源的方法。实现细节:sem减一,判断是否小于0,是就阻塞,否则继续
- V():释放资源的方法。实现细节:sem加一,判断是否大于等于0,是就唤醒阻塞线程
- q:等待队列
其中信号量提供给用户用,但P()和V()方法由操作系统来实现,优先级比应用进程的优先级,不会被中断,保证了两个方法的原子性
信号量是一种公平的同步方法,先阻塞的线程先进等待队列,并且优先被唤醒。相较而言,自旋锁无法保证公平
用信号量解决生产者消费者问题
在早期操作系统中使用较多,现在很少使用,因为使用复杂,容易出错。并且信号量中P是操作系统来实现,优先级高,可能会保持阻塞,形成死锁
管程
很多现代编程语言(java)使用的一种同步模型,是一种高级同步机制
由以下成分组成:
- 锁:保护共享资源,持有锁的线程才能访问共享资源
- N个条件变量:类似信号量中的sem,区别在管程有多个变量。可以作为同步、唤醒操作的前提条件。通过这些条件变量,管程不仅能实现二元互斥,还能限制线程数量
- 等待队列:没拿到锁的线程都进入等待队列
- Wait:持有锁的线程进入等待队列,释放锁
- Signal:唤醒等待队列中的一个线程
管程解决生产者消费者问题
其中count就是条件变量
其中Wait操作会释放锁,避免出现信号量中的死锁问题,使用相对容易
Henson管程
在T2唤醒T1之后,为了高效不切换,优先继续让T2线程执行(无法确定到底哪个执行),直到T2释放锁,T1才恢复执行。java中采用此方案
while(count==n){
wait()
}
所以上述代码中,wait的线程被唤醒后,可能还是无法执行,导致count的值可能遭到修改,于是必须使用while循环重新判断一次
Hoare管程
在T2唤醒T1后,T2释放缩,T1抢占并执行(确定T1抢占)
if(count==n){
wait()
}
所以上述代码,wait的线程被唤醒后,会抢占执行,期间不会有其他线程修改count的值,所以无需重复判断
hoare侧重于作为原理讲解管程执行流程,而henson才适合用在真正的系统
哲学家就餐
五个哲学家,五根筷子,每个人两边都放了筷子,组成环路。哲学家有两个操作:
- 就餐:需要拿到两边的筷子,进入就餐状态
- 思考:放下两边的筷子,进入思考状态
可能出现一种情况,所有哲学家拿起左边的筷子,等着右边的筷子,最后都无法进食,活活饿死。这就是所谓的死锁情况
解决方案:
- 提供服务员,服务员只能给一个哲学家授权,被授权的哲学家才能拿两边的筷子,然后就餐。这样不会出现死锁,但是同一时刻只有一个人就餐,只需要两根筷子,另外两根浪费了
- 之所以出现死锁是因为它们按照相同顺序拿筷子,只需要让偶数编号的哲学家先拿左边再拿右边,奇数编号的哲学家先拿右边再拿左边即可。这样不但不会出现死锁,不相领的哲学家还可同时就餐,保证四根筷子能够同时利用
读者-写者问题
有四种情况:
- 允许多个读者同时读。多个读者可以同时进入临界区
- 在读的时候不能写。读者进入临界区后,后面的写者必须等待
- 在写的时候不能读。写者进入临界区后,后面的读者必须等待
- 在写的时候不能写。写者进入临界区后,后面的写者必须等待
根据读者、写者的实现机制不同,可能采用不同的优先策略:
- 读者优先策略:优先让就绪的读者马上执行,可能导致写者饥饿
- 写者优先策略:优先让就绪的写者马上执行,可能导致读者饥饿
java的读写锁就是利用管程思想实现了读者-写者机制
死锁
多个进程之间互相等待对方释放资源,并且永远无法结束的一种现象
死锁出现的必要条件:
-
资源互斥:至少有一个资源是互斥类型的,不能允许两个或两个以上的进程同时访问
-
占有并等待:至少有一个进程占有资源并等待另一部分资源才能完成任务
-
非抢占:资源是不可抢占的,必须由占用进程自愿释放
-
循环等待:必须存在类似如下现象
- A,B,C三个进程和一组资源
- A申请B占用的资源
- B申请C占用的资源
- C申请A占用的资源
- 三者形成循环等待
只有满足这四个条件,才会出现死锁
针对死锁的处理方法:
- 死锁预防
- 死锁避免
- 死锁检测和恢复
- 忽略死锁
死锁预防
提前预防死锁,死锁的必要条件有四个,只要其中任何一个不满足,死锁就不会出现
- 资源共享:有些资源无法共享
- 避免占用并等待:避免使用部分资源的同时等待另一部分资源,需要的时候提前申请所有资源,这样导致资源利用率下降
- 抢占:要保证资源必须是可存储可恢复的才能抢占
- 避免循环等待:无法避免用户使用资源的顺序,所以无法避免循环等待
死锁避免
在进程申请资源的时候动态判断有没有出现死锁的可能,如果有就不允许进程使用资源
算法:当进程请求资源时,系统判断资源分配后是否处于安全状态,如果是则分配,否则暂停分配
安全状态指:
一系列进程P1到Pn,保证Pi申请的资源<=当前可用资源+所有Pj占有的资源,j<i。如果Pi申请的资源不能立即分配,暂停Pi,等到所有Pj完成
安全状态与死锁的关系
银行家算法
是一种基于安全状态判断的死锁算法
核心思想:每个客户会申请需要的最大资金量,银行会根据你已经占有的资金,力所能及的将剩余部分借给客户。客户在借完资金后会及时归还,保证银行有充足的储备继续借款
- 银行:操作系统
- 资金:资源
- 客户:进程
数据结构:
- n:线程数量
- m:资源类型数量
- Max:n*m矩阵,代表每个进程所需的最大资源数量,Max[i,j]代表进程i需要的资源j的最大数量
- Allocation:n*m矩阵,代表每个进程已占用的资源数量,Allocation[i,j]代表进程i占有的资源j的数量
- Need:n*m矩阵,代表每个进程需要的资源数量,
Need=Max-Allocation
- Available:m维数组,代表系统中每种资源的可用数量
- Finished:n维数组,代表进程是否执行完成(释放资源)
算法:
- Work=Available,将可用资源拷贝到工作空间Work,将Finished元素初始化为false
- 找到进程i,满足
Finished[i]=false,Need[i] < Allocable
的进程,表示i可以申请资源,否则执行步骤4 Work += Allocation[i],Finished[i]=true
,因为i申请的资源最后会被回收,所以忽略分配过程,直接跳到回收环节。执行步骤2- 当所有Finished元素都是true时,说明发现了安全分配的进程序列,系统处于安全状态;否则会出现死锁,不会为进程分配资源
死锁检测
基于银行家算法的思想,判断会不会出现死锁。在完成算法后,存在Finished[i]=false,代表进程i会导致死锁状态,称i为死锁进程
死锁恢复
通过结束进程来恢复死锁:
- 一次终止所有死锁进程
- 一次终止一个,直到消除死锁
显然后者更好,但是会根据如下条件考虑该优先终止哪个进程:
- 进程优先级:优先低
- 交互进程还是批处理进程:优先批处理
- 进程已执行时间和还需要的执行时间:优先时间短
- 进程已占用资源和还需要的资源:优先资源少
- 终止进程数目:优先关联进程少
通过抢占进程资源来恢复死锁:
将死锁进程回退到安全状态,并抢占它的资源,开销小,但是会出现饥饿现象——进程恢复后又进入死锁状态,继续被抢占
进程通信
简称IPC,进程间交互信息
- 直接通信:进程A、B,A直接将消息发给B,两者必须都在线
- 间接通信:进程A、B,A将消息发给内核提供的消息队列C,B从C取消息,两者可离线
可以分为阻塞与非阻塞
- 阻塞:同步,A发消息给B,必须等待B收到A才能执行后面的操作
- 非阻塞:异步,A发消息给B,不用等待也能执行后面的操作
根据通信链路的缓冲容量可分为
- 0容量:发送方必须等待接收方
- 有限容量:发送方可以一直发,直到容量充满
- 无限容量:发送方可以一直发,没有限制
信号
进程间基于中断传递消息的一种机制。如通过ctrl+c终止运行的程序
原理:应用进程注册信号处理函数,当出现中断时,操作系统会根据信号回调信号处理函数。ctrl+c是操作系统默认给每个进程添加的信号处理函数
缺点是局限性大,只能传递信号类型的消息
管道
进程基于内存文件的通信方式
在命令ls | more
程序中
- shell首先创建管道,管道拥有读写两端
- 创建ls进程,将它的的stdout指向管道写端
- 创建more进程,将它的stdin指向管道读端
这样就在内存中建立了两个进程的消息通道
消息队列
内核提供一个FIFO队列,提供给不同进程通信
优点:
- 使用简单
缺点:
- 将消息从用户态传递给内核,需要进行用户态内核态切换,而且传递消息的过程就是数据拷贝的过程,开销大
共享内存
- 不同线程之间可以直接共享内存
- 不同进程之间共享内存,需要明确设置共享内存段
优点:
- 不需要从用户态切换到内核态,也不需要数据拷贝,开销小
缺点:
- 需要用户层处理数据同步的问题
实现机制:
基于页表来实现。不同进程有自己的页表,操作系统将它们的页表映射到同一个物理地址空间,就完成了进程间共享内存
文件系统
操作系统中将磁盘数据组织成文件系统,提供对这些持久化数据的操作
功能:
- 为数据分配和管理磁盘空间
- 管理文件集合
- 保障数据可靠性和安全性
文件系统需要被挂载才能被访问,它被挂载的目录称为挂载点
根据文件系统的类型分为:
- 本地磁盘文件系统:ext4、NTFS等
- 网络/分布式磁盘文件系统:NFS、GFS等
分布式文件系统会产生安全性、一致性等问题,比本地磁盘文件系统更复杂
文件
文件系统组织磁盘上数据的方式,并抽象了一些属性用于简化这些持久化数据的管理
文件属性:
- 文件名
- 创建日期
- 类型
- 位置
- 大小
- 创建者
- ...
将文件分为两部分:
- 文件头:文件属性、文件存储位置和顺序,在文件系统中管理。这是文件系统管理文件的基本依据
- 文件数据:磁盘上的字节数据
目录
目录是一种特殊的文件,其内容是文件索引表,表的每一项是二元组<文件名,指向文件的指针>。操作目录就是操作文件索引表
应用程序都是通过系统调用来访问目录
文件别名
让多个文件名指向同一个文件,看起来就像给文件起了别名
有两种起别名的方式:
- 硬链接:A为B的硬链接,A和B地位对等,都指向文件实体。文件有一个计数,记录多少个文件名指向它。每增加一个硬链接计数加一,删除一个硬链接计数减一,当计数为零时,文件才会真正被删除。操作方式:
ln src dest
- 软链接:A为B的软链接,A是一个指向B的“快捷方式”,A存储了B的路径。删除B,A成为死链接;删除A,B不受影响。操作方式:
ln -s src dest
文件目录循环
可能出现文件子目录指向父目录,导致出现循环目录的情况,这种情况会致使目录遍历永远无法结束
常见的处理方式是设置一个最大遍历层数
名字解析
文件系统中的名字解析就是根据文件路径,读取文件信息
如/bin/ls
解析过程为:
- 读取根目录的文件头
- 读取根目录的数据块,搜索bin项
- 读取bin目录的文件头
- 读取bin目录的数据块,搜索ls项
- 读取ls目录的文件头
当前工作目录
进程每读取文件的时候,都要从根目录进行名字解析,会导致效率很低。所以操作系统为每个进程设置了一个目录PWD,代表当前工作目录。这样当访问一些当前目录存在的文件时,不需要再从根目录开始遍历
这种机制提供了一种基于相对路径来访问文件的方式
虚拟文件系统
简称VFS,是对底层各个不同文件系统的一个抽象(各个磁盘文件系统和网络文件系统),对上层提供一个统一的api(open、close、read、write)
所有文件系统实现不同,但有些公共数据结构还是一致的
- 超级控制块
- 文件控制块
- 目录项控制块
超级控制块
简称superblock,每个文件系统有一个,记录了文件系统相关信息:文件系统的类型、数据块个数与大小、空闲块个个数与大小等信息
在文件系统挂载时加载到内存
文件控制块
简称inode,每个文件有一个,记录了文件相关信息:文件大小、数据块位置、访问权限、拥有者等
在访问文件时加载到内存
目录项控制块
简称dentry,每个目录项(文件、目录)有一个,记录了目录相关的信息:文件控制块的位置、父目录、子目录等
与文件控制块不同的是,它侧重于对目录信息的描述,而且VFS利用目录项控制块将文件系统组织成树状结构
在遍历目录时会加载到内存
文件系统视图结构
文件系统组织视图
文件系统存储视图
其中
- vol:superblock
- dir:dentry
- file:inode
- datablock:数据块
数据块缓存
内存中缓存磁盘数据块的一部分区域
在读的时候提供预读取:预先读取后面的数据块
在写的时候延迟写:写到缓存就返回,操作系统定时flush
页缓存
简称page cache。由于虚拟内存是基于页来管理内存,文件系统基于块来管理文件数据,页和块大小不同,需要一个机制统一两者的管理方式
页缓存实现中屏蔽了底层数据块的管理,上层统一提供基于页的管理方式,供虚拟内存使用
在应用进程进行内存映射mmap(将文件映射到内存)或者文件读写时,虚拟内存会先访问页缓存,找不到就会出现缺页中断,触发对文件系统的查询,然后再把数据加载到页缓存
文件描述符
很多文件操作都需要搜索文件,为了保证只在第一次读取的时候搜索,操作系统需要在第一次访问的时候打开文件,返回一个文件描述符,文件描述符就是一个索引,指向打开文件表中的打开文件,操作系统在该打开文件中维护了一些状态信息,保证以后在操作该文件的时候无需再次搜索
打开文件表
操作系统会创建一个全局打开文件表,同时也会为每个进程创建一个打开文件表,其中维护了这些信息:
- 文件指针:每个进程都会维护一个读写指针,互不干扰
- 打开的次数:指向全局表的该文件,记录了该文件被打开的次数,当次数为0时才事该文件才能被删除
- 文件的磁盘位置:指向全局表的该文件,以后每次文件操作都只要从该表(内存)读取位置即可,无需搜索磁盘
- 访问权限:每个进程维护一个访问权限信息,如只读、读写等
读取一个文件的流程:
- 打开文件,返回文件描述符
- 根据文件描述符定位进程的打开文件表
- 根据进程打开文件表定位全局的系统打开文件表
- 根据全局打开表定位文件控制块
- 根据文件控制块定位文件的数据块地址
- 将数据块地址转换为磁盘扇区物理地址
- 将扇区地址读到内存buffer
- 将内存buffer数据返回给应用进程
打开文件锁
多个进程打开文件的时候,可能会出现并发安全问题
操作系统提供了两种文件锁:
- 强制:打开文件时可以指定该文件被打开,其他进程不能打开
- 劝告:进程根据锁状态决定怎么做
文件分配
当文件需要磁盘空间的时候,文件系统需要为该文件分配数据块
常用分配算法:
- 连续分配
- 链式分配
- 索引分配
分配的时候主要考虑两点:
- 碎片多少:由于数据块大小固定,所以内碎片已经确定,这里考虑碎片主要是外碎片。即数据块分配给文件后,如果磁盘中有大量较小的空闲块,就说明外碎片较多
- 访问性能:访问文件数据块的速度
连续分配
根据文件需要的块大小,查找磁盘中空闲块,发现有足够大小的,就分配给文件
优点:
- 实现简单
- 访问性能高
缺点:
- 碎片多
链式分配
根据文件需要的块大小,找到磁盘中各个空闲块进行分配,并通过链表指针联系到一块
优点:
- 没有碎片
- 实现简单
缺点:
- 访问性能差
索引分配
根据文件需要的块大小,找到磁盘中的空闲块进行分配,并单独利用一个块存储索引这些数据块
优点:
- 没有碎片
- 访问性能高
缺点:
- 需要单独的索引块,如果文件较大,可能需要更多索引块,开销较大
实际unix文件系统(UFS)会结合索引分配和链式分配组成多级索引分配算法
空闲块管理
要为文件分配数据块,首先得知道所有空闲块在哪。先来看几种空闲块管理机制:
-
位图:用位图表示所有块,0表示空闲,1表示已分配
- 优点:查询快
- 缺点:磁盘越大,位图存储开销越大
-
链表:在一个空闲块中加入指针指向下一个空闲块,形成链表。
- 优点:存储开销小
- 缺点:查找快
-
索引:通过一个块存储索引,记录这些空闲块的位置
- 优点:查找快
- 缺点:如果表大,需要更多的索引块,开销较大
实际系统是组合这几种方式
磁盘分区
机械硬盘都是通过磁头进行磁盘寻址,如果磁盘越大,寻址时间越大。所以将磁盘划分成多个分区可以减少寻址时间,提升磁盘访问速度,但是如果同时在多个分区之间切换也会带来性能的下降
分区就是一组柱面的集合,每个分区可以视为一个独立的磁盘
分区组织方式:
- 在一个磁盘有多个分区,每个分区一个独立的文件系统,提高磁盘访问速度
- 在一个分区有多个磁盘,提高存储空间
其中第二种方式,在一个分区使用多块磁盘,可以基于此方案提升文件系统的可靠性与吞吐量,利用到的就是磁盘冗余技术
磁盘冗余阵列
基于上述第二种方案,可以利用冗余磁盘提高吞吐量(并行)与可靠性(冗余)
磁盘冗余阵列简称RAID,是一种基于冗余磁盘的思想实现的磁盘管理技术,常用的有RAID-0,RAID-1,RAID-5等
可以通过文件系统或者RAID硬件控制器来实现
RAID-0
将数据块分成多个小块,并行存储在独立的磁盘上,这种方案可以提升吞吐量
RAID-1
向两个盘写,从任意一个读,利用其中一个做镜像盘来提高可靠性。这种基于冗余盘的思想同时缓解了一个盘的读压力,相当于也会提升读性能
RAID-4
利用一个盘做校验盘,其他盘基于RAID-0的方案拆分小块的基础上并行访问,校验盘的数据是其他盘数据的校验和。这种方案可以保证即使其中一个数据盘坏了,也能基于校验和和剩余磁盘来恢复它的数据。同时因为利用到了RAID-0的并行方案,也提升了读写性能
RAID-5
与RAID-4思想一致,但是为避免校验盘称为瓶颈,取消唯一的校验盘,在每次数据写入时,将计算好的校验和轮流存放在所有磁盘上,形成一个分布式系统的负载均衡算法
RAID-3
RAID-4和RAID-5都是基于块计算校验和,而RAID-3是基于位来计算校验和
RAID-6
RAID-5每次写都会计算出一个校验块,保证最终可以容忍一个磁盘坏掉。RAID-6则是计算出两个校验块,保证可以容忍两个盘坏掉
RAID 0+1
RAID嵌套技术之一
让两个磁盘采用RAID-0方案并行读写,再利用两个盘作为镜像盘同步数据。安全性、性能较好,成本较高
RAID 1+0
RAID嵌套技术之一
让两个盘采用RAID-1方案一个主一个备,再利用两个盘一主一备,前后两者采用RAID-0的方案并行读写。安全性、性能较好,成本较高
I/O设备
可以分为三类:
- 字符设备:如键盘鼠标等
- 块设备:如磁盘驱动器等
- 网络设备:如无线、以太网、蓝牙等
各设备有不同的I/O实现。根据cpu与这些设备的交互,将I/O分为阻塞I/O、非阻塞I/O、异步I/O
阻塞I/O
用户进行读写操作后,需要等待设备完成读写操作,并返回结果
非阻塞I/O
用户进行读写操作后,不需要等待返回结果
异步I/O
用户进行读写操作后,不需要等待返回结果,但是操作系统完成读写操作后会通知用户
I/O设备控制器
cpu通过北桥与内存、显卡等高速设备连接,通过南桥与I/O等设备连接
cpu依赖I/O设备控制器与I/O设备交互,设备控制器结构为:
- 总线接口:cpu通过总线访问总线控制器,总线控制器访问总线接口;I/O设备产生中断,响应给中断控制器,控制器反馈给cpu
- 硬件控制器:与I/O设备交互
- 内存映射:内存映射后,存储映射的内存地址,cpu访问该内存地址就相当于访问I/O设备
- 寄存器:存储I/O状态信息,如I/O操作是否执行完成等
cpu与I/O设备交互的三种方式:
- 轮询:cpu不断轮询I/O设备
- 中断:I/O设备通过中断将事件通知给cpu
- DMA:I/O设备可以不通过cpu,直接通过DMA访问内存,避免cpu忙碌
I/O指令
cpu通过I/O端口访问I/O设备
内存映射
MMU将I/O设置的存储地址映射到内存,cpu通过访问内存来实现对I/O设备的访问
I/O结构
I/O访问流程就是从用户进程上到下的访问流程,当完成访问后设备通过中断,从下到上通知用户进程
I/O数据传输
cpu和I/O设备之间基于两种方式交换数据:
-
程序I/O:直接通过cpu指令控制I/O设备交互数据
- 优点:简单
- 缺点:数据量大的时候会导致cpu忙碌
-
DMA:cpu让I/O设备通过DMA直接读写内存,cpu访问内存即可得到这些数据
- 优点:传输数据的时候不阻塞cpu
- 缺点:复杂
完成数据传输后,设备需要通知cpu,有两种机制:
-
轮询:cpu轮询控制器寄存器,获取设备状态信息,根据状态进行操作
- 优点:简单
- 缺点:轮询间隔短会导致cpu忙碌,间隔长会导致响应延迟高
-
中断:设备通过中断将事件反应给cpu。cpu每执行一条指令后都会进行中断检查,发现中断后立马执行中断请求,然后再恢复执行原来的进程
- 优点:实时性高,延迟最多就一条指令
- 缺点:复杂,并且当中断请求特别多的时候,cpu会花费大量时间响应中断,开销大
DMA
设备控制器直接访问内存的一种机制
基于DMA和中断的方式读取磁盘数据的流程:
- 应用发起I/O请求
- 磁盘驱动请求磁盘控制器
- 磁盘控制器读取数据,并通知DMA控制器
- DMA控制器将数据发给内存,并产生中断
- 中断会通知到cpu,cpu将数据传递给应用进程
磁盘I/O传输时间
磁盘工作机制
读取数据其实就是磁头定位到指定的磁道,然后从磁道读取指定扇区的内容
寻道时间:磁头定位到磁道所花的时间
旋转延迟:在磁头定位到磁道后,磁盘不断旋转,直到磁头发现数据所在扇区,这个时间开销就是旋转延迟
磁盘I/O传输时间=等待设备可用+等待传输通道可用+寻道时间+旋转延迟+数据传输时间
磁盘调度算法
是一种优化寻道时间的算法,基于以下前提,该算法才有意义:
- 寻道是最耗时的环节
- 同一个磁盘有多个请求。如果每个磁盘只有一个请求,那么无法优化
- 大量随机请求导致性能差
当大量随机I/O请求到来时,磁头会先后指向不同的磁道,导致寻道开销较大,使用一种优化的顺序减少磁头需要“走动的距离”,是调度算法需要考虑的
FIFO磁盘调度算法
优先服务先来的I/O请求
优点:
- 公平
缺点:
- 实际使用接近随机I/O,性能差
SSTF磁盘调度算法
优先处理从当前磁臂移动距离最短的I/O请求
优点:
- 总寻道时间短,性能高
缺点:
- 容易导致请求饥饿,导致磁道距离较远的I/O请求一直得不到执行
SCAN磁盘调度算法
扫描算法,也成电梯算法,先从一个方向处理所有I/O请求,到头之后从另一个方向处理剩余I/O请求
这种算法缓解了SSTF中的饥饿情况,但是还是会出现某个方向磁道边缘的I/O请求饥饿的情况
C-SCAN磁盘调度算法
循环算法,从一个方向处理所有I/O请求直到磁道尽头,磁头马上移动到另一端(中间不处理请求),沿着这个方向继续处理所有请求
相比较SCAN,缓解了饥饿现象。保证磁道边缘的请求也能很快得到处理
C-LOOK磁盘调度算法
C-SCAN会沿着一个方向处理I/O请求直到磁道尽头,C-LOOK的优化是沿着一个方向处理I/O请求直到该方向没有I/O请求,其他都一样
现代操作系统往往会结合这些算法进行使用,保证寻道时间短的同时,又不会出现请求饥饿的情况
磁盘缓存
由于访问磁盘的速度和访问内存的速度查了几个量级,所以需要使用磁盘缓存来缓解这个差异导致的瓶颈
这个缓存可以采用单缓存和双缓存
- 单缓存:有一个缓存区,I/O设备和cpu同时只能有一个进行访问
- 双缓存:有两个缓存区,I/O设备和cpu可以同时操作各自的缓存区
访问频率置换算法
磁盘缓存需要一套置换算法来保证,经常用的扇区数据被缓存,少用的被置换到磁盘
这种算法结合了LRU和LFO的优点进行处理,短周期的以LRU为准,长周期的以LFO为准