操作系统之---1.进程与线程 2.内存管理

# 操作系统

## 第2章-进程与线程

1-CPU管理的直观想法

1.CPU上电以后所执行的动作

image-20220323191955671

image-20220323192509676

补充:只要给PC赋值一个初值地址,后面的地址都是地址累加,自动执行指令。

2.如何管理CPU

image-20220323192927397

只要将PC的初值指向一段程序的初始地址,然后自动取址执行,就可以达到管理CPU的目的。(如图完成了0+1操作)

3.存在的问题--CPU等待问题

image-20220323194244950

fprintf(fp,"%d",sum);该条语句设计I/O对磁盘的写操作,如图,如果用一条普通语句(不涉及I/O)代替该语句,总共运行10000000次,也只有0.015秒,而如果不换的话,执行1000次就花了0.859秒,用普通语句执行的时间(0.015/107)比上该I/O语句执行的时间(0.859/103)就得出5.7*105:1(将近106:1)

image-20220323195529108

由此可以计算CPU执行效率为50%(此数值可有下面5示例计算得到)。如果随着执行的次数越来越多,CPU执行效率就会越来越低,由此产生CPU低效率问题。

4.当进行I/O输出的时候,CPU先切到别的程序去执行,当别的程序执行不下去的,再切回来。(由此CPU不需要等待)

image-20220323200022946

5.多道程序交替执行管理CPU

image-20220323200826046

6.多道程序的交替执行就叫做--并发

image-20220323201240158

怎样做到:修改PC的指针

7.修改PC指针的同时保存信息

image-20220323202050275

8.进程-进行中的程序,区别于静态程序,进程中将每一个进程信息保存在PCB中

image-20220323202614730

2-多进程图像

2-1.用户一开机就启动了多个进程(ppt,word,mp3),CPU需要管理好这些进程并以合理的次序推进。

image-20220330151356313

2-2.多进程从开始到关机大致过程

image-20220330152812976

总结:在main方法中,执行init();函数创建一个shell进程(一个cmd窗口),shell再启动其他进程,一个命令启动一个进程,放回shell进程窗口再启动其他进程,然后逐个执行,并逐渐关闭进程,到关机。

任务管理本身是一个进程,从中可见多个进程在运行。

image-20220608145332453

2-3 多进程图像,多进程如何组织?

image-20220608145908028

如图,PCB(Process Control Block)用来记录进程的数据结构,其实是一个c语言实现的结构体。然后可以观察到3个例子:

第1部分:因为是单核CPU,所以某一瞬间只能执行一个进程,如图PCB4指向的进程在执行。

第2部分:该部分可见,将PCB5,PCB8,PCB7这3个等待执行的进程用一个就绪队列组织起来,正等着CPU切换执行。

第3部分:PCB1,PCB6这两个进程用磁盘等待队列组织起来,即将进行磁盘的读写。

。。。。(还有很多其他部分)。

2-4 进程状态图

image-20220608154342905

2-5 多进程如何进行交替?

image-20220608155225564

getNext()方法是得到下一个在等待执行队列中的某一个进程,又称为进程调度。

2-6 两个基本的进程调度算法

image-20220608160153240

2-7 进程的切换

image-20220608160707561

image-20220608161036518

进程切换(PCB1进程切换到PCB2进程)的总结:

PCB1进程占用CPU执行,此时PCB1想进行磁盘读写操作,因此PCB1进入阻塞状态,在进入阻塞状态时,保留PCB1在CPU执行的信息,并将此信息保存到PCB1执行的结构体中。

PCB1进入阻塞状态,PCB2即将从等待执行队列中占用CPU执行,在占用之前,先将PCB2指针指向的结构体中的保存的上一次在CPU执行的信息加载到CPU,CPU到对应的寄存器(缓存)中查看PCB2保留的信息,然后PCB2继续接着上一次在CPU中的执行的位置执行,现在PCB2进程占用CPU运行进程。

补充:CPU对多个寄存器需要精确的控制,所以采用汇编代码实现。

2-8 多个进程存在于内存中会出现如下问题

DPL与CPL是用来保护操作系统的一种机制,用户态程序DPL与CPL的值都是3,只有操作系统DPL才是0.

image-20220608164747640

问题:进程1:将10100b值赋给ax,ax将值赋给内存地址为[100]上的地方,但是进程2可能使用了地址[100]上的值,所以可能会导致程序2出现异常。

解决办法:限制对地址100的读写(如何限制?)

2-9 进程带动内存的使用(如何限制?)

