操作系统基础学习记录
操作系统基础

操作系统概念
什么是操作系统
操作系统(Operating System,简称OS)是管理计算机硬件和软件资源的程序,是计算机的基石。
操作系统本质上是一个运行在计算机上的软件程序用于管理计算机硬件和软件资源,如运行在电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等硬件。
操作系统屏蔽了硬件层的复杂性,用于CPU,内存,设备的管理。
操作系统的内核(kernel)是操作系统的核心部分,他负责系统的进程管理,内存管理,硬件设备管理、文件系统管理以及应用程序管理。内核是连接应用程序和硬件的桥梁,决定系统的性能和稳定性。
操作系统的内核(Kernel):内核是计算机中一个用来管理软件发出的数据I/O要求的电脑程序,将这些要求转译为数据处理的指令并交由中央处理器(CPU)及其他电子组件进行处理。他是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问时有限的,并由内核决定一个程序在什么时候对某部分硬件操作多长时间。直接对硬件操作是非常复杂的,所以内核提供一种硬件抽象方法,来完成这个操作。有了这个,通过进程间通信机制及系统调用,应用进程可间接控制所需的硬件资源(特别是处理器及IO设备) ----维基百科

操作系统内核Kernel的特征:
- 并发:一段时间内同时运行多个线程/进程,需要操作系统的调度
- 共享:同时/互斥访问
- 虚拟:直接面对硬件,将CPU虚拟化为进程,把磁盘虚拟化为文件,把内存虚拟化为地址空间
- 异步:程序的执行时断断续续的,中间CPU可能去做其他事,然后回来继续执行
操作系统的结构:
- 简单操作系统:部分模块的单体内核,就是将所有功能放在一起,缺点是复杂,紧耦合
- 微内核:将所有的操作系统功能放在一起的一个软件过于庞大,因此出现了微内核的设计,只在内核放最基本的功能,如中断处理,消息传递,然后将内存管理,进程管理放在外围以一种进程或服务的方式存在,服务与服务之间是通过内核的消息传递机制来进行通信(微服务的思想也是如此)
- 外核:外核内核分别为一块,一块与硬件打交道,一块和应用打交道
- 虚拟机:在OS上通过VMM(虚拟机监视器)虚拟出一个或多个计算机系统,
操作系统启动
首先计算机最重要的三部分,CPU,内存,IO是通过总线连接,操作系统软件存放于磁盘,当电脑通电后,基本IO处理系统(BIOS)就会使用Bootloader小程序将操作系统加载到内存中,然后让CPU执行操作系统
中断和异常
- 异常:来源于不良的应用程序,如内存出错等坏的处理状态或非法指令
- 中断:来自于不同硬件设备的计时器和网络的中断
中断和异常处理机制
中断时外设的时间,异常时CPU的时间;中断和异常迫使CPU访问一些被中断和异常服务访问的功能
中断处理机制
中断处理分为硬件和软件两部分完成:
- 硬件:将设置中断标记,CPU看到后去产生一个中断号,然后将该中断号发给操作系统,操作系统就知道中断号对应的中断处理过程
- 软件:操作系统保存当前处理状态,然后通过中断号去执行中断服务程序,执行完后清除中断标记,最后恢复之前保存的处理状态
异常处理机制
异常出现后,操作系统会先保存现场,然后根据s异常编号通过杀死异常程序或重新执行异常程序的方式去处理异常,最后恢复现场
系统调用(使用系统态的子功能)
应用程序主动向操作系统发出服务请求
根据进程访问资源的特点,我们把进程在系统上的运行分为两个级别:
- 用户态(user mode),用户态运行的进程或可以直接读取用户程序的数据
- 系统态(kernel mode),系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制
用户态切换到内核态的方式
- 系统调用:这时用户态进程主动切换到内核态的一种方式,用户态进程可以通过系统调用申请使用操作系统提供的服务完成工作。
- 异常:当CPU在执行用户态程序时,发生了某些不可预知的错误,这时会切换到处理此异常的内核态相关程序中,比如缺页异常。
- 中断:当外围设备完成用户请求的操作后,会向CPU发出相关中断信号,这时CPU会暂停执行下一条指令转而执行与中断信号相关的处理程序
什么是系统调用呢?
我们运行的程序基本都运行在用户态,但如果我们要调用操作系统提供的系统态级别的子功能怎么办呢?这时就需要用到系统调用了。也就是说再我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理,进程管理,内存管理等),都必须通过系统调用的方式向操作系统提出服务请求,并由操作系统代为完成。
这些系统调用按功能大致可分为以下几类:
- 设备管理:完成设备的请求与释放,以及设备启动等功能
- 文件管理:完成文件的读、写、创建以及删除等功能
- 进程管理:完成进程的创建、撤销、阻塞、唤醒以及进程间的通信等功能
- 内存管理:完成内存的分配,回收
进程管理
进程管理
进程的定义
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,简单来说就是操作系统将静态程序放置到内存让CPU执行起来后形成的动态执行过程就是进程进程是系统进行资源分配和调度的基本单位,实现了操作系统的并发
程序与进程的区别
- 进程是一个动态的概念,程序是一个静态的概念
- 进程是竞争计算机系统资源的基本单位,而程序不反映执行也就不会竞争计算机系统资源
- 不同的进程可以包含同一程序,只要该程序所对应的数据集不同。
- 进程具有并行特征,而程序不反映执行所以没有并行特征
进程的组成
进程包含了一个程序运行的所有状态信息:
- 程序的代码
- 程序处理的数据
- 程序计数器中的值,指示下一条运行指令的位置
- 一组通用的寄存器的当前值,堆、栈
- 一组系统资源
进程控制结构
每个进程都有一个进程控制块PID,进程控制块包含了进程运行的状态信息
- 进程标识信息:相关ID,如本进程的标识,本进程的产生者(父进程)标识;用户标识
- 处理机状态信息保存区:保存进程的运行线程信息
- 用户可见寄存器:用户程序可以使用的数据,地址等寄存器
- 控制和状态寄存器:如程序计数器(PC),程序状态字(PSW)
- 栈指针:过程调用/系统调用/中断处理和返回时需要
- 进程控制信息:调度和状态信息,进程间通信信息,存储管理信息,所用资源信息,有关数据结构连接信息
进程控制块通过链表进程组织,同一状态的进程的控制块组成一个链表,多个状态对应多个不同的链表,比如就绪链表,阻塞链表等等
线程管理
为什么需要线程
主要实现进程内的并发,能够在进程内同时完成多件事情,共享进程资源。虽然使用多进程也能实现相同功能,但是进程的维护开销大,创建进程时分配资源,建立PCB;撤销进程时,回收资源,撤销PCB。同时切换代价高,每个进程拥有独立的地址空间,因此切换时需要切换页表
线程的定义
线程是进程的一条执行流程,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点运行中必不可少的资源(如程序计数器,一组寄存器和栈),因此切换代价比进程低,但是他可以与属于同一个进程的其他线程共享拥有的全部资源,管理较进程来说更繁琐,可以实现进程内部的并发
线程的实现
主要有三种线程实现方式:
-
用户线程:在用户空间实现,操作系统无法看到
缺点:当一个线程发起系统调用而阻塞时,整个进程都会阻塞,因为操作系统只能看到进程而看不到用户线程;
-
内核线程:在内核中实现,操作系统管理的线程
-
轻量级线程:在内核中实现,支持用户线程,就是由内核支持,又能由用户控制,java线程就是轻量级线程
线程的组成:
- 程序计数器:用来指向线程执行中的下一条指令的位置
- 寄存器集合:用寄存器来表不同线程的执行状态
- 堆栈:暂时存放数据和地址,一般用来保护断点和现场

