操作系统学习笔记

操作系统的定义

  操作系统是一个大型的程序系统 ,它是计算机硬件上的第一层软件。他用于软硬件资源的分配管理,控制协调并发活动。抽象硬件,为用户提供友好的接口,为开发人员提供良好的工作环境。

 

操作系统结构演变

  1. 初代简易操作系统:最初操作系统严格说不成体系。它是有计算机本身提供的一些设备驱动接口,和通过汇编在此基础上封装一些接口以及一些常驻程序,几部分构成,用户程序在使用时可以直接调用这些程序和接口。由于程序是使用汇编编写的,针对硬件编程,所以这种操作系统不可移植,而且没有任何安全保障
  2. 分层结构:unix的发展催生出分层操作系统,此种结构将系统分为多个层次,最内层是硬件相关功能,最外层是为用户提供的接口,中间各层是根据系统的功能划分的模块。上层只能使用下一层提供的功能,是系统使用的安全性得到一定保障。系统分层也将上层功能与硬件隔离,需要移植操作系统时只需要修改最底层代码,可移植性也变得很强
  3. 微内核结构:分层结构的特点是上层只能调用下一层提供的功能,但是随着操作系统的功能越来与复杂,划分的层次越来越多,各层之间的依赖性也越来越复杂,导致系统性能下降。为了解决这一问题,微内核结构应运而生。微内核结构与分层结构类似,也将系统分层,不过只有两层,内核态和用户态,微内核结构的内核只保留操作系统一些核心功能,如进程间通信等,其他大多数模块都在用户态。此种结构的解决了分层结构因为层次过多性能下降的问题。但是这种结构也不是完美的,当用户程序需要使用内核功能时,系统需要在内核态和用户态之间来回切换,十分耗费性能。
  4. 外核结构:外核结构为了解决微内核系统调用问题提出的,外核结构将系统的绝大多数功能都提取出来,由系统函数库提供,用户程序直接调用函数,内核中只保留资源的保护和隔离功能。
  5. 虚拟机管理器:硬件资源是复杂的,管理和使用都比较困难,虚拟机管理器可以使硬件资源的使用变得简单。虚拟机管理器直接和硬件打交道,它将硬件资源虚拟化,为上层程序(一般就是操作系统)提供抽象接口使用硬件资源,上层程序不再需要考虑硬件资源的管理,也感知不到硬件资源的变化。

 

操作系统启动流程

  计算机本身只有简单的数字运算能力,想要在启动后能做一些复杂事必须需要代码数据,数据的来源自然是内存。我们知道内存并不是一个可持久化的存储设备,一旦断电内存的数据就消失了,但是计算机一旦加电就立即需要内存的数据来完成后续的启动,这就好像形成了一个死锁,计算机启动需要内存的数据,内存的数据需要计算机启动后填充。为了解决这个问题,内存被分为两种,RAM(Random access memory),ROM (read only memory)。前者就是我们通常说的断电数据就消失的内存,后者属于只读内存,计算机断电后数据也不会消失。那么我们可以将计算机启动需要的数据和代码存储到只读内存,计算机启动后就直接到只读内存去寻找启动锁需要的代码和数据就可以完成计算机的启动。计算机内存区域的前面1M区域就属于ROM, 保存了系统启动需要代码数据。其实这这1M内存并没有完全被使用,其中的640K - 1M区域保存了计算机启动的必要代码(BIOS启动固件)其他区域是空闲的

  操作系统的启动流程大体分为两步:首先是计算机启动,然后操作系统加载

  1. 计算机加电,将CS:IP寄存器值设置为0xf000:fff0(内存的1M位置),执行BIOS启动固件的代码,这部分固定代码提供了计算机自检程序,基本输入输出程序,系统启动的基本配置等。到此计算机启动完成
  2. BIOS启动完成后,计算机会自动的加载磁盘的第一扇区(引导扇区,512字节)数据到内存的0x7c00处,寄存器CS:IP的值也会跳转到0x7c00,开始执行这部分代码。引导扇区内部一般保存的是系统的加载代码,执行此部分代码就可以让计算机去加载操作系统的核心代码,进一步完成操作系统的加载。到此操作系统加载完成

  上面的第二步可以看出,引导扇区是操作系统加载的第一步,而且引导扇区的数据并不是固定的,而是人为写进去的,如果我们能手动的将自己写的代码写入到引导扇区,那么计算机启动就会立即执行我们自己的代码而不是去加载操作系统

 

