【操作系统-哈工大李治军】---学习笔记(中)---操作系统管理CPU

6 CPU管理的直观想法

6.1 CPU的工作原理

在这里插入图片描述
(1)CPU-不断的自动的取指执行:

  • 程序存放在内存里;
  • 设置PC指针,CPU根据PC=50发出一个取指命令;CPU把50放在地址总线上,
  • 内存将地址50的指令由总线传回CPU;
  • CPU解释执行这个指令,从地址100的地方取出数值给ax;
  • PC自动+1;

因此,只需要给一个初始地址,接下来CPU就开始不断的取指执行取指执行;

6.2 管理CPU的最直观的方法

1、设好PC初值就可以!!
在这里插入图片描述
2、存在什么问题???
在这里插入图片描述
(1)将一条IO指令(fprintf)替换为一条计算指令,IO指令10的6次方,**IO指令慢很多,**CPU工作是在电路上工作较快,IO指令需要磁盘,机械设备,会比电路慢很多;

**(2)假设有10的6次方条计算指令+一条IO指令,那么此时CPU就需要一直等待IO指令完成;**例如从磁盘上读取数据;此时CPU的效率就很低,这个时候10的6次条计算指令才50%的利用效率,如果几十条,效率就几近于0!!

3、解决方案—多个程序在内存中,多道程序交替执行
在这里插入图片描述
在这里插入图片描述
(1)一个CPU上交替的执行多个程序:并发,管理CPU从并发开始
在这里插入图片描述
4、并发处理,不只是改变寄存器PC的值,同时还需要更改相应的信息结构
在这里插入图片描述
5、运行的程序与静态程序不一样!—进入“进程”概念

(1)**CPU来回切换就需要记住运行程序的样子!!!**因此需要进程;
在这里插入图片描述

6.3 多进程图像–操作系统要管理CPU

(1)启动了的程序就是进程,所以是多个进程推进;操作系统只需要将这些进程记录号,要按照合理的次序推进**(分配资源,进行调度);**

1、多进程图像从启动开始到关机结束
在这里插入图片描述
2、!!!用户启动计算就就是启动了一堆进程,计算机管理就是管理进程!!!

6.3.1 多进程图像:多进程如何组织?—PCB

(1)PCB—Process Control Block—用来记录进程信息的数据结构
在这里插入图片描述
(2)多个进程对应的不同PCB在不同的位置等待队列;

6.3.2 多进程的组织:PCB+状态+队列

(1)将进程根据状态进行分类,操作系统做好这种控制,根据状态转换来进程切换
在这里插入图片描述

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

在这里插入图片描述
(1)比如一个进程需要启动磁盘读写(IO指令),

  • 然后将其状态设置为阻塞态;
  • 并将这个进程放在磁盘等待队列中,
  • 调用**schedule();**schedule()函数中使用getNext进行调度,
  • 找出下一个进程,并switch_to交换进行新的进程pNew;

(2)操作系统都是通过PCB感知进程!

(3)交替的三个部分:队列操作+调度+切换

(4)调度:
在这里插入图片描述
(5)切换:汇编代码实现精细控制
在这里插入图片描述

6.3.4 多进程如何影响–地址空间分离?

(1)因为多个进程会同时被放在内存中;例如进程1要求修改地址100的数值结果进程2的代码就在地址100处,这样进程2就会被修改;就会导致进程2崩溃出错!
在这里插入图片描述
(2)DPL和CPL是为了保护操作系统的设置,这个进程都属于用户态,CPL=3;DPL=3;

(3)解决方法:限制对地址100的读写

(4)多进程的地址空间分离:内存管理的主要内容;

(5)进程带动内存的使用—即使相同,映射后也不会相同,实现共存
在这里插入图片描述

6.3.5 多进程图像如何合作?

在这里插入图片描述
(1)具体实例:生产者-消费者实例–共享缓冲区buffer,生产者进程往buffer里放内容,消费者往buffer里取内容;counter是判断buffer是否满了的重要依据;
在这里插入图片描述
(2)两个合作的进程都要修改counter,有可能右侧交替执行,导致counter不正确;
在这里插入图片描述
(3)解决办法:核心在于进程同步(合理的推进顺序)–不能想切换就切换
在这里插入图片描述

