玩转操作系统

操作系统概述

1.定义:操作系统是管理计算机硬件和软件资源的计算机程序
注意了!!!虽然说操作系统可以管理硬件资源,但是它其实是一个软件系统。总而言之,操作系统就是管理硬件,提供用户交互的软件系统,是硬件层和软件层的适配层

2.为什么要用操作系统这东西:

  • 我们对于硬件不能够直接的去操作,因此选择通过操作系统

  • 像这些电脑,手机品牌很多,设备种类十分繁杂,操作系统则统一了界面,避免了这些繁杂的影响

  • 操作系统的使用肯定可以带来极大的简便性,能够让更多的人去使用计算机,因此操作系统的出现也极大地促进了电脑,手机的普及

3.那操作系统有什么功能呢?

  • 可以统一的去管理计算机资源,比如说处理器资源,存储器资源,文件资源,IO设备资源。

  • 可以实现计算机资源的抽象,即能够把那些所谓我们平时看不见的资源,抽象到各种软件接口上面去,比如说IO设备管理软件,文件管理软件,并且提供操作,读写文件的接口,能够让用户操作起来更加方便

  • 可以提供用户与计算机的接口:图像窗口形式,命令形式(CMD),系统调用形式(编程)

操作系统相关概念

  • 并发性:这里所说的并发性就得和我们常听到的并行性给区分开来了。
并行 两个或者多个事件在同一时刻发生
并发 两个或者多个事件在同一时间间隔发生

在操作系统当中的并发性就体现在多道程序在同一时间间隔内交替运行。

  • 共享性:也就是说操作系统当中的资源可以供多个并发的程序共同使用(即进行资源共享),程序A和程序B可以一起使用主存资源。
    当然,资源共享也有两种不同的形式:
互斥共享 A占用资源的时候,其它程序只能等待,只有A释放后,其它进程才能使用
同时访问 资源可以在一段时间内并发地被多个程序访问,不需要释放
  • 虚拟性:把物理实体转变为若干个逻辑体,前者真实存在,后者是虚拟化的。而虚拟化的过程中涉及到两种虚拟技术,一种是时分复用技术,一种是空分复用技术:

1.时分复用技术:即资源在时间上进行复用,不同的程序并发使用,多道程序分时使用计算机硬件资源,提高资源利用率。这里也涉及到两种技术:

虚拟处理器技术 借助多道程序设计为每个程序建立进程,多个程序分时复用处理器
虚拟设备技术 就是把物理设备分为多个逻辑设备,每个程序占用一个逻辑设备,程序通过逻辑设备并发访问资源

2.空分复用技术:实现虚拟内存和虚拟磁盘:

虚拟内存 逻辑上扩大程序的存储容量(其实比实际存储大),来提高编程效率
虚拟磁盘 就是我们平时见到的D盘,C盘等等,它本质上是一个物理磁盘,只不过分为多个虚拟磁盘而已,为了起到安全作用,D盘文件损坏不会影响到C盘数据

续上述几个概念

  • 异步性:多道程序设计的环境下,允许多个进程并发执行,而进程在使用资源的时候,不是一直执行下去,而是断断续续的,中途可能会等待,放弃。

操作系统之进程管理

进程实体

1.我们为什么需要进程?

  • 进程是我们的系统进行资源调度和分配的基本单位

  • 进程可以隔离环境,资源,能够让程序独立运行,保证程序的正常运行

  • 这也可以使资源利用率大大提升

进程实体

1.主存当中的进程状态:在主存当中,进程其实就是一段连续的存储空间,(至于为什么会连续的存储,那就涉及到局部性原理了,局部性原理博主已经在博客**《吃透计算机组成原理》**当中讲的比较清楚了,大家感兴趣可以去看看。)这一段存储空间也称为进程控制块。
说到进程控制块,博主就展开叙述它的常用具体内容:
进程控制块

  • 标识符(Identifier):就是相当于给进程去一个名字ID,让我们认识这是哪一个进程,并且标记是唯一的。

  • 状态(State):标记进程的状态,比如说是否在运行

  • 程序计数器(Program Counter):指向进程即将执行的下一条命令的地址,就是让进程的指令有序的进行。

  • 内存指针(Memory Pointers):指向程序代码和进程数据

  • 上下文数据(Context Data):就是存储进程执行时的处理器的数据

  • IO状态信息(IO Status Information):被进程IO状态信息占用的文件列表

  • 记账信息(Accounting Information):记录使用处理器的实际,或者时钟的总数等等

以上便是进程控制块的具体常用内容,当然,还有其他的,就不一一叙述了,根据上面的内容我们可以大概分成四个部分:分别为进程标识符,处理器状态,进程调度信息,进程控制信息。

接下来咱们再谈谈进程控制块的其它概念:

2.进程控制块(PCB):
进程控制块是干什么的?

  • 其是描述和控制进程的通用数据结构,就是有效并且高效地存储进程信息的

  • 它可以记录进程状态,并且控制进程的全部信息

  • PCB是操作系统在进行资源的调度的常家客,进程会被读取

3.进程与线程:
对进程和线程我们必须区分开来,进程包括很多线程,一个进程对应一个或者多个线程,可以说进程是线程的爸爸。一个进程好比一个程序,而线程就相当于这个程序当中的每个任务(界面展开,菜单选择,打开,关闭,执行等等),那我们来了解一下进程与线程的关系:
进程与线程

  • 线程是系统进行运行调度的最小单位,而进程是基本单位

  • 线程是进程当中实际运行并且工作的单位,可以想象成进程为线程提供了庇护所和资源,让线程自己去工作。

  • 一个进程可以并发执行多个线程,每个线程执行的任务不同

  • 进程当中的线程共享进程资源

五状态模型

进程总共分为五个状态,分别为创建状态,就绪状态,执行状态,阻塞状态和终止状态。博主就一一展开叙述了。
1.创建状态:进程被分配了PCB,即分配了进程控制块,但是其它资源并未准备就绪。

2.就绪状态:其就是比创建状态多了点东西,就是其它资源已经都准备好了(比如说内存,堆栈空间等等),此时只是缺少CPU的使用权,一旦获得便可执行。此时就绪状态的进程都插入到就绪队列当中了。

3.执行状态:进程获得CPU使用权,使得程序执行。