中断、异常、系统调用

  • 中断:硬件设备对操作系统内核发出处理请求后,内核的处理活动。 例:键盘输入一个字符,但是cpu在处理其他的事,此时就会发生中断,暂停当前正在处理的事物转而处理键盘输入
  • 异常:非法指令导致的当前指令执行失败,内核的处理活动。例:除零、或者访问了一个不存在的内存,CPU会异常标识调用对应的异常处理例程
  • 系统调用:系统调用是操作系统为用户提供的一些功能接口,程序需要完成某个功能可以调用这些接口完成相应功能,通常我们并不会直接接触到这些系统调用,而且由具体语言再封装一层,我们调用语言提供的函数库,语言底层调用对应系统调用。例:记事本需要读取磁盘文件

 

内存管理

存储结构:

  1. 计算机的数据存储设备有很多种,我们听到最多的是内存、磁盘,其实计算机的数据存储设备还包括的寄存机,高速缓存等等,这些所有的数据存储设备形成一个复杂存储体系
  2. 计算机的存储体系根据访问速度的不同形成一个金字塔式的结构,从上到下依次是 寄存器 》 高速缓存 》 内存 》磁盘,这个金字塔式的结构有一些特点,越往塔顶,数据读写速度越快,容量也越小,越往塔底,速度越慢,容量越大,设备价格也越便宜
  3. 存储的体系之所以形成类似金字塔结构主要是为了提高计算机数据的读写效率。早期计算机的存储设备只有寄存器和主存,寄存器容量太小,主存可以说是唯一的存储设备.随着计算机发展,这种设计的不足开始显露,如当某一时刻存在大量的数据读写,而且其中大量被读写的数据会被重复使用,内存压力就会很大,也会存在很多重复读写,降低系统效率.为了解决这一问题,制造商在cpu内部加入高速缓存,数据从内存读取后先进入高速缓存,cpu需要数据先从高速缓存读取,如果高速缓存没有再到内存获取,如果内存也没有再到磁盘获取。基于这种思想,计算机的存储设备越来越多,逐渐形成金字塔式的层状结构

连续内存管理:

  1. 早期的计算机程序的很小很简简单,计算机本身也不是很强大还没有复杂的内存管理体系,操作系统的内存分配简单粗暴,程序需要多少内存就分配不多少内存,而且程序在编译期就能确定自己代码和数据的内存位置,当程序加载到计算机,它所有的地址都已经确定,不可改变。比如早期的功能手机,你不能自己安装程序,手机齐动后内部程序应该待在那里都已经安排好了
  2. 连续内存分配在早期可以应付大部分场景,它的好处是分配策略简单,一旦程序加载,以后就不需要改变了。而且内存被分成完整的一块一块的,管理起来也很方便。但是他的缺点也很明显,一是不够灵活,程序一旦加载,完全不能再进行调整,二是会产生大量内存碎片,程序运行结束后内存会被回收,但是它周围的内存还没有被回收,就导致内存被分为一块一块的,每块大小不一,如果后面新加载一个程序需要大块内存,哪怕内存中空闲内存总量远远足够,也会因为这些内存是分散的而导致无法被使
  3. 内存碎片的存在催生了动态内存分配方案,即计算机维护一个内存管理表,保存已经被分配的内存区和空闲内存区,新进入的程序需要申请内存都到空闲内存区去申请,程序运行完成,被回收的内存区会再次回到空闲内存区。程序申请内存时具体会被分配到哪一块内存是动态决定的,具体分配策略有以下几种:
    • 最先匹配:新程序申请内存时,到空闲内存表遍历,遇到的第一个满足条件的内存就直接分配。好处是实现简单,内存碎片管理简单,坏处是可能产生更多不大不小的内存碎片
    • 最佳匹配:将内存碎片有小到大排序,新程序申请内存时,到空闲内存表遍历,遇到的第一个满足条件的内存就直接分配。好处是能保证内存碎片都比较小,内存利用率高,坏处是查找效率低
    • 最差匹配:将内存碎片有大到小排序,新程序申请内存时,到空闲内存表遍历,遇到的第一个满足条件的内存就直接分配。好处是寻找到合适内存的速度最快,坏处是会产生大量的小内存碎片
  4. 除了利用合理分配算法最大化利用碎片,清理碎片也是一种选项,也就是当计算机的碎片内存不足以应对内存申请时,可以将一部分原有程序在内存中移动到一起,合并几块碎片内存。但是因为早起程序的内存都是直接指定物理内存的,移动起来难度很