6.3.6 小结

在这里插入图片描述

7 操作寄存器完成切换

7.1 用户级线程(多进程切换)

在这里插入图片描述

7.1.1 资源不动而切换指令序列?资源->地址映射表(内存)

在这里插入图片描述
(1)在切换中,不更改(映射表)资源,只是更改指令;称为线程切换;

(2)实质就是映射表不变而PC指针变;

7.1.2 线程的意义—一个例子

在这里插入图片描述
(1)不依次下载网页文本,图片等,而是多线程同时下载文本和图片,此时,多进程对应的地址映射表不需要更换,因为本身就是在同一个地方进行操作;因此,线程切换在此处十分有意义!;

7.1.3 实现线程切换的核心代码

在这里插入图片描述
(1)char URL[],char buffer[1000]创建共享缓冲区;pthread_create()创建线程,每个线程执行一段程序;void GetData()下载从网站上数据包,Show()从缓冲区里取出内容,显示到显示器上;核心就是启动多个线程,执行相应函数;重点在于如何交替进行!!线程切换是Yield()函数,创建进程是Create()函数

7.1.4 Create?Yield–线程切换是重中之重!!

在这里插入图片描述
在这里插入图片描述
(1)注意,问题在于当D()第二个调用Yield()时,右侧Yield找到204,回到第一次切换的位置,继续执行,然后遇到B的},}在汇编中就是弹栈的意思,此时404被弹出,PC指向404因此出现错误!!!;

(2)因为只能在同一个线程里跳来跳去,左侧104和204是一个线程,右侧304和404是一个线程,**注意,A调用B不是线程切换,只是同一线程中的函数调用而已!!!**此处出现错误,是因为,两个不同的线程共用了同一个栈!!

(3)每个线程的函数调用,应该有自己的栈!

(4)一个栈到两个栈的伟大过渡!!!
在这里插入图片描述
(5)Yield()切换线程同时也要更换栈,栈的信息存储在全局的数据结构TCB中;下方的代码指的是红色的第二个出现的Yield();

(6)第二个Yield()执行时,每次都jmp 204跳走了,一直没有执行到Yield的}弹栈,204就没有从栈中被弹出来!!每次都直接Jmp到204,无法弹栈。因为刚好就是要返回Field()的下一条语句,就删除Jmp=204;指令,直接弹栈返回;

(7)Yield()因此只更新/切换栈就可以,执行弹栈,直接就回去了,不需要切换PC,用Yied()的}返回弹栈就可以

(8)总结: 用Yield()+栈相互配合,实现栈的切换,返回的PC压在栈中,所以栈切换,再一弹栈,PC就跟着切换了!!

7.1.5 两个线程的样子:两个TCB、两个栈、切换的PC在栈中

在这里插入图片描述

(1)创建线程ThreadCreate就是做出切换的样子,三样东西,分别是TCB,栈和切换的PC在栈中;

(2)创建线程ThreadCreate流程:

*** 首先申请一个TCB,**

*** 再申请一个栈stack,**

*** 再在栈中填入PC内容,**

*** tcb.esp=stack将TCB与栈关联;**

7.1.6 所有东西组合—用户级线程的完整样子

在这里插入图片描述

7.1.7 Yield是用户程序,是用户级线程

在这里插入图片描述

(1)操作系统感知不到这个线程切换的存在;缺点在于如果要访问硬件IO,就必须通过操作系统,因为操作系统就是管理硬件的,操作系统在切换进程时,就看不到之前的线程(存在于之前的进程中),因此会切换到别的进程中;

(2)核心级线程—线程就在内核中,TCB也在内核中,内核知道TCB的存在
在这里插入图片描述
(3)之前的栈都是用户栈,field,create都没有进入到内核中;而在内核中,称为Schedule()为了和用户级进行区别

(4)用户级线程是核心级线程的一个基础!

7.2 内核级线程