线程的优缺点
优点:
- 一个进程中可以存在多个线程,各个线程可以并发执行
- 线程之间可以共享进程的地址空间和文件等资源,通信方便
缺点:可靠性较低,一个线程崩溃,会导致其所属进程的所有线程崩溃,因为资源共享,一个数据被破坏,其他线程也无法使用
进程与线程的区别
区别:
- 进程是线程的容器,线程是进程的一个实体,而进程是程序的实体,是程序关于某数据集合上的一次运行活动;
- 进程是资源分配的最小单位,线程是CPU调度的最小单位

进程有哪几种状态
- 创建状态:进程正在被创建,尚未到就绪
- 就绪状态:进程已获得除处理机以外的其他所需资源,等待分配处理机资源
- 运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数
- 阻塞状态:进程等待某种条件,在条件满足前无法执行,如I/O请求读写文件,等待定时器,
- 结束状态:进程正在从系统中消失,可能是进程正常结束或其他原因退出(被强制结束)

进程挂起
目的:合理且充分的利用系统资源,比如遇到内存不够用时,将一些进程挂起
进程在挂起时,意味着进程没有占用内存空间,处在挂起状态的进程映射在磁盘上,挂起有两种状态
- 阻塞挂起状态:进程在外存并等待某时间的出现
- 就绪挂起状态:进程在外存,但只要进入内存,就可以运行
将阻塞/就绪的进程转换到外存就是阻塞挂起/就绪挂起,反之一样
上下文切换
进程切换包含上下文切换,在切换时,一个进程存储在处理器各寄存器中的中间数据就是进程的上下文,所以进程切换的实质就是被中止运行进程与待运行进程上下文的切换,在进程未占用处理器时,进程上下文的存储是在进程的私有堆栈中。
进程切换过程
进程切换就是停止当前运行进程(从运行状态改变到其他状态)并且调度其他进程(转变为运行状态)。实质上就是中断运行进程与待运行进程的上下文切换。
切换过程大致可分为两步:
- 将被中断进程的各种寄存器信息保存到PCB控制块中
- 使用进程调度算法将待运行进程控制块中的寄存器信息恢复到CPU的寄存器中去
- 跳转到程序计数器所指向位置并恢复进程
具体过程可分为:
- 中断/异常触发,被中断进程保存现场信息
- 将各种寄存器信息保存在进程控制块PCB中
- 调整被中断进程的PCB信息,如进程状态
- 把被中断进程的PCB加入到相关队列
- 从待运行队列中选择下一个占用CPU运行的进程
- 修改被选中进程的PCB信息,如进程状态
- 设置被选中进程的地址空间,恢复存储管理信息
- 恢复被选中进程的现场信息到处理器
- 将线程2中进程控制块的寄存器信息恢复到CPU中去
何时发生进程切换
进程切换发生在中断/异常/系统调用的处理过程中
- 时间片用完,CPU调度下一个任务
- 被其他高优先级任务抢占
- 执行任务遇到IO阻塞
- 用户主动阻塞当前任务(sleep(),wait(), yield(),park())
- 硬件中断
线程切换过程
线程切换需要切换线程的程序计数器和CPU寄存器等数据,具体过程如下:
- 保存被中断线程的程序计数器和CPU寄存器等数据
- 改变被中断线程状态
- 恢复待运行线程的程序计数器数据和CPU寄存器数据
进程和线程切换的区别
线程切换较进程切换代价更低,因为进程切换涉及虚拟地址空间的切换,因为每个进程都有独立的虚拟地址空间,而线程切换则不会,因为线程几乎不拥有系统资源,它可以共享进程的虚拟地址空间,一旦切换虚拟地址空间就意味着要切换页表和快表TLB(translation lookside buffer)缓存,而缓存失效会导致程序变慢
切换进程:包括虚拟地址空间,程序计数器,数据段,堆栈等
线程切换:包括寄存器,程序计数器,堆栈等
为什么进程上下文切换比线程上下文切换代价高?
进程切换分两步:
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文
对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
切换的性能消耗:
1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor's Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。
进程的调度算法
为了确定首先执行哪个就绪进程以及最后执行哪个就绪进程以实现最大CPU利用率,计算机科学家定义了一些算法
-
先到先服务(FCFS)调度算法:从就绪队列中选择一个最先进入队列的进程为之分配资源,使他立即执行到完成或发生某时间而被阻塞放弃占用CPU时再重新调度
特点: 简单,但是平均等待时间波动较大 -
短作业优先(SJF)调用算法:从就绪队列中选出一个估计运行时间最短的进程为之分配资源
这其中又分为抢占和非抢占,抢占的意思是如果正在执行当前进程,如果进来一个运行时间更短的进程,那么最短时间进程将抢占该CPU资源
特点:等待时间最短,但是会导致长任务饥饿,需要知道任务的执行时间(用当前执行时间和过去执行时间加权预估未来执行时间)
-
时间片轮转调度算法:时间片轮转调度是一种最古老,最简单,最常用的进程调度算法,又称RR(Round robin)调度,每个进程被分配一个时间段,称他为时间片,即进程允许运行的时间。
特点:公平,但是上下文切换开销大,时间片设置需要合适,如果太小切换则很频繁,如果太大则基本会等效为先来先服务调度,根据经验一般维持上下文切换开销处于1%以内
-
优先级调度:为每个进程分配优先级,首先执行具有最高优先级的进程,依次类推。可以根据内存要求,时间要求,任何其他资源要求来确定优先级。
-
多级反馈队列调度算法:短作业优先调度算法仅照顾到了短进程而忽略了长进程,多级反馈队列调度即可以使高优先级的作业得到响应又能使短作业迅速完成。
算法原理:设置多个就绪队列(Q1,Q2...Qn),各个队列的优先级是不一样的,每个队列遵循时间片轮转法,各个队列的时间片是随优先级的增加而减少的
算法描述:
- 进程在进入待调度的队列等待时,首先进入优先级最高的Q1等待;
- CPU首先调度优先级最高队列中的进程,若高优先级中队列已没有调度进程,则调度次优先级队列进程,即只有Q1中没有进程等待时才会调度Q2
- 同一队列的各个进程,按时间片轮转法调度,比如Q1的时间片为N,那么Q1中的作业在经历N个时间片后还没有完成则进入Q2等待,若Q2时间片用完还没有完成则进入下一级,直至完成
- 在低优先级队列中的进程在运行时,如果又有新的作业,那么运行完这个时间片后,CPU马上分配给新到达的作业(抢占式)
-
最高响应比优先:关注进程等待了多长时间,根据等待时间和任务时间确定优先级,防止无限期推迟,R = (w+s)/s
w:waiting time为等待时间,s:service time为任务时间
除上面的最大CPU利用率外,还有一些其他的评价指标:
- CPU利用率:CPU处于忙状态所占用时间的百分比
- 吞吐量:在单位时间内完成的进程数量
- 周转时间:一个进程从初始化到结束,包括所有等待时间所花费的时间
- 等待时间:进程在就绪队列中的总时间
- 响应时间:从一个请求被提交到产生第一次响应所花费的总时间
同步和互斥
由于进程存在异步性,其上下文切换可能导致一些不可预知的错误发生,比如变量加后赋值的问题,为了解决这些问题引入了同步和互斥机制
同步和互斥的区别:
- 互斥是临界资源同时只能被一个进程所占用,具有唯一性和排它性,但互斥无法限制访问者对资源的访问顺序,即访问时无序的,可能出现先使用后生产的情况
- 同步是指在互斥的基础上,通过一定的机制实现多进程对临界资源的有序访问,实现多个进程的彼此合作,如先生产后使用
总的来说,互斥是进程对临界资源的独占使用,不用知道其他进程的存在,执行顺序是乱序;而同步时协调多个进程来访问临界资源,进程间需要彼此通信来维持访问顺序
进程互斥
对临界资源(一次只能被一个进程所占用的资源,如打印机)的访问,需要互斥的进行,即同一时间段只能允许一个进程访问该资源
临界区Critical section:指进程中的一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会执行的代码区域
对临界资源的互斥访问可分为四个部分:
- 进入区:检查是否可以进入临界区,若可以进入,则进行“上锁”,否则进程被阻塞
- 临界区:访问临界资源的那段代码
- 退出区:负责“解锁”,清除临界区被占用的标志
- 剩余区:其余代码部分
为了保证系统的整体性能,需要遵循以下原则:
- 空闲让进:当临界区空闲时,可以允许一个请求进入临界区的进程进入临界区
- 忙则等待:如果已有进入进入临界区,其他视图进入临界区的进程必须等待
- 有限等待:对请求访问的进程,应能保证在有限的时间内进入临界区
- 无忙等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待
进程同步
为什么需要进程同步
并发性带来了异步性,即各并发执行的进程以各自独立的、不可预知的速度向前推进,而进程有时候会和其他进程共享一些资源,比如内存、数据库等。当多个进程同时读写同一份共享资源的时候,可能会发生冲突。因此需要进程的同步,多个进程按顺序访问资源。
进程同步的主要任务:是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效地共享资源和相互合作
原子操作、信号量机制、自旋锁管程、会合、分布式系统
互斥同步实现
进程互斥的实现方式
- 硬件中断(仅限单处理器)
- 软件中断(复杂)
- 原子指令操作(更常用)
- 信号量:允许多个进程进入临界区,或者只锁写操作,不锁读操作
软件方法
两个进程:Peterson算法
多个进程:Bakery算法
算法演进过程:单标志法,双标志先检查法,双标志后检查法,Peterson算法
-
单标志位: 通过一个标志位来设定两个进程访问权限,即每个进程进入临界区的权限只能被另一个进程赋予