非连续内存分配

  1. 随着计算机和程序的发展,连续内存分配策略越来越无法满足人们的需求,于是非连续内存分配策略出现,非连续内存分配的目标是提高内存的利用率和灵活性
  2. 非连续内存分配就是换整为零的思想,将内存划分为多个块,一个程序的不同部分可以保存在不同的块里面,存储时可以没有顺序,程序运行时动态去寻找数据。因为是数据的存储没有顺序,计算机可以随意的调整程序的位置,也可以随意的添加新程序
  3. 根据内存块拆分的尺寸大小,非连续内存分配分为段式存储和页式存储
    • 段式存储:站在程序的角度划分,一个程序可能分为代码段,数据段,栈段等等,这些段互相之间关联性不大,将一个程序的每个段分开存储,相比于整个程序存储为一整段,拆分后的程序可以使内存的利用率更高。每个段之间的数据类型一致,比如代码段,一些标准库代码是很多程序都需要使用,多个进程就可以共享一个段的数据。
    • 页式存储:站在内存的角度划分,将内存划分为一个一个相同大小的块,每一块称为一个页,分配内存按块分配,可以将程序无序的存放在任意块,只是在逻辑上形成一个整体。这种分配方式可以最大化的利用内存,几乎不用担心内存碎片
  4. 非连续内存分配固然灵活,但是其弊端也是明显的
    • 内存管理复杂。无论是段式还是页式,程序都是被乱序存储的,想要在逻辑上维持一个整体,而且在程序运行时能准确的找到对应的数据,就必须记录每个内存块存储的什么内容,哪些内存块使用了,哪些内存块空闲。这种管理成本非常高
    • 内存访问速度慢。为了实现程序的数据的分散存储和动态内存分配,程序的内存分配不能是物理内存地址而是虚拟地址。程序本身维护一个映射表(页表),表包含内存的虚拟地址和物理地址,程序被分配的地址都是虚拟地址,在运行时去查找映射表,将虚拟地址转化为物理地址,找到数据。如此每次访问数据都要访问两次内存,首先是查表找到物理地址,然后才是访问数据。而且当内存过大或者内存页过多时,页表就会变得很大,物理地址的查找速度的就会变慢
  5. 为了解决内存访问效率问题,出现了一系列内存管理策略
    • 快表:每次访问数据都去查询一次物理地址,当数据重复使用时,这种行为是很浪费资源。快表就是在CPU内部高速缓存再建立一张小的映射表,程序访问数据需要获取物理地址时先去高度缓存的表内获取,高缓存没有命中再去页表查询
    • 多级页表:为解决页表变大物理地址的查询效率就会变低的问题,将页表设计成树状结构,类似于目录索引,可以很快的定位物理地址
    • 段页式存储:段式存储的优势是对数据的保护性好,而且可以方便实现共享。页式存储的优点是对内存的利用率极高,将两者结合起来可以综合两者优势。具体实现是先一段式将内存划分成大块,每个段内部再使用页式将内存划分成小块

 

虚拟存储

把一部分不是紧要的代码,数据等放入外存中,当需要时再加载到内存,扩展内存的逻辑空间。

覆盖技术与交换技术:

  1. 覆盖技术:把程序划分为相对独立的模块,同时把代码和数据分为必要部分和可选部分,必要部分的代码和数据常驻内存,可选部分的数据只在需要的时候加载到内存,并且,可选部分一般使用同一块内存区域,相互覆盖,所以,可选部分的代码不允许有调用的关系。覆盖技术牺牲了一部分运行效率换区更大的内存空间,实现了逻辑上可用的内存空间比实际内存空间大。缺点是编程的难度增加了,程序员需要确定哪些模块是可选的,哪些模块可以相互覆盖。
  2. 交换技术:当一个程序准备加载到内存时候发现计算机的内存不够用了,原因是其他进程占用了大量的内存空间,理论上说,这个程序是无法加载到内存的, 但是实际上并不是所有的程序当前都是必须执行的,这样我们就可以把那些当前不是很紧急的进程放到外存中,腾出一定的空间给新程序使用。交换技术的调度单位是进程,可以由操作系统实现。
  3. 可以看出,覆盖技术和交换技术实现的原理是一样的,但是讨论的情况却不一样,覆盖技术针对同一个程序,交换技术讨论的是多个程序。

局部性原理:

  1. 局部性原理是程序运行的一般特征,被使用的数据在相邻的几条指令中很可能还会被使用,同一部分的代码在相邻的时间段很可能会被重复执行,基于这些都特征局部性原理被分为三类
    • 时间局部性:一条指令的连续几次执行,和一段数据的连续几次访问一般集中在一段时间
    • 空间局部性:当前指令和附近几条指令,当前访问的数据和邻近的几条数据一般集中存储在一段内存空间
    • 分支局部性:一条跳转指令的连续两次执行一般跳转到同一块内存区域
  2. 局部性原理可以利用虚拟内存极大的提升内存替换的合理性。

