哈工大李治军老师的操作系统学习笔记

文章目录

1 什么是操作系统

操作系统是计算机硬件和应用之间的一层软件。其分别对以下硬件进行管理:CPU管理、内存管理、终端管理、磁盘管理、文件管理、网络管理、电源管理、多核管。本课程不涉及后三个的内容。
计算机如何工作? 四个字——取指执行。控制器从存储器中取出数据后,分析指令,运算器执行逻辑运算。
在这里插入图片描述

2 操作系统启动

汇编语言编写的文件以.s为结尾。
其实就做了两件事情:第一件事是读入系统;第二件事就是setup完成OS启动前的设置(读取硬件信息等后跳转至sysytem模块)。
在这里插入图片描述
system最开始是一个main.s ,随后在汇编里面跳转到main.c(没错,是c语言)永不退出。
【启动的流程】将操作系统的程序,从硬盘读到了内存中从0地址开始的地方,随后对系统进行初始化(大多与硬件关联)。而随后使用的应用程序,都将放在内存的上段。而其上段的应用如何调用硬件,这就是下节所讲的《接口》了

3 操作系统接口

操作系统接口是:连接上层用户和操作系统软件

在这里插入图片描述

命令行发生了什么?

在这里插入图片描述

首先应用程序编写的程序将编译成一个可执行文件。而与此同时,系统在刚开始的初始化完成后,会循环停留在shell里(可以理解为桌面,不断等你施加命令),当用户输入命令行指令后,系统将运行上面的那个可执行文件。
在这里插入图片描述

图形按钮怎么回事?

在这里插入图片描述
由getmessage函数把消息从内核的队列中抽出来,然后根据消息调用消息处理函数,做相应的反应。

操作系统接口(系统调用)

连接谁?连接操作系统和应用软件,注意是下图红点的地方,并不是直接与硬件交互了。
如何连接?C语言程序
所以,操作系统提供这样的重要函数,表现为:函数调用,所以又称为系统调用system_call。
在这里插入图片描述
常用的接口有:
在这里插入图片描述

4 操作系统调用

不应该随意访问内核

应用程序是不可以随意地调用内核的数据,不可以随意jmp。这会导致安全和隐私问题,如:可以看到root密码,可以修改root密码,可以通过显存的缓冲看到别人的东西等。而操作系统的调用便正好提供了能够合理进入内核的一种手段。

怎么不让你访问内核

通过处理器的“硬件设计”来防止你访问。它把非内核的和内核的东西划分成了用户态和内核态。因此对应的内存中的区域叫用户段和内核段。
在这里插入图片描述

不让我访问怎么办?

硬件提供了“主动进入内核的办法”
对于intel x86,那就是中断指令int,这是用户程序发起的调用内核代码的唯一方式。因此系统调用的核心:

  1. 用户程序包含一段包含int 0x80指令的代码(c语言库函数)
  2. 操作系统写中断处理,获取想调程序的编号
  3. 操作系统根据编号执行相应的代码

在这里插入图片描述
整个过程可详细展开为:
在这里插入图片描述
其中,调用第二个框时,CPL=3,其DPL也会初始化成3,所以才可以进入内核,随后CPL就置成0了,因为进入内核进行了。

5 操作系统学习目标

下图中的CPU管理和内存管理又统称为进程View。所以一共两个大方面。
在这里插入图片描述

6 CPU管理的直观想法

操作系统正是在管理CPU的时候,引出了多进程图像(操作系统中的核心图像)这个概念。

CPU的工作原理

工作过程: 将一段程序存放在内存里,设置PC指针的地址,CPU会根据PC指针发出取址的命令,命令通过总线到达内存,内存将PC地址上的指令再通过总线传回给CPU。CPU看到该指令后,CPU便开始执行该指令。

1、CPU上电之后,会去自动的取址—执行。只需要给CPU设置一个PC初值 (一个程序的开始地址),他就会不断的去取址执行。

【但这样存在一个问题】例如,普通的sum计算和io操作的指令间耗时差别十分巨大,比例视频中说的大概是六百万比一,如果传统的取址,执行完了再取址,那么CPU等待时间长,从而利用率低。
2、于是CPU就会在多道程序(多个程序在内存中)里交替执行从而提高时间。
一个CPU上交替地执行多个程序:并发
在这里插入图片描述

“进程”的概念

在并发的过程时,注意不仅仅修改PC寄存器就可以,需要记录信息(切出去的时候,这个程序执行到哪里,执行时刻的样子)。因此静态程序和运行的程序不一样(不运行时,就那么多字,但一旦运行起来,随时准备记录切出去前的样子)。
在这里插入图片描述
进程是进行(执行)中的程序,假设一个程序A在进行的时候会切换到程序B,而B执行的时候又会切换到程序A,那么代表有两个进程。
【进程与程序的区别】
1、进程有开始有结束,程序没有
2、进程走走停停,走停对程序没有意义
3、进程需要记录ax等一些寄存器,程序不用

7 多进程图像

什么是多进程图像

