【操作系统】操作系统知识点学习与整理

本笔记基于 小林coding的图解系统 学习与整理

一、硬件结构

冯诺依曼模型

五个部分: 中央处理器(CPU)、内存、输入设备、输出设备、总线

  1. CPU: 32位CPU一次可以计算4个字节的数据,能计算的最大整数是42 9496 7295, CUP中还有寄存器、控制单元、逻辑运算单元等
  2. 总线:用于CPU和内存及其他设备之间的通信,可分为三种地址总线、数据总线、控制总线
  3. CUP读取内存数据用地址总线来指定内存地址,用数据总线来传输数据。输入设备需要和CPU交互时,需要用到控制总线

线路位宽和CPU位宽

  1. 线路位宽:CPU操作内存要用地址总线,一根线一次能操作两个内存地址, CPU操作4G内存,需要用到32条地址总线,232 = 4G,
  2. 如果计算的数额不超过 32 位数字的情况下, 32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的情
    况下, 64 位的优势才能体现出来。
  3. 32位CPU只能操作4GB内存,就算装了8GB内存条,也没用,64位CPU理论上最大寻址空间位264

程序的CPU执行时间

程序的CPU执行时间 = CPU时钟周期数(指令数 x 每天指令平均时钟周期数) x 时钟周期时间(CPU主频)

32位、64位区别

硬件的64位和32位指的是CPU的位宽,64位CPU可以一次计算超过32位的数字,可以寻址更大的内存空间。
软件的64位和32位指的是指令的位宽,即一条指令是32位还是64位

CPU缓存一致性

在这里插入图片描述
我们将内存中的数据写入Cache 中之后, Cache数据改变了,肯定得同步到内存才行。有两种写入数据的方法:

写直达

写直达(Write Through): 把数据同时写入内存和Cache中。
写入前会判断数据是否已经在Cache中了, 如果在就先更新到Cache里面,再写入到内存里面。如果不在,就直接把数据更新到内存

写回

写直达每次写操作都会把数据写回内存,比较影响性能,为了减少数据写回内存的频率,就出现的写回的方法
写回: 对于已经缓存在 Cache 的数据的写⼊,只需要更新其数据就可以,不⽤写⼊到内存,只有在需要把缓存⾥⾯的脏数据交换出去的时候,才把数据同步到内存⾥,这种⽅式在缓存命中率⾼的情况,性能会更好;

MESI协议

  1. Modified:已修改, 脏标记,代表该 Cache Block 上的数据已经被更新过,但是还没有写到内存⾥。
  2. Exclusive:独占,独占状态的时候,数据只存储在⼀个 CPU 核⼼的 Cache ⾥,⽽其他CPU 核⼼的 Cache 没有该数据
  3. Shared:共享,状态代表着相同的数据在多个 CPU 核⼼的 Cache ⾥都有,所以当我们要更新 Cache⾥⾯的数据的时候,不能直接修改,⽽是要先向所有其他 CPU 核⼼⼴播⼀个请求,要求先把其他核⼼的Cache 中对应的 Cache Line 标记为⽆效状态,再更新当前 Cache ⾥⾯的数据。
  4. Invalidated:已失效 表示的是这个 Cache Block ⾥的数据已经失效了,不可以读取该状态的数据

要想实现缓存一致性

  1. 写传播,当某个CPU核心发生写入操作时,需要把该事件广播通知给其他核心。【实现:总线嗅探】
  2. 事务的串行化,,只有保证了这个,我们的程序在各个不同的核心上运行的结果也是一致的【实现:MESI协议】

CPU伪共享问题

多个线程同时读写同⼀个 Cache Line 的不同变量时,⽽导致 CPU Cache 失效的现象称为伪共享(False Sharing)
例:1线程修改Cache Line1中的A,将Cache Line1标记为独占状态, 2线程修改Cache Line1中的B, 将Cache Line1标记为共享状态, 1线程修改了A,将1线程中的Cache Line1标记为已修改,并通知2线程将Cache Line1标记为已失效。2线程想修改B,就得重新加载这个Cache Line1。这就是伪共享

避免伪共享的方法:同⼀个 Cache Line 中的共享的数据,如果在多核之间竞争⽐较严重,就不把他们放在一个Cache Line 中。
Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,可以让变量在Cache Line 中是对齐的。

软中断

中断是系统⽤来响应硬件设备请求的⼀种机制,操作系统收到硬件的中断请求,会打断正在执⾏的进程,然后调⽤内核中的中断处理程序来响应请求。
操作系统收到了中断请求,会打断其他进程的运⾏,所以中断请求的响应程序,也就是中断处理程序,要尽可能快的执⾏完,这样可以减少对正常进程运⾏调度地影响。
Linux 系统为了解决中断处理程序执⾏过⻓和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」 。

  1. 上半部分用来快速处理中断,一般会暂时关闭中断请求。直接处理硬件请求
  2. 下半部分用来延迟处理上半部分未完成的工作,一般以内核线程的方式运行。由内核触发。也就是软中断。

计算机怎么存储小数

十进制小数转二进制,小数点前是除2取余法,小数点后是乘2取整法,如果把0.1转换成二进制数,将会是一个无限循环的数。(所有一直乘2取整后不能变成整数的,都是无限循环的)

计算机是用浮点数来表示小数的。类似于数学上的科学计数法
在这里插入图片描述
符号位:0是正数,1是负数
指数位: 2的多少次方
尾数:小数点右侧的数字
整数部分只有一位且必须为1,所有就不记录。

在这里插入图片描述
1010.101 其浮点数就是 1.010101 * 23 3就是指数。 010101是尾数
在这里插入图片描述
在这里插入图片描述

二、操作系统结构

内核