4.阻塞状态:因为某种原因进程放弃了CPU,而进入阻塞状态。比如说进程需要使用打印机,但是发出请求以后,打印机并未准备就绪,此时无法执行,进入阻塞状态。

5.终止状态:进程执行完毕,此时系统会清理进程内存,并且归还PCB。

五个状态之间的关系:
五状态模型

进程同步

两种模型

在了解进程同步之前,我们需要了解两种模型,来加深对进程同步的理解,以及了解为什么会要求进程同步:
1.生产者-消费者模型:
消费者-生产者模型

模型概述: 生产者每生产一个进程便把进程都放入到仓库,即缓冲区里面,此时仓库进程数量加一,然后消费者需要执行某一个进程的时候,便会把对应的进程取出来,此时仓库里进程数量减一,其中缓冲区位于缓存Cache当中。

模型过程(特殊情况下):
假设register表示某个部分(生产者或者消费者)的进程的数量,count表示缓存当中进程的数量。
生产者过程:

register = count      //1
register = register+1    //2
count=register           //3

消费者过程:

register = count        //1
register = register-1   //2
count = register       //3

假设缓冲区里面有10个进程,此时若是生产者生产一个进程,消费者也消费一个进程,那么按理来说,最终缓存区的进程还是十个,是不是这也呢,我们来模拟一下。

由于操作系统当中是并发性执行操作的,因此不一定是按照顺序来,有时候进程会断断续续,比如说生产操作的进程
1.生产者执行生产者的第一步,register=count此时,register生产=10,count=10

2.生产者执行第二步,register=register+1,此时register生产=11,count=10

3.由于并发,此时可能是生产者断了,消费者开始执行第一步,register消费=count,此时,register消费=10,count=10

4.消费者执行第二步,register消费=register消费-1,此时register消费=9,count=10

5.消费者执行第三步,count=register消费,此时register消费=9,count=10

6.生产者又开始了,执行它的第三步,count=register生产,此时count=11,register生产=11

咦,奇怪了,我们按理来说这个缓冲区进程数量应该是10,为什么实际过程当中却变成了11呢,好,问题暂时留在这里。

2.哲学家进餐模型:
概述:有五个哲学家,他们在一张大圆桌上吃饭,总共有五个叉子,每个哲学家左右手各一个,规定当左右手都拿到叉子时,才能够进餐。
哲学家进餐

两种情况:
理想情况下:
拿左边的叉子->右边没有叉子,等待->右边哲学家释放叉子->拿起右边叉子->进餐

实际特殊情况下:
5个人同时拿起左边的叉子->都发现右边没有叉子->等待->一直没有叉子,都在等待->最终饿死

为什么会这样呢?他们之间没有进行通信,没有互相告诉对方我干了什么,好像有点道理,是会这样发生,为了解决这个问题以及上面的生产者-消费者模型,我们来学习进程同步吧。

进程同步

1.目的:

  • 在多进程进行资源竞争的时候,进行使用次序的协调

  • 使并发执行的多个进程之间能够相互通信,有效的使用资源,有效地进行合作

2.进程同步的原则:

  • 空闲让进:资源没有被占用的时候,允许使用资源

  • 忙则等待:资源被占用的时候,请求进程进行等待

  • 有限等待:进程不会总是等待,而是会在有限的等待时间内能够使用被占用的资源,避免进程像哲学家问题一样等待僵死。

  • 让权等待:等待的时候,进程需要让出CPU,进入阻塞状态

3.实现进程同步的方法:

  • 消息队列

  • 共享存储

  • 信号量
    具体大家可以网上查询资料,博主后面的实践环节也会进行相应的讲解。

4.线程同步:既然进程需要同步,那么作为进程的实际执行者线程也需要同步。

5.线程同步的实现方法(博主后面会细讲):

  • 互斥量:保证多线程互斥共享资源

  • 读写锁:解决多读少写或者少读多写的情况

  • 自旋锁

  • 条件变量

Linux的进程管理

Linux系统当中进程的相关概念

1.进程类型:

  • 前台进程:具有终端并且能够和用户进行交互的进程,就是用户能够在终端上面看到进程的具体执行过程,并且这个进程会占用终端,而使得我们不能在终端中输入命令,除非退出进程

  • 后台进程:不会占用终端,基本上不会和用户交互,优先级比前台进程低,而且具体执行过程在终端上面看不到,如果我们要去结束这个进程,将需要执行的命令以&符号结束。

  • 守护进程:属于特殊的后台进程,大部分是在系统启动的时候就已经运行了,直到系统关闭,一般进程名字以d字符结尾的都是后台进程,比如说crond进程。

2.进程的标记:

  • 进程ID:就和我们之前说过的标识符差不多,它是进程的唯一标识符,每个进程的ID都不一样,一般都是非负整数,至于ID最大值都由操作系统限定
    扩展:
    父子进程:就是进程当中创建进程,前者为父进程,后者为子进程。Linux系统当中一般调用pstree命令查看

特殊进程:ID为0的idle进程,是系统创建的第一个进程,以及ID为1的init进程,是idle的子进程,其完成了系统的初始化,可以说它是所有用户进程的祖先进程了。

  • 进程的状态标记(一般输入man ps命令获取):每个进程的状态都对应一个标记字符,以下是常用的状态标记:
R 表示进程为运行状态
S 进程为睡眠状态
D 进程处于IO等待的睡眠状态
T 进程处于暂停状态
Z 进程处于退出状态

Linux命令:一般输入 ps -aux|grep 对应进程ID来获取对应进程的状态

Linux进程的相关操作命令

1.ps命令:查看进程状态
比如说:

  • ps -aux可以查看进程详细信息
    单独输入ps命令查看的是进程简略消息

  • ps -u 用户名:查看对应用户的所有详细进程

  • ps -aux|grep 进程ID:查看特定ID号进程的信息

  • ps -ef --forest:打印进程的父子状态

  • ps -aux --sort=-pcpu:根据CPU的使用频率来对进程进行排序
    等等命令

至于Linux操作系统命令的相关应用,入门,实战,博主以后也会写博客,到时大家感兴趣可以看看

2.top命令:查看进程的使用内存,进程所有状态:虚拟内存,占用CPU,进程运行时间等等