image-20220608165102609

总结:虽然两个进程的静态代码都有地址为100区域,但是当静态代码加载到内存中时,不同进程会在内存的物理地址上映射为不同的区域。如图:进程1静态地址【100】映射为物理地址【780】,进程2静态地址【100】映射为物理地址【1260】,由此在内存中的物理地址上,两个进程互不影响。

2-10多进程之间的合作以及所面临的问题

image-20220608170518699

2-11 从纸上到实际:生产者与消费者实例探究进程之间的合作

image-20220609045935713

对于两个合作进程都要修改counter值:

image-20220609050536919

如图:按照预期效果,原来的counter的值是5,然后生产者生产一个产品,消费者消费一个产品,理论上counter的值应该还是5.但是当两个合作的进程相互交替时,可能出现绿色3处的代码执行,生产者生产了一个产品,保存到了CPU的寄存器中,但是没有对counter进行更新,然后就开始消费,最终导致counter=4,而非预期的5,这就导致了进程之间合作出现问题。

解决办法:利用锁机制,实现了进程同步

image-20220609052307206

承上启下:接下来对如下内容具体剖析

image-20220609052703932

3--用户级线程

3-1多进程是操作系统的基本图像

image-20220609054218044

PID指向的进程执行实际上是一连串指令的执行,可能涉及到对磁盘的读写等操作,指令的执行都有一个静态代码地址与实际在内存中的地址的一个映射表,假设PID代表的是一个函数,两个PID函数之间的切换只进行指令之间的切换,不进行映射表之间的切换,可不可以?由此引出线程。

3-2 资源不动,只进行指令序列的切换

image-20220609055415048

保留的并发的优点:就是保证了线程同步。

避免了进程切换的代价:该代价是指,当进行进程切换的时候,由于进程在内存中要占用不同的内存块,所以进程切换伴随着映射表的切换,而这个切换映射表是极其耗费时间的,这就是所谓的代价。而线程则可以避免这个代价,同一个进程,里面包含多个线程,因为是同一个进程,所以其映射表是固定的,这就是所谓的一个资源;而线程的切换只涉及到PC指针切换(例如:PC指针在该进程的多个函数之间进行切换),映射表还是原来的,所以避免了代价。(这就是分治思想使用在进程上)

3-3 实例:浏览器网页显示文本,图片

image-20220609080435595

说明:以浏览器网页显示文本,图片为例子,点击打开一个浏览器进程,此时该浏览器进程占用CPU。该开启一个线程用来从服务器接收数据,开启一个线程显示网页的文本,开启另一个线程处理图片(如:对图片的解压缩),再开启一个线程用来显示图片。

第一:如果是单线程执行的话(一个线程执行完才能开启另一个线程),将会出现如下情况:

从服务器接收数据的线程将接收到的数据存放到响应的寄存器(缓存)中,显示文本的线程从缓存中取到文本,一直到将所有文本显示出来,才开启处理图片的线程,将所有的图片解压完成之后,才开启显示图片的线程。这样将导致用户需要等待一段时间(将所有文本显示的时间),才能看到图片。而这样在实际生活中的网页加载是落后的,采用多线程交互可以解决此问题。

第二:多线程交互执行:

不必等到显示文本线程执行完才开始处理图片线程,不必等到处理图片线程执行完才开始显示图片线程。所谓多个线程交互执行就是显示文本线程显示一段文本后,切换到处理图片线程显示图片线程,将图片显示出来。这样用户看到一段文本后,可以立即看到相应的图片,这是符合现在的浏览网页的情况。

第三:这些线程当然要资源共享,也就是共享一个内存映射表,如果不共享一个内存映射表,那么每一个线程都需要复制一个内存映射表(一个进程只有一个内存映射表),这也是需要花费时间的 。

3-4 实现上述功能的代码逻辑分析:

image-20220609084726736

image-20220609085030402

3-5 两个线程用一个栈-分析出现的问题

image-20220609100031378

分析如下:如图,两个长方形代表两个线程。

从第一个线程开始,先从A()函数开始执行,调用B()函数执行,在进入到B执行之前,需要将在A函数中的执行位置104信息入栈,以便于下一次继续执行A()函数;进入到B函数执行,执行到204位置时,遇到Yield()函数,将执行到204位置的信息入栈保存,以便于下一次继续执行B函数;