计算机是由各种外部硬件设备组成的,⽐如内存、 cpu、硬盘等,如果每个应⽤都要和这些硬件设备对接通信协议,那这样太累了,所以这个中间⼈就由内核来负责, 让内核作为应⽤连接硬件设备的桥梁,应⽤程序只需关⼼与内核交互,不⽤关⼼硬件的细节。

在这里插入图片描述

内核的功能

  1. 管理进程、线程、决定哪个进程、线程使用CPU,也就是进程调度的能力
  2. 管理内存, 决定内存的分配于回收。也就是内存管理能力
  3. 管理硬件设备,为进程与硬件设备之间提供通信的能力
  4. 提供系统调用,如果应用程序要运行更高权限的服务,就得用到系统调用,是用户程序与操作系统之间的接口

内核是怎么工作的

内核具有很高的权限,可以控制CPU、内存、硬盘等硬件,而应用程序具有的权限很小,因此,多数操作系统把内存分成了两个区域:

  1. 内核空间, 这个空间只有内核程序可以访问, 可以访问到所有的内存空间
  2. 用户空间,这个空间专门给应用程序使用,只能访问到局部的内存空间

当程序使用的是用户空间时,我们常说该应用在用户态执行,当程序使内核空间时,程序在内核态执行
在这里插入图片描述

Linux 的设计

Linux内核设计理念主要有:

  1. MutiTask 多任务,即可以并发或并行执行,对于单核就是并发、多核就是并行
  2. SMP 对称多处理, 代表每个CPU的地位是相等的,对资源的使用权限也是相同的,这个特点决定了Linux系统不会用某个CPU单调服务应用程序或者内核程序,而是每个程序都可以被分配到任意一个CPU上执行。
  3. ELF 可执行文件链接格式
  4. Monolithic Kernel 宏内核, 意味着Linux的内核是一个完整的可执行程序,且拥有最高权限。
    宏内核的特征是系统内核所有的模块,比如进程调度、内存管理、文件系统、设备驱动等都是运行在内核态。

与宏内核相反的是微内核,微内核架构的内核只保留最基本的能力,比如进程调度,虚拟机内存,中断等。会把一些应用放到用户空间,比如驱动程序、文件系统等。这样服务与服务之间是隔离的,提高了稳定性和可靠性(一个坏了不会导致整个系统挂掉)
微内核功能少,可移植性高,缺点是驱动程序不在内核中、驱动频繁调用底层能力,需要频繁的切换到内核态,带来性能损耗。

Windows设计

当今win7、win10采用的内核叫 Windows NT (New Technology) ,内核设计是混合型内核。
即有一个微内核作为最小版本的内核,然后其他模块在这个基础上搭建。把整个内核做成一个完整程序,大部分服务都在内核中。

三、内存管理

虚拟内存

单片机的CPU是直接操作内存的 物理地址,这样如果想同时运行两个程序,那么假如程序1在2000位置写入一个新值,这将会把程序2相同位置的值直接覆盖掉。所有同时运行两个程序是行不通的。
所以操作系统用绝对物理地址一定是不行的。我们可以让操作系统为每个进程独立分配一套【虚拟地址】,各玩各的,互不干涉。
操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来
我们程序中使用的内存地址叫做虚拟内存地址,实际存在硬件空间的地址叫做物理内存地址。

操作系统如果管理虚拟地址和物理地址之间的关系?
主要有两种方式:内存分段、内存分页。

内存分段

程序是有若干逻辑分段组成的,不同的段用不同的属性,所以就用分段的形式把这些段分离出来。
分段机制下是,虚拟内存和物理内存如何映射
分段机制下,虚拟地址由两部分组成,段选择因子段内偏移量
段选择因子中有个段号,可以根据段号去段表里查到段基地址和段界限,偏移量在段界限内,物理内存地址就是段基地址+段内偏移量
在这里插入图片描述

  1. 段选择因⼦就保存在段寄存器⾥⾯。段选择因⼦⾥⾯最重要的是段号,⽤作段表的索引。
  2. 段表⾥⾯保存的是这个段的基地址、段的界限和特权等级等。
  3. 虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

内存分段,解决了程序本身不需要关系具体的物理内存地址的问题。但是也有不足之处:

  • 内存碎片的问题
  • 内存交换效率低的问题。

内存碎片问题

在这里插入图片描述
这里内存碎片会有两个问题:

  • 外部内存碎片,即产生了多个不连续的小物理内存,导致新的程序无法被装载
  • 内部内存碎片, 程序所有的内存都被装载到了物理内存,但是有些内存并不常用,导致内存的浪费
    解决外部碎片的问题: 内存交换, 即可以先把音乐占用的内存写到硬盘上,然后在读回到内存中,读回的时候装载位置紧贴着已被占用的内存后面。

内存交换效率低的问题

因为分段产生了内存碎片,然后不得不执行内存交换(Swap),硬盘访问速度比内存慢太多,如果内存交换的时候,交换的是一个占内存很大的程序,那机器就会显得很卡。

内存分页

分段好处是能产出连续的内存空间,但是会有出现内存碎片和内存交换的空间太大的问题。

要想解决这些问题,就得让少出现内存碎片,并且出现内存碎片需要内存交换的时候,让交换写入或者从磁盘装载的数据更少一些。

内存分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小,这样一个连续并且尺寸固定的内存空间称为 。Linux下每页大小是4KB
在这里插入图片描述
在这里插入图片描述

  • 把虚拟内存地址,切分成⻚号和偏移量;
  • 根据⻚号,从⻚表⾥⾯,查询对应的物理⻚号;
  • 直接拿物理⻚号,加上前⾯的偏移量,就得到了物理内存地址