3.kill命令:发送信号指令给进程。
比如说:kill -9 62016:发送9信号给ID为62016的进程,其中9信号代表无条件停止,此时该进程会停止运行。

作业管理

进程调度

1.概述:计算机会通过决策来决定哪个就绪进程可以获得CPU使用权,因而调度进程。

2.调度步骤:

  • 收拾包裹:保留旧进程运行信息,请出该旧进程。

  • 新进驻:选择新进程,准备运行环境并且分配CPU

就是一个处理起当中新旧进程切换的过程。

3.调度机制:

  • 就绪队列排队机制:将就绪进程按一定的方式排成队列,以便于调度程序可以最快找到就绪进程

  • 选择运行进程的委派机制:调度程序以一定的策略选择就绪进程,将CPU分配给它

  • 新老进程上下文切换机制:保存当前进程的上下文信息,装入被委派执行进程的运行上下文,并且将旧进程的运行环境备份到主存当中(便于旧进程的下一次执行),将新进程环境装入到CPU。

4.调度分类:

  • 非抢占式调度:调度程序不以任何原因抢占正在被使用的处理器,直到进程工作完成或因为IO阻塞才会让出处理器

  • 抢占式调度:允许调度程序以一定的策略暂存当前进程并且保存旧进程上下文信息,分配处理器给新进程

抢占式调度 非抢占式调度
系统开销
公平性 相对公平 不公平
应用 通用系统 专用系统

5.进程调度算法:

  • 先来先服务调度算法:就绪队列越靠前就越容易调度

  • 短进程优先调度算法:选择就绪队列当中运行时间最短的进程进行调度,但是这也不利于长进程的执行,因为这会使得长进程优先级很低

  • 高优先权优先调度算法:进程附带优先权,优先调度权值高的进程,一般越紧迫的进程优先级越高

  • 时间片轮转调度算法:在就绪队列当中按照先来先服务原则,从队首取出一个进程,分配一个时间片执行,在时间片内执行完可还行,但是若没有执行完,则阻塞,继续放入队尾,等待下一次执行,这种方式相对比较公平,但是不能保证及时的相应进程请求

死锁

1.定义:两个或者两个以上的进程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象,如果没有外力作用,那么都会无法推进,互相等待僵死,产生死锁状态。

死锁

就好比T1和T2都在使用资源R1和R2,但是此时T1请求使用R2,T2请求使用R1,但是双方都在占用,不肯放弃,互相等待,因此就产生了死锁状态。

2.死锁的产生:

  • 竞争资源:资源不够或者共享资源不满足各个进程的要求,因此资源相互竞争而产生死锁。就比如上面的示例图,如果多一个R1和R2,那么情况就不一样了,他们就可以直接使用,而不需要等待。

  • 进程调度顺序不当:如果上面的例子是先T1使用R1,然后T1再使用R2,而T2再使用R1,此时T2没有占用资源,T1释放R1后T2占用,然后T1释放R2后T2占用,这种调度顺序就可以避免死锁,而当调度顺序为原来那种的时候,结果就不一样了,因此调度顺序很重要。

3.**死锁的四个必要条件:**只要是死锁,就必然会产生这四个条件。

  • 互斥条件:排他性资源,同时只能够由一个进程使用

  • 请求保持条件:进程至少保持一个资源,并且还会提出新的资源请求,但是新的资源请求又被占用了,产生阻塞,但是这个被阻塞进程得不到别的资源就算了,自己的还是舍不得放弃

  • 不可剥夺条件:获得的资源只能够由进程自己释放,不可以剥夺未使用完成的资源

  • 环路等待条件:必然会形成进程-资源环形等待链

4.死锁的避免:
既然要避免死锁,那么我们只需要破坏其中一个必要条件即可,但是第一个必要条件破坏不了,因为操作系统的资源本就由互斥共享资源。因此破坏后三者之一即可。
预防方法:

  • 破坏请求保持条件:规定进程运行之前一次性申请所有需要的资源,从而使其不再申请新资源

  • 破坏不可剥夺条件:当一个进程请求新的资源得不到满足的时候,必须释放已有资源,

  • 破坏环路等待条件:可用资源进行线性排序,申请资源的时候必须按照递增顺序或者递减顺序申请。

当然,为了解决这种死锁,我们还有一种算法:

  • 银行家算法:以银行贷款策略为基础:
具体过程:
1.客户申请的贷款有限,每次申请都必须声明最大贷款资金

2.银行家如果能够满足贷款条件都应该拨款

3.客户应该及时归还贷款

在进行资源调度的时候,这几个对应过程需要用到三个表,分别为所需资源表(客户需要的资源量),已分配资源表(每个客户已经被分配得到的资源)和可分配资源表(目前还可以分配的资源)
求得每个表以后,所需资源表减去已分配资源表得到新表,之后和可分配资源表进行一一比对,如果能够继续满足所有条件并且分配,则进行资源分配,等待这个进程完成以后释放这些资源,更新表,然后依次按照上述进行分配,直到分配完为止。

上面的算法破坏了死锁的几个必要条件从而避免了死锁,比如说声明最大贷款量就是破坏了请求保持条件,分配以后及时归还破坏了不可剥夺条件,以此来达到避免死锁的效果。

存储管理

内存分配与回收

1.为什么要进行内存分配和回收?(之后博主会相应的写一篇GC回收机制的博客)

  • 确保计算机有足够的内存进行使用(回收)

  • 确保程序可以从可用内存中获取一部分内存(分配)

  • 确保能够归还使用后的内存

内存分配

1.单一连续分配:只能够在单用户,单进程的操作系统当中使用(其中内存分为系统区和用户区用户进程在用户区里面连续分配,系统进程在系统区里面连续分配)

2.固定分区分配:支持多道程序设计,内存会被划分为若干个固定大小的区域,每个分区只能够供一个进程使用,互不干扰。

3.动态分区分配(这个就比较灵活了,也比较常用):
涉及的数据结构:

  • 动态分区空闲表,这个表存储着各分区的标号以及标记,标号是各个分区的ID,标记(0和1)代表着是否被使用,0则未使用,1则以及被占用

  • 动态分区空闲链表:每一个空闲区链表节点都记录着可存容量,其中连续的空闲区可以合并为一个链表节点。