算法缺陷:违背“空闲让进”的原则,比如P1进入临界区并完成了工作,然后将值设为1,但是P0不去执行临界区的代码,而去执行其他事,那么这时候虽然临界资源是空闲的,但是P1想访问却不能访问
-
双标志先检查后上锁法:设置一个布尔型数组flag[],数组中的各元素用来标记各进程想进入临界区的意愿,每个进程进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自己的标志位flag设为true,之后开始访问临界区,访问完成后将自己的标志位恢复为false,相当于在访问前上锁,在访问后解锁

算法缺陷:违背了“忙则等待”的原则,如果按照1,5,2,6...的顺序执行,那么P0和P1都将进入临界区,其主要原因在于检查和上锁不是原子性操作。
-
双标志先上锁后检查法:该算法相比上一个是先上锁后检查

算法缺陷:如果按照1,5,2,6..执行,那么P0和P1都无法进入临界区,出现死锁,虽然解决了忙则等待的问题,但是又出现了空闲让进和有限等待问题。
-
Peterson法:单标志位和双标志位都有一定的局限,那么就融合两种算法的优点。 单标志法存在的问题是,当前进程没有使用临界资源的意愿,但是却得到了使用临界资源的权利,从而导致另一个想要使用临界资源的进程没办法访问正在空闲着的临界资源。而双标志后检查法中当两个进程都表达了想要使用临界资源的意愿后,由于双方都不想放弃使用的意愿,从而都没办法使用临界资源。

缺点:虽然同时遵循了空闲让进,忙则等待,有限等待的原则,但一个进程访问临界资源时,另一个进程不会释放处理机,会一直循环等待,违背了"无忙等待"的原则
-
Bakery算法:
N个进程的临界区,在进入临界区之前,进程会接收一个数字,得到数字最小的进入临界区,如果进程Pi和Pj收到相同的数字,那么如果i<j,Pi先进入临界区,否则Pj进入临界区;编号方案总是按照枚举的增加顺序生成数字
硬件方法
-
关闭中断,CPU进程进程切换是需要中断来进行,如果执行临界区前屏蔽了终端就可以保证当前进程能顺利的将临界区的代码执行完,从而实现互斥;实现步骤就是:屏蔽中断-执行临界区-开中断;
缺点:大大限制CPU的交替处理能力,无法再响应外部事件,可能导致其他线程处于饥饿状态;同时在多CPU情况下屏蔽一个CPU中断无法解决问题
原子操作指令
- test-and-set:类似于java的CAS(compare and sweep),如果内存值为预期值,则将该值设置为需要设定的值
- echange:交换内存值
如何避免忙等呢
一般的算法都会让不能进入临界区的进程进行循环等待,如果进入临界区的进程执行时间很长的话,那么会浪费CPU的资源,所以我们可以将这个进程加入阻塞队列,让其阻塞,当临界区有资源时,再来唤醒该队列,进行资源抢夺,当然,如果进入临界区的时间较短的话,还是使用自旋等待比较好,因为进程的上下文切换代价较大