采⽤了分⻚,那么释放的内存都是以⻚为单位释放的,也就不会产⽣⽆法给进程使⽤的⼩内存。
如果内存空间不够,操作系统会把其他正在运行的进程中最近没被使用的内存页面给释放掉(短暂写道硬盘上【换出】, 用的时候再加载进来【换入】),所以一次性写入磁盘的也只有少数几个页,内存交换效率就相对较高

多级页表

简单分页,每个进程都有自己的页表,会需要很大的内存来存储页表。

要解决这个问题,需要采用多级页表的解决方案,就是将很大的单级页表再分页,再搞个页表用来索引大页表。
32位下,每个进程都有4GB的虚拟地址空间,但是一般用不完,很多页表项都是空的,没有分配,所以如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将这些页面换出到硬盘
我们必须得覆盖4G虚拟地址,一级页表需要100多万个页表项来映射。
一级页表1024项就可以覆盖4G的虚拟地址空间,二级页表在需要用到的时再创建。

如果我们把二级页表再推广,页表占用的内存空间就更少了。对于64位操作系统,就变成了四级目录:

  1. 全局页目录项PGD (Page Global Directory)
  2. 上层页目录项PUD (Page Upper Directory)
  3. 中间页目录项PMD (Page Middle Directory)
  4. 页表项 PTE (Page Table Directory)
    在这里插入图片描述

TLB

多级页表解决了空间上的问题,但是多转了几道工序,显然降低了地址转换得速度,带来了时间上的开销。

再根据局部性原理, 程序是具有局部性的,在一段时间内,整个程序的执行仅限于程序中某一部分。对应的,执行所访问的存储空间也是局限某个内存区域。
我们就可以利用这一特性,把最常访问到的几个页表项存储到访问速度更快的硬件中,即TLB, 通常称为⻚表缓存、转址旁路缓存、快表等
在这里插入图片描述
有了TLB,CPU寻址会先查TLB,没有才继续查常规的页表,TLB命中率很高,因为程序最常访问的页就那么几个。

段页式内存管理

内存分段和内存分页组合起来
先把程序划分为多个有逻辑意义的段,然后把每个段分为多个页。地址结构由段号、段内页号、页内位移三部分组成。

Linux 内存管理

Linux 系统主要采⽤了分⻚管理,但是由于 Intel 处理器的发展史, Linux 系统⽆法避免分段管理。
于是Linux 就把所有段的基地址设为 0 ,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被⽤于访问控制和内存保护。
Linxu 系统中虚拟空间分布可分为⽤户态和内核态两部分,其中⽤户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。

四、进程与线程

进程

我们编写的代码,通过编译后会生成二进制可执行文件,当我们运行这个可执行文件后,他会被装载到内存中,接着CPU会执行程序中的每一条指令,这个运行中的程序就叫做进程。

并发和并行

在这里插入图片描述

进程的状态

一个进程活动期间至少具备三种基本的状态:即运行状态、就绪状态、阻塞状态。
在这里插入图片描述
还有两个基本状态:创建状态、结束状态
在这里插入图片描述
如果有大量阻塞状态的进程,进程可能会占用物理内存空间,所有在虚拟内存管理的操作系统中,通常把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候在换入物理内存。所以需要一个新的状态, 描述进程没有占用实际的物理内存空间的情况–挂机状态。
挂起状态可以分为两种:阻塞挂起状态(进程在外存,等待某个事件出现)。就绪挂起状态(进程在外存,只要进入内存就立刻开始运行)
在这里插入图片描述
导致进程挂机的原因还包括:通过sleep让进程间歇性挂起,其工作原理是设置一个定时器、到期后唤醒进程

PCB 进程控制块

PCB是进程存在的唯一标识,一个进程存在必有一个PCB,如果进程消失,PCB也会随之消失。
PCB具体包含:

  • 进程描述信息: 进程标识符,用户标识符
  • 进程控制和管理信息: 进程当前状态、进程优先级
  • 资源分配清单:有关内存地址空间或虚拟地址空间的信息,所打开⽂件的列表和所使⽤的 I/O 设备信息。
  • CPU相关信息: CPU中各个寄存器的值,当进程被切换时,CPU的状态信息会被保存到相应的PCB中

每个PCB通常通过链表的方式进行组织,把具有相同状态的进程链接在一起形成各种队列。

  1. 把所有就行状态的进程链在一起,称为就绪队列
  2. 把所有因等待某事件而处于阻塞状态的进程链在一起,称为阻塞队列
  3. 对于运行队列,在单核CPU中只有一个运行指针
    -在这里插入图片描述

除了链接的组织形式,还有索引方式,其工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的PCB

一般会选择链表,因为可能面临进程创建、销毁等调度导致进程状态发生变化,而链表能更灵活的插入和删除。

进程的控制

1.创建进程
操作系统运行一个进程创建另一个进程,而且允许子进程继承父进程的资源。终止子进程时,其在父进程继承的资源得归还给父进程,同时,终止父进程也会终止其所有子进程。
创建过程如下:

  1. 为新进程分配唯一进程标识号,并申请一个空白的PCB,PCB是有限的,申请失败则创建失败
  2. 为进程分配资源,资源不足,进程就会进入等待状态,以等待系统资源
  3. 初始化PCB
  4. 如果进程的调度队列能够接纳新进程,那么就将插入到就绪队列,等待被调度运行。

2.终止进程
进程有3种终止方式:正常结束、异常结束、外界干预(信号Kill掉)
终止过程:

  1. 查找需要终止的进程的PCB
  2. 如果处于执行状态,则立即终止该进程的执行,将CPU资源分给其他进程
  3. 如果还有子进程,则将其所有子进程终止
  4. 该进程所拥有的全部资源归还给父进程或操作系统
  5. 将其PCB从所在队列中删除