动态分区分配算法:

  • 首次适应算法(FF算法):这个需要使用到空闲链表,在分配内存的时候,遍历空闲链表,但是每次遍历都需要从头部开始,如果说在遍历完毕的时候,还没有找到适当的空闲区,则分配失败,反之则分配成功。但是这种算法会使得头部的很多空闲区地址被不断划分,到时其越来越小,从头到尾遍历的时候,前面的基本不符合要求,但是仍然从头开始,会十分麻烦,效率低下
    因此后面得到了改进:

  • 循环适应算法:从上次检索完的位置开始遍历,这个就弥补了首次适应算法的缺点。

  • 最佳适应算法:把空闲链表按照容量大小排序,一般是从小到大,遍历分配的时候会找到最佳空闲区,因为此时空闲区基本可以恰好满足进程空间,而不会造成大量空间浪费

  • 快速适应算法:这个要求有多个空闲链表,每个链表存储相同容量大小的节点,找的时候就从容量小的链表开始找,如果不满足,则从下一条链表找,直到找到合适的为止

内存回收

内存回收总共有四种情况:

  • 回收区和空闲区连续并且回收区位于后面的时候:只需要把空闲区的容量扩大为(原来空闲区+回收区)的容量即可。

  • 当回收区为与空闲区之前并且二者连续时:不仅需要扩大空闲区容量(原来空闲区大小+回收区),还需要把空闲区地址改为回收区的

  • 当回收区为两个空闲区之间并且三者连续的时候,扩大空闲区容量,地址改为前面一个空闲区的地址

  • 当为单独的回收区的时候:创建新的空闲区节点,存入到空闲区链表中

段页式存储管理

页式存储管理

1.特点:

  • 其将进程逻辑空间等分为若干大小的页面(页面就相当于我们之前听过的字块,只不过前者为逻辑层面,后者为物理层面)

  • 相应的把物理内存空间分成页面大小一样的物理块,即字块。

  • 以页面为单位把进程空间存储在物理内存当中分散的物理块。

注意!!!页面大小的选取必须适当,如果过大则难以分配,如果过小则会造成内存碎片过多(空闲区大于页面的时候,空闲区空间过剩,形成碎片)

2.页表:逻辑空间与物理空间的映射。
页表

页号代表其进程空间在逻辑上的存储,块号代表进程空间在对应物理空间上的存储

3.地址表示:
一般时页号+页内偏移,其实可以类比于字块+字的地址

4.多级页表:也就是说页表中还有页表,其存有页表的地址,当访问的时候,才会开辟对应的空间,这个也是为了避免页表太多而占用大量内存。

5.该存储方式的缺点: 如果说把一段连续的逻辑分布在多个页面当中,就会大大降低存储效率,因为页面的存储是分散的。

段式存储管理

1.特点:这个恰好与页式存储相反,它不是等分,而是按照进程逻辑进行空间分段,哪个逻辑占用空间大,其分配的段空间就大,并且段的长度是有连续的逻辑长度决定。

2.段表:
段表

其中,段号就是指对应的段区域的ID,基址就是段的起始内存地址,段长就是指段的空间大小。

3.地址表示:和页式存储类比,段号+段内偏移

4.对比:

都是离散的管理了进程逻辑空间 页的分配是等分的,而段则是按照逻辑进行空间分配
分页是为了合理利用空间,而分段是为了满足用户需求
页的大小固定,但是段的大小可以变化
页表的信息是一维的,而段表是二维的

段页式存储管理

其就是段式存储和页式存储相互协调进行存储,特点:

  • 逻辑空间分段

  • 段内空间分页

  • 地址为:段号+段内页号+页内地址

虚拟内存

1.概述:
为什么要使用虚拟内存? 这个问题前面将操作系统的虚拟性的时候也提到过,我们细细谈来:

  • 有些进程的实际内存需求很大,但是现实不允许,它的物理内存却比这个小

  • 多道程序设计使得每个进程能够用的物理空间更加稀缺(每个进程都占空间,而多道程序一起运行就更加了)

  • 物理内存总是有不够的时候,我们不可能无限的增加,因此需要使用虚拟内存

如何使用虚拟内存??
把程序的使用内存划分(按照上面所说的段页式管理方法),在这个程序运行的时候,肯定有些内存是暂时不需要的,有些内存是当前运行需要使用的,这个时候,就会将部分暂不使用的内存放入辅存,腾出更多内存给其它进程。
虚拟内存

红色代表的是需要使用的,灰色代表暂不需使用,对于暂不使用的,系统把被其需要使用的存储空间地址转移到辅存,即磁盘当中,当需要的时候,就会访问磁盘当中的地址,然后在对应的内存当中开辟对应地址的空间即可。

2.程序的局部性原理:

  • 程序运行的时候,无需全部装入内存,只需要装入当前需要使用的部分即可

  • 如果说访问页不在内存当中,则会发出缺页中断信号,此时会进程页面置换,即从辅存中置换空间到内存当中

  • 所以说程序才会看起来有很多使用空间,因为有虚拟内存的存在

3.虚拟内存置换算法:
这个算法在博主之前在吃透计算机组成原理中写的缓存置换算法当中已经提到了,博主就不再多提了,这个可以类比缓存置换算法,把之前的缓存-主存置换看成辅存-主存置换即可。

Linux的存储管理

Buddy内存管理算法

1.概述:Buddy内存管理算法主要是用来解决内存外碎片的。以下是具体解释:

  • 内存外碎片:当内存过小,无法进行分配的时候,就会留下一整块空闲区,即内存外碎片

  • 内存内碎片:当内存大于页面请求空间的时候,剩余的空间不能被利用,就产生了内存内碎片

  • 目的:努力让内存分配和相邻的内存段进行合并的过程能够快速进行

2.原则:

  • 在请求内存或者分配内存的时候,尽量从小到大,并且每一个请求大小最好向上取整为2的幂次方的大小,比如说进程请求了70K的空间,此时不是2的幂次方,因此需要向上取整,而70K上面的并且能够为2的幂次方的最小数值为128,因此会分配128K的内存给它。

3.伙伴系统: 一片连续内存的“伙伴”是相邻的另外一片大小一样的连续内存。