信号量机制
信号量(semaphore):就是一个变量,用这个变量来表示系统中某种资源的数量
对信号量的操作只有三种:初始化,P操作,V操作
- P操作(荷兰语增加):相当于wait,sem减1,如果sem<0,等待,否则继续
- V操作(荷兰语较少):相当于signal,sme加1,如果sem<=0,唤醒一个等待的进程
P和V操作必须承兑使用,遗漏P将不能保证互斥,遗漏V则不能使用临界资源后将其释放
信号量实现进程互斥: 互斥信号量mutex初始值为1,在临界区之前执行P(mutex),在临界区之后执行V(mutex)。
信号量实现进程同步:同步信号量S初始值为0,在 线程A中执行P操作,sem<0,那么就会阻塞,等待线程B执行V操作之后sem=0,线程A可以继续执行
信号量机制分为整型信号量机制、记录型信号量机制。
整型信号量机制
int S=1; // 整型信号量S,表示资源数量
void wait(int S){ //wait 原语,相当于“进入区”
while(S<=0); //如果资源数不够,就一直循环等待
S=S-1; //如果资源数够,就占用一个资源
}
void signal (int S){ //signal 原语,相当于“退出区”
S=S+1; //使用完资源后,释放资源
}
记录型信号量机制
在整型信号量机制上进行改进,让不能进入临界区的进程从运行态装换为阻塞状态,进程进入阻塞队列中等待。
typedef struct{
int value; //剩余资源数
Struct process *L; //等待队列
} semaphore;
/*某进程需要使用资源时,通过wait原语申请*/
void wait(semaphore S){
S.value--;
if(S.value<0){
block (S.L); // 阻塞队列
}
}
// 进程使用完资源后,通过signal原语释放资源
void signal(semaphore S){
S.value++;
if(S.value<=0){
wakeup(S.L); //唤醒队列
}
}
S.value的初始值表示系统中某种资源的数目。 对信号量S的一次P操作意味着进程请求一个单位的该类资源,因此需要执行S.value--,表示资源数减1,当S.value<0时表示该类资源已经被分配完毕,因此使申请资源的进程调用block原语进行自我阻塞(运行态->阻塞态),主动放弃处理机,并插入该类资源等代队列中,该实现遵循了让权等待原则,不会出现忙等现象。 V操作中,由于是释放资源所以先S.value++,如果加一后,S.value仍然小于等于0,表示依然有进程在阻塞队列中等待该类资源, 那么就使用原语wackup(S.L)去唤醒一个进程。
管程
管程是更高层次的互斥同步实现抽象,由于信号量中需要进程自备同步操作,同时PV操作分散在各个进程中,不易管理,容易发生死锁,而管程封装了同步操作,用户能够更方便的实现同步,比如java在1.5之后引入的wait()和notify(),notifyAll()就是管程的组成部分。
- Lock(锁):
- Lock.acquire():获取锁
- Lock.release():释放锁
- Condition Variable(条件变量):
- Wait():释放锁,睡眠,重新获取锁后返回
- Signal():唤醒等待线程,如果有
管程实现生产者消费者模式:

什么是死锁
在两个或多个并发进程中,如果每个进程拥有某个资源,而这个资源又是对方所需要的,双方都等待对方释放自己需要的资源,而都不释放,在未改变这种状态之前不能继续推进,这一组进程就产生了死锁,也就是两个或多个进程无限期的阻塞,相互等待的一种状态
产生的主要原因是:系统资源不足,进程推进顺序非法
死锁产生的四个必要条件(有 一个不成立,则不会产生死锁)
- 互斥条件: 任意时刻一个资源只能给一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不可剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系
死锁的基本处理策略和常用方法
一般来说,处理死锁问题有三种方法:
- 通过协议来预防或避免死锁,确保系统不会进入死锁状态
- 可以允许系统进入死锁状态,然后检测他,并加以恢复
- 可以忽视这个问题,认为死锁不可能在系统内发生(此方法大多数操作系统最常用)
预防和避免死锁
死锁预防:
只要破坏死锁出现的某一个必要条件,死锁就不会成立了,由于资源互斥是资源使用的固有特性,所以是无法改变的,但是可以去改变其他三个条件。
- 破坏
请求和保持条件:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源(破坏运行请求,但是长时间占用资源会造成系统资源利用率低);第二种方法动态分配是每个线程申请所需要的资源时本身不占有资源(破坏保持)。 - 破坏
不可剥夺条件:当一个进程不能获得所需要的所有资源时便处于等待状态,等待期间会释放占有的资源,只有等待获取旧的资源以及它请求新的资源才开始执行。 - 破坏
循环等待条件:采用资源有序分配,系统给每类资源分配一个编号,每个进程按编号递增的顺序请求资源,只有先获得较小编号的资源才能申请较大编号的资源,释放则相反(破坏环路等待条件)
死锁避免:
为何使用:由于所施加的限制条件往往太严格,因而可能导致系统资源利用率和系统吞吐量降低,因此在避免死锁时需要施加较弱的限制,从而获得较满意的系统性能。
基本思想:允许进程动态地申请资源。系统在进行资源分配之前进行资源申请动态检查,看是否会出现不安全状态,如果分配后系统可能发生不安全状态,则不予分配,进程进入等待,否则予以分配。最具代表性的避免死锁的算法是银行家算法
如果操作系统能保证所有进程在有限时间内得到所需要的全部资源,则系统处于
安全状态,否则是不安全的系统处于安全状态是指针对所有进程,存在安全队列,即进程序列P1,P2....Pn按顺序执行会正常后执行下去
算法前提:需要系统具有一些额外的先验信息提供,如每个进程需要的每类资源最大数目,对于一般操作系统来说,该信息很难获得
银行家算法
目的:找到一条安全进程资源分配序列,避免死锁
背景:在银行中,客户申请贷款的金额是有限的,每个客户在第一次申请贷款时需要声明完成该项目所需要的最大资金量,在满足所有贷款需求时,客户应及时归还。银行家在客户申请的贷款数量不超过自己拥有的最大值时,都应该满足客户的需要。在这样的描述中,银行家就好比是操作系统,资金就是资源,客户就相当于是申请资源的进程。
组成部分:
- 操作系统:资源数量,即资源池(系统资源总量=资源池剩余+已分配给所有进程的资源数量)
- 进程:资源最大需求量;已分配资源数量;还需资源数量
算法思想:操作系统按照银行家指定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统当期的资源可以满足它的最大需求量则按当前的申请量分配资源,否则推迟分配。在进程执行过程中继续申请资源时,先测试进程已占有资源与本次申请资源之和是否超过该进程对资源的最大需求量,如果超过就拒绝分配资源,如果没有超过就再测试系统现有资源是否能满足该进程所需的最大资源量,如果能满足则按当前申请量分配资源,否则推迟分配。最终操作系统会判断出一条安全序列以使系统达到安全状态,否则系统不安全。
比如进程P1申请资源,银行家算法先去试探的分配给他,若申请的资源小于等于可用资源,就分配给P1分配剩余资源,若进程可执行完毕,则假设回收已分配给他的资源,把这个进程标记为完成,并继续判断队列中的其他进程,若所有进程都可执行完毕,则系统处于安全状态,并根据可完成进程的分配顺序生成安全系列(如P0,P3,P1..),