3.阻塞进程
进程需要等待某件事完成时,它可以调用阻塞语句把自己阻塞等待。一旦被阻塞等待,它只能由另一个进程唤醒。
阻塞进程的过程如下:

  1. 找到将要被阻塞进程标识号对应的PCB
  2. 如果该进程为运行状态、则保护其现场,将其状态转化为阻塞状态,停止运行
  3. 将PCB插入到阻塞队列中去

4.唤醒进程
进程由运行转变为阻塞,必须由其他进程来叫醒他。
如果某进程正在等待I/O事件,则只有当进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。
唤醒过程如下:

  1. 在该事件的阻塞队列中找到相应进程的PCB
  2. 将其从阻塞队列中移出,并置其状态为就绪状态
  3. 把该PCB插入到就绪队列中,等待调度程序调度

进程的上下文切换

一个进程切换到另一个进程运行,称为进程的上下文切换
CPU 寄存器和程序计数是 CPU 在运⾏任何任务前,所必须依赖的环境,这些环境就叫做 CPU上下⽂。
CPU上下文切换主要包括:进程上下文切换、线程上下文切换、中断上下文切换。

在这里插入图片描述

线程

线程是进程当中的一条执行流程
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但是每个线程各自都有一套独立的寄存器和栈,确保线程的控制流是相对独立的

线程优缺点

线程的优点:

  • 一个进程可以同时存在多个线程
  • 各个线程之间可以并发执行
  • 各个线程之间可以共享地址空间和文件等资源

线程的缺点:

  • 当进程中一个线程崩溃时,会导致其所属进程的所有线程崩溃

线程与进程的比较

  • 进程是资源(包括内存、打开的文件等)分配的单元,线程是CPU调度的单位
  • 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈
  • 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系。
  • 线程能减少并发执行的时间和空间开销

线程减少开销体现在:

  • 线程的创建时间比进程快。因为创建进程需要还需要资源管理信息,如内存管理信息、文件管理信息,而线程创建是直接共享它们。
  • 线程的终止时间比进程快。因为线程释放的资源少的多
  • 同一个进程内的线程切换比进程切换快。因为线程具有相同的地址空间,即有同一个页表,所以不用切换页表。页表切换开销是比较大的
  • 同一进程的各线程间共享内存和文件资源,那么在线程之间传递数据就需要经过内核了。线程间数据交互效率更高

线程的上下文切换

线程是调度的基本单位,进程是资源拥有的基本单位
所以操作系统的任务调度,实际上调度的是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。

  • 当两个线程不属于同一个进程,则切换的过程和进程上下文切换的一样
  • 当两个线程属于同一个进程,因为虚拟内存的共享的,所以在切换时,只需要切换线程的私有数据、寄存器等不共享的数据。

线程的实现

  • 用户线程: 在用户空间实现的线程,是由用户态的线程库来完成线程的管理
  • 内核线程:内核中实现的线程,由内核管理的线程
  • 轻量级进程: 在内核中来支持用户线程。

用户线程和内核线程的对应关系:多对一、一对一、多对多。

用户线程

用户线程是基于用户态的线程管理库来实现的。线程控制块TCB也是在库里面实现的,对于操作系统而言,看不到TCB,只能看到进程的PCB
所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理的,包括线程的创建、终止、同步、调度等。
在这里插入图片描述
这种进程与用户线程之间1:N的关系称为一对多的线程模型

用户线程的优点

  • 每个进程都需要有它私有的TCB列表,用来跟踪记录各个线程状态信息,TCB由用户线程的库函数来维护,可以用于不支持线程技术的操作系统
  • 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度很快

用户线程的缺点

  • 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。

  • 当一个线程开始运行,除非它主动交出CPU的使用权,否则他所在的进程中其他线程无法运行。
    因为用户态的线程没法打断当前运行中的线程,只有操作系统才有这个特权,但是用户线程不由操作系统管理。

  • 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行的会比较慢

内核线程

内核线程由操作系统管理,线程对应的TCB是放在操作系统里的,这样线程的创建终止和管理都是由操作系统负责。
使用内核线程实现的方式被称为1:1实现,即一个用户线程对应一个内核线程。
内核线程的优点

  • 在一个进程中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行
  • 多线程的进程获得更多CPU运行时间

内核线程的缺点

  • 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如PCB和TCB
  • 线程的创建、终止、切换都是通过系统调用的方式进行,系统开销大
轻量级进程

轻量级进程LWP是内核支持的用户线程,一个进程可以有一个或多个LWP,每个LWP是跟内核线程一对一映射的

大多数操作系统中,LWP与普通进程的区别在于它只有一最小的执行上下文和调度程序所需的统计信息。
在这里插入图片描述
在LWP上方是用户线程,可以为1 :1模式,如进程4,其优点是

  • 优点:实现并行,当一个LWP阻塞,不会影响其他LWP
  • 缺点:每一个用户线程就产生一个内核线程,创建线程开销大

N :1模式,如进程2

  • 优点: 用户可以开多个线程,且上下文切换在用户空间,切换效率高
  • 缺点:如果一个用户线程阻塞 ,整个进程都将会阻塞,在多核CPU中,没办法充分利用CPU

M :N模式,如进程3

  • 多个用户线程对应到多个LWP,再对应到内核线程,综合了前两种优点,大部分线程上下文发生在用户空间,且多线程能充分利用多核CPU资源

组合模式,如进程5

调度

CPU选择一个进程运行的功能通常称为调度程序scheduler

调度时机

当进程从一个运行状态到另一个状态变化的时候,就会触发一次调度
从就绪态 --> 运行态、从运行态->阻塞态、从运行态->结束态
这些状态变化的时候,操作系统需要考虑是否要让新的进程给CPU运行,或者是否让当前进程退出CPU,换另一个进程运行