4.过程: 假设存储空间为1MB,而且需要分配100K的内存

内存分配过程:
查询过程:
1.100K不是2的幂次方,请求空间向上取整为128K

2.查询是否有128K的空闲内存块,发现没有

3.向上取整为256K,查询是否有256K的空闲内存块,发现没有

4.向上取整为512K,查询是否有512K的空闲内存块,发现没有

5.向上取整为1024K(1M=1024K),查询是否有1024K的空闲内存块,发现刚刚好有

分配过程:
1.把1M内存分配出去

2.发现1M内存大了,向下取整拆分成两个512K两个内存块,留下一个空闲块节点放入到512K的空闲链表中(此空闲链表存储的每个节点容量大小都为512K),其余分配出去

3.发现512K也大了,512K的内存卡拆分为两个256K的内存卡,发现256K可以分配,分配一个出去,另外一个256K的空闲块节点存入到256K容量的空闲链表中(同上)

4.发现256K不满足最小需求,继续拆分为两个128K的内存块,一个存入到128K空闲链表当中,另外一个分配出去,发现128K满足最小需求,因此分配出去,分配过程完毕!

分配完以后,进程使用完内存后会进行回收:

回收过程:
1.判断分配出去的128K内存在空闲链表当中是否有"伙伴",发现有一个容量为128K的空闲块节点,移除该伙伴,并且合并两个内存块

2.合并之后的256K大小的内存块,继续找自己的“伙伴”,发现有,移除伙伴并合并为大小为512K的内存块

3.继续找“伙伴”,发现有,则合并为1024K,即1M的内存

4.继续找“伙伴”,发现没有,回收完毕,把该内存插入到1M的空闲链表当中

这个算法解决了内存外碎片的问题,尽量的让分配的内存满足请求空间的最小要求,但是很多时候会造成分配的空间大于实际需求空间的情况,会产生内存内碎片。

Linux交换空间

1.概述:Linux交换空间实际上是磁盘的一个分区,当Linux物理内存使用爆满的时候,会把一些内存交换到Swap空间,以腾出更多的内存。这个swap空间是初始化系统的时候就已经配置好的。

2.用途:

  • 冷启动内存依赖:一些大的程序在启动的时候,会需要大量的内存,但是有些内存在后面运行的时候,很多都很少使用,因此可以将这些内存放入到swap空间,释放更多的物理内存

  • 系统睡眠依赖:当Linux系统需要睡眠的时候,把系统所有数据放入到Swap空间,便于下次启动

  • 大进程空间依赖:有些进程内存的消耗很大,需要把其它进程内存放入到swap空间当中,以腾出更多的内存供其使用
    由于交换空间是磁盘,速度会比较慢。

当然,对于这个交换空间,我们需要和虚拟内存区别开来

都存在于磁盘当中,并且都是与主存进行置换
交换空间是相对于操作系统而言,解决的是系统空间不足的问题,而虚拟内存是相当于进程而言,解决的是进程物理内存不足的情况

文件管理

操作系统的文件管理

文件的逻辑结构

1.文件类型:

有结构文件 文件内容由定长记录(存储文件格式,文件描述等结构化数据) 和可变长记录组成(文件的具体内容)组成 文本文件,文档,媒体文件
无结构文件 属于流式文件,内容长度以字节为单位,比如说exe,dl,so文件 二进制文件,链接库

2.顺序文件:顾名思义,就是按照顺序存放在存储介质中的文件,比如说磁带文件这种顺序性比较简单,使得存储效率很高,但是和我们所说的数组一样,不方便进行增删减改文件。
因此,针对这个缺点做出了改进:

3.索引文件: 为了解决可变长文件的存储(增删减改),这个需要使用到索引表完成存储。
索引表

索引表当中由键值(key)和逻辑地址(pointer)组成,键值可能就是文件建立的日期,而逻辑地址指向这个日期内建立的文件列表,增删减改只需要在对应键值当中修改地址,或者文件列表当中进行操作即可。

辅存存储空间的分配

1.辅存的分配方式: 分别有连续分配,链接分配(分为隐式链接和显式链接分配)以及索引分配:

  • 连续分配:按内存块的顺序进行分配,读取文件的速度快,但是对于存储文件的要求高,必须得要求其容量满足连续的存储空间,要刚刚好。

  • 链接分配:连续分配的改进版,能够将文件离散地存储,需要额外的存储空间来存储文件盘块链接顺序,即告诉我们它存在哪。

隐式分配 隐式分配的下一个链接指向存储在当前盘块内,文件存储在盘块1,4当中,那么1这存储了指向4盘块的链接,这样子十分适合顺序访问,但是不利于随机访问,如果访问中间某个盘块,还需要从头一个个链接来访问,而且只要任何一个链接出问题了,影响都是整体的,可靠性差
显式分配 会使用到FAT表,FAT表存储了每个盘块的标号和其对应的下一个盘块,这支持随机访问,查表一下子就找到了,但是不支持高效的直接存储,因为FAT表占空间比较大
  • 索引分配:把所有的文件的索引集中存储,每一个文件会离散地存储在多个盘块中,而且每个文件都有一个索引块来记录所有盘块的信息,在这个盘块中可以根据索引直接访问每一个盘块,如果说我们需要读取某个文件的时候,直接读取文件索引就可以了。
  • 索引分配

比如说第十九个盘块存储了其它盘块的位置信息,通过19盘块可以找到索引其它盘块。

2.存储空间管理:分为空闲表,空闲链表,位示图。

  • 空闲表:里面有每一行的序号,每一行存储着第一个空闲盘块号和盘块数,比如说空闲盘块有1,2,3,4这几个连续的,那么这个存储信息就是第一个空闲盘块号为1,空闲盘块数位4.
    空闲表

  • 空闲链表:类比空闲表,每一个节点存储第一个空闲盘块号和盘块数,存储在链表当中

  • 位示图:位示图里面的信息有,每一行都代表这一个磁道,每一行里面存储着磁道号,以及磁道当中的盘块占用状态。
    位示图
    如上图,第一行存储盘块号,第一列存储磁道号,中间存储是否被占用的信息,1代表该盘块已经被占用,0代表空闲盘块。位示图比较好用,维护成本低,占用空间也小,而且还比较容易区找到空闲块。

目录管理

