操作系统
基本概念
主要功能
- 对上层的应用程序进行管理和控制
- 同时直接面向下层的硬件资源,所有上层应用程序都是通过操作系统提供的服务来获取和使用计算机硬件资源的
- 资源调度、外设管理
概念理解
- 操作系统对计算机资源进行了抽象:
- CPU资源抽象为进程
- 内存资源抽象为地址空间
- 硬盘资源抽象为文件
- 操作系统对外暴露的接口可以分为两类:
- Shell:面向用户
- Kernel:内核,主要包含以下组件
- CPU调度器
- 物理内存管理、虚拟内存管理
- 文件系统管理
- 中断处理和设备驱动
- OS Kernel的特征:
- 并发:同时存在多个运行程序
- 共享:多个程序需要访问相同的资源
- 虚拟化:涉及到多道程序设计技术,让每个用户都感觉似乎独占一个计算机
- 异步:程序运行会因为系统的调度出现暂停执行和继续执行的情况,且推进速度不可知,此时需要保证相同环境下的运行结果的一致性
操作系统的结构
- 单体架构:不同功能间通过函数调用进行访问,是一种紧耦合的方式,不容易扩展
- 微内核架构:基本功能(如:中断处理、消息传递)由内核实现,其他服务(如内存管理、文件系统、网络协议栈)放在外围以进程服务的形式存在
- 优点:松耦合架构,服务间通过内核的消息传递机制进行通讯,服务间隔离,难以恶意破坏其他服务的地址空间
- 缺点:性能差,开销大
- 外核架构:将内核分为两块。一块跟硬件打交道,完成硬件功能的复制;另一块称为lib OS,与具体应用程序绑定,每个应用程序都有一块专门设计的lib OS
- 优点:性能很高
- 缺点:设计复杂
- 虚拟机监视器(VMM)
启动、中断、异常、系统调用
启动
- OS存放在硬盘中
- BIOS(基本I/O处理系统)是计算机启动运行的第一个程序,它以固件的形式存储在ROM芯片中,其主要有以下几个功能:
- 加电自检,检查计算机硬件是否功能良好
- 初始化硬件设备的参数
- 加载引导程序(bootloader),加载完后由引导程序把操作系统装载进内存
- 操作系统正常运行后,将对外提供接口,完成对外设以及应用程序的控制
- 对外设:中断、I/O
- 对应用程序:系统调用、异常
系统调用
- 应用程序主动向操作系统发出的服务请求(来源于“应用程序”)
- 同步或异步
- 等待系统调用完成后,应用程序可以继续执行
异常
- 应用程序运行中,非法指令或其他坏的处理状态引起的(来源于“不良的应用程序”)
- 同步
- 可能会让相应的应用程序指令重新执行,甚至可能会杀死应用程序
- 一旦应用程序执行过程中发生异常,操作系统会先保存现场
- 每个异常对应一个唯一的异常ID,操作系统根据该异常ID,可以知道该如何面对该异常
- 如果要求杀死程序,该程序立即退出
- 如果操作系统认为可以重新调度资源来满足程序指令,那么就会在完成资源调度后重新执行产生异常的那条指令(而不是直接继续执行后续指令),没有问题后,才会恢复现场并继续执行后续指令
中断
- 来自于不同的硬件设备的计时器和网络的中断(来源于“外设”)
- 异步
- 对应用程序是透明的,应用程序感知不到中断的存在
为了实现中断,硬件和软件分别需要有以下的一些功能:
-
硬件:
外设发出中断请求时,就会设置中断标记,根据中断标记,CPU进行判断、得到中断ID并发给操作系统,操作系统根据中断号和BIOS初始化时形成的一张映射表,就可以找到对应的要执行的中断例程的位置
-
软件:
中断例程运行前,要保存当前的处理状态,然后才能去执行中断例程。执行完中断例程需要清除中断标记,并恢复之前保存的处理状态,从而使应用程序继续执行。
内存
计算机体系结构——内存的层次结构
按与CPU的距离(近→远)、大小(小→大)、速度(快→慢)的顺序罗列如下
- 寄存器
- cache
- 主存(物理内存)
- 磁盘(虚拟内存)
操作系统在内存管理中的任务
- 抽象:逻辑地址空间
- 保护:独立地址空间
- 共享:访问相同内存
- 虚拟化:更多地址空间
连续内存分配
内存碎片问题
有一些空闲内存可见无法被利用,分为两类:
- 外部碎片:分配单元间未被使用的内存
- 内部碎片:分配单元内部未被使用的内存
分配策略
-
首次适配(first fit):按顺序搜索空闲块,选择第一个适配的空闲块
- 优点:实现简单
- 缺点:不确定性,容易产生外部碎片
- 需求:
- 按地址对空闲块列表进行排序
- 回收空闲块时考虑能否合并于相邻的空闲块
-
最优适配(best fit):使用尺寸大小能刚好满足分配需求的空闲块
-
目的:为了尽量避免拆分较大的空闲块
-
优点:当大部分分配请求是小尺寸时,非常有效
-
缺点:容易产生很多没用的的微小碎片,重分配慢
-
需求:
- 按尺寸对空闲块列表进行排序
- 回收空闲块时考虑能否合并于相邻的空闲块
-
-
最差适配(worst fit):优先使用最大的空闲块
-
目的:为了尽量避免产生微小碎片
-
优点:当大部分分配请求是中大型尺寸时,非常有效
-
缺点:当未来有较大的分配请求出现时,可能就无法满足了(因为大块已经被拆分)
-
需求:
- 按尺寸对空闲块列表进行排序,直接选取最大的那个(分配效率很高)
- 回收空闲块时考虑能否合并于相邻的空闲块
-
进一步优化:碎片整理策略(解决分配后出现碎片的问题)
-
压缩式碎片整理:重新调整已分配内存块的布局,使它们更加紧致,将剩余内存块变成连续的空间
需要考虑两个问题:①何时压缩(重置)②开销如何
-
交换式碎片整理:当剩余内存不够时,就算重置内存块布局也无法满足分配请求,此时可以将部分已分配内存块中的内容存储到硬盘中,从而腾出空间拿来分配
非连续内存分配
- 重点:如何建立虚拟(逻辑)地址和物理地址之间的转换
- 由于软件方案开销很大,所以考虑硬件方案
硬件方案
- 分段:通过硬件支持,将逻辑分段地址,映射到物理内存地址
- 用段表来存储,包含三个字段:段号、物理内存基地址、空间限制
- 寻址时,通过逻辑上的段号(segment num)在段表中查询到对应的基地址和空间限制,CPU通过两个寄存器,分别进行两个操作:判断偏移是否超出限制、计算得到准确的物理内存地址
- 分页:
- 将物理内存划分为固定大小(2的幂)的帧
- 物理内存地址可以被划分为两部分:帧号(f)和帧内偏移(o)
- 帧号(f)包含F位,所以共有2^F个帧
- 帧内偏移(o)包含S位,所以每一帧共有2^S个字节
- 物理地址=2^S * f + o
- 逻辑地址空间也被划分为一个个页
- 逻辑内存地址可以被划分为两部分:页号(p)和页内偏移(o)
- 页号(p)包含P位,所以共有2^P个页
- 页内偏移(o)包含S位,所以每一页共有2^S个字节
- 页与帧的大小是相同的
- 将逻辑地址转换为物理地址的方案:
- 页表:
- 寻址时,需要通过页表基地址寄存器(PTBR)获取页表(page table)的基地址,然后在该表中,以页号为索引,查询对应的帧号
- 每个运行的程序都有一个页表
- 通过页表查询到的内容,除了帧号,还包括一些标志位,表明该表项的状态(例如表明对应的物理内存中是否存留当前数据)
- MMU/TLB
- 页表:
- 分页机制的性能问题:
- 访问速度上:访问一个内存单元需要2次内存访问,一次用于获取页表项,一次用于访问数据
- 空间开销上:页表可能会非常大(例如64位机器上,如果每页有1024个字节,那么将会有2^54个页,即页表项,这是无法存储在内存中的)
- 分页机制性能问题的解决方案:
- 使用缓存(cache)提高访问速度:在CPU中有一个内存管理单元(MMU),MMU中包含一个缓存叫做TLB(Translation Look-aside Buffer/快表)。TLB表项是由相关存储器来实现的,可以并发查找。
- 如果TLB命中,则可以很快获取到物理内存的帧号(无需查询页表)
- 如果TLB未命中,则只能去查询页表,然后将页表更新到TLB中。该动作由硬件还是软件来实现,取决于CPU类型,例如x86CPU就是直接通过硬件完成该动作,无需通过操作系统
- 使用多级页表减少空间开销:
- 多级页表形成页表树,非叶子节点的表项存储的是下一级页表的基地址,而叶子节点的表项存储的是物理内存的帧号
- 64位机器上,假设每页有2^12个字节,那么将会有52位用于表示页号,如果不采用多级页表,那么每一页都需要对应一个表项,即共有2^52个表项,而往往绝大多数表项都是无用的,仅需要部分bit就能表示所有的内存地址空间
- 采用多级页表后,就可以在到达叶子节点前,在某一级页表中拦截掉无效地址(通过设置标志位),设置了标志位的表项就无需产生对应的后续的页表了,这样就大大减少了存储无效页表项造成的空间开销
- 随着级数增加,访问时间会增加,但是会节省内存空间,属于以时间换空间
- 使用页寄存器(反向页表)减少空间开销:页寄存器的表项,以物理内存的帧号为索引,建立帧号和页号的映射关系
- 利:转换表大小相对于物理内存来说很小,并且转换表大小与逻辑地址空间大小无关
- 弊:需要高速的硬件处理机制(如哈希计算),来根据页号查询对应帧号
- 使用缓存(cache)提高访问速度:在CPU中有一个内存管理单元(MMU),MMU中包含一个缓存叫做TLB(Translation Look-aside Buffer/快表)。TLB表项是由相关存储器来实现的,可以并发查找。
- 将物理内存划分为固定大小(2的幂)的帧
虚拟内存
出现的起因
进程和线程
为什么要提出“进程”概念?
- 为了提高CPU的运行效率,需要在不同的运行程序间来回切换,例如在某个程序等待I/O、无需CPU处理的空闲时间,CPU切换去处理其他程序
- 有了切换,就一定要保证切回的时候,能从原来切出的位置继续执行程序,这就需要保存切出时程序执行的所有状态信息
- 明显看出,运行的程序与静态的程序有很大的不同,为了能够专门刻画“进行中的程序”,提出“进程”概念
进程的基本概念
- 定义:一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
- 组成:程序的代码、程序处理的数据、程序计数器中的值、一组通用寄存器的当前值、一组系统资源(如打开的文件)
- 进程与程序的关系:
- 程序是产生进程的基础,进程是程序功能的体现
- 程序的每次运行构成不同的进程
- 程序是有序代码的集合;进程是程序的执行,包含核心态/用户态
- 通过多次执行,一个程序可以对应多个进程;通过调用关系,一个进程可以对应多个程序
- 程序是静态的、永久的,进程是动态的、暂时的
多进程的组织
PCB(Process Control Block)
- 进程与PCB一一对应,进程创建时生成一个PCB,进程终止时回收它的PCB,通过对PCB的组织管理来实现对进程的组织管理
- 不同进程的PCB分别放在对应的队列中(就绪队列、磁盘等待队列……)
- 进程标识信息:如本进程标识、父进程标识、用户标识等
- 处理机状态信息保存区:保存进程的运行现场信息
- 用户可见寄存器
- 控制和状态寄存器
- 栈指针
- 进程控制信息
状态
- 新建态
- 就绪态
- 运行态
- 终止态
- 阻塞态
总结来说:多进程就是以PCB的形式组织在不同队列中,操作系统根据进程的状态执行相应操作,使得在状态转化的过程中完成对进程的推进
队列
- 队列与PCB和进程状态都有紧密联系
- 每个进程都以PCB的形式组织在不同的队列中
- 每个队列一般对应一个进程状态,例如:准备就绪的进程,组织在就绪队列中,等待操作系统从中选取下一个要执行的进程;运行过程中,某个进程需要等待从磁盘中读出的数据,操作系统就会将其设置为阻塞态,存放进磁盘等待队列中
总结
- 操作系统对多进程的组织方式主要靠PCB、队列以及状态来实现
- 当需要从一个进程A切换到另一个进程B时,操作系统具体会做出以下行为:
- 更改当前正在占用CPU资源的进程的状态
- 将当前进程的PCB放入特定队列中
- 保存进程A的现场,恢复进程B的现场(使用汇编实现)
多进程的地址空间分离
每个进程都单独建立一个页表,相同的逻辑地址会被映射到不同的物理内存上,不同进程使用的物理内存被完全隔离
多进程共享内存
通过加锁,使得不同进程不能同时访问共享内存中的内容
进程内存分布
- 栈区
- 堆区
- 全局区(静态区)
- 程序代码区
- (文字常量区)
用户级线程
- 进程 = 资源 + 指令执行序列
- 线程间切换,可以认为仅切换了指令执行序列,但没有切换资源(例如:同一进程的不同线程共享同一个页表,)
为什么每个线程都要有自己的栈区?
- 函数调用时,遇到
{就会将调用者的ebp(栈基地址)压栈,遇到}就会在释放当前函数栈(即将esp移到当前ebp位置)后弹栈,弹栈得到调用者的ebp并将其赋给CPU的ebp寄存器,同时esp因为弹栈下降到调用者栈的栈顶 - 对于多线程,如果共用一个栈,就会导致在进程间切换的时候,根据函数的调用顺序压栈。当切换到一个线程并且执行完一个函数、遇到
}时,会触发弹栈操作,而此时弹出的地址可能是另一个线程中某个函数的ebp,而不是我们预期的当前线程中函数调用者的地址 - 给每个线程分配独立的栈空间,可以避免上述问题。每次弹栈后都会返回到调用者中继续执行。
如何保存线程信息?——TCB
- 使用
TCB(Thread Control Block)保存线程信息,每个线程都对应一个TCB - TCB中可以保存线程栈的相关信息,例如栈顶指针
esp的值 - 切换线程本质就是切换栈。当切换线程时,当前线程的所有相关信息就被保存在TCB中,然后操作系统读取目标线程的TCB,将其中的信息还原出来
用户级线程的缺点
- 由于用户级线程完全在用户态创建,操作系统内核感知不到多线程的存在
- 当某个进程在执行某个线程的时候,出现需要等待硬件的情况,操作系统便认为该进程阻塞,自动切换到其他进程
- 此时,该进程中的其他线程就无法并发执行了
内核级线程
相比于用户级线程的特点
- 可以对硬件进行操作
- 可以发挥“多核”的作用,同时运行在不同的核上,做到多线程真正的“并行”而不是交替执行的“并发”
- 多核:有多个CPU,但是CPU共用一个cache(缓存)和一个MMU(内存管理单元)
- 多处理器:有多个CPU,且每个CPU独占一个cache和一个MMU
- 线程间的切换由操作系统统一调度,
Yield()函数对用户不可见
内核栈
- 每个内核级线程,需要拥有一个用户栈和一个内核栈
- 线程在用户态运行时,一旦出现(内/外)中断,通过一些硬件的寄存器就可以找到对应的内核栈,栈空间从用户栈切换到内核栈,并且将以下信息依次压入内核栈:
- 源SS:栈段寄存器
- 源SP:栈偏移寄存器
- EFLAGS
- 源PC:指令指针寄存器
- 源CS:代码段寄存器
内核级线程的切换
- 假设线程A发出中断进行系统调用时,出现阻塞需要切换线程,根据
schedule()函数找到目标线程B - 假设线程B也是内核级线程,操作系统根据TCB,保存线程A信息,恢复线程B信息
- 将栈空间切换到线程B,且找到线程B的PC继续执行指令
- 执行完线程B的内核态程序,执行
IRET指令,其内核栈弹栈,得到用户态的指令地址和栈地址,降低权限等级,恢复到用户态运行线程B