参考链接:https://www.cnblogs.com/ConstXiong/p/11991459.html
https://blog.csdn.net/chejinqiang/article/details/80157424
https://blog.csdn.net/u014510460/article/details/82903529
课程概要
- 操作系统介绍
- 中断及系统调用
- 内存管理
- 进程及线程
- 调度
- 同步
- 文件系统
- I/O子系统
1. 操作系统介绍
- 操作系统是控制软件,作用:管理应用程序,为应用程序提供服务,杀死应用程序,分配资源,管理外设
- 抽象:CPU - 进程,内存 - 地址空间,磁盘 - 文件
- OS分为Shell(界面)和Kernel(内核)
2. 中断及系统调用
- 启动过程:BIOS检测外设,加载BootLoader,再由BootLoader加载OS
- 系统调用:应用程序向OS发出请求
- 异常:应用程序发生异常,需要OS处理
- 中断:外设发出请求
- 系统调用之上有API供应用程序调用:Win32 API,POSIX API(UNIX LINUX Mac OS)
- CPU不同状态:用户态 内核态
- 内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
- 用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
为什么要有用户态和内核态?
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。
3. 内存管理
内存模型
从离CPU的距离越近速度越快,容量越小
依次是:寄存器(Register),缓存(Cache),主存,硬盘,前两者属于CPU
- 内存管理能够实现:物理存储抽象为逻辑地址空间,保护各程序各自内存空间,程序间共享内存
- 虚拟化 - 暂时不用的数据移动到磁盘
- 程序经过编译、汇编、链接、载入对应到逻辑地址
- **连续内存分配:**首次适配、最优适配、最差适配
- 内存碎片:外碎片 - 程序之间、内碎片 - 程序之内
- 内存碎片整理:压缩式(移动空闲内存)、交换式(暂时不用的数据移动到磁盘)
- 非连续内存分配:解决碎片问题,提高内存利用率
- 分段segment:内存块大小可变
内存地址表示为(segment number, offset)
段表:段号 - 基址,长度 - 分页page:内存块大小固定(帧)
物理地址:(frame number, offset),帧,真正内存的地址
逻辑地址:(page number, offset),页,程序里的地址
页表:page - frame,每个进程有独立的页表,用于通过逻辑地址,找到映射的物理地址
解决页表过大:
TLB(Translation Lookaside Buffer)用于缓存常用地址
多级页表
反向页表:frame - page
反向页表的实现:页寄存器、关联内存、hash table
虚拟内存
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
- 提出:缓解内存不足
- 旧时代的技术:
- 覆盖技术(overlay),常用程序模块独占常驻内存,不常用程序模块分时共享内存,但需要程序员设计覆盖关系,增加了程序设计复杂度,并且有从硬盘读写模块的开销,粒度是程序模块
- 交换技术(swapping),由OS执行换入换出,以程序为单位,内存可能不足时进行内存和硬盘间数据交换,粒度是程序
- 虚拟内存,由OS执行,程序设计需要有局部性(指令和数据访问在时间和空间上较为集中),低粒度,以页为单位,按需从硬盘调入程序和数据
- 有效存储访问时间(effective memory access time, EAT)
- 数据结构,通过页表实现:
驻留位(resident bit, 1表示在内存中,0表示在硬盘中)
保护位(设置权限,包括只读、读写、可执行等)
修改位(dirty bit, 数据是否被修改过,换出时是否需要写回硬盘)
访问位(access/used bit, 是否被访问,长时间未访问的数据在内存不足时优先被换出)
锁定位(lock bit, 标记需要常驻内存的数据,如OS进程和对时间敏感的进程) - 算法:在页表中查找所需数据的物理地址,如存在则直接读取;如不存在(即产生缺页中断),先判断是否有空余的页,如无,则需要进行页面置换先换出内存上的数据(未被修改直接free,被修改过则写回硬盘),再从硬盘读取数据
- 局部页面置换算法
置换算法选择将哪些数据换出,目标是尽可能减少换入换出- 最优页面置换算法,换出未来等待最长时间才会被访问的数据,是一种理想情况,作为其他评价其他算法的标杆
- 先进先出算法(FIFO),换出在内存中存在时间最长的数据,但存在时间久的数据有可能会被频繁访问,导致缺页较多,造成Belady现象
- 最近最久未使用算法(Least Recently Used, LRU),换出距离上一次被访问间隔时间最久的数据,缺页较少,但需要记录每个数据的访问时间,开销较大
- 时钟算法(Clock),在环形链表中记录frame的access bit,数据被访问时bit置为1,所有bit定期归零。发生缺页时,在环形链表中寻找access bit为0的frame,若为1则置为0,若为0则换出。相当于近似的LRU,缺页次数稍大,但节省空间
- 二次机会算法,改进clock,(access bit, dirty bit) ,被修改过的frame换出需要会写磁盘,开销大,因此要减少被修改过的frame换出
- 最不常用算法(Least Frequently Used, LFU),将访问次数最少的数据换出
Belady现象,分配更多物理内存,缺页反而更多的现象,如FIFO
- 全局页面置换算法
实现全局动态分配各个程序的内存,保持平衡,效果好于局部算法- 工作集模型,W(t, Δ),t时刻往前Δ时间段(窗口)内,page的集合,集合size越小,程序局部性越好
- 工作集页置换算法,固定窗口大小,随着时间窗口移动,换出窗口外的页,换出最久未访问的页,限制每个程序的可用内存
- 缺页率页置换算法(Page Fault Frequency, PFF),用缺页发生的时间间隔评价缺页率,间隔短说明内存不足,加载页,间隔长说明内存过多,换出未访问的页
- 抖动(thrashing),内存严重不足时,大量时间耗费在换入换出操作上,程序运行效率大大降低的现象
4.进程与线程
进程(Process)
- 定义:进程是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
- 每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。
- 为了支持运行多个程序、多个实例而生
- 记录程序的所有状态信息,包含代码、数据、程序计数器和寄存器中存储的值、其他系统资源(文件、网络)等
- 描述进程的数据结构:进程控制块(Process Control Block, PCB),与进程一 一对应
- PCB保存:
进程标识(进程ID、父进程ID、用户ID)
状态信息(寄存器,主要有程序计数器PC、栈指针SP)
控制信息(调度、通信、存储、IO、树形结构) - PCB实现方式:链表、索引表,进程频繁被创建和终止,适合链表
- 生命周期管理:
-创建(源自系统、用户、进程)
-运行(OS调度)
-等待(阻塞,等待数据就绪、其他进程完成)
-唤醒(由OS或其他进程完成,回到就绪态)
-结束
- 进程(线程)状态:new、ready、running、blocked、exit
- OS对各状态分别维护队列,并按优先级区分多个队列
- 各就绪态进程分时占用CPU,OS管理时钟
- 进程挂起(suspend):将内存中的数据换出到磁盘中;就绪或阻塞时可挂起,优先挂起阻塞进程;相反的过程:进程解挂/激活(activate)
- 进程控制
-创建、加载和执行进程: Windows - CreateProcess(); Linux - fork()复制进程,exec()加载新程序,取代当前进程。优化方式:vfork() 轻量级fork(),Copy On Write只复制元数据,其他数据只在写操作时进行复制
-等待和终止进程:父进程的wait()和子进程的exit()配合完成子进程资源的回收;子进程调用exit()后,资源完成回收前的状态称为Zombie态;root进程也会定期进行资源回收
线程(Thread)
- 进程中的更小的运行单位
- 进程分配资源,实现代码、数据、文件的共享;线程将执行过程独立出来,是CPU调度单位,各线程有各自的寄存器、堆栈
- 描述线程的数据结构:线程控制块(Thread Control Block,TCB)
- 线程机制提高并发性能,但数据共享导致线程间容易发生互相干扰,安全性差。需要性能时使用线程(如科学计算),需要安全时使用进程(如浏览器)
- 线程实现方式:
- 用户线程,由用户线程库管理调度,自定义调度算法,OS只管理进程层面;
- 内核线程,由OS管理,粒度小,但产生用户态到内核态切换的开销,如Windows;轻量级进程,每个进程拥有多个轻量级进程,各自对应一个内核线程,如Solaris/Linux
- 上下文切换:停止当前进程/线程,开始其他进程/线程时,需要在PCB/TCB中保存当前线程的信息,读取下一个线程的信息,这些信息称为上下文
- 上下文具体包括:寄存器信息(程序计数器PC:程序执行阶段,栈指针SP:调用关系和局部变量位置),CPU状态
协程(Coroutine)
-
纤程(Fiber),或者绿色线程(GreenThread)。其实最直观的解释可以定义为线程的线程
-
协同运行的子程序,或者说是协同执行的任务。
-
coroutine可以认为是单线程多任务的工作方式(当然,进程中实现coroutine也是可以的),因为它在单个线程中的多个任务之间直接跳转,而多线程是通过上下文切换来实现多任务的。换句话说,coroutine提供了并发却不并行的功能。通过coroutine,还能实现更为"实时"的上下文任务,因为coroutine之间的跳转切换不需要任何系统调用和可能的阻塞调用,不需要像多线程一样为了线程之间的资源同步而使用额外的互斥锁、信号量等。
-
传统的J2EE系统都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,比如JDBC是同步阻塞的,这也是为什么很多人都说 数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。暴殄天物啊。另外过多的线程,也会带来更多的ContextSwitch开销。
-
Java的JDK里有封装很好的ThreadPool,可以用来管理大量的线程生命周期,但是本质上还是不能很好的解决线程数量的问题,以及线程空转占用CPU资源的问题。
现阶段行业里的比较流行的解决方案之一就是 单线程加上异步回调。其代表派是node.js以及Java里的新秀Vert.x。他们的核心思想是一样的,遇到需要进行I/O操作的地方,就直接让出CPU资源,然后注册一个回调函数,其他逻辑则继续往下走,I/O结束后带着结果向事件队列里插入执行结果,然后由事件调度器调度回调函数,传入结果。这时候执行的地方可能就不是你原来的代码区块了,具体表现在代码层面上,你会发现你的局部变量全部丢失,毕竟相关的栈已经被覆盖了,所以为了保存之前的栈上数据,你要么选择带着一起放入回调函数里,要么就不停的嵌套,从而引起反人类的Callback hell。
因此相关的Promise,CompletableFuture等技术都是为解决相关的问题而产生的。但是本质上还是不能解决业务逻辑的割裂。 -
协程的本质上其实还是和上面的方法一样,只不过他的核心点在于 调度那块由他来负责解决,遇到阻塞操作,立刻yield掉,并且记录当前栈上的数据,阻塞完后立刻再找一个线程恢复栈并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。比如Golang里的 go关键字其实就是负责开启一个Fiber,让func逻辑跑在上面。而这一切都是发生的用户态上,没有发生在内核态上,也就是说没有ContextSwitch上的开销。
-
协程在Golang有比较好的支持,Java 官方目前是还没推出协程,目前可用性比较高的有 Quasar 和 ea-async 两个第三方库,都是通过 byte code Instrument,把编译后同步程序class文件修改为异步的操作。
-
协程的优点:
- 轻量,创建成本小,降低了内存消耗
- 用户态调度,减少了 CPU 上下文切换的开销,提高了 CPU 缓存命中率
- 减少同步加锁,提高了性能
- 可以用同步思维写异步代码
-
协程的缺点:
- 在协程执行中不能有阻塞操作,否则整个线程被阻塞
- 不 擅长处理 CPU 密集型
进程间通信 Inter-Process Communication, IPC
- 概述
- 通信模型,建立通信链路,进行操作send(message),receive(message)
- 直接及间接通信:直接,通过共享区域,进程间直接收发消息;间接,通过内核kernel转发,进程与消息队列之间收发消息
- 阻塞与非阻塞:阻塞方式,同步,收发消息开始后让进程阻塞,直至通信完成;非阻塞方式,异步
通信链路缓冲:收发速度不一致时提高效率,有限容量缓冲 - 实现IPC的方式:
- 信号 Signal,短小的bit,不能用于传递数据;不同的信号对应有handler,kernel发送消息给进程,并跳转到handler的stack
- 管道 Pipe,用于传递字节流,使得多个程序组合起来实现更负责的功能;shell对其子进程的输入输出重定向,串联成管道,kernel中的一个buffer
- 消息队列 Message queue,无继承关系的进程间,结构化的数据结构
- 共享内存 Shared memory,两个进程的共享区域,直接通信,快速,需要设置同步互斥;通过将同一块物理内存分别映射到两个进程的page中
5.CPU调度
-
调度(Schedule),进程、线程切换
-抢占,线程运行完成前被打断,OS发展:不可抢占 -> 只支持用户态抢占 -> 用户态和内核态都可抢占
-CPU忙与闲(I/O操作) -
调度原则:选择哪一个线程来执行?
-
评价指标:CPU忙时百分比、吞吐量、完成时间(从初始化到结束的时间)、等待时间(处于就绪态的总时间)、响应时间(从发起请求到第一次响应的时间)
-
目标:低响应时间、高吞吐量,两者很难兼顾
-
公平,每个进程占用同等的CPU时间,但会牺牲效率
-
调度算法:
-
基本调度算法
- FCFS,First Come First Served,先来先服务,简单,等待时间波动大
- SPN,Shortest Process Next,短进程优先 == SJF,Shortest Job First,短作业优先,不可抢占
- SRT,Shortest Remaining Time,短剩余时间优先,可抢占;增加长进程等待时间(饥饿);需要根据历史执行时间估算未来执行时间
- HRRN,Highest Response Ratio Next,最高响应比优先,R=(waiting time+service time)/service time,R最高者优先;防止无限期推迟,也需要预估执行时间
- Round Robin,轮循,按时间片轮流执行,提高公平,增加了上下文切换开销,时间片大小很重要,过大则变成FCFS,过小则切换开销太大,一般根据经验决定,使得上下文切换开销<1%
- Multilevel Feedback Queues,多级反馈队列, 将进程放置在多个队列中,高优先级队列时间片小,低优先级队列时间片大,等待时间长的任务提高优先级(I/O密集型,即前台交互任务,保证公平性),服务时间长的任务降低优先级(CPU密集型,即后台运算任务,提高效率)
- Fair Share Scheduling,公平共享调度,用户层面的公平,适用于多用户共享服务器的场景
-
实时调度算法(嵌入式)
- 实时系统:强调deadline,平均性能相对不重要
- 强实时系统:重要任务在规定时间必须完成
- 弱实时系统:重要任务优先级更高,尽量完成
- RM,Rate Monotonic,速率单调调度,静态优先级,周期越短优先级越高
- EDF,Earliest Deadline First,最早期限调度,动态优先级,deadline越早优先级越高
-
多处理器调度算法(多核)
- 追求负载平衡(load balance)
-
优先级反转
- 低优先级任务占用了某共享资源,高优先级任务不能及时执行
-
解决优先级反转
- 优先级继承,与高优先级任务共享资源的低优先级任务,优先级被提升
- 优先级天花板,将资源的优先级设置为可以锁定该资源的任务的最高优先级,任务的优先级取决于所占用资源的优先级
6. 同步
-
解决进程间的交互产生的各种问题
-
为什么需要合作:共享资源、并行提高效率、模块化
-
如果没有同步互斥机制,则一系列指令执行中被打断,会导致结果不符合预期
-
需要保证任何交替执行方式都能得到正确结果,反之,则形成Race Condition 竞态条件:结果依赖于执行顺序
-
一些概念:
- Atomic Operation 原子操作,不可被打断的操作,要么完整执行,要么不执行,不会发生部分执行
- Critical Section 临界区,访问共享资源的代码区域
- Mutual Exclusion 互斥,保证只有一个进程处于临界区,不允许多个进程访问同一共享资源
- Dead Lock 死锁,进程之间互相等待,无法执行的情况
- Starvation 饥饿,一个进程被调度器忽略,无限期等待,无法执行
- Lock 锁,获得锁,获得控制权,释放锁,失去控制权
- Busy-waiting 忙等待,进程在等待进入临界区时,循环执行无意义操作,浪费系统资源
- Progress,希望进入临界区的进程总是能够进入临界区
-
互斥实现方式
- 硬件方式,禁用中断,进入临界区后禁用中断,离开后开启中断,临界区代码无法被停止,适用于临界区较小的情况,只适用于单处理器
- 软件方式,开销较大
- 两个进程互斥,Peterson算法
- 多个进程互斥,Eisenberg and McGuire算法(循环),Bakery算法(排队取号,同号比ID)
需要共享数据(flag、turn),存在忙等待,需要硬件支持(原子性LOAD、STORE)
- 更高级的抽象,主流方式
- 硬件提供原子操作指令,通过特殊的内存访问电路实现
- Test-and-Set指令,从内存读取值,返回该值,并将内存值设为1
- Exchange指令,交换内存中的两个值
-
以上是忙等版本,适用于临界区较小的情况;如果临界区较大,则加入阻塞和唤醒语句,会产生上下文切换开销
-
优点:适用于多处理器,多进程,多临界区,简单
-
需要考虑:忙等待,随机进入临界区导致饥饿,优先级反转
7.信号量
信号量和管程是比锁更高级的抽象,也是同步互斥的解决方式
-
锁只解决了互斥,为了让多个线程进入临界区(比如只在临界区进行读操作,不必要求互斥),需要解决同步
-
信号量 Semaphore,Dijkstra提出
- 用一个整形(sem)表示,提供两个原子操作,P()和V()
- P(),表示有线程需要进入临界区,sem–; if (sem < 0) wait; else progress;
- V(),表示有线程离开临界区,sem++; if (sem <= 0) wakeup a thread in waiting list // sem <= 0意味着有线程在等待进入临界区,唤醒常用FIFO方式
- sem初值设为1,就相当于锁了;初值设为更大的值,可以实现条件同步
-
调度约束,sem初值设为0,Thread A在某处P(),Thread B在某处V(),确保Thread A在P()之后的语句一定在Thread B的V()执行之后才会开始执行
-
Producer - Buffer - Consumer:buffer用于存放产品,buffer大小为n;生产者向buffer中放入产品,消费者从buffer中取走产品;mutex控制同一时间只有一个生产者或消费者可以对buffer操作,fullBuffers和emptyBuffers实现buffer满时阻塞生产者、空时阻塞消费者
-
信号量的实现,P()和V()的实现使用禁用中断、test-and-set等原子指令,需要等待时将线程放入等待队列使之挂起
-
信号量的一些问题:开发难度较大,忘记释放信号量,语句顺序错误导致死锁
-
管程 Monitor
-
比信号量抽象程度更高的机制
-
作为编程语言的特性而诞生,用来简化并发编程,而非为OS设计
-
包含了一系列变量(锁和条件变量)和方法的模块
-
条件变量 Condition,numWaiting表示等待队列中的线程数量,wait()使线程等待,signal()唤醒线程
用管程实现生产者-消费者模型
count表示buffer中产品的数量
条件变量notFull、notEmpty表示是否为满或空的状态,notFull可以理解为okToProduce -
Hansen vs. Hoare
- Hansen方式,signal()之后继续执行完release()再切换线程,易于实现,实际OS采用此方式
- Hoare方式,signal()之后立刻切换线程,直观,但实现困难,教科书一般按此方式
-
一些经典同步问题:
-
读者 - 写者问题(读者优先,信号量实现):
- 读者读取数据,写者修改数据
- 同一时间允许多个读者,或者一个写者
- 读者优先:同时有读者和写者需要操作时,读者优先操作
- Rcount初值为0,表示读者个数
- 信号量CountMutex初值为1,用于对count修改的互斥
- 信号量WriteMutex初值为1,用于读者和写者、写者之间的互斥
- sem_wait()为P(),sem_post()为V()
-
读者 - 写者问题(写者优先,管程实现):
- 写者优先:只有在没有写者等待时,读者才能操作
- signal() 只唤醒一个线程
- broadcast() 唤醒所有线程
-
死锁
-
死锁一般是由于双方各占用了资源,又需要对方占用的资源,但都不释放资源
-
资源的特征
- 某一时刻只能有一个进程使用,且不可被删除
- 一个进程获取资源,然后释放资源,以供其他进程使用
- 资源既可以是物理资源(CPU、内存、I/O通道),也可以是虚拟资源(数据结构、文件、信号量)
-
死锁出现的必要条件
- 互斥:一个资源同时只能有一个进程使用
- 持有并等待:进程持有资源,并在等待其他进程持有的资源
- 无抢占:资源只能被进程使用完后主动释放
- 循环等待:存在一个环,每个进程在等待下一个进程持有的资源
-
死锁解决方法,约束力度逐渐减弱;实际OS中往往选择忽略死锁问题,因为解决开销过大,影响了性能
- 死锁预防 Deadlock Prevention,改变资源申请方式,确保不出现死锁,比如对资源类型排序,进程只能按顺序申请资源,防止出现循环等待;资源利用率低,容易饥饿
- 死锁避免 Deadlock Avoidance,在进程申请资源时,判断是否会出现死锁,如果是则拒绝分配资源;要求进程声明所需资源最大数量,如果超过该数量则拒绝;还通过算法检测申请资源后是否会形成环形等待
- 死锁检测 Deadlock Detection,允许死锁出现,通过检测算法发现死锁并恢复
- 死锁恢复 Recovery from Deadlock
- reboot,终止所有死锁进程,逐个终止死锁进程(先终止优先级低的、运行时间短的、占用资源多的进程)