7.2.1 核心级线程的重要性
  • 进程的切换分为两个部分,一部分是切换指令(线程的切换)和切换资源(映射表)(内存管理讲);进程必须在内核中,没有用户级进程这一说法,进程切换必须是在内核中进行,是内核级线程,但是用户级线程切换是内核级线程的一个基础;因为用户态是不可以访问计算机硬件资源的,只能是线程,只有在内核态才可以!
  • 往往在计算机中,进程,用户级线程,内核级线程都存在,可以充分发挥
    在这里插入图片描述
  • 多核要充分发挥作用,必须支持核心级线程;多核就是多个CPU,多处理器是每个CPU有自己的一套映射,只有到内核中,不同线程可以执行在多个核,如果是多进程,就只有一套MMU,CPU也并行不起来;多核充分发挥就是要并行,同时执行;注意与并发区分,并发是同时启动,交替执行;
7.2.2 和用户级相比,核心级线程的不同之处

在这里插入图片描述
(1)两个栈到两套栈,核心级线程意味着要进入内核,在内核态中也有内核栈,用户态有用户栈,因此每个核心级线程需要一套栈(用户栈+内核栈);

(2)用户级线程是通过切换PCB来切换用户栈,核心级线程是通过切换TCB来切换一套栈(用户栈+内核栈);

(3)进入内核的唯一方法是中断;

(4)INT中断,也可以是硬件中断,中断出现之后就会到内核栈(这个由硬件自动完成);
在这里插入图片描述

7.2.3 一个具体实例

在这里插入图片描述

(1)sys_read是内核函数,开始执行内核操作;

(2)在内核中的切换如下;
在这里插入图片描述
(3)switch—to(cur,next),cur是当前的TCB,next是下一个内核级现成的TCB;

(4)找到TCB,切换TCB来切换栈,根据弹栈恢复切换PC指 针;

(5)一直执行的是用户态函数,偶尔需要到内核态执行函数,会返回,在中断进来的时候,右侧栈就是压入了之前的SS:SP。PC和CS段寄存器基址,切回来的时候,弹栈就会返回到用户态;????应该是一段包含iret()中断返回的代码;
在这里插入图片描述
(6)找到TCB,切换内核栈(套)内核栈关联着自己的线程的用户栈,再使用Iret()中断返回指令,回到用户栈;

7.2.4 内核线程switch_to的五段论

在这里插入图片描述
(1)核心级线程是两套栈的切换;

7.2.5 ThreadCreate!做成切换的样子

在这里插入图片描述
(1)需要有用户栈,内核栈和TCB;

(2)首先申请一段内存作为TCB;再申请一段内存作为内核栈并初始化;目前处于内核态,优先级很高,所以可以申请一段用户栈,指针连接号,将函数地址500和CS写入,准备好;

7.2.6 用户级线程、核心级线程的对比

在这里插入图片描述

7.3 内核级线程实现(代码)

7.3.1 核心级线程的两套栈,核心是内核栈

在这里插入图片描述
在这里插入图片描述
(1)fork会变成右侧 int 0x80中断指令;

(2)在INT 0x80执行中断时,PC自动+1指向mov res,%eax;在中断时,CPU会找到内核栈,并指向当前的用户栈;当前的CS:IP压入内核栈中;

7.3.2 切换五段论中的中断入口(第一段)和中断出口(第五段)

在这里插入图片描述
(1)system_call继续压栈(用户态寄存器)保护用户态现场;

(2)接下执行sys_fork,在sys_fork()执行过程中,可能有内核级切换的情况(中间的三段论);

(3)current是当前线程的PCB,cmpl看当前的PCB的state是否等于0;0表示就绪,**非0一定表示阻塞;**当前进程阻塞就会执行reschedule重新调度,schedule就是中间三段论,调度函数;

(4)第五段:中断出口:切换完成后,reschedule()重新调度,会首先将ret_from_sys_call压栈,然后会执行schedule调度函数,schedule是C函数,会有有括号}会有弹栈操作,完成后会调回ret_from_sys_call:系统中断/调用返回,执行中断返回函数;

7.3.3 切换五段论中的schedule和中断出口(最后一段)

在这里插入图片描述
(1)中断入口是一堆压栈Push,右侧左侧是int 中断自动push,右侧右侧是人工push用户寄存器;中断出口自然是一堆pop弹栈操作;