执行Yield()函数,跳到第2个线程开始执行C()函数,C()函数调用D()函数,在进入到D()函数之前,将304位置信息入栈保存,以便于下一次继续执行C()函数;进入到D()函数中执行,遇到Yield()进程切换函数,先将404位置信息入栈保存;执行Yield函数,切换到第一个线程,跳到B函数上一次执行的位置204处继续执行,执行完B函数,将要进行出栈(先进后出)操作。

将要出现问题:本来在第一个线程中要进行B()函数的出栈操作,然后继续执行该线程中的A函数;但是因为是同一个栈,后进先出,会导致D函数所代表的404信息出栈,从404处继续往下执行D函数,本来是204出栈在线程1中继续往下执行,由此出现错误。解决办法:多个线程创建多个栈,这样就能保证B()函数执行完后,相应的信息能够出栈。

3-6 多个线程创建多个栈

image-20220609103609127

分析:

第一:线程1中A函数调用执行B()函数,将在CPU执行到104的信息入栈到线程1所对应的栈中;进入到B函数执行,将在CPU执行到204处的信息入栈保存到线程1所对应的栈中,遇到Yield函数(线程切换函数),在即将切换到TCB2线程时,先将CPU中esp寄存器的信息保存到TCB1线程指向的结构体中的esp(TCB1.esp=esp).

第2:执行Yield函数,跳到TCB2线程中的C函数执行,同理,将204入栈到TCB2所对应的栈中;调用D函数,D遇到Yield()函数时,先执行(TCB2.esp = esp)将TCB2在CPU执行的信息保存。然后执行(esp=TCB1.esp),将TCB1原来保存的esp信息赋值给CPU中的esp(寄存器),继续到TCB1线程的204 处继续往下执行。

注意:图中蓝色部分说明。

3-7 代码实现:

image-20220609111206101

image-20220609111641820

3-8 引出核心级线程:

image-20220609112155900

如图:获取数据线程需要从服务器获取数据,需要进行网卡读写访问计算机硬件,而操作系统涉及到对硬件资源的调度,所以该线程就需要进入操作系统内核,但是如果在内核中发生该线程所对应的进程阻塞,就会切换到进程2去执行,而不会切换到进程1中别的线程去执行,由此产生问题,需要借助内核级线程解决。

image-20220609112114926

4--内核级线程

没有用户级进程这一说法,切换进程在内核中,切换进程实际上就是切换内核级线程

4-1 开始核心级线程

image-20220616155156060

说明:如右上图,MMU实际就是逻辑地址与在内存中的地址的一个映射,Cache是缓存,上面是多个CPU(多核)。比如:CPU1处理进程1中的线程1,CPU2处理进程1中的线程2,此时就实现了多线程的并行,然后又是共用一个MMU映射表,也就是多个线程共用该表。

4-2 内核级线程切换之说明

image-20220616161356980

一套栈说法:一个线程可能既要到用户态中运行,需要创建对应的用户栈,也需要到核心态中运行,也需要到内核中创建对应的核心栈,这两个栈就是相对于该线程的一套栈。

一个栈到一套栈:

原先在用户级线程是,TCB1线程切换到TCB2进程,TCB1线程所对应的一个栈也切换到TCB2线程所对应的一个栈。

现在是:假如TCB1,TCB2在内核中,TCB1关联着对应的用户栈和内核栈,TCB2也关联着对应的用户栈和内核栈,随着TCB1切换到TCB2,处理TCB1对应的内核栈要切换到TCB2的内核栈,其对应的用户栈也会跟着切换。

4-3 用户栈和内核栈之间的关联

image-20220616162831803

说明:

怎样从使该线程从用户级切换到内核级,需要使用INT等中断函数,如图中3所标识的。

进入到该线程所对应的内核中,当然也会关联该线程所对应的内核栈,内核栈中会将对应的用户栈加载到该栈所对应的寄存器中,由此构成了其所说的该线程的对应的一套栈,如图中1所标识的那样。对于图中2,保存的是用户栈中执行的信息,以便于退出核心栈(也就是图中5个寄存器出栈之后就变为了用户栈--仔细观察图)后,继续在该线程的用户级别使用用户栈。

接下来使用简单的代码说明一下:

image-20220616164825710

说明:执行int 0x80中断指令,进入内核,SS:SP入栈,计算机硬件自动关联内核栈与用户栈,执行到304处信息入栈,该线程对应的段基址cs入栈,执行内核中的1000地址处的信息入栈....;接下来就是退出内核,中断返回,以及核心栈中的每一个寄存器出栈,到用户态中取执行。

4-4 内核的切换

image-20220617023105263

关于????说明:

image-20220617023920544