目录管理一般都会使用目录树:
目录树

Linux文件的基本操作

1.Linux目录:以下是Linux系统常用的目录:
Linux目录

这里面也涉及到相对路径和绝对路径:

  • 相对路径:相对于当前操作目录的路径

  • 绝对路径:从根目录开始

2.Linux文件常用操作:

  • touch+文件名:创建文件(记得空格)

  • vim+文件名:创建并且修改文件

  • cat+文件名:读取查看文件

  • rm 文件名:删除文件(可看出remove)

  • mkdir+文件夹名:创建文件夹

  • rm -r 文件夹名:删除文件夹
    3.文件类型:
    一般输入 ls -al查看

套接字 一般前面标记位s
普通文件 前面标记位-
目录文件 前面标记为d
符号链接 前面标记为l
设备文件 前面标记位 c或者b
FIFO文件 前面标记为p

Linux文件系统

1.概览:

FAT文件系统 早期windows系统使用,使用一张FAT表存储文件盘块信息
NTFS文件系统 WindowsNT环境的文件系统,对FAT做出改进,取代
EXT文件系统 扩展文件系统。Linux系统使用,但是这个不能被Windows系统识别

2.EXT文件系统:
1.组成:其由一个Boot Sector和多个Block Group组成。

  • Boot Sector:启动扇区,安装开机管理程序

  • Block Group:块组,用来存储文件系统的数据
    EXT

2.Block Group介绍:
Block group

  • Inode Table:存放文件Inode,每一个文件都有一个Inode,即文件的索引节点,这里面存放着文件信息(有文件类型,权限,物理地址,文件长度,文件存取时间等),但是呢,文件的名字是存放在该文件的目录的Inode节点上面,而不是文件的Inode里面,这个也是为了方便,在列出目录文件的时候无需加载每个文件的Inode,效率非常高。

  • Inode bitmap:Inode的位示图,记录已分配的Inode和未分配的Inode

  • Data block:存放文件内容,每一个block都有唯一的编号,文件的block记录在文件的Inode中

  • Block bitmap:与Inode bitmap相似,用来记录Data block的使用情况

  • Superblock:记录整个文件系统的相关信息,比如说Block和Inode的使用情况,文件系统的时间信息,控制信息等等。

设备管理

IO设备

1.概述:对于CPU来说,凡是对CPU进行数据输入或者输出的设备都是输入设备或者输出设备,这是广义的IO设备。

分类

  • 按照使用的特性来分:
存储设备 U盘,内存,磁盘
交互IO设备 键盘,显示器,鼠标
  • 按照信息交换来分类:
块设备 磁盘,SD卡
字符设备 打印机,shell终端
  • 按设备共享属性:分为独占设备,共享设备,虚拟设备

  • 按照传输速率:分为低速设备,中速设备和高速设备

IO设备缓冲区

1.目的:IO设备缓冲区的存在是为了解决CPU与IO设备速度不匹配的问题

2.说明:

  • 其可以减少CPU处理IO请求的频率

  • 可以提高CPU与IO设备之间的并行性
    就比如说,有多个程序区请求IO设备,此时需要通过多个进程来请求,这个时候信息交换次数会比较多,因此把所有程序需要请求的IO设备的信息都放入IO设备缓冲区,这也IO设备取的时候只需要一次信息交换就欧克了,效率高。

SPOOLING技术

1.概述:SPOOLing技术适用于慢速字符设备和计算机的交互

2.原理:

  • 利用高速共享设备将低速独享设备模拟为高速共享设备

  • 把同步调用改为异步调用
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-snaRqHMW-1583134510452)(SPOOLING.jpg)]
    SPOOLING技术会把所有进程的输入数据和输出数据分别放入到输入井和输出井当中。

3.过程:

  • 在输入和输出之间增加了排队转储环节(即加入输入井和输出井)

  • SPOOLing技术负责输入输出井与低速设备之间的调度

  • 进程直接与高速设备交互,减少等待时间。

拓展

线程同步

线程同步之互斥量

1.原子性:一系列操作不可以被终断,要么全部执行完,要么就不执行

2.互斥量:处于两种状态之一的变量:解锁和加锁,这可以保证资源访问的串行

3.使用:

C语言:
声明互斥量:pthread_mutex_t mutex=PTHREAD_MUTEX_INITIAL;

加锁:pthread_mutex_lock(&mutex); //保证资源的互斥性,期间不准其它线程占用

解锁:pthread_mutex_unlock(&mutex)  //释放锁,资源可以被其它线程使用

python语言:
声明一个互斥锁:lock = threading.Lock() #导入threading包即可(import threading)

加锁:lock.acquire()

解锁:lock.release()

比如说,我们在生产者-消费者模型中,都加一个锁,完成他们的任务之后再解锁,就可以保证正常运行,免受并发的影响。

线程同步之自旋锁

其原理和互斥锁一样
1.概述:

  • 其是多线程同步的变量

  • 使用自旋锁会反复检查锁变量是否可用

  • 其不会让出CPU,而是一种忙等待的状态,除非解锁

2.作用:

  • 避免了新老进程切换的开销

3.使用:

以C语言为例:
定义一个自旋锁:pthread_spinlock_t spin_lock;

加锁:pthread_spin_lock(&spin_lock);

解锁:pthread_spin_unlock(&spin_lock);

使用前可以初始化:pthread_spin_init(&spin_lock);

线程同步之条件变量

1.概述:

  • 允许线程睡眠,直到满足所需条件

  • 满足条件的时候,会发出信号唤醒线程

2.生产者-消费者模型为例:

  • 缓冲区进程数量小于等于0的时候,消费者不允许消费

  • 缓冲区满的时候,生产者不允许生产

  • 生产者每生产一次,就发信号唤醒可能等待的进程(消费者)

3.使用(一般和互斥锁一起使用):

以C语言为例:
定义互斥锁: pthread_mutex_t mutex;
加锁
定义条件变量:pthread_cond_t = cond;

睡眠:pthread_cond_wait(&cond,&mutex);

唤醒发通知:pthread_cond_signal(&cond)

通知完就解锁

使用fork函数系统调用创建进程

1.概述:

  • fork创建进程初始化状态,此时该进程相当于子进程,调用这个函数的相当于父进程

  • fork创建完进程就分配资源