(https://img-blog.csdnimg.n/d7e741bea0d64bd18b3d2adc854f2b24.png)]

图中,负责记录好进程的便是PCB1,其为process control block进程控制块。
多进程图像从启动开始到关机结束
在这里插入图片描述

多进程如何组织?——PCB+状态+队列

操作系统对多进程的感知组织全靠PCB。下面以一个cpu执行只可以执行一个进程为例:
在这里插入图片描述
因此,我们可以根据进程的状态把不同的进程区分开来,而利用这些状态就可以更好的管理进程:
在这里插入图片描述

多进程如何交替?——队列操作+调度+切换

依靠一个函数,叫做schedule(),根据调度(下面会专门抽出一讲来讲调度的方法)取出来下一个要切换的进程,随后与当前进程进行switch切换(切换时,CPU会将当前进程的信息保存在相应的PCB中)即可。

多进程的相互影响怎么处理?

由于多个进程会同时存在内存中,此时他们就有可能会互相影响。例如进程1访问的地址,可能存放有进程2的代码,因此有可能进程1会修改进程2的代码从而导致进程2执行时发生错误。
解决办法:限制对地址的读写(会通过每个进程的映射表,将部分地址映射出去)
多进程的地址空间分离内存管理的主要内容
在这里插入图片描述

多进程如何合作?

核心在于进程同步(合理的推进顺序)不能进程之间随意切换,会导致执行错误,应该规定好合理的推进时机。
(通过count上锁、开锁和检查锁,之后会详讲)

总结

在这里插入图片描述

8 用户级线程

进程有PCB而线程有TCB
像映射表就是属于资源,如果进程之间切换,那么其映射表也会发生变化,而如果一个进程里面多个指令切换(也就是指令),那么就可以共享同一个映射表(也就是资源)。因此这属于将指令和资源执行所分开。
所以我们可以先理解好线程(thread)的切换(也就是指令的切换),之后再加上资源的切换(学完内存),就顺利成章地成为了进程的切换
在这里插入图片描述
【举例】
在这里插入图片描述
因为他们共享一个资源,因此也是线程而不是进程的一个原因之一。下图为浏览器中发生的事情:
在这里插入图片描述
其Create(需要创造出第一次切换时应该的样子)之后,就靠yield来交替切换执行。那么核心其实就是Yield(要知道切换时需要是个什么样子)。

先看Yield()做了什么

但是,正常的压栈取栈会发生问题,如下图,执行到最后会发现,其最终返回至404了,也就是说,跑到人家线程里去了。
在这里插入图片描述
因此两个线程不可以共用一个栈,因此多个进程多个TCB(线程控制块
)(里面存放多个栈),而Yield切换要先切换栈(靠TCB),然后就直接弹栈,开始执行另一个线程。但仅仅靠这个依旧不够,如下图,当B函数执行完毕后,204弹出堆栈后,又开始执行204;
在这里插入图片描述

再看看ThreadCreate做了什么

由上面可知,两个线程的样子:两个TCB、两个栈、切换的PC在栈中
而ThreadCreate的核心就是用程序做出这三样东西。申请栈、申请TCB
、func等入栈、关联TCB和栈

为什么说是用户级线程

因为Yield是用户程序。它是用户程序里面的线程不断切换。其缺点为:
因为线程都是在用户段的,其内核感知不到有多个线程。当浏览器整个进程在调用网卡等待的时候,内核会自动切换到下一个进程。因此一旦内核阻塞,其用户部分的多线程根本没有并发性的效果。
在这里插入图片描述
而核心级线程便可以通过系统调用,让内核也知道TCB,从而保持并发性。
在这里插入图片描述

9 核心级线程

其实现在处理器的多核能工作,都依靠核心级线程。由下图也可以知道,多核处理器中共用一个MMU,这正是符合线程共享资源的定义。
在这里插入图片描述
而正是因为核心级线程,一个进程被分成多个线程,从而通过OS分配给CUP的多个核心。因此用户级线程和进程都没法发挥多核处理器的特点。

与用户级线程有什么不同–两个栈?两套栈?

为什么说一套栈,因为核心要在内核中跑,因此需要内核栈,而在用户态执行代码,因此也需要用户栈!用户级线程是切换时,切换一个栈;而核心级线程在切换时,是切换一套栈。
在这里插入图片描述

用户栈和内核栈之间的关联

一旦出现了中断进入了内核,便会启用内核栈。此时计算机硬件寄存器会根据用户栈找到其对应的内核栈,并会将用户程序和用户栈的一些寄存器值压入内核栈。当结束中断时,此时通过取出压栈的内容,从而恢复至用户态执行。
在这里插入图片描述
随后,内核执行时便也会发生切换。但毕竟最终还是再用户态执行,因此需要最后再切到相应核心栈对应的用户程序(下图中????的位置应该是iret的代码,从中断再返回到用户态)

在这里插入图片描述

内核线程switch_to的五段论(内核线程切换过程)

两套栈的切~
在这里插入图片描述

1、用户态执行程序时,为了切换,引发中断
2、启用中断后,内核栈与用户栈联动,进入内核执行。
3、在内核找到TCB,随后切换TCB
4、根据新的TCB去切换内核栈
5、新的内核栈再根据中断返回,返回到新的用户态

在这里插入图片描述

ThreadCreate做什么

申请内存地址、创建TCB、创建内核栈和用户栈、关联栈和TCB。随后初始化内核栈和用户栈。

用户级线程和核心级线程的对比

灵活性表现在:用户级线程可以自己书写调度算法,灵活性高。而核心级线程只能用OS中规定的,无法随意改动。
在这里插入图片描述

10 内核级线程实现过程

根据五段论去分析:
1、第一步,最后的system_call来存储各种中断退出时的状态。
在这里插入图片描述
2.3.4、当其状态阻塞或者时间片不够的时候,则会发生schedule,内核栈便开始发生改变。
在这里插入图片描述

5、最后一段就是中断返回,将压入内核栈和system_cal中存储的各种状态进行pop返回。

11 CPU调度策略

引出问题

在这里插入图片描述
并且,在系统中,很多任务之间是会出现矛盾的,因此需要折中和综合下面的任务!
在这里插入图片描述

四种基本的调度算法

First Come,First Served(FCFS)

哪个任务先来,就先服务哪个任务。这个很简单但是同样其平均周转时间也较长

SJK短作业优先算法—周转时间最短

即cup区间短的作业优先执行,这样可以早早完成而不用再去周转这个作业。可以证明,该算法可以达到最短的周转时间。但与此同时,响应时间会变长,也许你一开始点了个鼠标,但由于你的cpu区间长,因此排到最后执行,实在难受。

RR(round robin)按时间片来轮转调度

在这里插入图片描述
但是时间片仍是一个都视为一体的算法,对于Word很关心响应时间,而gcc更关心周转时间,两类任务同时存在怎么办?

优先级调度

首先,若根据下图这样直接根据想法定义(绝对优先),则会出现有的后台任务一辈子也执行不了,因为可能前台任务一直是存在的。
在这里插入图片描述
因此,后台的任务优先级应该动态升高,并且为了照顾到各自的响应时间情况,应该前后台任务都用时间片。下文将介绍一个完整的不错的调度算法。

12 实际的schedule函数(linux 0.11调度函数)

该算法综合了counter的优先级技术和其时间片技术

counter的作用:时间片

在时间中断中counter每次都–,当减位0时,就进行调度。所以counter时典型的时间片,所以是轮转制度,保证了响应。

counter的作用:优先级

每次在找任务时,都是找counter最大的任务调度,因此counter也表示了优先级。
在这里插入图片描述
由上面的代码也可以看出来,当目前的counter都执行完毕后,执行完的会再赋初值,而未执行的也累加初值,从而造成了阻塞态的counter不断变大,从而完成动态调整。这样,属于前台特征的I/O需要等待的时候切出去,等就绪后,其优先级一定比正常的CPU约束型的优先级要高了!

总结

【对第一条进行解释】其时间片最长也就是2P,所以是可以保证有限时间的。也就是上面为什么除以2,而且右移一位对于硬件运算来说是非常快的(其实除以几都ok,但是右移动一位最快,而右移动一位正好是除以2)。
【第三条解释】其实时间片会不断轮转,这样还是短作业的先完成,因此近似SJF。
在这里插入图片描述

13 进程同步与信号量

进程同步—让多进程之间进行地合理有序,其依靠的工具就是信号量
【进程合作】多进程共同完成一个任务。以下面的“生产者——消费者”为例。
其中“停”是关键!
在这里插入图片描述
但是,只发信号还不能解决全部问题,可见下图。下面会导致P2永远无法被唤醒,即单纯依靠counter这种语义判断是不够的,因为它不知道到底有几个生产者在睡眠。
在这里插入图片描述
因此引出了信号量的概念,不可以简单的等待信号和发信号,而是应该记录一些信息!因此信号量——记录一些信息(量),并根据这个信息决定睡眠还是唤醒(信号)。因此,引入信号量后:
在这里插入图片描述
看到信号量是负的,代表在等待资源,因此此时可以wakeup(包括信号为0)(产生资源,其加1)也可以sleep(消耗资源,其减1)。看到信号量是正的,便无需操作,可以运行。这里不要代入生产者和消费者的具体身份,而是生产也需要资源,消费也需要资源这样看待!具体理解可以看下面的题目:
在这里插入图片描述

14 信号量临界区保护

为什么要保护信号量

由于共同修改信号量,就有可能前段消费者修改的信号量还没完毕,便因为调度算法而导致切到其他消费者处理,从而造成共享数据语义错误(即信号量混乱)。

  1. 错误由多个进程并发操作共享数据引起
  2. 错误和调度顺序有关,难于发现和调试

竞争条件:和调度有关的共享数据语义错误。

如何解决竞争条件

直观想法就是:一段代码一次只允许一个进程进入!在写共享变量时,组织其他进程访问。
在这里插入图片描述

保护信号量的核心——临界区

【定义】一次只允许一个进程进入的该进程的那一段代码。因此,通过读写信号量的代码一定是临界区来保护信号量!下面将对“上锁”和“开锁”过程的代码如何书写做详细介绍:
【基本原则】互斥进入:如果一个进程在临界区中执行,则其他进程不允许进入。
这些进程间的约束关系称为互斥。这保证了是临界区。
【好的临界区保护原则】
有空让进:若干进程要求进入空闲临界区时,应尽快使一进程进入临界区
有限等待: 从进程发出进入请求到允许进入,不能无限等待

如何进入临界区

轮换法】一人进去一会,轮番进入。但问题是不满足“有空让进”:P0完成后不能接着再次进入,尽管进程P1不在临界区。
在这里插入图片描述
标记法
在这里插入图片描述
但问题是可能进入无限循环,当执行顺序如下图时:
在这里插入图片描述非对称标记法】进入临界区Peterson算法
结合了标记和轮转两种思路:
在这里插入图片描述
验证:
满足互斥进入:如果两个进程都进入,则flag[0]=flag[1]=true,turn01,矛盾!
满足有空让进:如果进程P1不在临界区,则flag[1]=false,或者turn=0,都P0能进入!
满足有限等待:P0要求进入,flag[0]=true;后面的P1不可能一直进入,因为P1执行一次就会让turn=0。
【当遇到多个进程时】面包店算法——仍然是标记和轮转的结合
如何轮转: 每个进程都获得一个序号,序号最小的进入。
如何标记: 进程离开时序号为0,不为0的序号即标记

简单的进入临界区方法

上面的有些复杂了!上面是纯软件,所以越来越麻烦,而下面介绍的两个方法,将结合硬件,使其方法尽可能简化:
关中断】因为中断,才会发生调度,从而使得出现竞争条件!因此我们可以通过开关中断来实现开锁、关锁。但问题是在多核CPU的环境下不好用,因为关中断也只能关掉当前进程的中断,其他的CPU是不理会的,也就说其他的进程有可能再引起竞争条件!
硬件原子指令法】通过一个原子指令实现一个1的锁信号量,用其修改一个整型变量,根据这个变量,再来判断要不要进入临界区。因此,修改这个变量的过程要求一步完成(硬件帮忙),中间绝不能被打断
在这里插入图片描述

15 死锁处理

当形成环路等待时,将会使越来越多的进程和资源都阻塞,随后没程序可执行了,导致计算机不工作了。
【定义】多个进程由于互相等待对方持有的资源而造成的谁都无法执行的情况叫死锁

死锁的四个必要条件

在这里插入图片描述

处理方法之死锁预防(破坏死锁出现的条件)

举两个例子:
1、在进程执行前,一次性申请所有需要的资源,不会占有资源再去申请其它资源(缺点:资源利用率低、编程困难)
2、对资源类型进行排序,资源申请必须按序进行,不会出现环路等待(缺点:仍造成资源浪费)

处理方法之死锁避免(检查资源请求,如造成死锁则拒绝)

这里有个著名的算法:银行家算法(Dijkstra提出)
【安全状态】如果系统中的所有进程存在一个可完成的执行序列P1,…Pn,则称系统处于安全状态 。
【算法过程】其算法如下:就是不断对比工作向量Work和需要的资源量,分配后再加进程结束后归还的已分配资源,再去比较下一个需要分配的资源类,查看是否可以找到一个安全状态的可执行序列。(m是资源个数,n是进程个数,因此时间复杂度是较高的)
在这里插入图片描述
如何使用算法】当请求出现时,首先假装分配,随后调用银行家算法,若无法得到安全状态,则拒绝请求。

处理方法之死锁检测+恢复(若出现死锁,让一些进程回滚,让出资源)

上面的死锁检测比较繁琐,效率低。因此定时检测或者是发现资源利用率低时检测。当发现死锁时,需要回滚,但回滚是十分复杂的!

处理方法之死锁忽略

死锁出现不是确定的,又可以用重启动来处理死锁。因此,大多数非专门的操作系统都用它,如UNIX,Linux,Windows。

16 内存使用与分段

内存使用中的重定位

内存使用:将程序放到内存中,PC指向开始地址。
【重定位是什么?】在内存使用的过程中,当遇到指令call40(40正好是是main函数)时,理论上,存放main函数的物理地址就要是40,但是这样很明显可能与其他程序中的40等(毕竟os就放在低位的地址上)造成冲突,因此应该找个空闲的地址开始存放,同时把40当作一个逻辑地址,同时配合找到的空闲地址,将该逻辑地址修改为物理地址(这个过程就是即重定位)。
【什么时候做重定位】编译时(但由于编译和执行时,一个地址是否空闲不确定,因此这个时候只适用一些静态系统)、载入时(大多数机器,载入时才吃重定位,较灵活)
【两个重定位时候的区别】
编译时重定位的程序只能放在内存固定位置
载入时重定位的程序一旦载入内存就不能动了

运行时的重定位——重定位最合适的时机

内存的进程并不是一成不变的,因为进程之间的交换,所以内存也在发生着交换。而换入换出这个过程,可能就导致之前是空闲的地址,现在又不是了。因此,程序载入后还需要移动,仅仅载入时重定位也不够!
在这里插入图片描述
因此,在运行每条指令时才完成重定位(地址翻译)。
在这里插入图片描述
那么这个base放在哪里呢?这个base应该与进程相捆绑,因此放入PCB(描述进程的一个数据结构)中,执行指令时第一步先从PCB中取出这个基地址。(进程的换入换出时便会寻找空闲的地址,并将该地址放入其PCB中的base,同时,将这段程序放入这段空闲的内存地址。每次执行指令时,都需要进行地址翻译。因此无须担心前面中进程的换入换入而地址却不变)
【总结图】
这个图有点混乱,解释一下:首先当PC指向进程1的mov指令时,CPU实际操作的物理地址为100+进程1PCB中的base2000,而为2100。当发生进程间的切换时,PC指向进程2的mov指令时,CPU实际操作的物理地址为100+进程2PCB中的base1000,而为1100。
在这里插入图片描述

分段——不是整个程序都放入内存

【引言】在我们程序员编程时,每段程序都有各自的特点、用途:代码段只读,代码/数据段不会动态增长…我们以一种“分治”的思想来独立考虑每个段。
因此,为了让内存更高效地使用,不是将整个程序,是将各段分别放入内存
在这里插入图片描述
而此时,刚才地PCB可以放整段的base,而此时的PCB需要放各个段中的base。因此便引出进程段表的概念:
在这里插入图片描述
而操作系统的进程段表为GDT,而每个进程的进程段表为LDT。因此整个过程可以这样讲:
1、一个程序分成多个段,每个段在内存中找到一段空闲的地方,并把该地址的基址放入LDT表中,同时该段程序放入这个空闲内存中。
2、每段都搞定之后,整个程序相当于放入内存中了,随后这个LDT表赋给PCB
3、然后PC指针根据PCB设置好其实位置,然后取址执行,每次执行都查LDT表,找到程序中对应的物理地址。
在这里插入图片描述

17 内存分区与分页

由上面内容可知,内存管理一共三步:
1、在程序编译时,分成多个段。
2、在内存中找一段空闲区域(这个就是本节需要讲的内容)。
3、找到空闲区域后,将其读入到内存的这个地方,并做好映射关系。(怎么读是到后面磁盘目录什么的才讲的,而映射关系已经讲明白了)

内存如何分割?——内存分区(虚拟内存常用)

【固定分区】等分,操作系统初始化时将内存等分成k个分区。然后需求不一样,因此这种方式不行!
【可变分区】首先需要维护其核心的数据结构(即记录信息),分成两个表:
在这里插入图片描述
当出现请求分配或者施放内存时,查表,分配/施放,更新两个表即可。
但是当内存申请,出现多个空闲分区符合长度时:此时有三种适配方法
(并没有谁好谁坏,各自有各自的特点,根据特点选择方法):
1、按照申请的长度和空闲的长度相差的最小,为最佳适配
2、按照申请的长度和空闲的长度相差的最大,为最差适配
3、找到最先出现的空闲地址,直接选择,为首先适配
例如:如果某操作系统中的段内存请求很不规则,有时候需要很大的一个内存块,有时候又很小,此时用哪种分区分配算法最好? (最佳适配)

内存分页(物理内存常用)

【引言】由上面也可知,内存分区导致了内存效率问题,其容易产生一堆内存碎片(总的内存也够,但都分分散散放不下一段的程序了)。而想要移动存好的内存,腾出空来(内存紧缩),会花费大量的时间。因此,实际的操作系统将引入分页来解决这个问题,将连续变为离散
【分页的过程】操作系统启动时,将整个物理内存分成一页一页的(每一页的空间都很小),而针对每个段内存请求,系统一页一页的分配给给这个段。这样即便就是出现内存碎片,也绝不会超过一页。
在这里插入图片描述
因此,再重定位时,需要用到页表。以下图为例,如果有个逻辑地址为2240,因为每页长度为4k,因此2240/4k(右移12位)=2,这得到页号(由硬件MMU完成)而抛出去的240就是页面里面的偏移地址。而页号可知其在物理地址的页框为3,因此3再右移12位,得到对应的物理地址存放页2的地方,即3xxx,加上240的偏移,即为3240的物理地址。
在这里插入图片描述
页表同样也同样放在PCB中

18 多级页表与快表

由前文可知,为了提高内存空间利用率,页应该小,但是页小了页表就大了。
【第一次尝试】实际上大部分逻辑地址根本不会用到,那么能不能只有用到的逻辑页才赋予页表项。但这样会造成页表中的页号不连续,因此在查找逻辑地址对应的页号时(毕竟是为了找到页号来看页框号),就需要挨个去比较查找。而顺序查找的效率太低了,即便二分查找也有些慢(如果按正常时是连续的,直接起始地址进行偏移就好了,根本无需比较查找)。

多级页表:页目录表+页表

由上面可知,我们需要连续+占用内存少
当初如果页表每一项都保留,能达到连续,需要4M的内存(以32位地址为例,即2的32次方,为4G,而地址一块又为4K,因此共有4M个标号)。而采用多级页表,我们只需要将所有的目录放进去,而当目录里面没有页时,也就无需记录页表的项。以下图为例,其一共有4K个目录是一定要留着的,然而其中只有三个目录中存在页信息,而每个目录对应的一个页表也只有4K个项,因此三个4K+目录的一个4K为16K,这样是远远小于4M的。
在这里插入图片描述
【多级页表的问题】虽然提高了空间效率,但多级页表增加了访存的次数,尤其是64位系统。因此,我们可以把常用的页给记录下来,这也就是下面所要介绍的快表。先从快表查,找不到再去多级页表查,因此查找起来速度就可能提高

快表

快表是个寄存器,是一个昂贵的器件,尽管不连续也可以快速查找,但不可以存放过多页项。而该寄存器可以从硬件上做到相联的效果,即不连续的页号也可以一次找到。然后当页号不在快表中时,再去多级页表中查找。

在这里插入图片描述
但是,TLB越大越好,但TLB很贵,通常只有[64, 1024]。而相比220个页,64很小,为什么TLB就能起作用?
答:程序的地址访问存在局部性,因为程序多体现为循环、顺序结构。即空间局部性。因此, 计算机系统设计时应该充分利用这一局部性。
【总结】因此常采用快表+多级页表结合的方法,达到时间和空间效率都比较高的情况。

19 段页结合的实际内存管理

结合后的概念

实际的需求中,需要段、页同时存在:段面向用户/页面向硬件。因此需要两个环节相结合:先分段至虚拟内存,随后虚拟内存分页至真实的物理地址。
在这里插入图片描述

段、页同时存在是的重定位(地址翻译)

首先,根据段表,找到逻辑地址对应的基址,再加上段内的偏移,从而得到虚拟地址(段页结合的灵魂)。根据虚拟地址来算出它的页号,再根据页号去查页表,找到虚拟地址对应的物理地址的页框号,最后加上它的段内偏移,得到真实的物理地址
在这里插入图片描述

段、页结合的细节

内存管理核心就是内存分配,所以从程序放入内存、使用内存开始。因此可以从进程fork中的内存分配开始,过程可以简化为五步:分配段、建段表;分配页、建页表、可以重定位具体使用内存了。
【段、页式内存下程序如何载入内存?】
首先要在虚拟内存上用分区算法割出区域来分给程序的数据段、代码段等各类段,同时建立好段表。再把每一段分成多个页,放入物理内存中的页单位中,同时建立好页表。(只要段表和页表弄好,执行指令时MMU自动完成)
【假设进程1fork一个进程2】
在这里插入图片描述
【第五步的实例】
(进程1)当进程1执行*p=7时,编译完后的p为300。经过LDT查到虚拟地址为400300,随后根据页表找到物理地址7300。 然后cpu将7300打到地址总线上,同时将7打到地址总线上,因此就完成了指令——把7放入了7300地址上。
(进程2)当子进程2执行*p=8时,因为父子进程执行同一个代码,因此编译完后的p还为300。但进程2的LDT算出来不再是400300了,而是800300。但800300根据其页表得到的物理地址仍为7300。
【重点】但当时在前几步复制创建子进程时的建页表中,虽然都指向同一页,但其设置子进程所复制的页表指向父进程也指向的页只能只读状态
因此,写的时候,就要进行分离,新申请一个内存页,随后建立修改这个页表,建立一个新的映射(假设把p映射到8300),随后把8放入即可!

在这里插入图片描述

20 内存换入换出

内存为何需要换入换出

其实是为了实现虚拟内存,所以才引申出了内存的换入换出概念。因为虚拟内存并不完完全全等于物理内存的大小,毕竟用户编程中感受到的应该是磁盘大小,而不仅仅是内存大小。
其实在用户眼里,用户可随意使用该“内存”(虚拟内存),而对这个“内存”怎么映射到物理内存的,全然不知。而内存的换入换出就是专门为了解决由大的虚拟内存映射至小的物理内存的。造成大小区别的原因是:可能物理内存并没有那么大,但呈现给用户的时候,用户在编写程序的时候不关心真正的物理内存是多少,此时OS通过虚拟内存方便了用户使用。下图就是换入操作:用户的感觉就是这个2G都有,都能用。其实物理内存才1G,是通过换入换出来给用户拥有2G感觉的
在这里插入图片描述

内存换入——请求调页

当一条指令访问一个地址时,但当查询页表时,发现缺页(即页表没这个地址的信息),此时硬件(MMU)将做出配合引出中断,进行调页(进行中断的页错误处理程序)。首先从磁盘找到这页,然后再找个内存中的空白页,把磁盘的该页读进内存。最后在页表中做好映射。此时再去执行指令即可。
【例题】采用请求调页而不是请求调段,是因为?
答:请求调页的粒度更细,更能提高内存效率。

内存换出

前面讲了内存的换入,将磁盘的内容读入了内存中,但仅仅换入一会内存就满了,因此换入和换出应该一起工作。因此,在上面换入中找空白页之前,应该完成换出操作(即将该淘汰页内容写出到磁盘上),而换出到底选择哪一页换出去呢,因此就涉及到了以下的换出算法。

FIFO页面置换

其本质就是先入先出!但只适用于缺页次数少的场景。否则就会导致频繁调换。

MIN页面置换

其本质是:选最远将使用的页淘汰,是最优方案!例如下图的例子,当第一次遇到D时,发现要置换,但从D往后看,可以看到未来需要换过来的C是距离D最远的,因此D换C。
在这里插入图片描述
但本算法的缺点就是在置换时要往后看,这是办不到的,因为无法预测。

LRU页面置换(least recently used)

用过去的历史去预测未来。该算法本质:选最近最长一段时间没有使用的
页淘汰(最近最少使用)
。其就能达到MIN的效果。【题外话:这其实跟前面的快表一样,都是利用了局部性,就类比商店摆东西,只有顾客常买的才摆着,需要从仓库换掉的肯定是不常买的】LRU是公认的很好的页置换算法。

LRU的精确实现之用时间戳

在这里插入图片描述
但缺点就是每次地址访问都需要修改时间戳,需维护一个全局时钟,需找到最小值,其实现代价较大。

LRU的精确实现之用页码栈

在这里插入图片描述
每次地址访问都需要修改栈(修改10次左右栈指针) 实现代价仍然较大(需要双向链表+map)。
其实,在实际使用中,LRU准确实现用的很少!因此,可以考虑LRU的近似实现。

LRU的近似实现之再给一次机会(SCR)

该算法本质是:将时间计数变为是和否。具体原理如下图所示,每当访问该页时,该页将置为1。当最近没被访问过时,扫描到这个页时,这个机会就用完使引用位成了0,在找淘汰页发生扫描时,只要扫描到这个页,就把这个页淘汰出去了。
在这里插入图片描述
但缺页还是少数情况,当缺页很少发生时,会导致所有页的引用位都变成1,此时,再发生缺页时,这样算法就退化成了FIFO算法。而发生这一切的原因就是:记录了太长的历史信息,没法反应“最近”
因此需要定时清除R位(引用位),再定义一个扫描指针。故一个快指针清除R位,一个慢指针选择淘汰位(是1继续扫描,是0淘汰该页)。速度不用规定,因为选择指针就是不常用所以慢,相对而言清除指针就是属于快的时钟中断。
在这里插入图片描述

进程分配多少页框(帧frame)

内存的换入换出也都是对页进行置换操作,因此这个问题要确定好!
分配的多,请求调页导致的内存高效利用就没用了!
但当分配的少,当系统内进程增多时,每个进程的缺页率增大,而缺页率增大到一定程度,进程总等待调页完成 ,从而CPU利用率降低,至使进程进一步增多,缺页率更大。因此引发下图的颠簸现象
在这里插入图片描述
这里也涉及一些算法,如工作集算法,以来找到满足程序局部性的那么一个分配页框大小,此处不再讲解。

21 I/O与显示器

在计算机中,如何使外设工作起来呢?
第一步,CPU向控制器中的寄存器读写数据
第二步,控制器完成真正的工作,并向CPU发中断信号
在这里插入图片描述
为了让“向设备控制器的寄存器写(毕竟不同公司生产的不同硬件其格式要求都不一样)”变得简单易操作,操作系统要给用户提供一个简单视图—文件视图
【总结I/O读写的三个步骤】形成文件视图、发出out指令、形成中断处理。

文件视图

操作系统两大视图——进程视图(CPU、MEM)、文件视图(磁盘、设备);
【细节解释】
1、不论什么设备都是open, read, write, close操作系统为用户提供统一的接口!
2、不同的设备对应不同的设备文件(/dev/xxx),仅仅需要根据设备文件找到控制器的地址、内容格式等等!便可得到设备属性数据,和上面的接口完成对接。
下图就是文件视图:
在这里插入图片描述

显示器输出

printf(“Host Name: %s”, name);为例。
第一步,首先根据进程所分配的设备文件(对于终端设备文件(终端设备包括显示器和键盘)来说,是从系统启动时就有的,因此每次子进程都会复制这些设备文件的存在),找到显示器文件,取出其里面的信息。
第二步,随后对里面的信息,通过字符设备接口函数来找到驱动显示器显示的函数tty_write函数,然后将tty_write放在等待队列中
第三步,再找到显示器的写函数con_write来对显示器进行操作,同时当需要取出从缓冲区(就是第二步中的等待队列)里面的字符时,将要显示的内容移动到显存中。
在这里插入图片描述

22 键盘

本节将学习到上面的最后一步,中断处理
键盘对于不同对象有着不同的行为:对于使用者(人)来说: 敲键盘、看结果;对于操作系统来说: “等着”你敲键盘,敲了就中断。
【过程】通过键盘的按键,os将中断所产生的ascii码放入等待队列中(缓冲区),随后等待scanf等函数从等待队列里取出即可。
在这里插入图片描述
因此联系前面的显示器,可总结如下:其中secondary是因为得到的ascii需要转译,此处不再详细讲解,而往上走也无所谓不用知道(关系到用户取读了),其核心就知道中断得到ascii码,随后放入队列等待取即可。
在这里插入图片描述

23 生磁盘的使用——使用盘块号

在计算机中,如何使磁盘工作起来呢?(其实和前面一模一样)
第一步,CPU向磁盘控制器中的寄存器读写数据
第二步,磁盘控制器完成真正的工作,并向CPU发中断信号
在这里插入图片描述

磁盘的I/O过程

第一步,通过向磁盘控制器读写来控制磁盘。
第二步,移动磁头到相应的磁道上
第三步,旋转磁盘到相应的扇区上
最后,和内存缓存进行读(通过磁生电来读)/写(通过电生磁来写)。
在这里插入图片描述

因此只要往控制器中写柱面、磁头、扇区、缓存位置即可。
其中,柱面指:如上图,每个盘面都分为为一圈圈的磁道,而所 有盘面相同的位置的磁道组成一个柱面。
磁头就是选定哪个盘面。因此靠上面两个参数,就能确定一个圆了。而具体读圆的哪一个部分,就是靠扇区。而最后的缓存位置便是与磁盘进行交互的内存缓存位置了。下面便围绕具体的使用过程来展开:

通过盘块号读写磁盘(一层抽象)

如果每个程序都发柱面、磁头、扇区这三个个参数给磁盘,那太麻烦了。因此用户设计的程序应该只发送一个盘块号,随后磁盘驱动负责将盘块号来计算出上面的三个参数(其实就是三个维度的地址)。
【如何编址?】其实是一个三维转到一维的编址转换。并且需要要求block相邻的盘块可以快速读出(因为我们常常找多个连续的盘块)。
而根据下面的访问时间也可以看出,主要是磁头去寻道的时间比较慢,因此设计应该尽量减少寻道时间。
在这里插入图片描述
因此,相邻的盘块应该在一个磁道上,如下图所示:
在这里插入图片描述
因此计算公式如下(Heads就是盘面是固定值,而Sectors就是一个磁道可以分多少个扇区)以上图为例,H=4,S=7(也就是一个柱面)
【公式】柱面 x(HeadsSectors)+磁头 x Sectors+扇区 = 扇区号
根据盘块号反推三参数也很easy:先对sectors取余得到扇区,随后代入对(Heads
Sectors)取余得到磁头,随后都带进去就能得到柱面。上面公式的含义也好理解:第几个柱面,把前面柱面的盘号得到,依次是第几个磁头,最后是第几个扇区。
上面的仅仅得到扇区块,由于每次读写的单位大小不同,其实效果也不同。(例如,如果每次读写1M,那么即便内容不够1M,因为单位是1M,所以会导致单位剩下的无法使用,也就是碎片,从而影响空间利用率)
(而单位越大速度提升是因为,单位越大,越能减少寻道和旋转的次数,而这俩是主要影响因素)
在这里插入图片描述
因此,我们会取一个这种的大小,因此每个盘块其实是连续的多个扇区,因此我们用空间换速度。而每个盘块对应几个扇区都直接写好在磁盘驱动中。(Linux0.11盘块大小是2)
在这里插入图片描述

多个进程通过队列使用磁盘(第二层抽象)

一个进程可以直接找盘块,但多个进程使用时,需要放进请求队列中。然后通过调度算法来取出盘块号,来控制磁盘。
在这里插入图片描述
因此调度算法的目标是:平均访问延迟小。
调度时主要考察什么:寻道时间越短越好。

FCFS调度算法

依旧是谁先进队列,谁就先拿出来。因此也是最直观、最公平的调度。但这个算法会导致此磁头动来动去的(寻道时间长呀),因此为了解决该问题,我们应该在移动过程中,把经过的请求处理了!

SSTF磁盘调度(Shortest-seek-time First)

即如下图所示,在开始的53移动到98的过程中,距离队列中谁的差值小,就先移动到谁那里。但这个也有明显的缺点,因为磁盘中间的请求相较来说还是多的,因此会导致来自边缘位置的请求,一直划不过去,因此会导致边缘没有机会被选中。
在这里插入图片描述

SCAN磁盘调度

SSTF+中途不回折:每个请求都有处理机会 就是还按最小的距离,但是不能来回折返跑了,就是不能中途回头!
在这里插入图片描述

C-SCAN磁盘调度(电梯算法)

SCAN+直接移到另一端:两端请求都能很快处理
就是找最短距离+不允许折返+遇到边界就复位。其实相当于只能下楼的电梯,先从起点下到底,然后跑到最上面,再一步步把人都带下去。
在这里插入图片描述

生磁盘(raw disk)总结

第一步中,得到盘块号其实是依靠“文件”来完成的,也就是下面的内容,跟熟磁盘有关,这里生磁盘就假设已经得到了。而扇区号也不是三个参数,而是因为一个盘块号对应连续的扇区,因此是算出那个开头的扇区号。
第二步,(其实此时也需要申请缓冲内存位置等,它将提速磁盘读写,但本课程不讲述),随后将其放在等待队列中,并使用电梯算法。
第三步,放入电梯算法后,就由硬件来管控了,因此进程进入sleep,就不管了。然后切换其他进程进行协同。
第四、五步,磁盘中断处理,意味着开始处理上面的指令了。先根据上面的扇区号结合公式得到三个参数,随后开始工作
第六步,当工作完成时,又会中断处理,然后唤醒进程,此时进程就会发现内存缓冲区已经有我想要的内容了,就可以继续工作了。

在这里插入图片描述

24 从生磁盘到文件

其核心:如何从文件得到盘块号

引入文件,对磁盘使用的第三层抽象

用户在使用的时候,不能对盘块进行处理吧,因此再往上抽象一层,对文件进行处理!
【定义】文件: 建立字符流到盘块集合的映射关系。以用户要去删除200-212字符为例,一旦发出这个指令,操作系统将解释这个指令,通过查找200-212字符对应的盘块位置,随后发出电梯读写请求,放在队列上以来实现这个指令。而操作系统寻找的这个过程,便是通过一个表的映射。因此这个映射可有以下几种结构实现。

1、连续结构来实现文件
即连续的盘块来存放,只需要知道每个盘块能放多少个字符以及存放的初始盘块的块号将相应字符流的第多少个字符 变换到 相应的盘块
在这里插入图片描述
但是其缺点就是:增加到一定的程度后,就需要覆盖或者整个地去挪动其他文件。因此适合直接读写,而不适合动态增长(相当于数据结构中的数组)。
2、链式结构来实现文件
那么这个怎么求呢,其实跟上面是一样地求法,根据盘块里地字符和初始块来找,不过在找的过程是不一样地,是需要根据地址一点点进去查找的
在这里插入图片描述
3、索引结构(实际系统所采用的方式)
这个专门找一个盘块来做索引(目录),因此找字符流,直接从索引盘块中计算即可。
如下面,假设一个盘块放一百个字符,那么0-99九是在第9块盘符,而100-199就是在第17块盘符,以此类推。而下面的-1就是供随时扩展用的,因此解决了增减问题。
在这里插入图片描述
而实际系统中,使用的是多级索引,不同大小的文件,用不同的级数来索引(通常最多3级)。很小的文件一次即可,而中等大小的也只需要多读一次…
在这里插入图片描述

文件使用磁盘的实现

【整个过程】(前面由文件名/路径名找到inode将在下面将讲解)通过inode,得到索引块,再根据索引块找到数据所在的盘块号。随后,用盘块号、缓存等形成request放入“电梯”。随后,磁盘中断时,从电梯中取出来,从盘块号得到扇区号,再通过公式算出三个参数,最后通过out发送到磁盘控制器上,整个过程便结束了。
在这里插入图片描述
【备注】但是注意,因为设备也是一种特殊的文件,因此设备文件也是有inode的,不过他不是代表着索引内容了,但也存储着许多信息,通过解释inode最后也是输出out发生到设备控制器上。

25 目录与文件系统

上面所讲述的,是一个文件(一个文件对应一个字符流)如何找到盘块来进行读写。而最后一层抽象,便是将磁盘抽象成一堆有组织的文件。

文件系统,抽象整个磁盘(第四层抽象)

在不同的机器中,通过应用结构+存储的数据可以得到那棵目录树,找到文件、读写文件,这个就是文件系统。(也因此,SSD和机械硬盘即便换机器,其内容也不掉)。
【引入目录树(利用分治)】将划分后的集合再进行划分: k次划分后,每个集合中的文件数为O(logkN)
在这里插入图片描述
因此,实现目录变成了关键问题
先看目录怎么用】用“/my/data/a”定位文件a,随后得到文件a的FCB(里面就有inode,能能找到盘块了)
那么磁盘上应该存放什么信息来实现目录呢?由上面可知,我们应该可以由目录找到其下面的所有文件的FCB。但FCB因为对应着各个文件的盘块索引,因此通常FCB单单也是不小的,如果一次性把一个级中的所有文件的FCB都读进来会浪费空间(因为一级毕竟也就使用一个FCB)。因此:只需要有个FCB的指针,根据文件编号,就可以找到FCB的位置
以下图为例,下图便是上图中目录的磁盘存储情况:每个目录的数据盘块存放着其文件以及子目录中FCB的位置。然后,再通过其FCB的位置,进入子目录的数据盘块,从而访问子目录下的文件或者子目录的子目录等…

在这里插入图片描述
然而,根目录的FCB在哪是没人告诉的,因此它一定是存放在一个固定位置的。因此想让整个系统能够自举,还需要存放一些消息。
在这里插入图片描述
引导块:扇区引导。
超级块:规定引导块,超级块,两个位图的大小,从而得到i节点的第一个位置,由此便可以得到根目录了
盘块位图:总共有多少个盘块,指哪些盘块空闲,哪些被占用。
i节点(inode)位图:新建文件时,从哪里申请inode;删除文件时,将哪些inode清除。

磁盘读写的完整过程

在这里插入图片描述

26 操作系统全图

在这里插入图片描述

最后,附一张李治军老师的照片,感谢李老师,嘿嘿~
在这里插入图片描述

  • 7
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值