(2**)schedule()是调度找下一个核心级线程栈(调度算法),next是下一个TCB,TCB转换数据就可以,难点在于根据TCB完成核心栈的切换,根据核心栈完成用户栈的切换;中**间三段,稍后讲,schedule一定要记得返回ret_from_sys_call就是五段论中的最后一段,中断出口;

(3)ret_from_sys_call就是最后一段,一堆Pop,pop完成后iret一起Pop就是返回下一个核心线程的用户栈;

(4)五段论代码分布在操作系统不同地方;

7.3.4 切换五段论中的switch_to

在这里插入图片描述
(1)假定已经执行调度算法找到next,现在执行switch_to函数进行切换;

(2)基于TSS,任务结构切换,将其转化为现在的核心级栈切换(目前流行的);TSS代码就只有一句ljmp %0\t\t完成切换,但是执行效率低;

(3)tss是一个段segment,描述符就是指向段的指针,每一个段都有段描述符;有描述符就会有选择子TR,TR(Task Register)是一个固有寄存器用来找到描述符;

(4)TSS(n)就是下一个新的TR,指向新的TSS段;整个任务的内容都(所有CPU的寄存器信息)都用新的TSS扣上,原来的所有状态要定格在原tss;

(5)这里是内嵌汇编,**汇编的格式就是->代码:输出:输入;**ESP就是栈,TSS就是通过这样的方式,完成了栈ESP的切换;其实TSS就是TCB的一个子段;之前需要弹栈找EIP,这里也不需要,因为直接有EIP,就完成了EIP的切换;但是,这个方法慢,同时要换很多内容,同时,因为一条指令执行,不能通过指令流水实现(硬件加速),因此效率慢;最后通过iert中断返回函数,完成用户栈的切换;这便是中间的三段;

(6)小结:五段论核心代码3句话:

  • int 0x80;中断
  • switch_to中的ljmp;TSS跳转切换
  • iret()中断返回;
7.3.5 第二个故事ThreadCreate()做出切换的样子

在这里插入图片描述
(1)ThreadCreate()创建新的核心级线程;

(2)核心:设置好TSS;PCB线程控制块+内核栈+TSS;

(3)fork是两股,父进程创建子进程;call _copy_process就是把父进程的参数进行拷贝;copy——process的参数与栈中的寄存器一一对应;

7.3.6 copy_process的细节

在这里插入图片描述
(1)get_free_page()获得了一页内存;这里不可以用malloc,malloc是用户态代码,现在在内核态;找到mem_map==0的那一页,返回给get-free—page()并进行强制转换,就有了PCB;

(2)接下来代码就是设置TSS,将TSS初始化好;其中esp0是内核栈,esp是用户栈;其中ss和esp是传入的参数,就是和父进程相同共用栈;用户栈一样,内核栈不同,PCB不同;

(3)上述代码就完成了

  • 创建TCB;
  • 创建内核栈,用户栈,并初始化;
  • 关联栈和TCB;
    在这里插入图片描述
    **(1)eip就是Int 指令执行完的下一句地址,eip也是从父进程那里传入的参数;**与父进程一致;
    在这里插入图片描述
    (1)父子进程返回都在mov res,%eax,但是子进程的eax==0,父进程则不等于0;
7.3.6 第三个故事:给进程传递函数,执行我们的想要的代码

在这里插入图片描述
在这里插入图片描述
(1)exec(cmd)是系统调用,未执行exec之前,父子进程执行相同代码,一旦子进程进入exec,子进程再执行完返回时,就要执行LS->entry代码,子进程返回就是中断返回,中断返回要执行iret()指令,iret()就是要将栈中存储的eip赋值给真正的eip寄存器==PC;而我们需要让PC执行entry;联系两者,就是找到entry指令地址赋给ret(eip);在弹栈后,就顺利执行entry

(2)do_execve是一个参数压栈;参数就是EIP(%esp),%eax;其中esp就是当前执行指针;EIP是一个偏移;将EIP加给esp,赋给eax进行压栈pushl %eax;
在这里插入图片描述
(1)LS entry是一个可执行文件,在磁盘上,可以读取到其地址;

7.4 总结图(!重要!)