内核线程切换的5步骤:

image-20220617025208312

image-20220617025546263

4-5 核心线程切换以及线程创建函数

image-20220617140556352

4-6 用户级线程,核心级线程的对比

image-20220617141113905

2-5--内核级线程的实现(难:实践部分,视频需要反复看,c语言必须熟悉)

2-5-1 核心级线程的两套栈,核心是内核栈

image-20220617142014293

说明:线程1切换到线程2

线程1中,从用户栈切换到内核栈,从内核栈到了TCB(线程控制块),随着线程1的线程控制块切换到线程2的线程控制块,线程控制块中也有指向该线程的内核栈,随着该切换,其内核栈也跟着切换。随着线程2执行iret指令,线程2进入到用户态,也就是进入到线程2用户栈.这一切在用户的眼里,只是PC指针指向线程1的用户栈切换到了线程2的用户栈,涉及到内核操作是不可见的(也就是图中红线以上的位置)。

2-5-2 执行INT中断指令进入内核

image-20220617145533661

补充:

1.中断有许多种,Linux中的fork()中断,键盘中断,时钟中断。

2.如图中PC,每取一条指令,PC自动加1.(INT 0x80中断指令,执行完该指令之后才进入内核)

2-5-3 切换五段论中的中断入口和中断出口

image-20220617152006450

image-20220617154134410

image-20220617160334628

2-5-3 代码参考--切换5段论中的schedule和中断出口

image-20220617160811302

2-5-4 切换五段论中的switch_to(不懂)

image-20220617163753589

2-5-5 ThreadCreate函数设计以及调用(不懂,要求对C语言非常熟悉)

image-20220617164334533

2-5-6 copy_process的细节:创建栈(不懂)

image-20220617165021623

2-5-7 linux0.11不支持线程,只支持进程(该代码不懂)

image-20220617165739410

2-5-8 (不懂)

image-20220617170359090

image-20220617170607282

image-20220617170719783

2-6--操作系统之树(对前面的一个梳理)

image-20220618080240830

image-20220618080316852

1.pc指针到嗯内存中取址执行,运转CPU

image-20220618080435116

2.PC指针可以切换到别的程序执行,CPU功能增强

image-20220618080645683

3.程序从A跳到B

image-20220618080904514

4.一个栈+Yield造成的混乱

image-20220618081701283

5.两个栈+两个用户TCB

image-20220618081910739

6.以上只是在用户态进行线程切换

image-20220618082021836

7.内核栈的切换

image-20220618082229390

8 代码实现

8-1 Linux 0.01--只是在屏幕上打出A,B

image-20220618082537351

8-2 从用户代码开始

image-20220618082802203

image-20220618083032736

8-3 INT进入内核

image-20220618083237428

8-4 开始sys_fork

image-20220618083408950

8-5 开始copy_process(制造了一个子进程)

image-20220618083715580

8-6 开始返回

image-20220618083951777

8-7 mian继续执行

image-20220618084246380

8-8 主线程阻塞,执行子线程

image-20220618084620847

更正:进程改为线程

8-9 schedule

image-20220618084850644

8-10 switch_to函数

image-20220618085128759

image-20220618085246491

8-10 交替打印A,B需要时钟中断

image-20220618085655687

8-11 时钟中断

image-20220618085831184

image-20220618085924760

image-20220618085959920

image-20220618090114668

image-20220618090248369

2-7--CPU调度策略

2-7-1 多进程图像与CPU调度

image-20220621061820120

PID1进程执行进入阻塞状态,现在有两个进程进入就绪状态,PID2是read完进入就绪状态,PID3进程是time out后又进入就绪状态,应该选择PID2,还是PID3.

2-7-2 CPU进程调度的直观想法

image-20220621062331399

说明:先来先调度,或者任务短优先。

2-7-3 从以下3方面设计调度算法

image-20220621062815509

2-7-4 相应时间,切换次数,系统内耗,吞吐量

image-20220621152124076

image-20220621063835964

说明:

响应时间:假如word是进程1,在a处敲击键盘,此时切换到进程2,当再一次切换到进程1(d处)时,将文字显示出来,此间经历的时间称为响应时间

切换次数:进程1切换到进程2的次数

系统内耗大:并没有给用户干活,也就是ab段进程切换的时候,涉及到进程的切换中的映射表,内核等的切换,所以谓之内耗大。

吞吐量:内耗大了,吞吐量就小。