根据如何处理硬件时钟中断,把调度算法分为两类:

  • 非抢占式调度算法,让一个进程一直运行,不理会时钟中断,直到进程被阻塞或者进程退出
  • 抢占式调度算法,时间到了就把进程挂起,调度程序从就绪队列挑选另一个进程给CPU运行。
    这种抢占式调度处理,需要时间间隔的末端发生时钟中断,以便把CPU的控制返回给调度程序进行调度,也就是常说的时间片机制

调度原则

  1. 发送I/O事件导致CPU空闲,调度程序需要从就绪队列选择一个进程来运行

  2. 有的程序执行某人任务花费时间长,一直占着CPU会导致系统吞吐量(CPU单位时间完成的进程数量)降低。
    要提高吞吐量,调度程序要权衡长任务和短任务进程的运行完成数量。

  3. 进程开始到结束分为进程运行时间和进程等待时间,时间总和称为周转时间,进程的周转时间越小越好
    如果进程的等待时间长而运行时间短,那周转时间就很长,调度程序应该避免这种情况发生。

  4. 就绪队列中进程的等待时间也是调度程序所需要考虑的原则情况发生。

  5. 对于交互式比较强的应用,响应时间也是调度程序需要考虑的原则

总的来说就是: CPU利用率、系统吞吐量、周转时间、等待时间、响应时间

调度算法

单核CPU常用算法
1.先来先服务调度算法

最简单的算法就是,非抢占式的先来先服务算法,
每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或者被阻塞,才会继续从队列中选择第一个进程接着运行。

适用于CPU繁忙型作业的系统,不适用I/O繁忙型作业的系统。(长任务在前面的话,后面短任务等待时间过长)

2.最短作业优先调度算法

优先选择运行时间最短的进程来运行,有助于提高系统的吞吐量
不利于长作业

3.高响应比优先调度算法

每次进行进程调度,先计算响应比优先级,然后把响应比优先级最高的进程投入运行。
在这里插入图片描述
由公式可以看出,两个进程要求服务时间相同时,等待时间越长,响应比就越高,进程的响应随等待时间增加而提高
两个进程等待时间相同时,要求服务时间短的,响应比高,这样短作业进程更容易被选中运行。

4.时间片轮转调度算法

最简单、最公平、使用最广的时间片轮转调度算法
每个进程分配一个时间段,称之为时间片,允许该进程在该时间段中运行。
时间片的长度设置的太短会导致过多进程上下文切换,太长会导致短作业的响应时间变长,一般设为20ms-50ms

5.最高优先级调度算法

调度程序从就绪队列中选择优先级最高的进程进行运行。
进程优先级分为静态优先级和动态优先级。静态优先级就是进程创建的时候就确定了优先级,动态优先级是,随着进程等待时间的增加,增加进程的优先级。

处理优先级高的进程有抢占式和非抢占式两种,抢占式就是来了优先级高的直接运行,让正在运行的进程停下。非抢占式就是当前运行完了再运行高优先级的

最高优先级调度算法缺点就是,低优先级的进程可以永远不会运行。

6.多级反馈队列调度算法

「时间⽚轮转算法」和「最⾼优先级算法」的综合和发展。
「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短
「反馈」表示如果有新的进程加入优先级高的队列时,立即停止当前正在运行的进程转而去运行优先级高的队列

在这里插入图片描述
在这里插入图片描述
新来的直接进第一个队伍,第一个队伍只办一分钟,没办完就去第二个队伍排队,第二个队伍排队时间比第一个队伍长,当时办理业务时间也比较长。第一个队伍处理完了处理第二个队伍,突然第一个队伍又来新客户,就立马给他处理一分钟,然后让他排到第二个队伍末尾。

进程间通信

每个进程的用户地址空间都是独立的,一般不能互相访问,但是内核空间是每个进程都共享的,所有进程之间要通信必须通过内核

管道

在这里插入图片描述
所谓的管道,就是内核⾥⾯的⼀串缓存。从管道的⼀段写⼊的数据,实际上是缓存在内核中的,另⼀端读取,也就是从内核中读取这段数据。
另外,管道传输的数据是⽆格式的流且⼤⼩受限。
使⽤ fork 创建⼦进程, 创建的⼦进程会复制⽗进程的⽂件描述符,这样两个线程就可以通过管道通信了
在这里插入图片描述
我们在shell里通过 | 匿名管道将多个命名连接在一起,实际上也就是创建了多个子线程
在这里插入图片描述

对于命名管道,它可以在不相关的进程间也能相互通信。管道通信数据遵循先进先出原则。
匿名管道的生命周期随进程的创建而简历,随进程的结束而销毁
管道通信方式效率低,不适合进程间频繁的交换数据。

消息队列

消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义数据类型,如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

消息队列的生命周期随内核, 如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在。

消息队列的通信就像发邮件,可以频繁通信了缺点是: ①不及时、②附件大小有限制
消息队列不适合比较大的数据传输,消息队列通信过程中,存在用户态和内核态之间的数据拷贝开销。
进程写入数据到内核中的消息队列,是用户态到内核态拷贝数据的过程,读取是内核态到用户态的拷贝过程

共享内存

消息队列的读取和写入过程都会发生用户态和内核态之间的消息拷贝, 共享内存的方式很好的解决了这个问题

共享内存的机制就是,拿出一块虚拟地址空间来, 映射到相同的物理内存中, 这样这个进程写入的东西,另一个进程立马就能看到了,不需要来回拷贝、来回传输,提高进程间通信的速度
优点是:可以及时通信、不需要数据拷贝缺点:万一多个进程同时修改一个共享内存,很可能发生冲突
在这里插入图片描述