在这里插入图片描述

8 操作系统之“树”

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分别创建打印AB的两个子进程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
父进程wait()就是将自己设置为阻塞态,开始scheduel()两个打印的子进程;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9 CPU调度

9.1 CPU调度策略–常见策略

9.1.1 多进程图像与CPU调度

在这里插入图片描述

9.1.2 CPU调度(进程调度)直观想法

在这里插入图片描述
(1)FIFO先进先出(最简单);有很多方面考虑;

9.1.3 如何设计调度算法?

在这里插入图片描述
在这里插入图片描述
(1)切换时间完全是系统内耗;切换次数多就会造成系统内耗大;

9.2 各种调度算法

9.2.1 FCFS先来先服务(First Come,First Served)

在这里插入图片描述

9.2.2 SJF:短作业优先

在这里插入图片描述

9.2.4 轮转调度RR

在这里插入图片描述
(1)时间片T越小,响应时间越快;一个操作系统中进程数量n也不可以太大;

9.2.5 设置优先级

在这里插入图片描述
(1)前台任务更关心响应时间,采用RR轮转调度,减少响应时间;后台任务更关心周转时间,采用SJF短作业优先,前台后台采用不同的调度算法;通过设置优先级,前台任务优先于后台任务;

(2)问题:后台任务有可能一直被搁置!!–解决办法是将动态调整优先级
在这里插入图片描述
(3)但是后台任务一旦获得优先级,后台任务一般都是时间长的,这个时间前台的响应时间又无法得到保证,**处于矛盾;**所以后台任务也需要时间片,那后台任务的SJF??

(4)同时考虑前台任务先完成,和后台任务短作业优先;重点在于!!折中!!
在这里插入图片描述

9.3 一个实际的Schedule函数

9.3.1 Linux 0.11的调度函数schedule()

其目标在于找到下一个next,决定哪个进程下一个执行
在这里插入图片描述
(1)TCB的实现是数组,这里是从最后开始依次向前检索,判断TADK_RUNNING表示就绪态,如果是就绪,并且counter>c,将c赋值为counter;其实这个过程就是寻找最大优先级的过程!!

(2)每次调度给最大counter的那个进程**;—优先级算法;同时,counter是一个时间片,就综合了时间片和优先级算法;**

(3)如果就绪态的counter(时间片)都用完了counter==0,就将所有进程的counter右移一位(除以2)再加上counter的初值;对于就绪态就是counter初值,对于刚刚阻塞态(执行IO)进程,就是自己的counter/2+counter初值一定会大于刚刚的就绪态,会优先执行刚刚是阻塞态的进程;

9.3.2 counter的两个作用:时间片+优先级

在这里插入图片描述
(1)每次时钟中断就要修改counter;每次counter–,当counter==0就进行切换;是典型的时间片流转;
在这里插入图片描述
(1)第二段代码就是优先级的动态调整;因此一个进程在阻塞态中呆的时间越久,那counter
优先级就会越高;
在这里插入图片描述
(1)虽然代码简单,但是很好地权衡;

(2)分析counter时间片大小是否有界(保证响应时间);最大也就收敛到2P;

10 进程同步与合作

10.1 进程同步与信号量

多进程之间变得合理有序

10.1.1 一个例子,多进程共同完成一个任务

在这里插入图片描述
(1)两个进程需要合作完成;每个进程有自己的执行体系,但是不是随便执行,有时需要等待;等待信号就是进程同步,合理有序的向前推进,关键在于信号!;

10.1.2 生产者-消费者实例

在这里插入图片描述
(1)生产者和消费者共享一块缓存,生产者不断生产往里放,counter++;消费者不断取,counter–;多进程合作;

(2)注意是用户态程序
在这里插入图片描述
(1)信号表达信息有限,只能判断有还是没有;因此,使用信号量来表达更丰富的信息
在这里插入图片描述
**(1)此处还需要知道sleep的进程数量;不然就不能释放后来的进程;**counter不足以满足要求,还需要别的量记录此时睡眠的数量,决定发唤醒信号的次数;

10.1.3 从信号到信号量