利用安全性算法对上面状态进行分析,可以找到一条安全序列(P0,P3,P4,P1,P2),故系统是安全的
数据结构:
-
Max(总需求量):n*m矩阵,如果Max[i,j] = k,表示进程Pi最多请求资源类型Rj的k个实例
-
Avaliable(剩余资源量):长度为m的向量,如果Available[j] = k,表示有k个类型Rj的资源实例可用
-
Allocation(已分配量):n*m矩阵,如果Allocation[i,j] = k,则Pi当前分配了k个Rj的实例
-
Need(未来需求量):n*m矩阵,如果Need[i,j]=k,则Pi可能需要至少k个Rj的实例完成任务
-
操作系统:资源数量,即资源池(系统资源总量=资源池剩余+已分配给所有进程的资源数量)
-
进程:资源最大需求量;已分配资源数量;还需资源数量
死锁预防和死锁避免的区别
死锁预防是设法破坏死锁产生的四个必要条件之一,严格的防止死锁出现;而死锁避免则采用不那么严格的限制,因为即使死锁的必要条件存在,也不一定发生死锁。所以死锁避免是系统在运行过程中注意避免死锁的最终发生。
检测和解除死锁
这种方法在事先不采取任何限制性措施,而是允许系统在运行过程中发生死锁。系统设置死锁检测机制去及时检测出死锁,并精确的确定与死锁相关的进程和资源,然后采取适当的措施去将死锁清除掉。
检测死锁
-
每种类型资源只有一个实例:资源分配图中有环路, 从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
-
每种资源类型有多个实例:类似银行家算法的标记算法
假设公有m种资源,使用向量E=(E1,E2...Em)表示每种资源的数量,例如E1=4表示第一种资源的总数为4,使用向量A=(A1,A2...,Am)表示当前每类资源的可用数量,例如A1=2表示当前第一种资源剩余两个可用;使用矩阵Cn×m表示当前进程已分配资源,其中
C[1][2]表示进程1已经占有第二种资源的个数;使用矩阵Rn*m表示当前进程的请求资源数,如R[1][2]表示进程1需要第二种资源的数量基于上述数据结构,可以设计一种类似于银行家算法的进程标记算法来判断当前是否发生了死锁,如果发生了死锁涉及哪些进程,在初始时所有进程均未被标记,使用如下算法对进程进行标记
- 如果当前进程未被标记,且Ri<=A,则当前进程请求的资源可以被分配
- 分配资源,Cij=Cij+Rij,Aj=Aj-Rij
- 进程执行完毕后,将释放其占用资源,那么A=A+Ci
- 标记该进程,转回步骤1寻找下一个进程
如果有进程没有被标记,则系统存在死锁,其中未标记进程为死锁涉及进程
举个例子:

当前进程共有3个进程,此时进程1和进程2都无法被标记,因为R1>=A,R2>=A,但进程3所需资源可以被满足,因此进程3被标记,在进程3执行完成后,将释放他的资源,此时A=(2,2,2,0),进程1和进程2均能被满足,因此系统不存在死锁
何时检测死锁:
- 一种方法是每当有资源请求时去检测,但这种方法会占用昂贵的CPU时间
- 另一种是每隔k分钟就检测一次, 或者当CPU的使用率降到某一阈值时再检测
解除死锁
当检测到系统存在死锁时,就要考虑如何去解除死锁,这里介绍3中方法:
- 利用抢占恢复:挂起某些死锁进程,并抢占他的资源分配给其他死锁进程,但因防止被挂起的进程长时间得不到资源而处于匮乏状态
- 利用回滚恢复:让一个或多个进程回退到足以避免死锁的地步,进程回退是资源释放资源而不是被剥夺;这要求系统保持进程的历史信息,设置还原点
- 杀死进程恢复:直接杀死部分或全部进程并剥夺这些进程的资源,撤销的原则可以按进程的优先级和撤销代价的高低进行
忽略死锁
当没有算法用于检测和恢复死锁时,可能出现这样的情况,系统处于死锁,而又没有方法检测到底发生了什么。在这种情况下,未被发现的死锁会导致系统性能下降,因为资源被不能运行的进程占有,而越来越多的进程会因申请资源而进入死锁。最后,整个系统会停止工作,且需要人工重新启动。
虽然这看起来似乎不是一个解决死锁问题的可行方法,但是它却为大多数操作系统所采用,许多系统死锁很少发生,因此与使用频繁的并且开销昂贵的死锁预防、死锁避免和死锁检测与恢复相比,这种方法更为便宜。
进程通信的方式有哪些(7种)
为什么需要进程通信:
- 进程间交换数据
- 进程间彼此协作完成某个任务,即进程同步
每个进程都有自己独立的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程间交换数据必要要通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区中把数据读走,内核提供的这种机制叫进程间通信(IPC,InterPreocess Communication)

-
管道:
-
一种半双工的通信方式,数据只能单向流动,并且只能在具有亲缘关系的进程间流动(父子进程,兄弟进程),
-
管道是一种独立的文件系统,他不是普通的文件,不属于文件系统,而是单独构成一种文件系统,只存在于内存中。
-
进程中数据的读出和写入,是一个进程向管道中写入内容,另一个管道在另一端读出,写入的内容每次都写在管道缓冲区的末尾,并每次从缓冲区头部读取数据,模型类似生活中的管道
-
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,该缓冲区可以看做一个循环队列
-
管道的局限在于只支持单向流动,必须在亲缘关系的进程间通信,没有名字,管道的缓冲区是有限的

应用举例:比如管道命令|,将一个输出传递给下一个输入

-
-
命名管道:由于管道没有名字,只能在具有亲缘关系的进程间通信,为了克服这个缺点提出了命名管道,其与匿名管道不同之处在于它提供了一个路径名与之关联, 这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信 也是半双工通信方式,但他允许无亲缘关系的进程间通信
-
信号:信号是Linux系统中用于进程间互相通信或操作的一种机制,相当于软件中断,信号可以在任何时候发给某一个进程,而无需知道这个进程的状态,如果该进程并未处于执行状态,则将此信号保存在内核中,直到该进程恢复执行并传递给他为止,如程序终止信号Ctrl+C可以产生该信号
信号的生命周期及处理流程:
- 信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
- 操作系统将信号传递给对应进程
- 目的进程收到信号后,暂时终止当前代码,保护上下文(临时寄存器数据,当前程序位置以及CPU状态),进入中断执行信号处理函数

-
消息队列:消息队列是存放在内核中的消息链表,与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或文件系统)不同的是
- 消息队列存放于内核中,只有内核重启或显示删除一个消息队列时才会真正删除
- 消息队列在某个进程往一个队列写入消息之前,不需要另一个进程在该队列上等待消息的到达,此外,消息队列允许一个或多个进程向他写入或读取数据
- 消息队列可以实现消息的随机查询,消息也不一定以先进先出的次序读取,也可以按消息的类型读取
-
共享内存:使得多个进程可以直接读取同一个内存空间,是最快的IPC方式,是针对其他通信机制效率低而设计的,为了在多个进程间交换信息,内核专门留出一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间,进程就可以读取这一块内存而不需要数据拷贝,大大提升效率, 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。