2.函数说明:

  • 该函数没有参数,直接调用即可

  • 调用以后会返回子进程id和0,前者是父进程返回的,后者子进程返回

进程同步

进程同步之共享内存

1.概述:

  • 多进程共享物理内存

  • 逻辑内存空间是独立的

  • 允许不相关的进程访问同一片物理内存,而这一片内存可以通过页表映射到不同的进程

  • 共享内存是两个进程之间进行共享和传递数据最快的方式

  • 共享内存不会提供同步机制,需要通过外力或者其它机制进行管理

2.步骤:

  • 申请共享内存

  • 连接到进程空间

  • 使用共享内存

  • 脱离进程空间并且删除

3.使用举例:

假设客户端需要和服务端使用共享内存进行通信
那么我们需要一个共享内存的数据结构(外力机制)
定义一个共享内存的结构体:
struct shareEntry{
bool can_read;    //是否可以访问共享内存,用于进程间的同步
char msg[2048];    //共享内存的信息
}

进程同步之Unix域套接字:一般使用与网络当中

1.过程:
对于服务端来说:

1.创建套接字(socket)

2.绑定套接字

3.监听套接字

4.接收并且处理信息
以python代码为例:
def server():
    # 创建socket
    s = socket.socket()
    host = "127.0.0.1"
    port = 6666
    #绑定套接字
    s.bind((host,port))
    #监听套接字
    s.listen(5)
    while True:
        c,addr = s.accept()
        print('Addr: ',addr)
        c.send(b'Welcome to my course.')
        c.close()

以客户端为例:

1.创建套接字

2.连接套接字

3.发送消息
以python为例:
def client(i):
    s=socket.socket()                 #创建
    s.connect(('127.0.0.1',6666))     #建立连接
    print('Recv Mesg:%s,Client id:%d'%(s.recv(1024),i))
    s.close()

实践

任务要求:
用户布置多个普通任务,这些任务都需要进入到任务队列当中进行排队,当线程池需要拿走这个任务,并且用一个线程来执行的时候,任务队列会出队一个任务到线程池当中执行,此时执行完用户是不知道任务结果的。
如果说用户还需要获取任务结果的话,那么它就需要提交异步任务到任务队列,线程池取出执行的时候,就能够使用异步的方法获取结果。

实现一个线程安全的队列

要求:

  • 该队列的方法至少有三个:获取当前数量,入队,出队操作

  • 当多个线程同时访问队列元素的时候,为了保证多个线程获取的资源是串行的,避免产生死锁,因此需要使用锁进行保护。

  • 队列为空的时候,会进行阻塞(使用条件变量,等待睡眠)
    代码示例:

# ! _*_ encoding=utf-8 _*_
import  threading
import time
#定义线程安全的队列
class ThreadSafeQueueException(Exception):   //抛出异常
    pass
class ThreadQueue(object):
    def __init__(self,max_size=0):
        self.queue = []                     #队列
        self.max_size= max_size
        self.lock = threading.Lock()         #互斥锁
        self.condition = threading.Condition()    #条件变量

    def size(self):                 #获取队列元素数量
        self.lock.acquire()          #加锁
        size = len(self.queue)
        self.lock.release()          #解锁
        return  size
    def put(self,item):            #加入一个元素到队列当中
        if self.size()>self.max_size and self.max_size!=0:    #若发生异常
            return ThreadSafeQueueException
        self.lock.acquire()
        self.queue.append(item)
        self.lock.release()
        self.condition.acquire()
        self.condition.notify()                #加入一个元素就通知其它线程告诉他们队列当中有元素
        self.condition.release()
    def put_list(self,item_list):            #加入一组元素
        if not isinstance(item_list,list):              #判断是否为list
            item_list=list(item_list)
        for item in item_list:
            self.put(item)
    def pop(self,block = False,time = None):                #取出队列元素
        if self.size() ==0:
            if block:                                 #判断是否需要阻塞
                if time == None:
                    time = 0
                self.condition.acquire()
                self.condition.wait(timeout=time)      #阻塞等待
                self.condition.release()
            else:
                return None
        # if self.size() ==0:
        #     return  None                           #但是可能现在size为0,但是由于是多线程,下一个转台任务队列可能也为0,被取走了,
        #也就是说44到46行之间,尽管可能判断size不为0,但是这一瞬间可能就被取走了,为0。
        self.lock.acquire()
        item = None
        if len(self.queue)>0:
            item = self.queue.pop()



        self.lock.release()
        return item

    def get(self,index):           #获取某一个位置的元素
        self.lock.acquire()
        item = self.queue[index]
        self.lock.release()
        return item
if __name__ == '__main__':
    queue = ThreadQueue(max_size=100)
    def producer():             #生产者
        while True:
            queue.put(1)
            time.sleep(1)
    def consumer():             #消费者
        while True:
            item = queue.pop(block=True,time=2)
            print('from queue: %d'%item)
            time.sleep(1)

    thread1 = threading.Thread(target=producer)            #绑定对应线程
    thread2 = threading.Thread(target=consumer)
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()

实现基本任务对象Task

要求:
Task类里面必须有任务参数,唯一标记以及这个任务的执行逻辑:
python代码示例:

import  uuid
import threading
class Task:
    def __init__(self,func,*args,**kwargs):    #func代表任务具体逻辑,使用函数引用传递进来
        self.id = uuid.uuid4()      //利用uuid得到任务id
        self.callable = func        //Task具体逻辑,通过函数引用
        self.args = args
        self.kwargs = kwargs
    def __str__(self):
        return 'Task id:' + str(self.id)      //该方法可以返回任务id

实现异步任务对象

这个继承Task类,之前Task使用的属性异步任务对象都需要,但是因为要获取任务结果,因此需要定义一个结果变量,而且当任务还没结束,也就是说结果还没有的时候,不能获取结果,只能等待,如果结果出来了才可以获取,此时需要用到条件变量
代码示例:

#异步任务对象
class AsyncTask(Task):                           #继承Task
    def __init__(self,func,*args,**kwargs):
        self.result = None     #任务结果
        self.condition=threading.Condition()     #条件变量
        super().__init__(func,*args,**kwargs)       #调用Task的构造函数
    def set_result(self,result):                    #加入任务结果,通知线程进行相应的获取
        self.condition.acquire()                   #加锁
        self.result=result          
        self.condition.notify()                    #通知
        self.condition.release()                   #解锁
    def get_result(self):
        self.condition.acquire()
        if not self.result:
            self.condition.wait()                 #没有结果就睡眠,直到满足条件为止
        result = self.result
        self.condition.release()
        return result


实现任务处理线程

要求:

  • 从任务队列当中不断取任务执行,即需要传入任务队列

  • 需要一个标记,标记线程什么时候结束,以便于获得结果

  • 需要线程处理逻辑

代码示例:

import  threading
import psutil
from operate_system.Task import Task,AsyncTask
from operate_system.Queue import ThreadQueue
class ProcessThread(threading.Thread):
    def __init__(self,task_queue,*args,**kwargs):
        threading.Thread.__init__(self,*args,**kwargs)
        self.task_queue = task_queue         #任务队列
        self.args = args
        self.kwargs = kwargs
        self.dismiss_flag = threading.Event()         #任务线程停止标记
    def run(self):
        while True:
            if self.dismiss_flag.is_set():            #如果停止
                break
            task = self.task_queue.pop()
            if not isinstance(task,Task):
                continue
            result = task.callable(*task.args,**task.kwargs)    #通过函数调用执行函数逻辑,获得任务结果
            if isinstance(task,AsyncTask):       #如果任务是异步任务,需要加入任务结果
                task.set_result(result)
    def dismiss(self):
        self.dismiss_flag.set()
    def stop(self):               #结束线程
        self.dismiss()

实现线程池

线程池介绍

1.定义:线程池是存放多个线程的容器,CPU在线程池当中调度线程执行完毕以后不会进行销毁,而是把线程放入到线程池当中继续使用,避免不必要的开销。

2。为什么要使用线程池:

  • 线程是稀缺资源,不应该频繁创建销毁(涉及到上下文新老线程切换),这也太消耗资源和空间了

  • 架构解耦,线程池能够使线程的创建和业务处理解耦,也就是说,我这个线程不是我想执行这个任务的时候才创建,而是提前创建好,使程序和服务更加优雅

  • 其是使用线程池的最佳实践,像阿里巴巴的Java黄金手册里面明文要求必须使用线程池,说明大公司很看重这个,这个可以减少开销,而且效率高。

代码实现

class  ThreadPool:
    def __init__(self,size = 0):
        if not size:                        #如果size=0
            size = psutil.cpu_count()*2              #约定线程池的大小位CPU核数的两倍
        self.pool = ThreadQueue(size)                 #大小为size的线程池
        #任务队列
        self.queue = ThreadQueue()                    #任务队列
        for i in range(size):
            self.pool.put(ProcessThread(self.queue))     #往线程池当中加入任务处理线程


    def start(self):                             #线程池开始执行
        for i in range(self.pool.size()):
            thread = self.pool.get(i)
            thread.start()                    #执行线程
    def put(self,item):                    #往线程池当中提交任务
        if not isinstance(item,Task):
            raise TaskTypeErrorException        #抛出异常
        self.queue.put(item)                     
    def batch_put(self,item_list):         #批量提交
        if not isinstance(item_list,list):
            item_list=list(item_list)
        for item in item_list:
            self.put(item)
    def size(self):
        return self.pool.size()


    def join(self):                           #结束线程池
        for i in range(self.pool.size()):
            thread = self.pool.get(i)
            thread.stop()
        while self.pool.size():
            thread = self.pool.pop()
            thread.join()
class TaskTypeErrorException(Exception):
    pass

综合

代码测试:

# ! _*_ encoding=utf-8 _*_
from operate_system import Task,pool     #引入我们之前写的Task,pool文件
import time
class SimpleTask(Task.Task):             #任务对象
    def __init__(self,callable):
        super(SimpleTask,self).__init__(callable)
def process():                            #任务逻辑
    print('This is a simple task')
    time.sleep(100)

def test():                               #基本任务测试
    #1.初始化一个线程池
    test_pool = pool.ThreadPool()          #创建线程池
    test_pool.start()                      #执行线程池

    #2.生成一系列任务
    for i in range(10):
        simple_Task=SimpleTask(process)        #加入逻辑,并且生成任务
        test_pool.put(simple_Task)              #提交任务并且执行
    #3.往线程池提交任务并且执行
    pass
def test_AsyncTask():                          #异步任务测试
    def Async_process():                       #任务逻辑
        num=0
        for i in range(100):
            num+=1
        return num
    # 1.初始化一个线程池
    test_pool = pool.ThreadPool()
    test_pool.start()

    # 2.生成一系列任务
    for i in range(10):
        Async_Task = Task.AsyncTask(func=Async_process)      #生成异步任务
        test_pool.put(Async_Task)                            #提交任务
        result = Async_Task.get_result()                       #获取结果
        print('get result:%d'%result)
    # 3.往线程池提交任务并且执行
def test_AsyncTask2():          #测试是否等待
    def Async_process():
        num=0
        for i in range(100):
            num+=1
        time.sleep(1)
        return num
    # 1.初始化一个线程池
    test_pool = pool.ThreadPool()
    test_pool.start()

    # 2.生成一系列任务
    for i in range(10):
        Async_Task = Task.AsyncTask(func=Async_process)
        test_pool.put(Async_Task)
        print('time start:%d'%time.time())
        result = Async_Task.get_result()
        print('get result:%d:%d'%(time.time(),result))
    # 3.往线程池提交任务并且执行
if __name__ =='__main__':
    # test()
    # test_AsyncTask()
    test_AsyncTask2()
    pass

完结

操作系统的讲解到此完毕,这是博主自学操作系统时的心得,理解和总结,对于操作系统,我们还必须了解的就是Linux操作系统了,对于Linux操作系统,博主到时也会出一篇详细的讲解博客,欲知下回如何,敬请期待!
最后,欢迎大家关注我的个人博客,在下所有的文章都是从个人博客导入进来的,文章会首先更新在个人博客里。希望我的博客能够给大家带来收获!

发布了22 篇原创文章 · 获赞 27 · 访问量 2844

猜你喜欢

转载自blog.csdn.net/weixin_44346470/article/details/104612775