在这里插入图片描述
(1)设计一个信号量来记录睡眠数量,决定唤醒信号的发送次数,而不是通过counter决定
在这里插入图片描述
(1)根据信号量sem来决定操作(等待还是执行);
在这里插入图片描述

10.1.4 信号量的定义(实现)

在这里插入图片描述
(1)信号量的实现,就是一个结构,value就是刚刚的变量,同时要关联一个队列PCB queue;*

(2)同时,通过右侧函数P来判断消费资源和V产生资源;

10.1.5 用信号量解生产者-消费者问题

在这里插入图片描述
(1)首先考虑,停止的条件,对于生产者,停止条件:缓冲区满,信号量emptyBUFFER_SIZE,empty表示空闲缓冲区,生产者要根据P(empty)判断是否满了(没有空闲缓冲区empty0);对应的,哪里会增加空闲缓冲区个数呢?对应的就是消费者V(empty);

(2)同样,消费者的停止条件,实际内容为0,定义一个信号量表示没有内容了,要停在0这个地方,因为P(full)判断是否为0;对应的,生产者增加fullV(full);

(3)P(mutex)表示互斥,生产者执行时,消费者不能进入,只有一个进程,因此引入mutex称为互斥信号量;

10.2 信号量临界区保护

!!依靠临界区保护信号量,依靠信号量来保证进程同步!!

(1)为什么要保护信号量?(2)怎么保护信号量?

10.2.1 共同修改信号量引出的问题

在这里插入图片描述
不是编程错误,而是因为共享数据没有保护信号量没有保护,造成的竞争错误
在这里插入图片描述
(1)右侧图说在增加空循环,但是每次调度情况不一样,空循环也无法解决;

10.2.2 解决方法—上锁

在这里插入图片描述
(1)称为“原子操作”不可分割,一旦获得权限就做完,别的进程不可以插手;进程1在进入修改empty的代码区时,进程2不可以进入它自己的修改empty的代码区;

10.2.3 临界区(Critical Section)

在这里插入图片描述
(1)读写信号量的代码一定是临界区

(2)生产者P1在执行自己的临界区代码时,生产者P2一定不可以进入自己的修改信号量的临界区代码

(3)必须对临界区代码加以保护!

10.2.4 临界区代码的保护原则

在这里插入图片描述
1、进入临界区的第一个尝试—轮换法
在这里插入图片描述
(1)满足互斥进入要求,但是不满足有空让进的要求;
2、进入临界区的有一个尝试—标记法
在这里插入图片描述
在这里插入图片描述
3、进入临界区的再一次尝试—非对称标记
在这里插入图片描述

10.2.5 临界区Peterson算法

在这里插入图片描述
在这里插入图片描述

10.2.6 多个进程,面包店算法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.2.7 阻止调度,阻止中断

1、开关中断法(只适用于单核,简单系统)
在这里插入图片描述
(1)只有中断才会有调度函数;cli()关中断,sti()开中断;

(2)多CPU不可以,中断是CPU每执行完一条指令后检查INTR中断寄存器中一个标记位是否为1来判断是否有中断;cli()就是CPU不检查INTR就是关闭中断;

(3)虽然关闭了当前运行CPU的中断,但是中断会都将其置1,那么右侧CPU就无法组织其响应中断;

2、硬件原子指令法
在这里插入图片描述
(1)上锁,就是检查一个变量是否符合数值执行;一个锁就对应一个变量;

(2)做一个修改mutex的指令,这个是原子指令(不需要别的变量来保护,硬件自己就保护好)(一条指令就不会被别的打扰);根据mutex来判断是否可以进入临界区;

10.3 信号量的代码实现

10.3.1 实验五练习

在这里插入图片描述
(1)大家可以写用户态代码,写信号量的系统调用来实现上层应用程序的进程同步!上述代码就是

(2)还是生产者-消费者例子,右侧producer.c是生产者程序,要申请一个信号量,要通过内核申请,一个信号量包括(value+PCB的一个队列指针),value是多个进程都可以看到的变量,应该放置在内核中,而且PCB是内核的数据结构,因此要通过系统调用进入内核;

(3)左侧就是在内核中定义一个信号量结构,里面的char name[20]就是信号量名字“empty",打开信号量,都可以看到value;