-
信号量: 信号量是一个计数器,可用来控制多个进程对共享资源的访问。 它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
socket套接字:可用于不同设备及其间的进程通信
线程之间的通信方式
线程间由于共享进程的内存空间,因此线程中没有像进程通信中那样的数据交换通信机制,线程间的通信目的主要是用于线程同步
- 共享变量
- 互斥量(Mutex) : ( Synchronized/Lock )采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。 因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
- 信号量: 它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
- 事件 :(Wait/Notify)通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较
内存管理
操作系统的内存管理主要是做什么
操作系统的内存管理主要负责内存的分配与回收(malloc函数:申请内存,free函数:释放内存),另外地址转换即逻辑地址转换成相应的物理地址等功能也是内存管理做的事情
内存的层次结构

常见的内存管理机制
主要分为连续分配管理和非连续分配管理方式,连续分配管理方式指为一个用户程序分配一个连续的内存空间,常见的如块式管理,同样的,非连续非配管理方式允许一个程序使用的内存分布在离散或不相邻的内存中,常见的页式管理和段式管理。
- 块式管理:很早时计算机操作系统的内存管理方式,将内存分为几个固定大小的块,每个块包含一个进程,当一个运行的程序需要内存的话,操作系统就分配一块,如果程序运行时只需要很小的空间的话,分配的空间中一部分就会被浪费了,这个块中未被利用的空间我们称之为碎片。
- 页式管理:把内存分为大小相等且固定的一页一页的形式,页较小,相对于块式管理划分力度更大,提高了内存利用率,减少了碎片,页式管理通过页表对应逻辑地址和物理地址。
- 段式管理:页式管理虽然提高了内存利用率,但页式管理其中的页实际无任何实际意义。段式管理把主存分为一段段的,每一段的空间比页空间小很多,但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息, 例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
- 段页式管理:结合段式管理和页式管理的有点,现将主存分为若干段,每个段分为若干页,也就是说段页式管理机制中段与段的之间及段的内部都是离散的。
连续内存分配:块式管理
块式管理的内存分配策略有:
-
首次适配:比如现在需要给程序分配n字节空间,从低地址开始找,遇到第一个空间比n大的空闲块就使用它
条件:需要存在一个按地址排序的空闲块列表
优点:简单,易于产生更大的空闲块
缺点:由于空闲快的空间不确定性,外碎片和内碎片问题严重
-
最佳适配:分配n字节的空间,使用最小的比n大的可用空闲块
条件:需要按尺寸排列的空闲块列表
优点:当大部分分配是小尺寸是非常有效
缺点:外部碎片严重
-
最差适配:分配n字节空间,使用最大的可用空闲块,目的是避免微小碎片
条件:按尺寸排列的空闲块列表
优点:避免了微小碎片,如果分配的是中等尺寸效果最好
缺点:剩下的内存块难以满足后来更大的内存需求
连续内存分配的碎片问题:
- 外部碎片:在分配单元间的未使用内存
- 内部碎片:在分配单元中未使用的内存
块式管理的几种分配方式都有可能产生碎片,有什么方法能够减少碎片呢
-
压缩式碎片整理:通过调整内存中程序的位置来减少碎片

比如上述内存中存在3个程序,各个程序之间存在内存碎片,那么将P2和P3程序移动一下内存位置,就可以消除他们之间的内存碎片,但是这种方式的开销较大
-
交换式碎片管理:使用虚拟内存技术,将目前没有运行的程序先放到磁盘中,需要运行的程序占用当前内存空间
连续内存分配的缺点:分配给一个程序的物理内存是连续的,内存利用率低,碎片化问题严重。
非连续内存分配:分段
非连续内存分配提高了内存利用率,减少了内存碎片
段式管理把主存分为一段段的,每个段定义了一组逻辑信息, 例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。

非连续内存分配:分页
与分段的段大小可变不同,分页是把内存分为大小相等且固定的一页一页的形式,页较小,相对于块式管理划分力度更大,提高了内存利用率,减少了碎片,页式管理通过页表对应逻辑地址和物理地址
- 划分物理内存至固定大小的帧,大小是2的幂,512,4096..
- 划分逻辑地址空间至相同大小的页,页内偏移的大小等于帧内偏移的大小
- 建立逻辑地址映射为物理地址的方案:页表,TLB

地址转换实例
每个运行的程序都有一个页表,CPU查到页表的起始位置,通过逻辑地址的页号找到对应的物理内存页号,通过物理页号加上偏移量就可以得到实际的物理地址
例子:已知每页为1024byte(即逻辑页表和实际内存分页大小都是1024byte)查页表可知逻辑页3号对应的物理内存页为4号,那么物理内存izhi = 4*1024 + 1023

分页机制的性能问题:
分页机制的实现主要存在时间和空间上两个问题
- 时间上:访问一个内存单元需要两次内存访问,一次用于获取页表项,一次用于访问数据
- 空间上:页表可能非常大,如果一个计算机是64位的,每一页的大小为1024KB,那么64位的地址空间需要
2^64/2^10个页面,那么存储这个页表项的页表就会很大,同时每个程序对应一个页表(因为每个进程都有自己的逻辑地址空间),那么n个程序就对应n个页表,那么就不可能将所有的页表都保存在内存中
快表和多级页表
快表和分级页表时页表管理机制中两个很重要的概念,这个两个东西分别解决了页表管理中很重要的两个问题,
- 虚拟地址到物理地址的转换要快
- 解决虚拟地址空间大,页表也会很大的问题
快表(TLB)
为了解决虚拟地址到物理地址的转换速度,操作系统在页表方案的基础上引入了快表来加速虚拟地址到物理地址的转换,可以把快表理解为一种特殊的高速缓冲存储器(Cache),它位于CPU内部,其中内容是页表的一部分或全部部分,作为页表的Cache,他的作用与页表类似,但提高了访问速率。由于采用页表作为地址转换,读写内存数据时CPU要访问两次主存,有了快表,有时只访问一次高速缓冲存储器,一次主存,这个样子可以加速查找。

使用快表的执行流程:
- 根据虚拟地址中的页号查快表
- 如果该页在快表中,直接从快表中获取相应的物理地址
- 如果该页不在快表中,那么就访问内存中的页表,再从页表中获取到物理地址,同时将页表中的该映射表添加到快表中
- 当快表填满时,采用一定的淘汰机制淘汰掉表中的一个页
多级页表
引入多级页表时为了避免把全部页表放在内存中占用过多的空间,特别是那些经常不需要的页表, 多级页表属于时间换空间的典型场景
多级页表的原理
同样都是将页表存储在内存中,为什么多级页表就比一级页表要省空间呢?先看一下二级页表
二级页表的本质是通过一个顶级页表为真正有用的页表提供索引,也就是一个页目录表