信号量

共享内存的新问题是多个进程同时修改同一个共享内存地址,会发生冲突,为了防止多进程竞争共享资源,造成混乱。所有需要保护机制

信号量就实现了这一保护机制,信号量是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是缓存进程间的数据

信号量表示资源的数量,控制信号量有两种原子操作:

  • P操作,把信号量减1,如果相减后信号量<0,则表明资源已被占用,进程需阻塞等待,如果相减后>=0,则表明资源可用
  • V操作,把信号量加1,相加后信号量<=0,则表明当前有阻塞中的进程,会将该进程唤醒运行,如果相加后信号量>0,则表示没有阻塞的进程

信号量初始化为1,就代表着是互斥信号量,可以保证共享内存中任何时刻只有一个进程在访问
在这里插入图片描述

如果执行必须有先后顺序,A先写入,B去读取,可以初始化信号量为0。代表着是同步信号量
A进行V操作B进行P操作,没有V操作P操作就会阻塞

在这里插入图片描述

信号

上面介绍的都是常规状态下的工作模式, 对于异常情况的工作模式,就需要用信号的方式来通知进程
信号的来源主要有硬件来源(如键盘的Ctrl + C) 和软件来源(如Kill命令)
信号是进程间通信机制中 唯⼀的异步通信机制,可以在任何时候发送信号给某一进程,一旦信号产生,有以下几种用户进程对信号的处理方式

  1. 执行默认操作。 每个信号都有一个默认操作
  2. 捕捉信号。可以定义一个信号处理函数,信号发生时执行处理函数
  3. 忽略信号。可以忽略信号,但 SIGKILL 和 SEGSTOP 无法捕捉和忽略。

Socket

想要跨网络与不同主机的进程之间通信,就需要Socker通信了

TCP协议通信的socket编程模型
在这里插入图片描述
监听的 socket 和真正⽤来传送数据的 socket,是「两个」 socket,⼀个叫作监听 socket,⼀个叫作已完成连接 socket。

UDP协议通信的socket编程模型
在这里插入图片描述
UDP 是没有连接的,所以不需要三次握⼿,也就不需要 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端⼝号,因此也需要 bind。

针对本地进程间通信的 socket 编程模型
本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端⼝,⽽是绑定⼀个本地⽂件,这也就是它们之间的最⼤区别

进程间通信总结

匿名管道
通信数据:无格式的流并且大小受限,缓存在内核中,先进先出
通信方向:单向
生命周期随着进程创建而建立,随着进程终止而消失
只能用于存在父子关系的进程间通信, 不适用频繁通信

命名管道
可以实现毫无关系的进程通过这个类型为P的设备文件进行通信

消息队列
通信数据:消息链表,消息体是用户自定义的数据类型,所有接收和发送的消息体数据类型的保存一致
缺点是:不及时、大小受限、需要用户态和内核态之间的拷贝

共享内存
直接分配一个共享空间,每个进程都可以直接访问
通信速度快,带来新的问题是,多进程竞争引发混乱

信号量
信号量可以解决保护共享资源。P操作+1, V操作-1
信号量可以实现进程访问的互斥性(信号量初始化为1),也可以实现进程间的同步(信号量初始化为0)

信号
信号是进程间通信机制中唯一的异步通信机制,信号可以在进程和内核之间直接交互
信号来源:硬件(如键盘的Ctrl + C)、 软件(如Kill命令)
进程有三种响应信号的方式:①执行默认操作、②捕捉信号、 ③忽略信号

Socket
如果要与不同主机的进程间通信,那么就需要 Socket 通信了
三种常见的通信方式: 基于TCP协议的通信方式、基于UDP协议的通信方式、本地进程间通信方式

多线程同步

互斥:保证一个线程在临界区执行时,其他线程应该被阻止进入临界区
同步:并发进程/线程在一些关键点上可能需要互相等待与互通消息
主要方法有:

  • 锁: 加锁、解锁操作 (互斥)
  • 信号量: P、V操作 (互斥、同步)

忙等待锁的实现

原子操作指令–测试和置位指令
在这里插入图片描述
这段代码把old 更新为新值,返回old旧值,可以实现 忙等待锁
假设一个线程运行,调用lock()
没有其他线程持有锁,flag = 0,当调用TestAndSet(flag, 1)方法,返回0。会跳出循环,执行代码。unlock将flag清理回0.
有其他线程持有锁,flag = 1, 当调用TestAndSet(flag, 1)方法,返回1,一直循环等待,直到flag被改为0,

忙等待锁也叫自旋锁,在单处理器上,需要抢占式的调度器(即不断通过时钟中断线程,运行其他线程),否则自旋锁无法在单CPU上使用,因为它永远不会放弃CPU

无等待锁

无等待锁即获取不到锁的时候,不用自旋,没有获取锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把CPU让给其他线程

信号量

互斥信号量:用于互斥访问缓冲区初始化值为1
资源信号量:用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化为0(缓冲区为空)
资源信号量:初始化值为n (缓冲区为n)

经典同步问题:哲学家就餐问题

在这里插入图片描述
五个哲学家,桌上有五只叉子,每两个哲学家之间放一支叉子。
哲学家要两支叉子才吃饭,吃完后把两支叉子放回原处。

方案一

5个叉子编号,用同步信号量,拿起叉子就p操作,放下叉子就v操作,有叉子就拿起来,没有就阻塞

缺点:有可能所有人都拿起了左叉子,拿不到右叉子,一直阻塞导致死锁

方案二

既然可能所有人都拿到左叉子形成死锁,那我们再拿叉子之前加一个互斥信号量。同一时间只能一个人去拿叉子