(4)右侧(2)就循环5次,在fd文件中写出5个数,每个数5个字节,sem_wait(sd)判断有无空闲缓冲区;sem_wait具体工作是:根据传入的sd找到value,并–,如果value<0,就是没有了;就设置为阻塞态,并加入一个等待队列,调用schedule函数切换next进程;

5)cli()和sti()是开关中断,用来保护信号量value;

10.3.2 Linux0.11那里的代码实现

在这里插入图片描述
(1)bread用来读磁盘块;申请一块缓冲区buffer_head,将磁盘读取的内容通过总线DMA读入buffer,这个时候需要置为阻塞态,停在buffer,buffer自带一个信号量b_lock,如左下角,

(2)一个是while,一个是if(之前的)

(3)从sleep_on开始,睡眠就是将自己阻塞,然后放在队列中,调用schedule(),然后switch_to调用其他进程next;

*(4)tmp= p;*p=current;这两句是隐蔽的形成了队列,**如下图所示:
在这里插入图片描述
(1)struct task_struct *tmp;申请一个局部变量tmp;传入队列参数,就是一个指向队列的指针;struct task_struct **p是一个指针的指针;让局部变量等于 *p;因此如右侧,temp就指向了task_struct; *p=current,(红色的current);符合常识,就是当前进程的PCB指向队列首端;
在这里插入图片描述
(1)唤醒工作由中断执行;磁盘中断执行end_request(1),unlock_buffer,执行bh->b_lock=0(锁开了);执行wake_up唤醒函数;

(2)wake_up就是将 * *p当前队首,的进程的state==0.让其变为就绪状态,这是唤醒了队首的进程;一个进程被唤醒就会从之前停下的状态开始执行,就是执行右侧两句话,tmp指向阻塞队列中的下一个进程,tmp->state=0就唤醒了下一个进程;下一个被唤醒的进程也会从之前停下的地方,就会唤醒下下个进程,所有阻塞队列中的进程都会被唤醒;

(3)while()就是将阻塞队列中进程全部唤醒的机制,而if是将阻塞队列中队首进程唤醒的机制;

(4)为什么要唤醒所有进程?如果if第一个进程总是先执行,会导致后面进程(优先级更高)会一直等待;所有唤醒之后,就让优先级最高的进程执行;
在这里插入图片描述
(5)此处while()就是判断所有唤醒进程,选择最高优先级进程进入,执行sleep_on,然后将bh->b_lock=1,所以低优先级进程继续睡眠;如果是if第一个进程进入直接执行,别的进程就无法唤醒执行;

(6)while不需要记录阻塞队列中等待进程的个数;

10.4 死锁处理

10.4.1 生产者-消费者中死锁的存在

在这里插入图片描述
(1)**如果将申请empty和mutex次序更换,就会发生死锁;**empty空闲区的个数;

(2)mutex是1,P(mutex)会将其变为0,假设empty=0;P(empty)会变成-1,生产者会阻塞;

对于消费者开始执行,P(mutex)从0->-1,消费者也变成阻塞;mutex是全局信号量,用来互斥两个进程;

(3**)左侧想要改变阻塞态,需要右侧V(empty);右侧V(empty)需要上方mutex,mutex需要执行左侧V(Mutex);形成循环死锁;—环路等待;**

(4)假设再来一个进程,需要mutex也会一起被锁;如果进程都被锁,计算机就死机了;

10.4.2 死锁的成因及条件

在这里插入图片描述
在这里插入图片描述

10.4.3 死锁处理方法

在这里插入图片描述
1、死锁预防
在这里插入图片描述
2、死锁避免—判断请求是否引起死锁
在这里插入图片描述
(1)判断是否引起死锁,就是判断是否有一条可以走通的路;

(2)找出安全序列的银行家算法(Dijkstra提出)
在这里插入图片描述
(1)但是其时间复杂度很高,m是资源数量,n是进程数,每次有新进程都有很大的时间消耗;
在这里插入图片描述
3、死锁检测+恢复:发现问题再处理
在这里插入图片描述

(1)这种情况实现回滚是比较麻烦的;
在这里插入图片描述

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值