比如在一级页表中不存在某个页表项,那么对应的二级页表就不用放在内存中,而单级页表中就算有不存在的映射关系,那么对应的空间还是需要保留
如何节约内存:
- 二级页表可以不存在:如果某个一级页表项没有被用到,就不需要创建这个页表对应的二级页表了,等需要的时候再创建
- 二级页表可以不再主存:根据局部性原理,也可以将部分二级页表放在磁盘,在需要时再调用
总结:为了提高内存的空间性能,提出了多级页表的概念,但是提高空间性能是以浪费时间换来的,因此为了补偿时间上的损失,提出了快表的概念,无论是快表还是多级页表,都利用到了程序的局部性原理。
分页机制和分段机制的共同点和区别
共同点:
- 分页机制和分段机制都是为了提高内存利用率,减少内存碎片
- 页和段都是离散存储的,所以两者都是离散分配内存的方式,但是,每个页和段中都是连续的
区别:
- 页的大小是固定的,由操作系统决定,而段的大小不固定,由当前运行的程序决定。
- 分页仅仅是为了满足操作系统内存管理的需求,而段式逻辑信息的单位,程序中可以体现为代码段,数据段,能更好的满足用户的需要。
- 段向用户提供二维地址空间;页向用户提供的是一维地址空间
- 段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制。
逻辑(虚拟)地址和物理地址
这里分为两种地址空间,一种是物理地址空间,一种是逻辑地址空间
- 物理地址空间是硬件支持的地址空间,其实地址为0,到系统最大地址
- 逻辑地址空间是一个运行程序所拥有的内存范围
- 逻辑地址(Logical Address) 是指由程序产生的与段相关的偏移地址部分,即
程序中用到的地址 - 物理地址:数据在内存中的真实位置
与程序接触的一般就是逻辑地址,在分段和分页内存管理机制中,要想访问物理地址,必须通过逻辑地址去访问,就像C语言程序中的指针,指针存储的数值就是逻辑地址,通过这个地址我们能够找到真正的物理地址
所谓的逻辑地址,是指计算机用户(例如程序开发者)看到的地址。例如,当创建一个长度为100的整型数组时,操作系统返回一个逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为4个字节,故第二个元素的地址时起始地址加4,以此类推。事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址(在内存条中所处的位置),并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。
为什么不直接给物理地址而需要逻辑地址转换
如果没有虚拟地址,程序都是直接访问和操作物理内存,如果是这样的话会造成一些问题
- 用户可以访问任意内存,寻址内存的每个字节,这样很容易破坏操作系统,造成操作系统崩溃。(安全问题:系统崩溃)
- 想要同时运行多个程序很困难,比如先运行微信时,给的内存地址是1xxxx,而QQ音乐打开时同样赋值内存空间1xxx ,那么QQ音乐对内存空间的赋值就会覆盖微信之前的赋值,造成微信崩溃。(安全问题:多进程)
- 如何分配内存,分配多的话造成浪费并且没几个进程内存就耗尽了,分配少的话不够用(大小分配问题)
- 如果动态分配内存,则进程拿到的内存空间是不连续的,系统要解决碎片问题。(内存碎片化问题)
总结来说,如果直接暴露物理地址会导致很多问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。
通过虚拟地址访问的优势:
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的内存缓冲区(使用离散物理地址)
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常为4KB)保存到磁盘文件,数据或代码页会根据需要在物理内存与磁盘之间移动。(使用物理内存可大可小)
- 不同进程使用的虚拟地址彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。(虚拟内存物理内存彼此隔离)
虚拟内存(Virtual Mmory)
目的:解决内存不够用的问题,早期使用的是覆盖技术和交换技术
- 覆盖技术:程序员将一个程序划分为多个模块,并确定各个模块的覆盖关系,然后根据调用关系将需要的模块导入内存,增加了程序员的负担
- 交换技术:类似于虚拟内存技术,但是他的磁盘和内存交换的是整个程序的地址空间,增加了处理器的开销
其区别在于覆盖技术是一个程序内模块的换入换出,交换技术是磁盘和内存中一个程序的换入换出 ,但目的都是让内存能够跑大程序
虚拟内存技术结合了覆盖技术和交换技术的优点,让磁盘和内存交换的只是程序的一部分(比如一个页)即可实现程序的运行。(能实现程序的部分运行的基础是程序的局部性原理)
虚拟内存允许执行进程不必完全在内存中,可以把最需要的数据(部分页/段)放到内存中,而暂时不需要的部分放到硬盘中,然后通过交换的方式将硬盘中的数据放到内存中,将内存中用过的数据再放到硬盘中,从而实现一种虚拟的连续大空间内存
虚拟内存是计算机中系统内存管理的一种技术,它使得应用程序认为它拥有连续可用的内存(一段连续完整的地址空间),而实际上,他通常被分割成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上(将内存扩展到硬盘空间),在需要时进行数据交换。目前大多数操作系统都使用了虚拟内存,如Windows的虚拟内存,Linux的交换空间。
虚拟的意思是,其实这个地址空间是不存在的,仅仅是每个进程“认为”自己拥有一块连续可用的内存,但在物理内存中进程数据的存储是离散的(减少内存碎片,提高内存利用率),他用多少空间,操作系统就分配多少空间,等进程真正运行时,某些数据不在物理内存中,再出发缺页异常,由操作系统将需要的部分调入内存,进行数据交换
局部性原理
局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行
也就是说在某个较短的时间内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域
局部性原理表现在以下两个方面
- 时间局部性:一条指令的一次执行和下次执行,一个数据的一次访问和下次访问都集中在一个较短时期内,产生时间局部性的典型原因是由于在程序中存在大量循环操作
- 空间局部性:当前指令和邻近的几条指令,当前访问的数据和邻近的几个数据都集中在一个较小区域内,即程序在一段时间内所访问的地址可能集中在一定范围内,这是因为指令通常是顺序存放,顺序执行的。
虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
虚拟存储器
基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行,由于外存往往比内存大很多,所以我们运行的原件的内存大小实际可以比计算机系统实际的内存大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序,另一方面,操作系统可以将内存中暂时不同的内容换到外存上,从而腾出空间放入调入内存的信息,这样计算机好像为用户提供了一个比实际内存大的多的存储器--虚拟存储器。这也是一个时间换空间的策略。使用CPU的计算时间,页的调入调出时间,换来更大的空间。
虚拟内存的技术实现
虚拟内存的实现需要建立在离散分配的内存管理基础之上,虚拟内存的实现方式有以下三种方式
- 请求分页存储管理:建立在分页管理基础上,为了支持虚拟存储器功能而增加了请求调页功能和管理置换功能,请求分页是目前最常用的一种虚拟存储器实现方法,请求分页存储管理系统中,在作业运行之前,仅装入当前要执行的部分即可运行。加入在作业运行的过程中发现要访问的页面不在内存,则会发生缺页中断,如果内存中有空闲的物理页面,处理器将通知操作系统将需要访问的页装入到物理内存中,如果内存空间已满,则按照对应的页面置换算法将相应的页面调入到主存,同时操作系统将暂时不用的页面置换到外存中,然后继续执行被中断的命令
- 请求分段存储管理:建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段存储管理方式就如同请求分页存储管理方式一样,在作业运行之前,仅装入当前要执行的部分即可运行,在执行过程中,可使用请求调入中断动态装入要访问但又不在内存中的程序段,当内存空间已满时,根据置换功能调出某个段,以腾出空间装入新的段。
- 请求段页式存储管理
以上的几种实现方式,我们一般需要:
-
一定容量的内存和外存,在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,程序就可以运行了
-
缺页中断:如果需要执行的指令或访问的数据尚未在内存(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段调入到内存,然后继续执行程序
-
虚拟地址空间:逻辑地址到物理地址的转换
-
额外页表表项:页表表项本来只需要存储逻辑页号和物理页帧号的对应即可,但是为了能够请求分页和置换需要添加一些额外的表项

- 驻留位:表示该页在内存还是在外存。如果该位等于1,表示该页位于内存中,即该页表项是有效的,如果该位为0表示该页在外存中,如果访问该页将发生缺页中断
- 修改位:表示该页在内存中是否被修改过,当系统回收该物理页时根据此位来决定是否把他的内容写回外存
- 访问位:如果该页被访问过(包括读和写操作),就设置此位为1,这个可以用于最少使用页面置换算法
请求分页存储管理和分页存储管理的区别:请求分页存储管理是建立在分页存储管理基础之上的,他们的根本区别在于是否将一作业的全部地址空间同时装入主存,请求分页存储管理不需要将作业的全部地址空间同时装入主存(请求调页功能和页面置换功能),基于这一点请求分页存储管理可以提供虚拟内存,而分页存储管理却不能提供
页面置换算法
虚拟内存最重要的一个概念就是页面置换算法
什么叫页面置换算法?
功能:地址映射过程中若在页面中发现需要访问的页面不在内存中,则发生缺页中断,当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移除内存,以便为即将调入的页面让出空间,用来淘汰页面的算法叫页面置换算法
目的:为了尽可能的减少页面的置换次数(缺页中断的次数),提升系统性能
常见的页面置换算法
-
OPT(Optimal)页面置换算法(最佳页面置换算法):最佳(Optimal,OPT)置换算法所选择的被淘汰页面是以后永不使用的,或者是最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若干页面中哪个是最长时间不使用的,因而该算法无法实现,
一般用来衡量其他置换算法的方法 -
FIFO (First In First Out) 页面置换算法(先进先出页面置换算法):总是淘汰最先进入内存的页面,即选择在内存中
驻留时间最久的页面进行淘汰具体来说,系统维护一个链表,记录了所有位于内存中的逻辑页面,从链表的排列顺序来看,链首页面的驻留时间最长,则发生缺页中断时,将链首的页面置换出去,并将新的页面添加到链表尾部
缺点:这种算法性能较差,调出的页面可能是经常访问的页面,并可能有Belady现象
-
LRU( Least Currently Used )页面置换算法(最近最久未使用页面置换算法):LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间T,当选择一个页面淘汰时,将选择现有页面中T值最大的,即最近最久未使用的页面予以淘汰。
其他具体实现方法:维护一个链表:刚刚使用过的页面作为首结点,最久未使用的结点作为尾节点,每次访问内存时,找到相应的页面,将他移动到链表的首部,每次缺页中断时,淘汰链表末尾的页面。
-
时钟页面置换算法: 与LRU类似,也是替换最久未使用的页面,实现方法就是在页表项中设置一个访问位,当一个页面被装入内存时,该位的初始化为0,如果该页被访问,则将该位置1,把各个页面组织成一个环形链表,把指针指向最先进来的页面,当发生缺页中断时,判断指针指向的最老页面,如果访问位为0则立即淘汰,否则将该位置为0,然后指针指向下一格,直到找到被淘汰的页面,然后指针指向下一个位置
-
LFU( Least Frequently Used )页面置换算法(最少使用页面置换算法):该置换算法选择在之前时期使用最少的页面作为淘汰页。
实现方式:添加页面访问计数器,每当一个页面被访问时,该页面的访问计数器就加1,在发生缺页中断时,淘汰计数器最小的那个页面
FIFO算法出现的Belady现象
在采用FIFO算法时,有时会出现分配的物理页越多,缺页率反而上升的异常现象
Belady现象的原因:这是FIFO算法的置换特征导致的,他的置换特征与置换进程访问内存的动态特征是矛盾的,因为被他置换出去的页面不一定是进程不会访问的
什么是缓冲区溢出
缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在了合法数据上
危害:程序崩溃或跳转并执行一段恶意代码
文件管理
由于系统的内存有限并且不能长期保存,故平时总是把它们以文件的形式存放在外存中,需要时再将它们调入内存。 如何高效的对文件进行管理是操作系统实现的目标。
文件与文件系统
文件系统
一种用于持久性存储的系统抽象
文件系统的管理功能是通过把他所管理的程序和数据组织成一系列文件来实现的。
文件系统管理的对象有:文件(作为文件管理的直接对象);目录(为了方便用户对文件的存取和检索);磁盘;存储空间(文件和目录必定占有一定的存储空间)
虚拟文件系统
由于有各种各样的文件系统,如果让应用程序针对不同的文件系统写文件处理程序则非常复杂,因此需要操作系统来统一抽象成一个虚拟文件系统,来屏蔽底层不同文件系统的差异性,给用户提供相同文件和文件系统接口

文件
文件系统中一个单元的相关数据在操作系统中的抽象
文件是具有文件名的若干相关元素的集合,元素通常是记录,而记录是一组有意义的数据项的集合
- 数据项:最低级的数据组织格式,
- 记录:是一组相关数据项的集合
- 文件:是具有文件名额一组相关元素的集合,分为有结构文件和无结构文件。有结构文件由若干个相关记录组成,无结构文件则被看成一个字符流。
文件操作:
- 创建文件:创建一个新文件时,首先需要为新文件分配必要的外存空间,并在文件系统的目录中建立一个索引项,目录中索引项包含新文件的文件名及其外存地址等属性
- 删除文件:删除文件时,系统将先从某种找到要删除文件的目录项,然后回收所占用的存储空间
- 读文件:在系统中查找目录,找到指定目录项,从中得到被读文件在外存中的位置
- 写文件:系统查找目录,找到指定目录项,再利用目录中的指针进行写操作
- 截断文件:如果一个文件的内容需要全部更新,一种方法是将此文件删除,再创建一个新的文件,但如果文件名和属性均无改变,则可采用截断文件的方法,将其原有长度设置为0,放弃所有内容
文件的打开与关闭
当需要打开某个文件时,首先需要去文件目录检索该文件的属性及在外存上的位置;然后根据查到的信息对文件实施读/写操作,当用户对一文件实施多次读/写或其他操作时,每次都要去检索目录,为了避免多次重复检索,在大多数OS中都引入了打开这一文件系统调用,当用户第一次请求对某文件系统进行操作时,先利用open系统调用将该文件打开
open系统调用是指将指定文件的属性(包括文件在外存上的位置)从外存拷贝到内存打开文件表的一个表目中,并将该表目编号返回给用户,以后用户再对该文件操作时,便可利用系统所返回的索引号对系统提出操作请求,系统便可直接通过索引去打开文件。如果不需要对该文件实施操作,可关闭系统调用来关闭该文件
参考: https://www.zhihu.com/question/25532384/answer/81152571
https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/operating-system/basis.md
https://www.jianshu.com/p/c1015f5ffa74
https://blog.csdn.net/yangquanhui1991/article/details/47446151

浙公网安备 33010602011771号