缺页异常:

  1. 使用虚拟内存,程序加载时优先加载必须的代码,可选代码和数据需要的时候才被加载,操作系统到内存中寻找数据,当数据不存在时,就出现了缺页异常。
  2. 程序把缺页异常交给操作系统的异常处理机制,操作系统根据程序需要的数据的逻辑内存地址到磁盘中寻找到需要的数据。
  3. 操作系统到内存中找到一个空页,将磁盘的数据加载到空白页,再次触发程序。

置换算法:

  1. 缺页异常发生,操作系统寻找空白页存储外村数据但是没找到空白页,此时需要把内存中不太重要的数据转存到外存,再将需要的数据从外存中复制进来,此时就需要置换算法
  2. 置换算法的目标是尽可能减少页面调入调出的次数,此目标衍生出多种算法
    • 最优算法:预测每个页面未来的访问情况,把未来最长时间不会被访问的的页面置换掉。实际无法实现,但是可以作为其他算法性能的评判标准
    • 先进先出算法:替换在内存驻留时间最长的页面。算法有缺陷,最差效率极差,很少单独使用。
    • 最近最久未使用算法:替换离当期时间最远的那个页面。近似最优算法
    • LRU算法:维护一个链表,首节点是最近被访问的页面,尾节点是最久未访问的页面,每次访问一个页面之后就将其换到首节点,每次替换都替换尾节点。缺点是内存消耗大
        

进程

正在运行中的程序。具有完整功能的一段程序在一段指令集上的一次运行过程,是一个动态的概念,具有生命周期

进程基本状态模型:

  进程的基本状态模型有三个阶段 创建 》就绪》运行》等待 进程创建初始化需要的资源,比如内存,创建完成进入就绪队列,进入调度器调度,因为操作系统的进程调度是非公平的,除非此进程的优先级较高,否则只能听天由命的被随机调度。一旦被调用,进入运行态。拥有CPU的使用权。正在运行的进程如果再等待一个资源,就会进入等待状态,放弃CPU的使用权。当等待的资源到来,由等待进入就绪态,再次进入就绪队列,开始竞争CPU。如果时间片用完还未运行完成,再次进入就绪态循环。如果运行完成,释放资源,关闭进程。

进程的创建:

  一般的进程都是由父进程创建的, 父进程调用fock方法,将自己复制一份,此时子进程和父进程除了PID不同其他的都一样,然后再经过一次系统调用,复写子进程,此时子进程才算真正的被创建,子进程的代码和进程调度块里面的内容和父进程已经无关。但是这里子进程的创建明显有两个重复的复制步骤,所以现代的操作系统都经过了优化,windows只经过第一次复制,uniux采取懒复制,什么时候真正运行进程才进行复制。系统的第一个进程是由操作系统纯代码实现实现的。 

进程的调度:

  1. 多进程并发执行就必然要考虑进程之间如何分配CPU,单CPU要考虑如何调度进程,多CPU不仅要考虑进程的调度,还要考虑空闲CPU的分配。
  2. 进程的调度除了要考虑CPU和进程的挑选,还要考虑进程调度的时机,这里又分为非抢占式系统和抢占式系统,前者的调度很简单,因为非抢占式系统的CPU智能由进程自己放弃,操作系统无法强制停止一个进程,所以只需要一个就绪队列,线程一个一个执行即可,抢占式系统比较复杂,调度时机的选择也需要考虑很多因素,下文介绍。
  3. 进程调度需要考虑cpu使用率、吞吐量、周转时间、等待时间、响应时间等等。不同情况的侧重点不同,使用时就需要权衡。这些属于进程的调度准则
  4. 进程调度准则的要求比较多,实际使用中的情况也很复杂,为了满足多种多样的生产需求,出现了一系列进程调度算法
    • 先来先服务算法:优点是简单,缺点是时间波动大,如果排在一个时间比较长的进程后面需要等待很长时间
    • 短进程优先算法:优点是周转时间比其他算法都短,坏处是导致饥饿,执行时间比较长的进程可能一直不会执行。可抢占,于是就有了一个剩余最短时间优先原则
    • 最高响应比优先算法:基于短进程优先的更改进,可抢占,根据优先级调度,某个进程等待的时间越长优先级越高
    • 时间片轮转算法:时间分为时间片,一个时间片用完按照先来先服务算法,切换到下一个就绪进程,难点是确定时间片的大小,时间片太大会退化为先来先服务算法,时间片太小上下文切换的开销回很大,优点是很稳定
    • 多级队列调度算法:就绪队列按照不同的要求分成若干子队列,不同的需求队列采用不同的算法