CPU-bound:CPU约束--以CPU为主,如图可见,其中的CPU长度对用的矩形块很多,IO少,一般是对应后台任务(eg:gcc涉及的编译以及数据计算)

I/O-bound:io约束,如图可见,CPU长度对应的矩形块只有两个,CPU很少,而IO很多,一般对应处理调用IO接口多的应用(eg:word,对键盘又很大的依赖)

2-7-5 先来先服务算法和短作业优先调度算法

image-20220621153438194

说明:第一个队列是先来先服务进程调度算法,第二个队列是短作业优先调度算法。很明显,第2个算法平均周转时间要小。

image-20220621154152327

说明:

注意平均周转时间的计算公式,(p1)+(p1+p2)+(p1+p2+p3)+...=.

p1进程花费的周转时间是:p1

p2进程花费的周转时间是:p1+p2

p3进程花费的周转时间是:p1+p2+p3

综上:根据公式,短作业优先调度算法核心是,哪一个进程所花费的时间短,就越优先调用,这样平均周转时间越短。

2-7-6 响应时间

image-20220621155615998

说明:先来先服务调度算法决定进程调度的先后。响应时间按时间片轮转调度,这样保证每一个进程都能够及时得到执行。(注意:时间片大小所对应的时间一般是固定的,每一个进程分配的时间片可能不一样,如图第2个队列可验证)。

2-7-7 优先级调度

image-20220621160248366

说明:

前台任务的优先级是RR,后台任务的优先级是SJF,RR优先级高于SJF,也就是说明,前台任务更贴近用户,所以设定优先级高。但是,这儿有一个问题,假如前台任务很多(eg:一直在修改PPT),那将会导致后台任务可能永远也运行不了。

image-20220621160555494

解决上述问题:

1.将后台的优先级动态的提升,这样就可以执行一段前台任务后又执行后台任务,然后将后台优先级降低,这样又切换到前台执行。但是后天任务往往是CPU很长的任务(eg:编译一个很长的c文件),这样又会导致前台任务的响应时间很长,这又导致了矛盾。

2.如果前后台任务都使用时间片,这样又退化为了轮转调度,总是出现问题。

2-8--一个实际的schedul(不懂,没仔细听,待看)--对时间片与优先级设计的合理算法

image-20220621171412569

image-20220621171726323

image-20220621171709668

image-20220621172533378

2-9--进程同步与信号量

2-9-1实际生活例子

image-20220621214648665

2-9-2 生产者消费者示例再探索

image-20220621215042659

说明:

注意生产者进程中的while循环条件,当配分的缓存空间满了的时候,就需要进入等待状态;

消费者进程中的while循环是缓存中没有商品时,也需要进入等待状态

image-20220621215521109

说明:进程通过走走停停来保证多进程合作的有序推进。

2-9-3 生产者与消费者中的信号不能解决全部问题

image-20220621220516718

说明:

如图,(1)(2)是生成者产生依赖counter作为信号所产生的问题,因为(3)(4)暴露的该问题更明显,所以作简单分析。

此时,缓存中是满的,counter=BUFFER_SIZE。消费者C执行步骤(3),满足其中第二个作为信号的if条件,所以发信号给P1,P1wakeup.

消费者C再消费一个产品,不满足其中第二个作为信号的if条件,P2不能被唤醒。但是,实际消费中,消费一个产品,应该可以唤醒一个生产进程生产一个产品,但是由于依赖counter作为信号,过于局限,所以导致P2不能被唤醒。

2-9-4 信号量解决上述问题

image-20220621223059373

image-20220621223011807

说明:

使用sem作为信号量,决定是否--等待与唤醒。P1,P2都进入sleep状态,对于(3),当消费者C消费一个产品,一查看sem为负数-2,就判定可以唤醒一个进程1,sem设置为-1;消费者C再一次消费一个产品,再看sem仍然为负数-1,就判定可以唤醒一个进程2,sum设置为0;当消费者再消费一个产品,sem=0,说明没有进程唤醒,此时sem设置为1,表示剩余一个缓冲区。。。

image-20220621224154668

2-9-5 信号量的定义

image-20220621230050691

说明:semaphore:信号量

P操作就对应生产-消费者示例中的sleep(),当信号两量的值小于0时,说明缓存刚好满或者缺少生产者生产产品的缓存区,需要将该进程进入sleep状态。

V操作就对应生产-消费者示例中的wakeup(),s.vlanue++过后,信号两量的值小于等于0时,说明还有进程没有被唤醒,需要调用wakeup唤醒。

2-9-6 用信号量解决生产者-消费者问题