缺点:同一时间只能一个人吃饭,但是五个叉子其实可以供两个人同时吃。

方案三

用互斥信号只能一个人吃,那就不用了,方案一会出现全部拿左叉子的情况,
那我们给哲学家编号,还是用同步信号量,
偶数哲学家先拿左边的叉子,拿不到就阻塞,拿到了就再那右边的叉子,拿不到继续阻塞。
奇数的哲学家先拿左边的叉子,拿不到阻塞,拿到了再拿右边的叉子

方案四

记录每个哲学家的状态,只有左右两个哲学家都没在吃饭的时候,中间哲学家才去拿左右的叉子,如果有在吃饭的,就不拿叉子

读者-写者问题, 即读写锁

读-读 允许
读-写 互斥
写 写 互斥

方案一:

  1. 互斥信号量, 控制写操作,初始值为1
  2. 读者计数,对正在进行读操作的读者个数计数
  3. 互斥信号量,控制对读者计数的修改,初始值为1

这是一种读者优先策略,只要有读者读,后来的读者都可以进入,读者走完才唤醒写者。如果读者不断进入,会使写着处于饥饿状态

死锁

两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直互相等待,没办法继续运行,形成死锁。
只有同时满足四个条件才会发生死锁:

  • 互斥条件。 多线程不能使用同一个资源
  • 持有并等待条件。 线程A已经持有了资源1,又想申请资源2,等待过程不会释放已持有的资源1
  • 不可剥夺条件。 线程已持有了资源,在自己使用完之前,不能被其他线程获取
  • 环路等待条件。 两个线程获取资源的顺序构成了环形链

Java程序可以使用jstack工具,排查死锁问题。

  1. jps -l 得到正在运行的程序进程号
  2. jstack 进程号 可以验证是否是死锁发生

如何避免死锁的发生

破坏四个必备条件:①互斥条件。 ②持有并等待条件。 ③不可剥夺条件。 ④环路等待条件

最常见的时使用资源有序分配法,来破坏环路等待条件
即,线程A和线程B获取资源是顺序一样,这样就不会死锁了

悲观锁与乐观锁

互斥锁和自旋锁

  • 互斥锁加锁失败后,线程会释放CPU。由操作系统内核实现,加锁失败睡眠,锁被释放,找机会唤醒
  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁,自旋的时间和被锁住代码执行的时间是成正比的

互斥锁的开销成本是两次线程的上下文切换,如果你能确定锁住的代码执行时间很短,就不应该用互斥锁,而用自旋锁,否则用互斥锁。

读写锁

读写锁适用于能明确区分读和写操作的场景,在读多写少的场景能发挥优势。
读写锁可以分为读优先锁和写优先锁。

  • 读优先:线程获取了读锁以后,其他线程可以继续获取读锁,而不能获取写锁,直到所有读锁都释放,才能获取写锁
  • 写优先:线程获取了读锁以后,其他线程要获取写锁会阻塞,然后再有线程获取读锁也会阻塞,第一个读锁释放,写锁就可以获取到资源

读优先锁和写优先锁都可能导致对方饿死问题

公平读写锁:用队列把获取锁的线程排队,不管是写线程还是读线程都按先进先出的原则加锁,这样读线程依然可以并发,也不会出现饥饿现象

互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景选择其中一个进行实现

乐观锁、悲观锁

互斥锁、自旋锁、读写锁都是属于悲观锁
悲观锁: 它认为多线程同时修改共享资源的概率比较高,很容易出现冲突,所以访问共享资源之前先加锁。

乐观锁:它假定冲突概率低,直接去修改共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程修改过资源,那么操作完成,如果有其他线程已经修改过这个资源,就放弃本次操作。乐观锁全程没有加锁,所以也叫无锁编程。

乐观锁虽然除去了加锁的操作,但是一旦发生冲突,重试成本很高,所以只有在冲突概率很低,且加锁成本很高的场景才用乐观锁,如多人文档编辑

五、调度算法

在这里插入图片描述

进程调度算法—CPU调度算法

CPU调度发生时机

  1. 进程从运行态转到等待状态
  2. 进程从运行态转到就绪状态
  3. 进程从等待状态到就绪状态
  4. 进程从运行状态到终止状态

在这里插入图片描述
见第四章第三节调度

内存页面置换算法

缺页中断: 当CPU访问的页面不在物理内存时,就会产生一个缺页中断,请求操作系统将缺页调入物理内存

置换算法的功能是: 当出现缺页,需调入新页面而内存已满时,选择被置换的物理页面。
常见算法:

  • 最佳页面置换算法(OPT)。置换在「未来」最⻓时间不访问的⻚⾯。需要计算每个页面下次访问时间
  • 先进先出置换算法(FIFO)。选择在内存中驻留时间很长的页面进行置换
  • 最近最久未使用的置换算法(LRU)。选择最长时间没有被访问的页面进行置换
  • 时钟⻚⾯置换算法。和LRU近似,又是FIFO的改进。 页表放在环形链表,表针指向最老的页面,如果最老的页面最近被访问了,就下一个
  • 最不常用算法(LFU)。选择访问次数最少的页面进行置换

磁盘调度算法

在这里插入图片描述
多个相同编号的磁道形成一个圆柱。磁盘寻道是最耗时的,所以磁盘调度算法就是优化磁盘访问请求顺序,来提高磁盘访问性能

  • 先来先服务算法:先到来的请求,先被服务
  • 最短寻道时间优先: 优先选择从当前磁头位置所需寻道时间最短的请求,可能产生饥饿,磁头在一小块区域来回移动
  • 扫描算法:磁头在一个方向上移动,访问所有未完成请求,直到磁头到达该方向最后磁道,再调换方向,像电梯,走到顶
  • 循环扫描算法: 循环扫描就是只在磁头朝特定方向移动时才处理请求,返回时快速复位磁头,再去另一个方向
  • LOOK 与C-LOOK算法:扫描或循环扫描的优化,走到最远请求位置就立即反向。LOOK反向时也处理请求,C-LOOK不处理