进程的通讯:

  1. 进程的通讯包括直接通讯和间接通讯,直接通讯是两个进程之间建立一个管道,直接进行消息传递,间接通讯是进程将消息传递给操作系统,再由操作系统传递信息。
  2. 进程通信的方式有很多种具体如下
    • 信号:类似与中断的一种快速响应机制,比如ctrl + c能强制停止程序。优点是传输的数据量小,反应迅速,缺点也是传输的数据量小而单一,使用范围很有限
    • 管道:进程间基于内存文件的通信机制,是一种间接通讯,通讯时先创建一个管道,一个进程往管道里写文件,另一个进程从管道里读文件,两个进程是相互透明的。ls | grep xxx
    • 消息队列:由操作系统维护的以字节序列为基础的间接通讯机制,消息队列独立于创建它的内存,不会因为创建进程死了就不存在。优点是传输的信息量大
    • 共享内存:将同一物理地址的内存映射到若干进程的内存地址空间。不同进程的物理内存是独立的,需要考虑如何共享。优点是速度最快,没有系统调用过程,缺点是不提供同步机制
    • 还有其他种类的通信机制:信号量、有名管道、高级管道、套接字。(一共8种)

线程

  1.  线程是进程的一部分,描述指令流的执行状态,是cpu调度的最小单元。线程的产生是为了解决进程间通信困难的问题的。很多情况下为了提高资源利用率,程序需要并发处理。并发处理可以使用多进程方式,每个事物新开一个进程,但是进程是为了程序之间的数据隔离提出的概念,这就导致了进程之间的通信实现起来复杂而低效,于是提出进程内的“进程”,也即线程。
  2. 相比于进程,线程创建和销毁更简单,开销也更小,便于实现并发程序;因为线程共享数据,所以线程之间的配合工作更简单高效。线程的缺点就是一个进程中一个线程出现问题可能引起其他线程也出现问题
  3. 线程与进程都是为了提高程序的并发性而提出的概念,程序的并发必然伴随这多线程胡多进程的切换。线程的切换与进程的切换很类似,就是计算机根据一定策略(时间片,优先级等)进行调度,控制某些线程运行,某些线程暂停。保证线程最重要的一点就是现场保护,也就是在暂停现成的时候需要记录下本线程执行到哪里了,正在运算的数据是什么。需要明白,线程与进程本质上不过就是可执行代码而已,所以线程的切换本质上就是cpu暂时不执行这块内存的代码,而是转去拿另一段内存的代码执行。线程(进程)切换的实现就是计算机维护了的一个线程(进程)控制表,表中记录了每个线程(进程)的身份标识,当前代码执行的位置,以及一些其他程序执行必须的信息,线程切换时cpu将当前线程对应的表数据更新。然后获取下一行数据继续执行
  4. 线程很好的解决了并发程序的数据共享问题,但是新问题随之而来。多线程共享数据就会出现数据的竞争,这种竞争会导致程序出现一些不可预料的错误,所以线程多线程之间也要制定数据访问隔离策略,这就是线程同步。线程的同步就是指在同一时间只能有一个线程访问共享的数据,具体实现就是建立数据访问临界区,临界区的设计遵循如下规则:
    • 空闲则入:临界区没有线程,任何线程都可进入
    • 忙则等待:临界区存在线程,其他线程不可进入,必须等待临界区线程退出
    • 有限等待:线程等待需要进行限制,不能无限等待
    • 让权等待:正在等待的线程要根据情况挂起,让出CPU资源
  5. 实现线程同步,或者说实现访问临界区的方式有以下几种
    • 禁止中断:从硬件级别提供同步支持,线程进入临界区后CPU关闭中断,当前线程独占系统,其他程序无法运行。优点是实现简单,缺点是会出现饥饿
    • 基于软件方法:利用编程算法,人为制造一个临界区,peterson 算法,dekker算法,cas算法。优点是不依赖硬件,实现起来自主性很强,很多时候性能比较好,缺点是实现复杂
    • 操作系统实现:利用cpu提供原子操作指令(一类特殊指令,可以将多个指令合并为一个原子操作),操作系统实现锁的数据结构。优点是使用简单,可以满足很多复杂场景。缺点是需要硬件支持,可能存在饥饿和死锁

 

posted @ 2017-12-16 18:43  这个世界需要我  阅读(1113)  评论(0编辑  收藏  举报