image-20220621231516848

说明:

对于绿色部分:P(empty),V(empty)。生产者中的P(empty)是判断用来存储产品的缓冲区是否为空,也就是说没有缓冲区用于存储了,如果为空,就需要将当前进程进入sleep状态;消费者中的V(empty)每执行一次,就唤醒一个生产者进程

对于黄色部分:P(full),如果信号量full=0,说明没有产品了,消费者进入sleep状态。V(full),如果full<0,说明产品出现空缺,每执行一次该操作,可以唤醒一个消费者进程。

2-10---信号量临界区保护

2-10-1 共同修改信号量所引出的问题

image-20220622065918002

说明:如图,一个可能执行的(调度),信号量empty假如是-1(eg:缺少一个缓存区),执行第2条语句,P1.register变为-2,没有对empty进行更新,empty还是-1.所以P2获取empty = -1,执行第4条语句,P2.register变为-2,这样导致empyt都是-2.但是,按照道理,每生产一个产品,都需要对empty进行减1操作,empty因该为-3.这是共同修改信号量存在的问题。

2-10-2 对共享数据操作导致

image-20220625074808339

2-10-3 解决竞争条件的直观想法--上锁

image-20220625075330593

2-10-4 临界区

image-20220625075620014

说明:进入区和退出区是我们要关注的核心问题,因为这涉及到临界区的进入与退出。

2-10-5 临界区代码保护原则

image-20220625080030790

2-10-6 进入临界区的一个尝试--轮转法

image-20220625080821332

更正蓝色部分的说明:

进程P0进入临界区是turn=0,当turn!=0会进入死循环;进程P1进入临界区是turn=1,当turn!=1时会进入死循环。进程P0退出临界区将turn设置为1,此时进程P1阻塞,导致进程P1无法进入到临界区,此时P0也无法进入到临界区,所以不满足有空让进。

2-10-7 借鉴显示生活中的例子

image-20220625081137727

轮转法:相当于值日,一个星期,1,3,5,7是妻子买牛奶,2,4,6是丈夫买牛奶,可以避免重复买牛奶,但是有可能导致冰箱中没有牛奶。(原因:假如丈夫进程值日的那天有事,进程阻塞,但是又不是妻子进程买牛奶,就会导致冰箱中没有牛奶)。

解决办法:

标记法:发现冰箱中没有牛奶,贴一个标签表明已经去买牛奶。

2-10-8 标记法

image-20220625081751449

缺点:可能会造成无限制的等待,可以从以下(1),(2),(3),(4)发现。(也就是妻子,丈夫同事发现没牛奶,同时标记,导致无线等待)

image-20220625083117283

2-10-8 非对称标记(值日是典型的非对称标记)

image-20220625083450642

说明:即使两个人同时贴上标记,丈夫需要等待一段时间,如果妻子没有将牛奶买回来,还需要自己去等待,丈夫做更多的事情。

2-10-9 结合标记和轮转两种思想

image-20220625083948560

image-20220625084724253

以上只是解决两个进程之间的共享数据问题。

2-10-10 面包店算法

image-20220625085826834

image-20220625090605390

2-10-11 通过关中断与开中断到达临界区保护(实验5推荐使用该方法)

image-20220625091406812

说明:只对单核CPU有效,当执行关中断cli()指令,该CPU就不会调用别的进程,执行完之后,进程开中断sti()指令,起到临界区保护的目的。对于多核CPU,这个CPU关中断执行一个进程,不能阻止另一个CPU关中断执行另一个进程。

2-10-12 临界保护区的硬件原子指令法

image-20220625092508235

2-11 信号量的代码实现

2-12 死锁处理(需要重新看)

银行家算法解决死锁

image-20220625101137118

image-20220625101402080

image-20220625101601302

死锁回滚:

image-20220625101815505

image-20220625101928467

第3章--内存管理

3-1.内存使用与分段

1.计算机是如何工作的

image-20220420214557629

2.首先让程序加载进入内存

image-20220420220506841

如图所示,程序的入口地址从0开始,main函数的执行入口在偏移地址40处,执行call 40指令就可以调用main函数,由此可知,如果将该程序加载进入内存中去,也需要保证在内存中的物理地址入口地址也是0,main函数执行入口地址也是40,那么call 40方能生效。

3.由此产生两个问题:

1.0地址能够用来加载内存吗?

不能:0地址开始的一段内存地址用来存放操作系统的程序

2.如果别的程序也想放到0地址处,那将导致冲突。

4.解决问题:

image-20220420223331699

