目录
📚操作系统概述
🐇操作系统中的抽象概念
📚准备知识
🐇中断输入输出
🐇软件中断
🐇处理器特权级
🐇操作系统的结构
📚程序的结构
🐇运行时视图简介
🐇可执行文件
🐇编译器、汇编器、连接器、调试器与解释器程序
🐇直接回填法和间接地址法
🐇过程(子程序)结构
🐇从运行时库到运行时环境
🐇静态链接库和动态链接库
📚线程与时间
🐇指令流间的执行顺序
🐇指令流的三个基本状态
🐇线程的描述⭐️
🐇线程控制块(TCB)
🐇指令流 VS 线程
🐇线程的调度算法
🥕固定优先级调度算法⭐️⭐️
🥕调度策略——保证公正(公平+正义)⭐️⭐️⭐️
🥕复杂系统的调度:按线程行为分类
🥕广义的调度算法
🐇系统中的线程总览:按特权划分
🐇线程的基本操作
📚进程与主存空间
🐇程序内的分配策略⭐️
🥕固定分区法
🥕首次适配法(First-Fit)
🥕最好适配法(Best-Fit)
🥕最坏适配法(Worst-Fit)
🐇进程的描述
🐇进程控制块(PCB)
🐇进程与可执行文件
🐇线程与进程⭐️
🐇内存隔离机制:分段
🐇内存隔离机制:分页
🐇请求分页,替换算法⭐️⭐️⭐️
🥕最长前向距离算法(LFD)
🥕先进先出(FIFO)
🥕最久未用法(LRU)
🥕Belady异常
🥕Linux页面替换——二次机会法
🥕页面置换的其它考量
🐇进程的操作
📚操作系统概述
- 操作系统作为一切计算机软件的基石,统筹、协调和管理计算机系统中各类资源。机器本质上就是代行人的意志,因此协调这些程序就是协调人。
- 问什么是操作系统?操作系统就是去做抽象、协调和权衡。
🐇操作系统中的抽象概念
- 时间:多个任务不能同时使用同一个时分复用的部件,但每个任务在使用时都可以得到这个部件的全部,因此需要将时间合理地分配给它们。典型的时分复用的部件是处理器,它又包括中央处理器和协处理器。
- 空间:多个任务可以同时使用同一个空分复用的部件,但每个任务在使用时都无法得到这个部件的全部,因此需要将空间合理地分配给各个任务。典型的空分复用的部件是存储器,它又包括内存和外存。
- 原则:操作系统的最高设计目标,通常由操作系统所在领域的需求决定。
- 设计:操作系统的高层次抽象描述,描述各个组件的抽象功能。
- 实现:操作系统的低层次具体描述,描述各个组件的具体实施。
- 策略:操作系统中对资源进行管理的方法和政策。
- 机制:操作系统为实现策略使用的工具和手段。
📚准备知识
🐇中断输入输出
- 由设备生成中断表明自己已经准备好输入输出,再操作端口的程序。
- 特点:设备准备好接收操作时用中断通知CPU,CPU调用事先准备好的一段程序来处理输入输出。在中断尚未到来时,CPU可以专注于其他事情,直到被中断(意外地)打断;“Don't call me;I'll call you.”
- CPU和设备是异步的:异步指的是在一个系统中,不同的操作或事件不需要按照固定的时间间隔或顺序依次发生,而是根据自身的状态和条件自主地进行操作和事件触发。
- 中断服务程序:由CPU在响应中断时,自动调用的一段对应于终端号的特殊过程。也叫中断例行程序、中断向量。
🐇软件中断
- 软中断指令
- 几乎所有处理器都提供软件触发中断的指令。在8086中,INT指令可触发任何一个中断。
- 软中断指令是由程序员在程序中编写的一种中断指令,它可以在程序的任何地方被执行。
- 与硬件中断不同,软中断不需要外部硬件的干预,可以在程序内部自由调用,可以用于实现一些特殊的功能或者处理一些异常情况。
- 异常捕获
- 当程序中的指令执行发生错误,就会抛出异常,异常会被同步捕获(与通常的中断与CPU异步不同)。
- 引起异常的指令不会被执行,压栈的IP等于该异常指令的地址,并且CPU要先跳转到异常处理程序执行完再回来试图重新执行该指令(因为产生异常的条件可能已经被解除)。
- 8086只有一个异常,就是除零异常,它的中断号为0。
- 陷阱设置
- 除了设置TF之外,程序单步调试的一个重要手段就是将断点位置换成INT3。
- 这样,运行到此处便会触发3号中断,使调试器捕获该断点。此类用法又叫做陷阱;
- 系统调用是陷阱的一种特殊情况。
🐇处理器特权级
- 硬件特权级:CPU硬件实现
- 用户模式:执行应用程序的模式化,不允许直接访问系统的敏感资源。
- 内核模式:执行操作系统的模式,可直接执行敏感指令和访问敏感资源。 用户程序的非法操作不得影响内核。
- 特权指令:能对系统中敏感资源进行操作的指令,仅能在内核模式下执行。
概念梳理:
- 系统调用是一种机制设计,运行应用程序通过受限的方法调用操作系统内核,向内核请求功能处理。进行系统调用时,还会从用户态切换到内核态。
- 中断通常具有不可预测性。系统调用若实验外部中断来做具体实现,为了和设备的外部中断处理一套代码,往往需要在中断服务程序(内核模式)中保存和恢复全部的上下文。
- 陷阱具有绝对的可预测性。用户软件(用户模式)可以承担一部分上下文保存和恢复的责任。
🐇操作系统的结构
简单的设备——库结构(简要结构、单核结构)
- 无内核模式与用户模式的区分。
- 所有应用程序以及内核都在同一个保护域。
- 应用程序可以随时对任何资源做任何操作。
- 应用程序间为合作关系,操作系统的角色偏重协调而非管理。
通用的设备——宏内核结构
- 有内核模式与用户模式的区分。
- 每个应用程序在不同的保护域。
- 内核的所有功能位于同一个保护域。
- 应用程序必须请求内核完成敏感资源操作。
- 应用程序间为合作或竞争关系,操作系统协调与管理并重。
灵活的设备——微内核结构
- 有内核模式与用户模式的区分。
- 每个应用程序在不同的保护域。
- 内核除基本功能外,其它功能分别位于不同的用户模式进程中。
- 应用程序必须请求守护进程中的策略分配敏感资源。
- 守护进程则转而使用内核提供的机制完成这些分配操作。
- 嵌入式最广泛使用的内核结构之一,抗软件故障和攻击,操作系统的一部分损坏不会影响其它部分。
高效的设备——外核结构
- 有内核模式与用户模式的区分。
- 每个应用程序在不同的保护域。
- 内核仅负责硬件资源的安全分配与管理功能。
- 应用程序必须自行和被分配的硬件资源打交道完成功能。
- 去抽象化:将硬件资源直接暴露给应用程序以获得最大性能增益。
- 适用于高性能或高灵活性计算等应用场合。
📚程序的结构
🐇运行时视图简介
运行时视图:程序在执行时的主存储器布局,也叫运行时布局。
- 代码段:存放程序的可执行指令,所有的执行都在代码段发生。
- 数据段:存放程序的数据
- 只读数据段(.rodata):存放程序中的含初值常量。这些常量在程序运行中不得修改。
- 读写数据段(.rwdata):存放程序中的行初值常量。这些常量在程序运行可以修改。
- 零初始化数据段(.zidata/.bss):存放程序中的不含初值(初始化为0)的可修改常量。
- 堆段(.heap):存放程序的堆,即动态分配内存时内存的来源。
- 栈段(.stack):存放程序的运行栈,以供过程调用时保存和恢复的上下文。
🐇可执行文件
- 可执行文件是程序在外存上的存储方式,本质是保存了程序的逻辑布局的描述。
- 程序的装入:操作系统读取外存上的可执行文件中对程序逻辑布局的描述,在内存中生成程序的物理布局的一个实例的过程。
- 可执行文件头:描述程序运行时布局的元数据,一般附加在可执行文件头部。
🐇编译器、汇编器、连接器、调试器与解释器程序
- 编译器:把高级语言源程序翻译成机器语言程序。有时也先翻译成汇编语言源程序,然后调用汇编器。如GCC(C++)。
- 汇编器:把汇编语言源程序翻译成机器语言程序。
- 链接器:将机器语言程序中间文件与系统运行库链接生成可执行的机器语言程序。可能重定位各个段的位置。(LINK)
- 调试器:系统提供给用户的能监督和控制用户程序的一种工具,可以装入、修改、显示或逐条执行一个程序。(GDB)
- 解释器:直接在机器上解释并执行高级语言源程序。也不排斥使用编译器技术,内部先生成机器语言程序再执行。解释器一般不会生成可独立运行的机器语言程序文件。(JavaScript)
🐇直接回填法和间接地址法
- 直接回填:链接器生成所有符号的地址后,直接修改.text段中对这些符号的引用,将正确的地址填写到那些引用中去,这样生成的代码就可以访问那些符号了。修改.text段。
- 间接地址:编译器或汇编器生成目标文件时,在.rwdata段留出一个表格,.text段中的代码对其他符号的应用均通过这个表格进行。不修改.text段,但会修改.rwdata段。
- 直接回填法适用于空位较少的情况,而间接回填法则适用于空位较多的情况。
🐇过程(子程序)结构
- 程序指针可能在中途反复跳转到同一段过程执行,完毕后返回原处继续执行。
- 比循环结构更强大,因为过程的尾递归可以实现循环,过程还可以嵌套。
- 过程执行的全景(STDCALL)
- 序言:真正的运算操作开始之前所做的准备工作,如CALL指令、保存寄存器、读取参数、初始化局部变量等称为过程的序言。在某些说法中,序言仅包括后三项。
- 尾声:真正的运算操作结束之后所做的收尾工作,如恢复寄存器、调整栈框、RET指令等称为过程的尾声。在某些说法中,尾声仅包括前两项。
- 开销:过程调用的开销就是序言和尾声之和。
🐇从运行时库到运行时环境
- 编程语言的库文件:在一门高级语言中,有大量基础功能是必备的,在所有程序中都很常用。于是将这些基础功能的文件预先编译好,形成大量的目标文件,程序员只要在生成程序时链接它们就行,省时且便于程序的移植。
- 运行时环境:随着高级语言的发展,部分编程语言如Java等还要求垃圾回收、虚拟机管理、即时编译等功能,这些功能已经不由用户直接进行调用了,但它们仍然为语言运行提供基础的、关键的支持,必须随着程序的加载而被加载。因此,现代高级语言的运行时实际上是一种环境,不仅仅是库本身。运行时环境运行在用户态,它们也会使用系统调用。
- 垃圾回收:在C与C++语言中,分配给任何对象的内存都要程序员手动释放,程序员的心智负担太高,容易造成内存泄露。因此,Java等语言的运行时环境会自动探测哪些对象已经被废弃并释放它们。
- 虚拟机管理:Java等语言的可执行文件是字节码,无法直接交给处理器进行执行,必须由虚拟机代为解析执行。
- 即时编译:虚拟机字节码的解释执行效率是很低的。此时,即时编译器可以针对当前架构,将热点部分的字节码临场转译成原生二进制代码,在CPU上直接执行。
解释型语言的性能和编译型语言的性能哪个好?(AI回答)
- 编译型语言的性能通常比解释型语言好。
- 编译型语言是在程序运行之前将代码翻译成机器代码,这样程序在运行时可以直接执行机器代码,因此运行速度较快。
- 而解释型语言则是在程序运行时逐行翻译和执行,所以运行速度通常较慢。
- 当然,在某些情况下,解释型语言可能会更适用,比如需要频繁修改代码的场景,因为解释型语言不需要再次编译整个程序。
🐇静态链接库和动态链接库
静态链接库
- *.OBJ文件的简单合集。它通常是将一堆*.OBJ文件的内容合并在一起成为一个文件,包括这些*.OBJ文件中包含的各个符号,以及各个符号的内容。
- 静态链接库的后缀名一般是*.A或*.LIB。
- 这样,在引用运行时库或第三方程序库时就可以直接引用这个链接库本身,由链接器去解析这个库,来得省事。
- 对比:与可执行文件相比,静态链接库相当于将各个*.OBJ文件直接打了个包,并没有进行符号引用的链接。
- 存储压缩:除了将*.OBJ做简单合集之外,*.A文件往往还会使用一定的压缩算法。这是因为现代库(尤其是wxWidgets、Qt等图形界面库)的*.OBJ总量实在是太大了,动辄几十甚至几百MB,占用存储空间实在太多,因此干脆像真正的压缩文件那样使用压缩算法来保存它们。
动态链接库
- 在可执行程序被加载时,作为独立文件单独加载的库文件。它不包含在可执行文件之内;可执行文件将在需要它们的时候加载它们。
- 动态链接库的后缀名一般是*.SO(Shared Object)、*.DYLIB(Dynamic Library)或*.DLL(Dynamic-Link Library)。
- 外存的库共享:可执行文件中不再包含库文件。这样,库文件就只要在外存中存在一份。这个特征是动态链接库的根本特征;判断一个库是不是动态链接库,就看调用它的可执行文件中是否包含它的内容。如果不包含,它就是动态链接库。
- 内存的库共享:物理地址空间中仅包含库文件(的代码段)的一个副本。这样,即便库文件被多个虚拟地址空间中被使用,它也只会占用一份物理内存,减少了内存用量,也减轻了缓存负担。
- 动态链接器:加载动态链接库并将其与应用程序相链接的链接器。它在可执行文件加载时或应用程序运行时才运行。(静态链接器:在生成可执行文件时运行)
📚线程与时间
🐇指令流间的执行顺序
- 顺序执行:一个指令流执行完成后,再去执行另一个指令流。
- 合作执行:将每个指令流打断成多份,每一份之内都顺序执行,但背靠背执行的两份不一定来自同一个指令流。在每份指令流的末尾,都通知操作系统主动放弃CPU,CPU将转去执行下一份指令流。
- 并发执行:将合作执行的条件放宽一点,允许一个指令流在任何时候被打断,并且新的指令流插入进来。又叫抢占式执行。每个指令流都在自己的虚拟CPU上执行,而且虚拟CPU的先后没法预测。
- 并行执行:多个指令流依附于多个位于不同物理处理器上线程,做到了多个指令流的真正同时执行。并发是并行的一种特殊场合,它只有在多核处理器上才有可能实现。
并发与并行
- 并行是并发的一种具体实现,在并行环境中,不仅并发程序的各个指令流的指令执行的先后顺序无法预测,而且这些指令流实现了真正的同时、一齐执行。
- 在单核处理器的并发环境中,无法实现并行,因为只有一个CPU,不可能同时执行多道程序,仅仅是交替执行多道程序让它们看上去在同时运行。
🐇指令流的三个基本状态
🐇线程的描述⭐️
指令流:一个应用程序内部可以由一个或多个逻辑上相互独立执行的指令序列组成。这种独立执行的指令序列叫做指令流。CPU靠执行指令流来完成应用程序的功能。
指令流与时间:操作系统需要给指令流分配CPU时间,然后让这个指令流拿着这个CPU时间配额去运行。但是,指令流是用户程序的逻辑组成部分,操作系统并不知道用户程序里面有几条指令流。
线程(Thread)的概念
- 操作系统提供给应用程序的一种对CPU时间的抽象机制。
- 它是CPU时间分配的基本对象。
- 应用程序通过将自己的指令流与线程对应起来,使指令流获得CPU时间分配。操作系统通过运行线程,来运行依附在这个线程上的指令流。
- 也就是说,指令流通过依附于线程,获得了在CPU上运行的权利。
- 在应用程序看来,自己的逻辑组织是一系列并发执行的指令流。
- 在操作系统看来,应用系统的运行组织是一系列被分配了CPU时间的线程。
线程的描述——时间
- 线程是时间分配的基本对象,那线程必然有一个参数描述它被分配了多少时间。这个数值称为时间预算,可以是一个有限的数值,也可以是一个无限的数值。
- 操作系统在运行线程时会时刻关注线程的时间预算是否耗尽;如果耗尽,操作系统就切换到其它有时间预算的线程去执行。
线程的描述——优先级
- 系统中有多个线程同时具备非零的时间预算,因此,线程需要一个参数来描述其优先度。当系统遇到多个可以运行的线程时,系统可以决定运行其中优先级最高的那个。
- 优先级一般用一个数值(操作系统决定的,不绝对)来表示,在绝大多数系统中,数字越小,优先级越高。
- CPU抢占关系实际上是一个偏序集(越紧急的东西,不一定总是越重要;即便是紧急的东西,也不代表它可以不受限制地发生),优先级实际上是这个偏序集的一种简化全序描述。
- 关于上边提到的偏序集,举例说明:令A,B,C,D分别为四个独立运行的子系统,A可以抢占B(即有A>B)但不能抢占CD,C可以抢占D(即有C>D)但不能抢占AB,这也就是说A和CD实际上无法比较(已知A>B,C>D,但是不知道A和CD的关系)。
- 关于数据:指令流数据放在用户模式;线程数据放内核模式。
线程的描述——上下文
- 指令流的上下文
- 在并发执行中,指令流可能随时被打断。被打断的指令流的状态信息不能丢失。这些状态信息包括指令流自己的上下文和该指令流对CPU的占用状态。
- 一个指令流的上下文就是足够使其恢复被打断时的状态的内容。这一般包括了其寄存器组和执行栈。在切换指令流时,一个方便做法就是把寄存器保存在执行栈上,合适时再恢复。
- 线程的上下文
- 考虑线程上的执行流因为主动等待需要操作系统介入的I/O完成或者意外地被外设中断打断而暂停运行的场合,线程同样需要有上下文。
- 和指令流上下文一样,线程的上下文也是其寄存器组。
内核阻塞:
- 操作系统并不知道指令流的存在。因此,在遇到线程上的指令流陷入内核阻塞的时候,内核只能暂停执行当前这个线程,切换到别的线程去执行。
- 更麻烦的是,线程什么时候陷入内核,依附在线程上的指令流是不知道的,也即可能发生抢占。
- 一旦一个线程阻塞在内核,对它上面依附的所有指令流来说,时间就都凝固了。因此,这些指令流都停止运行。
线程的状态和指令流的状态是类似的,也包括运行、就绪和阻塞三个状态。(再放一遍图)
🐇线程控制块(TCB)
- 操作系统用以描述和管理线程的内核对象,一般至少包含线程的时间预算、优先级、运行状态以及上下文,有时还会包含一些身份信息(如线程名、线程号)或统计信息等。
- 它在数据结构上一般是C语言的一个结构体。
- 在那些有内核模式的处理器中,线程控制块位于内核空间,只有操作系统可以更改,应用程序无法更改。
🐇指令流 VS 线程
- 上文有提到,指令流通过依附于线程,获得了在CPU上运行的权利。
- 指令流与线程的关系:
- 一对一(简单)(如身份证和观光卡,一人一卡严格管控):线程处于什么状态,指令流就处于什么状态。
- 优点:简单,最好实现,最常见,常见到足以让人把指令流和线程的概念混淆。
- 缺点:每个指令流都成了单独分配的对象,这样会增加操作系统的负担,因为操作系统需要为每个线程创建一些单独的管理数据,而且每次切换当前CPU上的指令流都需要通知操作系统(→系统调用,比函数调用慢很多)。
- 一对多(性能好且实用)(如只管卡,不管几个人用):
- 优点:高效。同一个线程中的多个指令流可以借由附着在同一个线程上共享一份执行时间,它们在内核中也被当作一个对象来处理,其TCB只创建一份。对于每个线程上附着的多个就绪指令流,应用程序负责决定哪个指令流得到线程从而运行,并切换到它。通常而言,只有紧密协作的指令流才会被放在同一个线程上。
- 缺点:假如有某个指令流发起I/O输入,陷入内核阻塞的时候,内核只能暂停执行当前这个线程,切换到别的线程去执行,其它的指令流都要等待。
- 对应关系:
- 线程处于运行状态,说明其上的指令流中有一个在运行。
- 线程处于就绪状态,说明其上的指令流中至少存在一个就绪的。
- 线程处于阻塞状态,说明其上的指令流中了至少一个发起需要操作系统介入的阻塞态。
- 其他指令流状态无法预测,因为只有应用程序知道,操作系统是不知道的。
- 多对多(复杂,难以正确实现)
- 优点:灵活。多个指令流对应于多个线程,任何一个不阻塞的线程都可以运行任何一个不阻塞的指令流,指令流可以在线程之间迁移。
- 缺点:超级无敌难以实现,细节太多了。
- 一对一(简单)(如身份证和观光卡,一人一卡严格管控):线程处于什么状态,指令流就处于什么状态。
协程:
- 合作执行的一组指令流。
- 不仅强调它们不是时间分配的独立对象(区别于线程),而且强调只有其中某个指令流主动放弃CPU时,其他指令流才可以得到CPU进行运行,并且放弃CPU的那个指令流还倾向于指定谁来接替它的执行。
纤程:
- 合作执行的一组指令流。
- 相比于协程,放弃CPU的那个指令流不倾向于直接指定接替执行者,而倾向于唤起一个在用户空间的调度器,由它来决定下一个执行的指令流是谁。
- 纤程之间不一定有紧密的合作关系,仅仅是强调它们比线程要轻量,也即多个纤程共享一个线程。
同多于异,几乎是同义词,都是指令流,不过侧重点不同。
🐇线程的调度算法
🥕固定优先级调度算法⭐️⭐️
- 固定优先级(FP):所有线程按照实现给定的优先级排序运行。时间预算无限长。
- 非抢占式:调度仅在线程结束时发生,此时从队列里拿出一个优先级最高的线程运行。如下图,E3的优先级比E2高,但它先等E3结束了再运行。
- 抢占式: 调度在线程结束和线程就绪时都发生,此时从队列里拿出一个优先级最高的线程运行。这等于说,如果有一个新的高优先级线程加入进来(如下图的E3),它会取代当前的任务,立即获得CPU并运行。
🥕调度策略——保证公正(公平+正义)⭐️⭐️⭐️
系统的响应:多个程序可能同时竞争CPU。它们都说自己最需要,然后也都可能就占着不放了。我们不能让某个应用程序霸占CPU,而是需要保证多个程序都能分到一部分CPU来运行自己,保证CPU的利用公正。
公平的常见调度
- 吞吐率:单位时间内执行完的线程的个数。如果在某段时间内,执行完成的任务越多,说明CPU分配越普惠,就越公平。假设在时间T内,有N个线程完成执行,就说它的吞吐率为N/T。
- 平均等待时间:线程从就绪态到运行态平均等待的时间。如果任何一个就绪的线程都越能尽快得到CPU,说明CPU分配的歧视性成分越低,就越公平。假设一共有N个线程,它们从就绪到得到CPU之前必须等W1,...,Wn时间,则平均等待时间为(W1+W2+...+Wn)/N。
- 平均周转时间:线程从就绪态到阻塞或停止态平均花费的时间。如果任何一个就绪的线程都越能尽快完成其执行,说明CPU分配的歧视性成分越低、包容性成分越高,就越公平。假设一共有N个线程,它们从就绪到得到停止运作分别经过T1, ... ,Tn时间,则平均等待时间为(T1+T2+...+Tn)/N。若设线程的运行时间为Ri,则Ti=Wi+Ri。
- 平均等待时间是机会公平,平均周转时间是结果公平;
- 平均等待时间只是等待时间,平均周转时间处理等待时间,还考虑运行时间以及决策等待时间。所以在一定程度上平均周转时间更加公平。
正义的常见调度
- 响应时间:从用户提交第一个请求到系统做出第一个反馈的时间。用户必然希望系统越快做出响应越好,不要拖拖拉拉。如果响应时间越短,说明执行顺序的安排越符合用户的意图,就越正义。
平均周转时间和响应时间相比有何区别?为何说前者体现了公平,后者体现的则是正义?
- 平均周转时间关心一切线程的运行时间,而响应时间仅仅关心那个用户刚刚提交的线程产生的第一个反馈。
- 前者关心所有线程,后者只关心某个线程,仅仅是因为那个线程是用户刚刚提交的,它的响应结果可能是用户正在等待的。
-
平均带权周转时间:计算式为(T1/R1+T2/R2+...+Tn/Rn)/N。T/R又叫响应比,T/R=1+(W/R)
-
对于同样的W,R越小,W/R越大;对于同样的R,W越大,W/R也越大。一个越短任务的等待时间越长,对这个指标越不利。
-
公平体现在该式包含了对所有任务的考虑。
-
正义体现在该式对越短任务的越长等待越不容忍。
-
公平的极端:先来先服务(FCFS)
- 所有任务按照其提交时间排序,提交时间越早的任务优先级越高,越优先得到CPU。调度仅在任务结束时发生。
- 特点:公平简单有效,对于任务短小且简单的场合这就够了。
正义的极端:短作业优先(SJF)
- 所有任务按照其运行时间排序,运行时间越短的任务优先级越高,越优先得到CPU。调度可在任务结束时和任务提交时发生。
- 特点:响应性好,适合简单的交互式系统。
问题:如果在E3执行完成前,用户向系统中提交E4、E5、E6,这些任务所需要的执行时间都比E3短,那么意味着它们会一直插队到E3之前。那也就是说,用户一直提交短作业,那意味着E3将永远得不到执行了。
(这和FCFS的情况不同,对于FCFS,后到的作业一定后运行)
饥饿现象:依照某种资源分配策略,某些请求无限增加时,另一些请求将永远得不到分解。在CPU调度的问题上,它体现为某些线程将永远不能获得CPU。
响应比高优先(HRRN)
- 所有任务按照其响应比排序,响应比越高的任务优先级越高,越优先得到CPU。
- 调度仅在任务结束时发生,是FCFS和SJF的折中,具备两方优点但又不极端。
时间片轮转法(RR)
问题:前面提到的各种算法中,要么使用基于固定优先级的抢占(FP或SJF),要么就干脆排排坐吃果果。前者在复杂的场景会导致饥饿,从而可能导致系统卡死,后者则无法在一个任务开始执行后将其中途打断。接下来提到的时间片轮转法就是不产生饥饿,又能打断长任务以获得交互能力。
- 将每个线程的时间预算切成规模较小的时间片,每次只运行一片时间,然后再运行下一个任务。
- 可以看作是FCFS的一种改进:任务先来先执行,但执行时间到就切换下一个任务,等到所有任务都轮到一遍,再回到第一个任务执行。
- RR是抢占式的,因为它会打断超时线程的执行。
- 特点:交互性好,所有的线程都获得了执行的机会,无论长短;可以保证在某个时间周期内,所有的线程都获得至少一次执行机会。
- 时间片的大小,太长了,就变成FCFS了,如果太短那长期陷入内核,管理消耗多。
固定优先级时间片轮转法(FPRR)
问题:时间片轮转法无视了进程的固定优先级。如果把固定优先级加回系统,使其能够做到保证平均CPU获得的基础上,满足某些特别任务的高优先级需求?
- 将时间片轮转法进行改进,同一个优先级的任务采取时间片轮转法,不同优先级的任务之间则采取严格的抢占式固定优先级调度。
- 特点:非常适合某些小型实时系统。即便是在大型系统中,有时也能取得不错的性能。
🥕复杂系统的调度:按线程行为分类
- 多级队列:将线程按类型分成不同的队列,每个队列采用适合自身的调度算法,队列之间又有队列间的调度算法。
问题:同一个线程可能承载不同的指令流;同一个指令流的行为在不同的执行阶段也可能发生改变。
多级反馈队列:在多级队列的基础上,实时动态检测每个线程的行为,并在原有队列不合适时为其更换到合适的队列。
🥕广义的调度算法
- 长期调度:
- 决定选中哪些可执行文件,并将它们装入虚拟内存。
- 主要是围绕着内核对象的创建和虚拟内存的映射。
- 调度的是抽象层次的任务集合,一般对应着一项有实际意义的工作。
- 长期调度一般是用户编写的各类定时执行脚本或者用户模式实用程序完成的,因为要装入什么东西只有用户自己知道。
- 中期调度:
- 决定哪些准备好的工作需要实际装入物理内存,哪些装在物理内存里的工作暂时不执行,因而需要换出来。
- 主要是围绕着页面文件的扇入和扇出。
- 调度的一般是空间保护域也即进程。
- 短期调度:
- 决定哪个装入物理内存的应用程序示例(进制)的线程可以得到CPU来执行。
- 通常指的调度就是短期调度。
🐇系统中的线程总览:按特权划分
内核线程
- 运行在内核空间的线程;它们运行时,CPU处于内核模式。
- 它们可以直接访问内核的一切资源。正因如此,它们均由操作系统内核启动和管理,甚至可以视作是内核的一部分。
- 只有可抢占内核有内核线程。
用户线程
- 运行在用户空间的线程;它们运行时,CPU处于用户模式。
- 它们进入内核的唯一方法是系统调用。常说的线程就是只用户线程。
- “内核线程(Kernel Thread)/用户线程(User Thread)”不要和“内核级线程(Kernel-Level Thread)/用户级线程(User-Level Thread)”搞混了。
- 前者是指线程运行时的CPU模式,而后者则是指操作系统是否知道它们的存在。
- 内核线程和用户线程都是内核级线程,而所谓的“用户级线程”则是指协程和纤程。
🐇线程的基本操作
- 创建(Create):创建一个线程。这包括它的线程控制块等内核数据结构。该操作还会返回线程的句柄(Handle),这一般是它的线程号(Thread ID,TID)。
- 出让(Yield):通知操作系统当前线程不需要更多CPU时间,自愿放弃CPU。
- 终止(Terminate/Exit):线程自愿停止自己的执行并退出,可以返回一个返回值。
- 同步(Join/Wait):使一个线程等待另一个线程终止,并获取它的返回值。需要给出被等待线程的TID。很多时候,这个操作也会导致线程销毁,因为终止的线程除了交还它的返回值以外没有任何意义。同步操作等待的对象必须是由发起者创建的,且对象不能脱离(Detach)发起者。
- 销毁(Destroy/Delete):销毁一个同步完成的线程。是创建的逆操作。给出线程的TID,该操作将销毁它。
📚进程与主存空间
🐇程序内的分配策略⭐️
分配:应用程序的某个指令流发出一个从堆中申请内存的请求,堆的大小是已知的,某个应用程序内部的分配算法将从堆切出一部分内存并返回给这个请求。C语言的malloc、calloc等函数会从堆中申请内存。
使用:应用程序中的某个或某些指令流持有该内存块一段时间。在这段时间里,它们可能会读写该块内存。
释放:指令流不再使用这个内存块,并将其归还回堆中供以后申请。C语言的free函数将会释放内存,将内存归还给堆。
🥕固定分区法
- 固定块法:我们可以把堆空间划分为多个相等的内存块。每次动态分配内存的时候,我们都分配固定的一块空间。这相当于把malloc的参数给丢掉了,不管它传递进来什么都分配一个可能的最大请求。(仅适合最简单的应用程序)
- 碎片:处于某些原因,无法有效利用而被浪费掉的资源。在这里是出于内存分配策略被浪费掉的内存空间。
- 内部碎片:
- 实际上已经指派给某个分配,但是逻辑上无法被这个分配利用的资源。
- 这通常是由于分配粒度导致的:如果实际分配的粒度和分配请求的粒度不一致,每次分配的资源数目要向上取整到分配请求的粒度,造成浪费。这些因为取整而额外多出来的内存就是内部内存碎片。
- 如果分配的粒度与请求的粒度是一致的,不存在内部碎片。
- 外部碎片:
- 尚未被实际分配出去,但因为某些逻辑上的原因无法分配的资源。
- 这通常是由于资源请求的空间分布限制与实际资源的空间分布方式冲突所致的。
- 最常见的冲突是请求的连续性与资源分布的不连续之间的冲突,在内存分配问题上尤其如此。如果某些资源在空间上不连续,即便总量足够,也无法满足连续分配的需求。
如果1MB分配不超过500次,那么不会产生任何内存碎片。但如果分配第501次,我们将发现1MB内存池的资源耗尽了,此时只有从100MB的内存池里面拿出一块来满足这个请求。这一次将产生99MB的内部碎片。而且,分配第505次后,我们将无法再满足更多请求。100MB分配就更惨了:只能做五次分配,此时系统中便无100MB的整块内存了,即使1MB内存池有足够总量也无法再分配了。
🥕首次适配法(First-Fit)
- 在空闲区列表中按某个线性顺序(最常见的是空闲区的地址序,也可以是各个空闲区在列表中的登记顺序或者从上次分配的位置开始依次往后)检索,第一个能容纳该分配的空闲区被选中。
- 优点:简单。对大小内存申请都公平,没有倾向性。
- 缺点:不试着保留大的整块区间,也不去试图规避碎片。
🥕最好适配法(Best-Fit)
- 遍历空闲区列表,选择能容纳该分配的空闲区中的最小的那个。
- 优点:试图推迟对大空闲区的分割以便将其用于未来的整块分配,直到不得不分割它们。倾向于大块整块内存分配。
- 缺点:这么做等于劫小济大,会让小的空闲区碎的更厉害。一旦小的空闲区都碎到不能再碎而无法完成分配,大的空闲区也要遭殃。
🥕最坏适配法(Worst-Fit)
- 遍历空闲区列表,选择空闲区中的最大的那个。
- 优点:试图推迟难以用于任何分配的极小碎片的产生以便保持内存对于一般分配的可用性。倾向于小块碎块内存分配。
- 缺点:这么做等于劫大济小,很快就不会有大块的空闲区剩下了。如果程序此时要分配大空闲区,那基本就分配不了。
(所以都是各取所需哇
🐇进程的描述
地址空间与进程:和指令流-线程的对应关系类似,操作系统并不直接知道某个应用程序的存在,它只能看到某些程序通过某些方法向某个地址空间加载数据并在那里执行。只要用户愿意,且应用程序的设计许可,用户可以在一个地址空间中装入多个应用程序,也可以将一个应用程序分成多个互相协作的地址空间。
进程(Process)的概念
- 操作系统提供给应用程序的一种对地址空间的抽象机制。
- 它是内存分配的基本对象。
- 应用程序通过将自己装入地址空间与进程对应起来,使自己在内存中拥有一个活动副本。操作系统通过给进程分配内存,来给依附在这个进程上的应用程序提供运行空间。
- 也就是说,程序通过依附于进程,获得了占用内存空间的权利。
- 在应用程序看来,自己的逻辑组织是一系列段。
- 在操作系统看来,应用程序的空间组织是一个或一系列进程。
进程的描述——描述符表
- 进程本身是(更严谨地讲是包含)一个地址空间,那么必然有一个参数描述这个地址空间。
- 它一般是一个表格,足以令操作系统决定哪些内存该进程有权访问以及怎么访问。
进程的描述——其它资源
- 进程还可以包含一些其它权限的描述,比如对文件、设备等的访问权限。
🐇进程控制块(PCB)
- 操作系统用以描述和管理进程的内核对象,一般至少包含进程的地址空间描述符表及一些其他权限表,有时还会包含一些身份信息(如进程名、进程号)、统计信息(如当前正在运行的线程数、总计内存大小)、线程信息(当前的线程列表,内含指向各个TCB的指针)等。
- 它在一般是C语言的一个结构体。
- 进程控制块总是位于内核空间,只有操作系统可以更改,应用程序无法更改。
- 与TCB不同,没有内核模式的CPU不需要也无法(用硬件手段)实现PCB。
- 每个进程都有一个PCB,在创建进程时建立PCB,伴随进程运行的全过程,直至进程撤销而撤销。
🐇进程与可执行文件
- 可执行文件:应用程序在外存上的存储方式。它描述了应该为应用程序建立一个(或一些)什么样的进程、进程中要有什么样的线程,以及线程和具体的指令流如何对应。它是死的、干瘪的、静态的应用程序,没有执行环境和上下文,也没有执行活动。
- 进程:应用程序在内存中的活动组织。它是活的、丰满的、动态的应用程序,具备一个由地址空间和其它权限提供的执行环境,并充满了线程(或说依附于线程上的指令流)的执行活动和上下文。
- 关系:
- 可执行文件对进程为一对多关系。一个可执行文件每启动一次就可以创建一个(这是通常的实现)或一组进程;如果它启动多次,就可以创建一系列或一系列组进程。
- 同一个可执行文件,在启动为不同的进程时,可以处理不同的工作、使用不同的权限,或者以不同用户的名义启动。生成的多个进程之间是不同的,因为他们内部的执行环境、执行活动和内部线程的上下文均有差别。
🐇线程与进程⭐️
- 线程:CPU执行时间的分配对象,指令流通过依附于它获得执行时间。但它又需要依附在进程上获得执行空间。
- 进程:仅仅一个执行空间,本身不具备执行能力。作为特例,一个进程在创建时可以不包含线程,而是等待其他进程中的线程迁移过来。这在实现时间隔离的管程或服务器时非常有用。
- 关系:进程对线程可以为一对一、一对多、多对一、多对多关系。总的而言,至少在理论上讲,它们在数量上没有任何固定的对应关系。
一对一关系
- 最常见的关系,也是Linux在2.4版本之前的默认关系。在那和之前,Linux的线程和进程是一个东西,其task_struct里面同时含有线程的信息和地址空间的信息。
- 由于这种关系是如此普遍,因此很多书上会直接讲进程的调度、进程的状态。其中又以单指令流依附于单线程,单线程运行于单进程最为常见,因此很多人把进程、线程和指令流混为一谈也就不奇怪了。
- 实际上,进程本身并不运行,运行的是它里面的线程上的指令流。
一对多关系
- 相当常见的关系。多线程进程就是指一个进程中同时存在几个线程。
- 如下图所示,一个应用程序进程中有三个指令流,其中每一个指令流都依附于单独的线程。这时,我们就说这是一个多线程进程。
- 其本质是多个CPU时间分配对象共享一个内存空间分配对象。
🐇内存隔离机制:分段
- 按段划分虚拟地址:
- 从程序的逻辑组织出发,其基本单元是一个个段。那么,我们只需要将每个段重新映射到不同的物理地址就好了。
- 为此,我们需要给每个段分配一个段号,同时每个段都对应一个物理地址区间,还拥有一个访问权限。
- 每个程序的虚拟段到物理段的映射关系组成段表。
- 段氏内存管理单元(A-MMU):
- 常用于分段布局的存储器访问管理工具,具备按段地址重映射和访问权限管理两个职能。
- 段式内存管理单元使用一张段表,每个段都包括“段号”、“段物理地址范围”和“段权限”三个部分。由应用程序发起的每一次内存访问都需要经过段表指定的转换和检查:
- (1)按照访问的虚拟地址中的段号信息查找相应的段。
- (2)发起的访问的性质(读,写,执行)必须是该段的权限允许的。
- (3)访问的物理地址=段基址+虚拟地址,且虚拟地址不得超过段长度。
段表的查询
- 优点:能支持无限数目的段。段表放在内存中,大小是几乎无限的。
- 缺点:每次访存都膨胀成两次:首先访问段表,然后再访问内存本身。现代CPU的访存延迟都很高,这势必造成严重的性能损失,尤其是当段表也不在数据缓存中的时候。
分段不会产生内部碎片,但可能产生外部碎片。
局部性:一个活动操作的对象具备某种关联性。在这里是指,一个指令流(活动)在一段时间内访问的存储器总是有某些集中性(关联性的一种体现方式),那些集中性称为局部(短期调度的工作集)。它还可以细分为时间局部性和空间局部性。
- 时间局限性:指令流执行过程中,如果某存储单元被访问,那么在近期它很可能还会被再次访问。横切。
- 空间局限性:如果指令流访问某存储单元,那么近期内很可能会访问附近的其他存储单元。纵切。
快表(TLB)
- 专用以缓存那些当前工作集中常用的段描述符,以加快地址翻译和权限检查。
- 它也是一种缓存(Cache),并且在电路上和常规缓存一样,都采用了可并行查找的内容寻址存储器(Content Addressible Memory,CAM),速度非常快。
🐇内存隔离机制:分页
- 按页划分虚拟地址:
- 从物理地址空间的细粒度划分出发,我们将物理地址切割成一个个大小相等的小页,并将虚拟地址也切割成同样大小的页。那么,我们只需要将每个虚拟页映射到不同的物理页就好了。
- 为此,我们需要给每个页分配一个页号,同时每个虚拟页都对应一个物理页,还拥有一个访问权限。
- 每个程序的虚拟页到物理页的映射关系组成页表。
- 页式内存管理单元:
- 常用于分段布局的存储器访问管理工具,具备按页地址重映射和访问权限管理两个职能。
- 页式内存管理单元使用一张页表,每个页都包括“页号”、“页物理地址”和“页权限”三个部分。由应用程序发起的每一次内存访问都需要经过页表指定的转换和检查:
- (1)按照访问的虚拟地址中的页号信息查找相应的页。
- (2)发起的访问的性质(读,写,执行)必须是该页的权限允许的。
- (3)访问的物理地址=页基址+页内偏移量。
分页的特点:
- 外部碎片:理论上讲,外部碎片在页式管理中不存在,因为物理内存不存在必须连续才能分配的要求。物理上不连续的页完全可以组成虚拟空间中的连续的段。
- 内部碎片:理论上讲,页式管理存在内部碎片,因为程序的每个段现在都按照页为最小粒度划分了。如果一个段不能被页整除,我们就必须要分配一整个页给它的残余部分。
页越大(一般定为4KB),产生的内部碎片就越多,但是TLB的利用效率就越高。反之,内部碎片少,但TLB利用率低。
基数树(Radix Trie)
- 以某个基数组织的“桶套桶”序列(更专业的叫法是前缀树),该基数决定了每一层桶的多少。对于页表而言,每层的桶都是2的次方个,如1024个。
进程的工作集
- 和指令流一样,一个进程内的所有指令流的活动在一段时间内访问的存储器也总是有些集中性。(中期调度的工作集)
- 这意味着只要将包含进程工作集的那些页调入内存就可以了。
- 如果进程意识半会用不到某些页,那些页大可放在外存上。
单层存储模型
- 计算机上所有的存储器都被抽象成一种逻辑模型。
- 操作系统实际决定哪些内容存放在哪些层次的存储器中。
- 它有两方面的含义:
- 一方面:数据存储的逻辑模型向上层的存储器靠近。
- 另一方面:数据存储的物理特性向最下层的存储器靠近。
🐇请求分页,替换算法⭐️⭐️⭐️
- 在进程活动时,仅将其当前工作集调入内存,其余部分则存放在外存中,直到工作集再次包含它们。在这种情况下,内存被当成了外存的缓存,而外存则充当了内存的后备。这种机制又称页面交换、虚拟内存(不要和虚拟内存空间搞混了!)或分页文件。
- 具体地,操作系统故意不填充页表或段表的部分空间,不映射某些页,等到缺页/缺段异常时再去填充这个空间,确保所有映射的空间都是真正用到的。(这和TCB与页表或段表的关系很像)
缺页异常处理流程
问题:当工作集改变,需要包含新内容时,如何选择要被替换的老内容?页的换入换出需要读取外存,我们希望这种换出尽量少。
- 缺页率:触发页面交换的访存次数/总访存次数;我们希望这个数值尽量低。
🥕最长前向距离算法(LFD)
- 当每次替换时,都寻找当前页面中在最远的未来才会再次使用的那个页面,并替换掉它。特别地,若一个页不再使用,则其对应的未来可以被看做无穷远,应被首先淘汰。
- 这是一个贪心算法,它只考虑一个单一的局部状态(不一定代表整体最优状态),并且认为每次做出局部最优选择都能得到整体最优的结果。
🥕先进先出(FIFO)
- 我们每次替换,总是选择替换那个已经驻留了最长时间的页,将它驱逐换成新页。
🥕最久未用法(LRU)
- 如果一个页很久都没有用了,那大概未来也用不到了。每次都驱逐最久没有用过的那个页(和LFD正好形成反演)。
🥕Belady异常
在某些资源分配策略下,增加资源总量反而导致性能下降和效率降低的现象。在这里是指,对于某些替换算法,允许的物理页数量越大,缺页率反而升高。
🥕Linux页面替换——二次机会法
缺页异常:当页表中没有某个页时,产生操作系统可截获并处理的异常。
访问位A:当某个页被访问,其页表的访问位会被硬件置为。
脏位D(又脏位说明该页可写):当某个页被实际写入(内容遭修改),其页表的脏位会被硬件置位。
- 二次机会法
- 二次机会法改进——三次机会法
🥕页面置换的其它考量
- 运行是最低目标:没有操作系统会仅仅满足于能让程序运行,因为这是及格线。操作系统往 往会给多得多的页,来覆盖程序的工作集,保证程序能高效运行。
- 抖动(Thrashing):当被分配的页数小于当前工作集的时候,缺页率会大幅增长。此时,程序的访存性能向外存的性能急剧跌落。工作集有短期、中期和长期三个层面,抖动也是这样,存在着短期抖动、中期抖动和长期抖动。一般只考虑短期抖动(一个程序内部)
- 分配策略的动态性:一个进程的页数量可以是静态决定的也可以是动态调整的。如果工作集的 大小已知,只要将页数量设置为那个固定值就好了;如果出现阶段性的需求,就需要动态决定进程需要多少页框。
- 置换策略的全局性:页置换可以仅仅在一个进程内部发生,也可以在进程间发生。前者配合静 态分配可以将一个进程引发的抖动限制在自身之内,但页利用效率低;后者配合动态分配则正好相反,可能使一个进程的抖动影响其它进程。
🐇进程的操作
基本操作
- 创建:通知操作系统建立一个新的虚拟地址空间。
- 销毁:通知操作系统销毁虚拟地址空间。
- 分配资源:给进程分配更多资源。
其它操作
- 等待:等待另一个进程被销毁(也即其主线程终止)。
- 复制:产生一个当前进程的副本,或者加载其它程序覆盖当前进程。
- 设置特权 :赋予(Grant)或撤销(Revoke)进程的某些权限。
- 设置策略 :对进程设置资源总量限制或资源分配优先级等等。不要把这个优先级和线程的优先级搞混了,这里主要是指分配物理内存(或其他空间资源)时是否优先照顾它的请求。