六、文件系统

文件系统基本组成

文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,组织的方式不同,就形成不同的文件系统

Linux最经典的一句话:一切皆文件, 不仅普通的文件和目录,块设备】管道、socket等也是统一交给文件系统管理的

Linux文件系统为每个文件分配两个数据结构:

  • 索引节点,inode,记录文件元信息,比如inode编号、文件大小、访问权限、创建时间、磁盘位置等。是文件唯一标识,也在硬盘中
  • 目录项,,记录文件名字、索引节点指针以及与其他目录项层级关联关系。目录项是内核维护的数据结构,缓存再内存

目录项和索引节点的关系的多对一。
目录也是文件,也是用索引节点作为唯一标识,目录里面存的是子目录或文件。目录和目录项可不是一个东西

文件数据如何存储在磁盘呢?
磁盘读写的最小单位是扇区,扇区大小只有512B,文件系统把多个扇区组成一个逻辑块
这样每次读写的最小单位就成了逻辑块,Linux中的逻辑块大小为4KB
在这里插入图片描述
磁盘进行格式化的时候,会被分成三个存储区域

  • 超级块:用来存储文件系统的详细信息,如块个数大小、块大小、空闲块等,当文件系统挂载时进入内存
  • 索引节点区:用来存储索引节点,当文件被访问时进入内存
  • 数据块区,用存储文件或目录数据。

虚拟文件系统

文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,称为虚拟文件系统
在这里插入图片描述
Linux支持的文件系统根据储存位置的不同,可以把文件系统分为三类:

  • 磁盘的文件系统,直接把数据存储在磁盘中
  • 内存的文件系统,占用内存空间,读写的是内核中的相关数据, 如常用的/peoc和/sys
  • 网络的文件系统

文件的使用

文件系统的基本操作单位是数据块,而不是字节

文件的存储

连续空间存放方式

文件存放在磁盘的连续物理空间中,文件紧密相连,读写效率高,但是文件头里需要指定起始块位置和长度
缺点:

  • 会产生磁盘空间碎片
  • 文件长度不易扩展

非连续空间存放方式

链表的方式
⽂件要以「隐式链表」的⽅式存放的话, 实现的⽅式是⽂件头要包含「第⼀块」和「最后⼀块」的位置,并且每个数据块⾥⾯留出⼀个指针空间,⽤来存放下⼀个数据块的位置
隐式链表的缺点:

  • 无法直接访问数据块,只能通过指针顺序访问文件
  • 数据块指针消耗了一定存储空间
  • 稳定性较差,软件或硬件错误可能导致链表中的指针丢失或损坏,导致文件数据丢失

显示链表:取出每个磁盘块的指针,把他放在内存的一个表中
由于整个表都在内存中,检索速度快,但是不适用于大磁盘。

索引的方式
为每个文件传一个索引数据块,里面存放的是指向文件数据块的指针列表
文件头需要包含指向索引数据块的指针,通过文件头直到索引数据块的位置,再通过索引数据块的索引信息找到对应数据块

在这里插入图片描述
优点:文件创建、增大、缩小都很方便,不会有碎片问题, 支持顺序读写和随机读写
缺点是:索引带来一定硬盘开销

如果文件大到一个索引块放不下索引信息,咋办?
链表+索引的组合:数据索引块留出一个放下一个索引的指针,缺点是万一某个指针坏了,后面的都找不到

索引+索引:多级索引块,通过一个索引块来存放多个索引块

在这里插入图片描述

文件空闲空间管理

  • 空闲表法。 为所有空闲空间建一张表,表内容包括空闲区的第一个块号和该区空闲块的个数,属于连续分配方式
  • 空闲链表法。每一个空闲块里有一个指针指向下一个空闲块。缺点是不能随机访问,效率低。
  • 位图法。用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有盘块都有一个二进制位与之对应

文件系统结构

数据块的位图是放在磁盘块里的,一个磁盘块是4K,最大能表示215 个空闲块,能表示的最大空间是128M,所以一个块的位图+一系列块能表示的空间太小。(包括inode位图+一系列inode结构)
所以Linux文件系统用N多块组,表示N大的文件
在这里插入图片描述

  1. 超级块:包含文件系统的重要信息,如果inode总个数,块总个数,每个块组的inode个数,每个块组的块个数等。是全局信息
  2. 块组描述符:包含文件系统中各个块组的状态,比如块组中空闲块和inode的数目等,每个块组都包含了文件系统所有块组的描述符信息。
  3. 数据位图和inode位图:用于表示对应的数据块或inode是空闲的还是被使用
  4. inode列表:包含了块组中所有的inode,inode用于保存文件系统中各个文件和目录相关的所有元数据
  5. 数据块:包含文件的有用数据。

每个块组都包含超级快和块组描述符表,这两个都是全局信息,而且非常重要
这么做的原因有两点:

  • 如果系统崩溃破快了超级块和块描述符,用冗余的副本,该信息才有可能恢复
  • 文件和管理数据尽可能接近,减少磁头寻道和旋转,提高文件系统的性能。

软链接和硬链接

硬链接是多个⽬录项中的「索引节点」指向⼀个⽂件,也就是指向同⼀个 inode
但是 inode 是不可能跨越⽂件系统的,每个⽂件系统都有各⾃的 inode 数据结构和列表,所以硬链接是不可⽤于跨⽂件系统的。

  • 43
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 26
    评论
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甲 烷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值