采用重定位解决:也就找在内存中找一段空闲的内存地址,例如:从地址1000处空闲,入口地址就变为0+1000=1000,相应的main入口地址变为了1040

5.什么时候重定位?

法一:编译时重定位

优点:效率高(相对于载入时),一次编译,直接载入

缺点:比较死板,必须等到程序编译完成后才可以加载入内存中去,编译花费时间,有可能编译前指定的内存会被别程序占用,使用于参与运算的程序较少的应用(不容易导致程序争抢内存资源)

法二:载入时重定位:每载入一条指令就进行重定位

优点:灵活

缺点:一旦载入就不能移动

6.解决载入后不能移动问题

image-20220420225206594

image-20220420231541273

为什么需要移动呢?

答:如图1,进程1有可能会发生阻塞,如果不能移动的话,长时间阻塞的话会占用内存资源。

如何移动:如果进程1阻塞,将进程1进入睡眠并换出,将进程2交换进入腾出的内存空间。但是如果再一次将进程1加载进入内存,进程1开始地址仍然是1040,但此时地址是2040,由此产生冲突。

7.运行时重定位(解决如上的交换冲突)---现代计算机常用

image-20220420233612383

image-20220420232840360

PCB:描述进程信息的数据结构,其中包含base(基地址:从基地址开始的地方就是内存空闲之处,每加载进入一个新进程,都会更新一下基地址的值)

在程序加载进入内存中运行时,通过该程序的逻辑地址加上基地址确定实际内存中的地址。例如:对于图1,原来基地址是1000,main函数的逻辑地址是40,内存中的实际地址是1040;进程1阻塞,换出后其逻辑地址还是40,再一次加载进入内存中,由于基地址更新为了2000,所以加上40,其内存地址就变为了2040.很好的解决了载入时重定位的困境。

image-20220421073725080

整理一下思路:如上图,对应要执行mov ax,[100]指令,取逻辑地址为100的值,进程1所对应的基地址为2000,所以该指令实际在内存中的物理地址为2100;如果要切换到进程2执行mov ax,[100],PCB中就将基地址切换为1000,所以在进程2中执行该指令实际物理地址为1100.

8.分段

8-1. 引入分段(因为实际程序不是将全部程序都放入内存中)

image-20220427223327855

举一个例子:

数据段中第300个数(300为数据段内的偏移),代码段中的第10行(10为代码段内的偏移),栈段的第7个元素(7为栈段的段内偏移)

8-2.将各个段分别放入内存

image-20220427225101972

image-20220427230111368

补充:

CS=0:jmpi 100 CS命令表示跳转到段号为0,段偏移为100的内存地址中----》180K(基址)+100(段内偏移)=280K(内存中实际地址)

DS:100-----跳转到DS=1的段号,段内偏移为100的地方---》360K+100K(由于100>60,导致段号为1的内存溢出,找不到内存地址)

image-20220427225931743

PCB指向LDT表(内包含多个基址),运行程序时,PC指针根据表中的基址和程序中的地址一找,就找到相应的物理内存(执行读取,改写数据等操作),程序就执行了。

总结:image-20220427233323877

问题:

1.静态代码要存入到内存中,cpu才能调用指令执行,那么程序没有调入内存前存放在哪里,并且改静态代码都是从地址0开始的吗

答:静态代码一般放在辅存(硬盘中)中,运行程序时,将所需要的数据从辅存加载进内存中,CPU调用指令取址执行,静态代码一般是从0开始的。

3-2.内存分区与分页

9.分区--为段分配内存(几个算法,数据结构)

9-1. 固定分区--》可变分区

image-20220427234014780

9-2. 可变分区

image-20220427234209285

image-20220427234801571

image-20220427234928377

image-20220428000412429

补充分区算法:

最佳适配会导致空闲段越割越割小,造成内存中的空间分配的段不是均匀分布;

最差适配算法由于每一次都是选取最大空闲区间割,造成内存空间中的段分布算均匀。

image-20220428001011533

10-内存分页

image-20220428001114353

10-1 可变分区造成的问题以及如果使用内存紧缩解决该问题的缺点

image-20220507151401915

内存紧缩原理:用于解决内存碎片,最大利用内存,其原理是复制每一个段进行移动,使每一个段紧缩在一起,最大腾出空闲空间

内存紧缩最大问题:需要花费段移动时间,并且运行一个程序我们需要知道该程序在内存中的基址,这样程序方可在内存中运行,所以在段移动过程中,因为没法知道该段放在哪一个基址,也就导致在该段时间内,大部分程序都执行不了,在上层用户看来电脑死机了。

10-2 内存分页

image-20220507152721693

image-20220507162933542

总结:

image-20220507163223267

3-3.多级页表与块表

3-3-1 内存分页存在的问题

image-20220511142002915

补充:每一个页表项一般占内存的大小为4个字节,所以1M个页表项需要4M空间

3-3-2 寻找问题解决方案:

方案1:

image-20220511143044767

image-20220511143737214

该方案不行之处:

只存放用到的页,导致页号不连续,那么每计算一个页号,CPU都需要进入到内存中去查找页号(可以采用顺序查找或二分查找),(常识:CPU在内部计算很快,关键花费时间在通过总线访问内存)由此可知,多次CPU访问内存造成时间的花费会导致指令执行的速度减慢很多。所以页号必须是连续的,因为连续的页号不需要通过顺序查找或二分查找多次访问,就可以直接定位到指令计算出来的页号。

解决方案2:多级页表

image-20220511150104739

模仿生活中的书目录--章与节的关系--将220分为210 * 210,就是将内存分为210个页表项(代表章),然后每一个页表项里面有划分为210个页表项(代表书的页)。如果需要用到3章的页表(注意页目录主流内存占4K),每一章占1K(210页*4字节),加上页目录驻留内存,总共占用内存16K<<4M。空间效率大大提高,而时间效率有待提高,因为随着页表级数越来越多,指针访问时间随之增加。

方案3:TLB表结合多级页表

image-20220511152510384

image-20220511152930837

image-20220511153450362

3-4.段页结合的实际内存管理

1.段页结合理论

image-20220511155453547

image-20220511160129278

2.内存分配开始,内存使用结束

image-20220511160549105

虚拟内存:也是一个地址,只不过该地址不能够放到物理总线上访问而已

image-20220511162604772

将程序中的数据(int ,double,float)放到数据段中,然后将数据段打散分配,整个过程看来数据段就是一个虚拟的,而实际指令取址的是数据段打散后的那几个页表的地址。

分配内存的步骤:

image-20220511162533223

第一步:分配虚拟内存,建立段表(代码看不懂)

image-20220511163208286

image-20220511163550543

第二部:分配内存建页表(代码看不懂)

image-20220511164523526

image-20220511164729754

image-20220511165013206

image-20220511165229201

image-20220511165711684

image-20220511165809930

image-20220511170518485

3-5.内存换入-请求调页

image-20220518142722300

3-5-1 用户眼里的内存

image-20220625104230944

说明:在用户眼里,就好像拥有4G大的内存供用户使用

3-5-2 用换入,换出实现“大内存”

image-20220625105053634

说明:

只有1G的物理实际内存,怎样实现在用户看来又4G?

答:0G-1G是用户代码段,3G-4G是数据段。当一个执行代码段的时候,根据映射关系,将存放到辅存上的代码加载人内存,当执行代码的时候需要用到数据,就需要使用数据段,此时将代码段从内存中换出,然后将数据段根据映射关系从辅存加载进入内存。就像这样执行,只要换入和换出够快,在用户眼里,就相当于又2G内存在位他工作。4G的原理一样。

3-5-3 请求调页

image-20220625110438179

说明:用户逻辑地址找到虚拟地址,虚拟地址通过一个页框号映射表查找是否又该虚拟地址映射的页号,没有该页,启动页错误处理程序,产生页中断(操作系统硬件实现,如果是缺页产生的中断,就不要切换到别的进程,等待将页加载进内存),将所需要的页从磁盘中换入内存,得到内存中的物理地址,从而可以根据物理地址执行程序。

3-5-4 :也可以请求调段,给段打上标号,加载进内存

image-20220625111039516

3-5-5 一个实际系统的请求调页(以下设计源码,需要重复看视频)

image-20220625111734650

image-20220625111932494

image-20220625112436441

image-20220625112459466

3-6.内存换出

科普:

1.内存管理单元

MMU是Memory Management Unit的缩写,中文名是内存管理单元,有时称作分页内存管理单元(英语:paged memory management unit,缩写为PMMU)。它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制,在较为简单的计算机体系结构中,负责总线仲裁以及存储体切换(bank switching,尤其是在8位的系统上)。

2.DPL,RPL,CPL 之间的联系和区别?

https://blog.csdn.net/better0332/article/details/3416749

posted @ 2022-03-23 23:51  远道而重任  阅读(29)  评论(0编辑  收藏  举报