操作系统基础笔记(第二部分)

操作系统基础笔记(第二部分)

1. 进程的定义组成组织方式特征

在这里插入图片描述

1. 进程的定义

  • 程序
    • 就是一个指令序列
    • 早期的计算机(只支持单道程序)
  • 引入多道程序技术之后:
    • 为了方便操作系统管理,完成各程序并发执行,引入了进程进程实体概念
    • PCB,程序段,数据段三部分构成了进程实体(进程映像)。一般情况下,我们把进程实体简称为进程
      • 系统为每个运行的程序配置一个数据结构,称为进程控制块(PCB),用来描述进程的各种信息(如程序代码存放位置)
      • 所谓创建进程,实质上是创建进程实体中的PCB;而撤销进程,实质上是撤销进程实体中的PCB
      • 注意:PCB是进程存在的唯一标志
  • 从不同的角度,进程可以有不同的定义,比较传统典型的定义有:
    • 进程是程序的一次执行过程
    • 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
    • 进程是具有独立功能的程序在数据集合上运行的过程强调“动态性”),它是系统进行资源分配和调度的一个独立单位
  • 引入进程实体的概念后,可把进程定义为:
    • 进程是进程实体的运行过程,是系统进行资源分配调度的一个独立单位
  • 注意
    • 严格来说,进程实体和进程并不一样,进程实体是静态的,进程则是动态

2. 进程的组成

  • 进程(进程实体)由程序段,数据段,PCB三部分组成
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3. 进程的组织

  • 注意
    • 进程的组成讨论的是一个进程内部由哪些部分构成的问题,而进程的组织讨论的是多个进程之间的组织方式问题
      在这里插入图片描述
1. 链接方式

在这里插入图片描述

2. 索引方式

在这里插入图片描述

4. 进程的特征

在这里插入图片描述

知识回顾

在这里插入图片描述

2. 进程的状态与转换

在这里插入图片描述

1. 进程的状态–三种基本状态

  • 进程是程序的一次执行,在这个执行过程中,有事进程正在被CPU处理,有时又需要等待CPU服务,可见,进程的状态是会有各种变化。为了方便对各个进程的管理,操作系统需要将进程合理的划分为几种状态。
  • 在这里插入图片描述
  • 进程运行结束(或者由于bug导致进程无法继续执行下去,比如数组越界错误),需要撤销进程。
  • 操作系统需要完成撤销进程相关的工作。完成将分配给进程的资源回收,撤销进程PCB等工作

2. 进程的状态–另外两种状态

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

3. 进程状态的转换

在这里插入图片描述

  • 阻塞态->就绪态
    • 不是进程自身能好控制的,是一种被动行为
  • 运行态->阻塞态
    • 是一种进程自身做出的主动行为
  • 注意
    • 不能由阻塞态直接转换为运行态,也不能由就绪态直接转换为阻塞态
    • 因为进入阻塞态是进程主动请求的,必然需要进程在运行时才能发出这种请求

知识回顾

在这里插入图片描述

3. 进程控制

在这里插入图片描述

1. 什么是进程控制

  • 主要功能
    • 对系统中的所有进程实施有效的管理
    • 具有创建新进程,撤销已有进程,实现进程状态转换等功能
  • 简化理解:
    • 进程控制就是要实现进程状态转换
      在这里插入图片描述

2. 如何实现进程控制

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

  • 原语实现进程控制。原语的特点是执行期间不允许中断,只能一气呵成
  • 这种不可被中断的操作即原子操作
  • 原语采用“关中断指令”和“开中断指令”实现
  • 显然,关/开中断指令的权限非常大,必然是只允许在核心态下执行的特权指令
    在这里插入图片描述

3. 进程控制相关的原语

  • 更新PCB中的信息(如修改进程状态标志,将运行环境保存到PCB,从PCB恢复运行环境)
    • 所有的进程控制原语一定都会修改进程状态标志
    • 剥夺当前运行进程的CPU使用权必然需要保存其运行环境
    • 某进程开始运行前必然要恢复期运行环境
  • 将PCB插入合适的队列
  • 分配/回收资源
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

知识回顾

在这里插入图片描述

4. 进程通信

在这里插入图片描述

1. 什么是进程通信?

  • 进程通信就是指进程之间的信息交换
  • 进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立
  • 为了保证安全,一个进程不能直接访问另一个进程的地址空间
  • 但是进程之间的信息交换又是必须实现的。
  • 为了保证进程间的安全通信,操作系统提供了一些方法
    在这里插入图片描述
    在这里插入图片描述

2. 进程通信——共享存储

  • 两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统提供的工具实现)
  • 操作系统只负责提供共享空间和同步互斥工具(如P,V操作)
  • 基于数据结构的共享:
    • 比如共享空间里只能放一个长度为10的数组。
    • 速度慢,限制多,是一种低级通信方式
  • 基于存储区的共享:
    • 在内存中画出一块共享存储区,数据的形式,存放位置都由进程控制,而不是操作系统。
    • 速度快,是一种高级通信方式
      在这里插入图片描述
      在这里插入图片描述

3. 进程通信——管道通信

  1. 管道只能采用半双工通信,某一时间段内只能实现单向的传输,如果要实现双向同时通信,则需要设置两个管道
  2. 各进程要互斥的访问管道
  3. 数据以字符流的形式写入管道,当管道写满时,写进程的write()系统调用将被阻塞,等待读进程将数据取走,当读进程将数据取走,当读进程将数据全部取走后,管理变空,此时读进程的read()系统调用将被阻塞
  4. 如果没写满就不允许读,如果没读空,就不允许写
  5. 数据一旦被读出,就从管道中被抛弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情况
    在这里插入图片描述

4. 进程通信——消息传递

  • 进程间的数据交换以格式化的消息(Message) 为单位。进程通过操作系统提供的“发送消息/接受消息”两个原语进行数据交换。
    在这里插入图片描述

知识回顾

在这里插入图片描述

5. 线程概念与多线程模型

在这里插入图片描述

1. 什么是线程,为什么要引入线程?

  • 还没引入进程之前,系统中各程序只能串行执行
  • 进程是程序的一次执行。但这些功能显然不可能是由一个程序顺序处理就能实现的
  • 可以把线程理解为“轻量级进程”
  • 线程是一个基本的CPU执行单元,也是程序执行流的最小单位
  • 引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如QQ,视频,文字聊天,传文件)
  • 引入线程后,进程只作为除CPU之外的系统资源的分配单元(如打印机,内存地址空间等都是分配给进程的)
  • 线程则作为处理机的分配单元
    在这里插入图片描述

2. 引入线程机制后,有什么变化?

在这里插入图片描述

3. 线程的属性

在这里插入图片描述

4. 线程的实现方式

1. 用户级线程(User-Level Thread,ULT)
  • 用户级线程由应用程序通过线程库实现。
  • 所有的线程管理工作都由应用程序负责(包括线程切换)
  • 用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预
  • 在用户看来,是由多个线程,但是在操作系统内核看来,并意识不到线程的存在(用户级线程对用户不透明,对操作系统透明)
  • 可以这样理解,“用户级线程”就是“从用户视角看能看到的线程
    在这里插入图片描述
2. 内核级线程(Kernel-Level Thread,KLT,又称“内核支持的线程”)
  • 内核级线程的管理工作操作系统内核完成
  • 线程调度,切换等工作都是由内核负责,因此内核级线程的切换必然需要在核心态下才能完成
  • 可以这样理解,“内核级线程”就是“从操作系统内核视角看能看到的线程在这里插入图片描述
3. 二者组合

在同时支持用户级线程和内核级线程的系统中,可采用二者组合的方式,将n个用户级线程映射到m个内核级线程上(n >= m)

  • 重点
    • 操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位
      在这里插入图片描述

5. 多线程模型

在同时支持用户级线程和内核级线程的系统中,由几个用户级线程映射到几个内核级线程的问题引出了“多线程模型”问题

1. 多对一模型
  • 多对一模型:
    • 多个用户及线程映射到一个内核级线程,每个用户进程只对应一个内核级线程
  • 优点:
    • 用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
  • 缺点:
    • 当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高,多个线程不可在多核处理级上并行运行
      在这里插入图片描述
2. 一对一模型
  • 一对一模型:
    • 一个用户及线程映射到一个内核级线程,每个用户进程有与用户级线程同数量的内核级线程
  • 优点:
    • 当一个线程被阻塞后,别的线程还可以继续执行,并发能力强,多线程可在多核处理机上并行执行
  • 缺点:
    • 一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大
      在这里插入图片描述
3. 多对多模型
  • 多对多模型:
    • n 用户及线程映射到 m 个内核级线程(n>=m),每个用户进程对应 m 个内核级线程
    • 克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点
      在这里插入图片描述

知识回顾

在这里插入图片描述

6 处理机调度概念,层次

在这里插入图片描述

1. 调度的基本概念

  • 当由一堆任务要处理,但由于资源有限,这些事情没法同时处理。这就需要确定某种规则决定处理这些任务的顺序,这就是“调度”研究的问题
  • 在多道程序系统中,进程的数量往往是多于处理机的个数的,这样不可能同时并行的处理各个进程,处理机调度,就是从就绪队列中按照一定的算法选择一个进程将处理机分配给他运行,以实现进程的并发执行

2. 调度的三个层次——高级调度

由于内存空间有限,有时无法将用户提交的作业全部放入内存,因此就需要确定某种规则来决定将作业调入内存的顺序

  • 高度调度(作业调度)按一定的原则从外存上处于后备队列的作业中挑选一个(或多个)作业,给他们分配内存等必要资源,并建立相应的进程(建立PCB),以使它们获取竞争处理机的权利
  • 高级调度是辅存(外存)与内存之间的调度。每个作业只调入一次,调出一次。作业调入时会建立相应的PCB作业调出时才撤销PCB。高级调度主要是指调入的问题,因为只有调入的时机需要操作系统来确定,但调出的时机必然是作业运行结束才调出

3. 调度的三个层次——中级调度

引入了虚拟存储技术之后,可将暂时不能运行的进程调至外存等待。等它重新具备了运行条件且内存又稍有空闲时,再重新调入内存

  • 这么做的目的是为了提高内存利用率系统吞吐量
  • 暂时调到外存等待的进程状态为挂起状态。值得注意的是,PCB并不会一起调到外存,而是会常驻内存。PCB中会记录进程数据在外存中的存放位置,进程状态等信息,操作系统通过内存中的PCB来保持对各个进程的监控,管理。被挂起的进程PCB会被放到的挂起队列
  • 中级调度(内存调度),就是要决定将哪个处于挂起状态的进程重新调入内存
  • 一个进程可能会被多次调出,调入内存,因此中级调度发生的频率要比高级调度更高

4. 调度的三个层次——低级调度

  • 低级调度(进程调度),其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给他
  • 进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度
  • 进程调度的频率很高,一般几十毫秒一次

5. 进程的挂起态与七状态模型

  • 暂时调到外存等待的进程状态为挂起状态(挂起态,suspend)
  • 挂起态又可以进一步细分为就绪挂起,阻塞挂起两种状态
  • 五状态模型->七状态模型
    在这里插入图片描述
  • 注意“挂起”和“阻塞”的区别
    • 两种状态都是暂时不能获得CPU的服务
    • 挂起态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中
    • 有的操作系统会把就绪挂起,阻塞挂起分为两个挂起队列,甚至会根据阻塞原因不同再把阻塞挂起进程进一步细分为多个队列

6. 三层调度的联系,对比

在这里插入图片描述

知识回顾

在这里插入图片描述

7. 进程调度的时机切换与过程调度方式

在这里插入图片描述

1. 进程调度的时机

进程调度(低级调度),就是按照某种算法从就绪队列中选择一个进程为其分配处理机
进程在操作系统内核程序临界区不能进行调度与切换

  • 需要进行进程调度与切换的情况
    • 当前运行的进程主动放弃处理机(有的系统中,只允许进程主动放弃处理机)
      • 进程正常终止
      • 运行过程中发生异常而终止、
      • 进程主动请求阻塞(如等待I/O)
    • 当前运行的进程被动放弃处理机(有的系统中,进程可以主动放弃处理机,当有更紧急的任务需要处理时,也会强行剥夺处理机(被动放弃)
      • 分给进程的时间片用完
      • 有更紧急的事需要处理(如I/O中断)
      • 有更高优先级的进程进入就绪队列
  • 不能进行进程调度与切换的情况
    • 处理中断的过程中,中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换
    • 进程在操作系统内核程序临界区中(但是进程在普通临界区中时可以进行调度,切换的)
    • 原子操作过程中(原语),原子操作不可中断,要一气呵成(如之前讲过的修改PCB中进程状态标志,并把PCB放到相应队列

真题: 进程处于临界区市不能进行处理机调度(错误)

  • 临界资源:一个时间段内只允许一个进程使用的资源,各进程需要互斥的访问临界资源
  • 临界区:访问临界资源的那段代码
    内核程序临界区一般是用来访问某种内核数据结构的,比如进程的就绪队列(由各就绪进程的PCB组成)
  • 内核程序临界区访问的临界资源如果不尽快释放的话,极有可能影响到操作系统内核的其他管理工作,因此在访问内核程序临界区期间不能进行调度与切换
  • 普通临界区访问的临界资源不会直接影响操作系统内核的管理工作,因此在访问普通临界区时可以进行调度与切换

2. 进程调度的方式

1. 非剥夺调度方式
  • 又称非抢占方式
  • 只允许进程主动放弃处理机
  • 在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态
  • 实现简单,系统开销小但是无法及时处理紧急任务,适合于早期的批处理系统

3. 剥夺调度方式

  • 又称抢占方式
  • 当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程
  • 可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中断),适合于分时操作系统,实时操作系统

4. 进程的切换与过程

“狭义的进程调度”与“进程切换”的区别:

  • 狭义的进程调度指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程,也可能是另一个进程,后一种情况就需要进程切换
  • 进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。
  • 广义的进程调度包括了选择一个进程和进程切换两个步骤。

进程切换的过程主要完成了:

  • 对原来运行进程各种数据的保存
  • 对新的进程各种数据的恢复
  • (如:程序计数器,程序状态字,各种数据寄存器等处理机现场信息,这些信息一般保存在进程控制块)

注意:

  • 进程切换是有代价的,因此如果过于频繁的进行调度,切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少

知识回顾

在这里插入图片描述

8. 调度算法的评价指标

在这里插入图片描述

1. CPU利用率

由于早期的CPU造价十分昂贵,因此人们会希望让CPU尽可能多的工作

  • CPU利用率:指CPU“忙碌”的时间占总时间的比例
  • 利用率 = 忙碌的时间 / 总时间(有的题目还会要求计算某种设备的利用率)

例子: 某计算机只支持单道程序,某个作业刚开始需要在CPU上运行5秒,
再用打印机打印输出5秒,之后再执行5秒,才能结束。在此过程中,CPU利用率,打印机利用率分别是多少?

  • CPU利用率 = (5+5)/(5+5+5)= 66.66%
  • 打印机利用率 = 5 / 15 = 33.33%
  • 通常会考察多道程序并发执行的情况,可以用“甘特图”来辅助计算

2. 系统吞吐量

对于计算机来说,希望能用尽可能少的时间处理完尽可能多的作业

  • 系统吞吐量:单位时间内完成作业的数量
  • 系统吞吐量 = 总共完成了多少道作业 / 总共花了多少时间

例子: 某计算机系统处理完10道作业,共花费100秒,则系统吞吐量为?
10 / 100 = 0.1 道/秒

3. 周转时间

对于计算机的用户来说,很关心自己的作业从提交到完成花了多少时间

  • 周转时间
    • 指从作业被提交给系统开始,到作业完成为止的这段时间间隔
  • 周转时间四个部分(后三项在一个作业的整个处理过程中,可能发生多次)
    • 作业在外存后备队列上等待作业调度(高级调度)的时间
    • 进程在就绪队列上等待进程调度(低级调度)的时间
    • 进程在CPU上执行的时间
    • 进程等待I/O操作完成的时间
  1. (作业)周转时间 = 作业完成时间 - 作业提交时间 (对于用户来说,更关心自己的单个作业的周转时间)
  2. 平均周转时间 = 各作业周转时间之和 / 作业数 ( 对于操作系统来说,更关心系统的整体表现,因此更关心所有作业周转时间的平均值)
  3. 带权周转时间 = 作业周转时间 / 作业实际运行的时间 = (作业完成时间 - 作业提交时间)/ 作业实际运行的时间
    • 带权周转时间必然是 >= 1
    • 带权周转时间与周转时间都是越小越好
  4. 平均带权周转时间 = 各作业带权周转时间之和 / 作业数
  5. 对于周转时间相同的两个作业,实际运行时间长的作业在相同时间内被服务的时间更多,带权周转时间更小,用户满意度更高
  6. 对于实际运行时间相同的两个作业,周转时间短的带权周转时间更小,用户满意度更高

4. 等待时间

计算机的用户希望自己的作业尽可能少的等待处理机

  • 等待时间
    • 指进程/作业处于等待处理机状态时间之和,等待时间越长,用户满意度越低
      在这里插入图片描述
  • 对于进程来说,等待时间就是指进程建立后等待被服务的时间之和,在等待I/O完成的期间其实进程也是在被服务的,所以不计入等待时间。
  • 对于作业来说,不仅要考虑建立进程后的等待时间还要加上作业在外存后备队列中等待的时间
  • 一个作业总共需要被CPU服务多久,被I/O设备服务多久一般是确定不变的,因此调度算法其实只会影响作业/进程的等待时间。当然,与前面指标类似,也有“平均等待时间”来评价整体性能。

5. 响应时间

  • 对于计算机用户来说,会希望自己的提交的请求(比如通过键盘输入一个调试命令)尽早的开始被系统服务,回应。
  • 响应时间,指从用户提交请求首次产生响应所用的时间

知识回顾

在这里插入图片描述

9. 调度算法

在这里插入图片描述
Tips: 各种调度算法的学习思路

  • 算法思想
  • 算法规则
  • 这种调度算法是用于作业调度还是进程调度?
  • 抢占式?非抢占式?
  • 优点和缺点
  • 是否会导致饥饿(某进程/作业长期得不到服务)

1. 先来先服务(FCFS, First Come First Serve)

  • 算法思想
    • 主要从“公平”的角度考虑(类似于我们生活中排队买东西的例子)
  • 算法规则
    • 按照作业/进程到达的先后顺序进行服务
  • 用于作业/进程调度
    • 用于作业调度时,考虑的是哪个作业先到达后备队列;
    • 用于进程调度时,考虑的是哪个进程先到达就绪队列
  • 是否可抢占
    • 非抢占式的算法
  • 优缺点
    • 优点:公平,算法实现简单
    • 缺点:排在长作业(进程)后面的短作业需要等待很长时间,带权周转时间很大,对短作业来说用户体验不好
    • FCFS算法对长作业有利,对短作业不利
  • 是否会导致饥饿
    • 不会

例子:

  • 各进程到达就绪队列的时间,需要的运行时间如下表所示,使用先来先服务调度算法,计算各进程的等待时间,平均等待时间,周转时间,平均周转时间,带权周转时间,平均带权周转时间。
    在这里插入图片描述
  • 先来先服务调度算法:
    • 按照到达的先后顺序调度,事实上就是等待时间越久的越优先得到服务。
    • 因此,调度顺序为:P1->P2->P3->P4
  1. 周转时间 = 完成时间 - 到达时间(P1=7-0=7?;P2=11-2=9?;P3=12-4=8?;P4=16-5=11)
  2. 带权周转时间 = 周转时间 / 运行时间(P1=7/7=1?;P2=9/4=2.25?;P3=8/1=8;?P4=11/4=2.75)
  3. 等待时间 = 周转时间 - 运行时间(P1=7-7=0;?P2=9-4=5;?P3=8-1=7;?P4=11-4=7)
  4. 注意:本例中的进程都是纯计算型的进程,一个进程到达后要么在等待,要么在运行。如果是又有计算,又有I/O操作的进程,其等待时间就是周转时间 - 运行时间 - I/O操作的时间
  5. 平均周转时间 = (7+9+8+11)/4 = 8.75
  6. 平均带权周转时间 = (1+2.25+8+2.75)/4 = 3.5
  7. 平均等待时间 = (0+5+7+7)/4 = 4.75

2. 短作业优先(SJF,Shortest Job First)

  • 算法思想
    • 追求最少的平均等待时间,最少的平均周转时间,最少的平均带权周转时间
  • 算法规则
    • 最短的作业/进程优先得到服务(所谓“最短”,是指要求服务时间最短)
  • 用于作业/进程调度
    • 即可用于作业调度,也可用于进程调度。用于进程调度时称为“短进程优先(SPF,Shortest Process First)算法”
  • 是否可抢占
    • SJF和SPF时非抢占式的算法。但是也有抢占式的版本——最短剩余时间优先算法(SRTN,Shortest Remaining Time Next)
  • 优缺点
    • 优点:“最短的”平均等待时间,平均周转时间
    • 缺点:不公平,对短作业有利,对长作业不利。可能产生饥饿现象。作业/进程的运行时间是由用户提供的,并不一定真实,不一定能做到真正的短作业优先
  • 是否会导致饥饿
    • 会,如果源源不断的有短作业/进程到来,可能使长作业/进程长时间得不到服务,产生“饥饿”现象,如果一直得不到服务,则称为“饿死

例子:

  • 各进程到达就绪队列的时间,需要的运行时间如下表所示,使用非抢占式短作业优先调度算法,计算各进程的等待时间,平均等待时间,周转时间,平均周转时间,带权周转时间,平均带权周转时间。
    在这里插入图片描述
  • 短作业/进程优先调度算法:
    • 每次调度时选择当前已到达运行时间最短的作业/进程
    • 因此,调度顺序为:P1->P3->P2->P4
    • 对比FCFS算法的结果,显然SPF算法的平均等待/周转/带权周转时间都要更低
  1. 周转时间 = 完成时间 - 到达时间(P1=7-0=7?;P3=8-4=4?;P2=12-2=10;?P4=16-5=11)
  2. 带权周转时间 = 周转时间 / 运行时间(P1=7/7=1?;P3=4/1=4?;P2=10/4=2.5?;P4=11/4=2.75)
  3. 等待时间 = 周转时间 - 运行时间(P1=7-7=0?;P3=4-1=3?;P2=10-4=6?;P4=11-4=7)
  4. 平均周转时间 = (7+4+10+11)/4 = 8
  5. 平均带权周转时间 = (1+4+2.5+2.75)/4 = 2.56
  6. 平均等待时间 = (0+3+6+7)/4 = 4
  • 注意细节
    1. 如果题目中未特别说明,所提到的“短作业/进程优先算法”默认非抢占式
    2. 所有进程同时可运行时,采用SJF调度算法的平均等待时间,平均周转时间最少
    3. 抢占式的短作业/进程优先调度算法(最短剩余时间优先,SRNT算法)的平均等待时间,平均周转时间最少
    4. 虽然严格来讲,SJF的平均等待时间,平均周转时间并不一定最少,但相比于其他算法(如FCFS),SJF依然可以获得较少的平均等待时间,平均周转时间

3. 高响应比优先(HRRN,Highest Response Ratio Next)

  • 算法思想
    • 综合考虑作业/进程的等待时间和要求服务的时间
  • 算法规则
    • 在每次调度的时候先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务
    • 响应比 = (等待时间+要求服务时间)/ 要求服务时间
    • 响应比 >= 1
  • 用于作业/进程调度
    • 即可用于作业调度,也可用于进程调度。
  • 是否可抢占
    • 非抢占式的算法。因此只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比
  • 优缺点
    • 综合考虑了等待时间和运行时间(要求服务时间)
    • 等待时间相同时,要求服务时间短的优先(SJF 的优点)
    • 要求服务时间相同时,等待时间长的优先(FCFS的优点)
    • 对于长作业来说,随着等待时间越来越久,其响应比也会越来越大,从而避免了长作业饥饿的问题
  • 是否会导致饥饿
    • 不会

例子:

  • 各进程到达就绪队列的时间,需要的运行时间如下表所示,使用高响应比优先调度算法,计算各进程的等待时间,平均等待时间,周转时间,平均周转时间,带权周转时间,平均带权周转时间
    在这里插入图片描述
  • 高响应比优先算法:
    • 非抢占式的调度算法,只有当前运行的进程主动放弃CPU时(正常/异常完成,或主动阻塞),才需要进行调度,调度时计算所有就绪进程的响应比选响应比最高的进程上处理机。在这里插入图片描述
  • 0时刻:只有P1到达就绪队列,P1上处理机
  • 7时刻(P1主动放弃CPU):就绪队列中有P2(响应比=(5+4)/4=2.25),P3((3+1)/1=4),P4((2+4)/4=1.5)
    • P2和P4要求服务时间一样,但P2等待时间长,所以必然是P2响应比更大
  • 8时刻(P3完成):P2(2.5),P4(1.75)
  • 12时刻(P2完成):就绪队列中只剩下P4

知识回顾

在这里插入图片描述
注意:

  • 这几种算法主要关心对用户的公平性,平均周转时间,平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。
  • 因此这三种算法一般适合用于早期的批处理系统,FCFS算法也常结合其他算法使用,在现在也扮演着很重要的角色。

在这里插入图片描述

4. 时间片轮转

  • 算法思想
    • 公平的,轮流的为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
  • 算法规则
    • 按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片,若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队
  • 用于作业/进程调度
    • 用于进程调度(只有作业放入内存建立了相应的进程后,才能被分配处理机时间片)
  • 是否可抢占
    • 若进程未能在时间片内运行完,将被强行剥夺处理机使用权,因此时间片轮转调度算法属于抢占式的算法。由时钟装置发出来通知CPU时间片已到
  • 优缺点
    • 优点:公平,响应快,适用于分时操作系统
    • 缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度
    • 要求服务时间相同时,等待时间长的优先(FCFS的优点)
  • 是否会导致饥饿
    • 不会

5. 优先级调度算法

  • 算法思想
    • 随着计算机的发展,特别是实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度来决定处理顺序
  • 算法规则
    • 每个作业/进程有各自的优先级,调度时选择优先级最高的作业/进程
  • 用于作业/进程调度
    • 既可用于作业调度,也可用于进度调度。甚至,还会用于在之后学习的I/O调度中
  • 是否可抢占
    • 抢占式,非抢占式都有
    • 区别在于:
      • 非抢占式只需在进程主动放弃处理机时进行调度即可
      • 抢占式还需在就绪队列变化时,检查是否会发生抢占
  • 优缺点
    • 优点:用优先级区分紧急程度,重要程度,适用于实时操作系统。可灵活的调整对各种作业/进程的偏好程度
    • 缺点:若源源不断的有高优先级进程到来,则可能导致饥饿
  • 是否会导致饥饿
  • 补充
    • 就绪队列未必只有一个,可以按照不同优先级来组织。另外,也可以把优先级高的进程排在更靠近队头的位置
    • 根据优先级是否可以动态改变,可将优先级分为静态优先级和动态优先级两种
    • 静态优先级:创建进程时确定,之后一直不变
    • 动态优先级:创建进程时有一个初始值,之后会根据情况动态的调整优先级
  • 如何合理的设置各类进程的优先级?
    • 系统进程优先级 高于 用户进程
    • 前台进程优先级 高于 后台进程
    • 操作系统更偏好I/O型进程(或称i/O繁忙型进程)
    • I/O设备和CPU可以并行工作,如果优先让I/O繁忙型进程优先运行的话,则越有可能让I/O设备尽早的投入工作,则资源利用率,系统吞吐量都会得到提升
    • 注意:与I/O型进程相对的是计算型进程(或称CPU繁忙型进程)
  • 如果采用的是动态优先级,什么时候应该调整?
    • 可以从追求公平,提升资源利用率等角度考虑
    • 如果某进程在就绪队列中等待了很长时间,则可以适当提升其优先级
    • 如果某进程占用处理机运行了很长时间,则可适当降低其优先级
    • 如果发现一个进程频繁的进行I/O操作,则可适当提升其优先级

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

  • 算法思想
    • 对其他调度算法的折中权衡
  • 算法规则
    • 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
    • 新进程到达时先进入第一级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经是在最下级的队列,则重新放回该队列队尾
    • 只有第K级队列为空时,才会为K+1级队头的进程分配时间片
  • 用于作业/进程调度
    • 用于进度调度
  • 是否可抢占
    • 抢占式的算法。在K级队列的进程运行过程中,若更上级的队列(1~K-1级)中进入了一个新进程,则由于新进程处于优先级更高的队列中,因此新进程会抢占处理机,原来运行的进程放回K级队列队尾
  • 优缺点
    • 对各类型进程相对公平(FCFS的优点)
    • 每个新到达的进程都可以很快就得到响应(RR的优点)
    • 短进程只用较少的时间就可完成(SPF的优点)
    • 不必实现估计进程的运行时间(避免用户作假)
    • 可灵活的调整对各类进程的偏好程度,比如CPU密集型进程,I/O密集型进程(拓展:可以将因I/O而阻塞的进程重新放回原队列,这样I/O型进程就可以保持较高优先级)
  • 是否会导致饥饿

知识回顾

在这里插入图片描述
注意:
比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括分时操作系统,实时操作系统等)更注重系统的响应时间,公平性,平衡性等指标。而这几种算法恰好也能较好的满足交互式系统的需求。因此这三种算法适合于交互式系统。(比如UNIX使用的就是多级反馈队列调度算法)

10. 进程同步与进程互斥

1. 什么是进程同步

  • 知识点回顾:
    • 进程具有异步性的特征。
    • 异步性是指,各并发执行的进程以各自独立的,不可预知的速度向前推进
  1. 读进程和写进程并发的运行,由于并发必然导致异步性,因此“写数据”和“读数据”两个操作执行的先后顺序是不确定的,而实际应用中,又必须按照“写数据->读数据“的顺序来执行的。
  2. 如何解决这种异步问题,就是”进程同步“所讨论的内容
  3. 同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。

2. 什么是进程互斥

进程的“并发”需要“共享”的支持,各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又比如打印机,摄像头这样的I/O设备)
在这里插入图片描述

  • 我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像头,打印机)都属于临界资源。此外还有许多变量,数据,内存缓冲区等都属于临界资源。

  • 对临界资源的访问,必须互斥的进行,互斥,亦称间接制约关系进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待,当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。

  • 对临界资源的互斥访问,可以在逻辑上分为如下四个部分:
    在这里插入图片描述

  • 注意:

    • 临界区是进程中访问临界资源的代码段
    • 进入区和退出区是负责实现互斥的代码段
    • 临界区也可称为“临界段”
  • 为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:

    • 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
    • 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待
    • 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)
    • 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待

知识回顾

在这里插入图片描述

11. 进程互斥的软件实现方法

在这里插入图片描述

1. 单标志法

算法思想: 两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予

int turn = 0;	// turn 表示当前允许进入临界区的进程号

P0 进程:
while(turn !=0);	// 1
critical section;	// 2
turn = 1;			// 3
remainder section;	// 4

P1 进程:
while(turn !=1);	// 5 进入区
critical section;	// 6 临界区
turn = 0;			// 7 退出区
remainder section	// 8 剩余区
  • turn 的初值为0,即刚开始只允许0号进程进入临界区
  • 若P1先上处理机运行,则会一直卡在第5点。直到P1的时间片用完,发生调度,切换P0上处理机运行
  • 代码1 不会卡住P0,P0可以正常访问临界区,在P0访问临界区期间即时切换回P1,P1依然会卡在第5点
  • 只有P0在退出区将turn改为1后,P1才能进入临界区
  • 因此,该算法可以实现“同一时刻最多只允许一个进程访问临界区”

turn表示当前允许进入临界区的进程号,而只有当前允许进入临界区的进程在访问了临界区之后,才会修改turn的值,也就是说,对于临界区的访问,一定时按P0->P1->P0->P1->…这样轮流访问。这种必须“轮流访问”带来的问题是,如果此时允许进入临界区的进程是P0,而P0一直不访问临界区,那么虽然此时临界区空闲,但是并不允许P1访问。
因此,单标志法存在的主要问题是:违背“空闲让进”原则

2. 双标志先检查法

算法思想: 设置一个布尔型数组 flag[] ,数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0] = true”意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志flag[i]设为true,之后开始访问临界区。
在这里插入图片描述
若按照152637…的顺序执行,P0和P1将会同时访问临界区。
因此,双标志先检查法的主要问题是:违反“忙则等待”原则
原因在于,进入区的“检查“和”上锁“两个处理不是一气呵成。”检查“后,”上锁“前可能发生进程切换。

3. 双标志后检查法

算法思想: 双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到先“上锁”后“检查”的方法,来避免上述问题。
在这里插入图片描述
若按照1526…的顺序执行,P0和P1将都无法进入临界区
因此,双标志后检查法虽然解决了“忙则等待”的问题,但是又违背了“空闲让进”和“有限等待”原则,会因各进程都长期无法访问临界资源而产生“饥饿”现象
两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区

4. Peterson 算法

算法思想: 双标志后检查法中,两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区。Gary L.Peterson 想到一种方法,如果双方都想争着想进入临界区,那可以让进程尝试“孔融让梨”,主动让对方先使用临界区。
在这里插入图片描述

  • 进入区:
    • 主动争取
    • 主动谦让
    • 检查对方是否也想使用,且最后一次是不是自己说了“客气话”

Peterson 算法用软件方法解决了进程互斥问题,遵循了空闲让进,忙则等待,有限等待三个原则,但是依然未遵循让权等待的原则

知识回顾

在这里插入图片描述

12. 进程互斥的硬件实现方法

在这里插入图片描述

1. 中断屏蔽方法

利用“开/关中断指令”实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)

  • 关中断
    • 关中断后即不允许当前进程被中断,也必然不会发生进程切换
  • 开中断
    • 直到当前进程访问完临界区,再执行开中断指令,才有可能有别的进程上处理机并访问临界区
  • 优点:简单,高效
  • 缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)

2. TestAndSet 指令

  • 简称TS指令,也有地方称为 TestAndSetLock 指令,或 TSL 指令
  • TSL 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。以下是用C语言描述的逻辑
// 布尔型共享变量 lock 表示当前临界区是否被加锁
// true 表示已加锁,false 表示未加锁
bool TestAndSet(bool * lock){
	bool old;
	old = *lock;	// old用来存放lock 原来的值
	*lock = true;	// 无论之前是否已加锁,都将lock设为true
	return old;		// 返回lock原来的值
}
// 以下是使用 TSL 指令实现互斥的算法逻辑
while(TestAndSet(&lock));//"上锁“并”检查“
临界区代码段...
lock = false;	//"解锁“
剩余区代码段...
  • 若刚开始 lock 是 false,则 TSL 后 old 返回的值为 true,while 循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。
  • 相比软件实现方法,TSL 指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。
  • 优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境
  • 缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。

3. Swap 指令

  • 有的地方也叫 Exchange 指令,或简称 XCHG 指令
  • Swap 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。以下是用C语言描述的逻辑
// Swap 指令的作用是交换两个变量的值
Swap(bool *a,bool *b){
	bool temp;
	temp = *a;
	*a = *b;
	*b = temp;
}

// 以下是用 Swap 指令实现互斥的算法逻辑
// lock 表示当前临界区是否被加锁
bool old = true;
while(old == true)
	Swap(&lock,&old);
临界区代码段...
lock = false;
剩余区代码段...
  • 逻辑上来看 Swap 和 TSL 并无太大区别,都是先记录下此时临界区是否已经被上锁(记录在 old 变量上),再将上锁标记 lock 设置为 true,最后检查 old ,如果 old 为 false 则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。
  • 优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境
  • 缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。

知识回顾

在这里插入图片描述

13. 信号量机制

复习

  • 进程互斥的四种软件实现方式(单标志法,双标志先检查,双标志后检查,Peterson算法)
  • 进程互斥的三种硬件实现方式(中断屏蔽方法,TS/TSL指令,Swap/XCHG指令)
  • 在双标志先检查法中,进入区的“检查”,“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题
  • 所有的解决方案都无法实现“让权等待”
    在这里插入图片描述
    信号量机制:
  • 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥,进程同步。
  • 信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信息量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量
  • 原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的,软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区,退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。
  • 一对原语wait(S) 原语和 signal(S) 原语,可以把原语理解为我们自己写的函数,函数名分别为 wait 和 signal ,括号里的信息量S 其实就是函数调用时传入的一个参数。
  • wait,signal 原语常简称为 P,V操作(来自荷兰语 proberen 和 verhogen)。因此,做题的时候常把 wait(S),signal(S) 两个操作分别写为 P(S),V(S)

1. 信号量机制——整型信号量

用一个整数型的变量作为信号量,用来表示系统中某种资源的数量

  • 例子: 某计算机系统中有一台打印机
int S = 1;	// 初始化整型信号量S,表示当前系统中可用的打印机资源数

void wait(int S){	// wait 原语,相当于“进入区”
	while(S <= 0);	// 如果资源数不够,就一直循环等待
	S=S-1// 如果资源数够,则占用一个资源
}

void signal(int S){	// signal 原语,相当于“退出区”
	S=S+1// 使用完资源后,在退出区释放资源
}

进程P0:
...
wait(S);	// 进入区,申请资源
使用打印机资源...	// 临界区,访问资源
signal(S);	// 退出区,释放资源
...

进程P1:
...
wait(S);
使用打印机资源
signal(S);
...

进程Pn:
...
wait(S);
使用打印机资源
signal(S);
...
  • 与普通整数变量的区别:
    • 对信号量的操作只有三种:初始化,P操作,V操作
  • “检查”和“上锁”一气呵成,避免了并发,异步导致的问题
  • 存在的问题:不满足“让权等待”原则,会发生“忙等”

2. 信号量机制——记录型信号量

  • 整型信号量的缺陷时存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表示的信号量。
  • 如果剩余资源数不够,使用block原语使进程从运行态进入阻塞态,并把挂到信号量S的等待队列(即阻塞队列)中
  • 释放资源后,若还有别的进程在等待这种资源,则使用 wakeup 原语唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态
// 记录型信号量的定义
typedf struct{
	int value;	//剩余资源数
	Struct process *L;	//等待队列
}semaphore;

// 某进程需要使用资源时,通过 wait 原语申请
void wait (semaphore S){
	S.value--;
	if(S.value < 0){
		block (S.L);
	}
}

// 进程使用完资源后,通过 signal 原语释放
void signal(semaphore S){
	s.value++;
	if(S.value <= 0){
		wakeup(S.L);
	}
}
  • 在考研题目中 wait(S),signal(S) 也可以记为 P(S),V(S),这对原语可用于实现系统资源的“申请”和“释放”。
  • S.value 的初值表示系统中某种资源的数目
  • 对信号量S的一次 P 操作意味着进程请求一个单位的该类资源,因此需要执行S.value–,表示资源数减1,当S.value<0时表示该类资源已分配完毕,因此进程应调用 block 原语进行自我阻塞(当前运行的进程从运行态->阻塞态),主动放弃处理机,并插入该类资源的等待队列S.L中,可见,该机制遵循了“让权等待”原则,不会出现“忙等”现象。
  • 对信号量S的一次V操作意味着进程释放一个单位的该类资源,因此需要执行S.value++,表示资源数加1,若加1后仍是S.value<=0,表示依然有进程在等待该类资源,因此应调用 wakeup 原语唤醒等待队列中的第一个进程(被唤醒进程从阻塞态->就绪态)。

知识回顾

在这里插入图片描述

14. 用信号量机制实现进程互斥,同步,前驱关系

在这里插入图片描述

1. 信号量机制实现进程互斥

  1. 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)
  2. 设置互斥信号量 mutex,初值为1
  3. 在临界区之前执行 P(mutex)
  4. 在临界区之后执行 V(mutex)
    在这里插入图片描述
// 信号量机制实现互斥
// 要会自己定义记录型信号量,但如果题目中没特别说明,可以把信号量的声明简写成这种形式
semaphore mutex=1; //初始化信号量

P1(){
	...
	P(mutex);	// 使用临界资源前需要加锁
	临界区代码段。。。
	V(mutex);	// 使用临界资源后需要解锁
	...
}

P2(){
	...
	P(mutex);
	临界区代码段。。。
	V(mutex);
	...
}
  • 注意:
    • 对不同的临界资源需要设置不同的互斥信号量。
    • P,V操作必须成对出现,缺少P(mutex)就不能保证临界资源的互斥访问。缺少 V(mutex) 会导致资源永不被释放,等待进程永不被唤醒

2. 信号量机制实现进程同步

进程同步:要让各并发进程按要求有序的推进

  • 比如,P1,P2 并发执行,由于存在异步性,因此二者交替推进的次序时不确定的
  • 若 P2 的“代码4”要基于 P1 的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。
  • 这就是进程同步问题,让本来异步并发的进程互相配合,有序推进。
P1(){
	代码1;
	代码2;
	代码3}

P2(){
	代码4;
	代码5;
	代码6}

用信号量实现进程同步:

  1. 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)
  2. 设置同步信号量S,初始为0
  3. 在“前操作”之后执行 V(S)
  4. 在“后操作”之前执行 P(S)
// 信号量机制实现同步
semaphore S=0; // 初始化同步信号量,初始值为0

P1(){
	代码1;
	代码2V(S);
	代码3}

P2(){
	P(S);
	代码4;
	代码5;
	代码6}
  • 若先执行到 V(S) 操作,则 S++ 后 S=1。之后当执行到 P(S) 操作时,由于 S=1, 表示有可用资源,会执行 S–, S 的值变回0,P2 进程不会执行 block 原语,而是继续往下执行代码4.
  • 若先执行到 P(S) 操作,由于 S=0,S-- 后 S=-1,表示此时没有可用资源,因此P操作中会执行 block 原语,主动请求阻塞。之后当执行完代码2,继而执行 V(S) 操作,S++,使S变回0,由于此时有进程在该信号量对应的阻塞队列中,因此会在 V 操作中执行 wakeup 原语,唤醒 P2 进程,这样 P2 就可以继续执行代码4了

3. 信号量机制实现前驱关系

进程 P1 中有句代码 S1,P2 中有句代码 S2…P3…P6 中有句代码 S6。这些代码要求按如下前驱图所示的顺序来执行:
在这里插入图片描述
在这里插入图片描述
其实每一对前驱关系都是一个进程同步问题(需要保证一前一后的操作)因此,

  1. 要为每一对前驱关系各设置一个同步变量
  2. 在“前操作”之后对相应的同步变量执行 V 操作
  3. 在“后操作”之前对相应的同步变量执行 P 操作

知识回顾

在这里插入图片描述

15. 生产者消费者问题

1. 问题描述

  • 系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(注意:这里的“产品”理解为某种数据)
  • 生产者,消费者共享一个初始为空,大小为 n 的缓冲区。(刚开始空闲缓冲区的数量为 n,非空闲缓冲区(产品)的数量为0)
  • 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。(同步关系,缓冲区满时,生产者要等待消费者取走产品)
  • 只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。(同步关系,缓冲区空时,(即没有产品时),消费者要等待生产者放入产品)
  • 缓冲区是临界资源,各进程必须互斥的访问。(互斥)
    在这里插入图片描述
    如何用信号量机制(P,V操作)实现生产者,消费者进程的这些功能呢?
    信号量机制可实现互斥设置初值为1的互斥信号量),同步设置初值为0的同步信号量(实现“一前一后”)),对一类系统资源的申请和释放设置一个信号量,初始值即为资源的数量(本质上也属于“同步问题”,若无空闲资源,则申请资源的进程需要等待别的进程释放资源后才能继续往下执行))

PV操作题目分析步骤:

  1. 关系分析,找出题目中描述的各个进程,分析它们之间的同步,互斥关系
  2. 整理思路,根据各进程的操作流程确定P,V操作的大致顺序。(生产者每次要消耗(P)一个空闲缓冲区,并生产(V)一个产品。消费者每次要消耗(P)一个产品。并释放一个空闲缓冲区(V)。往缓冲区放入/取走产品需要互斥)
  3. 设置信号量,设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值时多少)
semaphore mutex = 1;  // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n;  // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0;  // 同步信号量,表示产品的数量,也即非空缓冲区的数量

2. 如何实现

  • 生产者,消费者共享一个初始为空,大小为 n 的缓冲区
  • 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待
  • 只有缓冲区不空时,消费者才能从中取出产品,否则必须等待
  • 缓冲区时临界资源,各进程必须互斥的访问
semaphore mutex = 1;  // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n;  // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0;  // 同步信号量,表示产品的数量,也即非空缓冲区的数量

producer(){
	while(1){
		生产一个产品;
		P(empty);  // 消耗一个空闲缓冲区
		P(mutex);
		把产品放入缓冲区; // 实现互斥是在同一进程中进行一对 PV 操作
		V(mutex);
		V(full);  // 增加一个产品
	}
} 

// 实现两进程的同步关系,是在其中一个进程中执行 P ,另一进程中执行 V

consumer(){
	while(1){
		P(full);  // 消耗一个产品
		P(mutex);
		从缓冲区取出一个产品;
		V(mutex);
		V(empty):  // 增加一个空闲缓冲区
		使用产品;
	}
}

3. 思考:能否改变相邻P,V操作的顺序?

producer(){
	while(1){
		生产一个产品;
		P(mutex);  // 1	mutex 的 P 操作在前
		P(empty);  // 2
		把产品放入缓冲区; // 实现互斥是在同一进程中进行一对 PV 操作
		V(mutex);
		V(full);  // 增加一个产品
	}
} 

consumer(){
	while(1){
		P(mutex);  // 3
		P(full);  // 4
		从缓冲区取出一个产品;
		V(mutex);
		V(empty):  // 增加一个空闲缓冲区
		使用产品;  // 能否放到 PV 操作之间
	}
}
  • 若此时缓冲区内已经放满产品,则 empty = 0, full = n
  • 则生产者进程执行 1 ,使 mutex 变为 0 ,再执行 2,由于已没有空闲缓冲区,因此生产者被阻塞。
  • 由于生产者阻塞,因此切换回消费者进程。消费者进程执行 3,由于 mutex 为 0,即生产者还没释放对临界资源的“锁”,因此消费者也被阻塞
  • 这就造成了生产者等待消费者释放空闲缓冲区,而消费者又等待生产者释放临界区的情况,生产者和消费者循环等待被对方唤醒,出现“死锁”
  • 同样的,若缓冲区中没有产品,即 full = 0,empty = n。按 3 4 1 的顺序执行就会发生死锁
  • 因此,实现互斥的 P 操作一定要在实现同步的 P 操作之后。
  • V 操作不会导致进程阻塞,因此两个 V 操作顺序可以交换

知识回顾

PV操作题目分析步骤:

  1. 关系分析,找出题目中描述的各个进程,分析它们之间的同步,互斥关系
  2. 整理思路,根据各进程的操作流程确定P,V操作的大致顺序。(生产者每次要消耗(P)一个空闲缓冲区,并生产(V)一个产品。消费者每次要消耗(P)一个产品。并释放一个空闲缓冲区(V)。往缓冲区放入/取走产品需要互斥)
  3. 设置信号量,设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值时多少)
  • 生产者消费者问题是一个互斥,同步的综合问题
  • 对于初学者来说最难的是发现题目中隐含的两对同步关系
  • 有时候是消费者需要等待生产者生产,有时候是生产者要等待消费者消费,这是两个不同的“一前一后问题”,因此也需要设置两个同步信号量
    在这里插入图片描述
    易错点:
    • 实现互斥和实现同步的两个 P 操作的先后顺序

16. 多生产者-多消费者

1. 问题分析

桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子赚等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果,仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。

  1. 关系分析,找出题目中描述的各个进程,分析它们之间的同步,互斥关系
  2. 整理思路,根据各进程的操作流程确定 P,V 操作的大致顺序。(互斥:在临界区前后分别 PV 同步:前V后P)
  3. 设置信号量,设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)
    在这里插入图片描述

互斥关系:(mutex = 1)
对缓冲区(盘子)的访问要互斥的进行

同步关系(一前一后)

  1. 父亲将苹果放入盘子中,女儿才能取苹果
  2. 母亲将橘子放入盘子后,儿子才能取橘子
  3. 只有盘子为空时,父亲或母亲才能放入水果(“盘子为空”这个事件可以由儿子或女儿触发,事件发生后才允许父亲或母亲放水果)

2. 如何实现

semaphore mutex = 1;	// 实现互斥访问盘子(缓冲区)(问题:可不可以不用互斥信号量)
semaphore apple = 0;	// 盘子中有几个苹果
semaphore orange = 0;	// 盘子中有几个橘子
semaphore plate = 1;	// 盘子中还可以放多少个水果

dad(){
	while(1){
		准备一个苹果;
		P(plate);
		P(mutex);
		把苹果放入盘子;
		V(mutex);
		V(apple);
	}
}

mom(){
	while(1){
		准备一个橘子;
		P(plate);
		P(mutex);
		把橘子放入盘子;
		V(mutex);
		V(orange);
	}
}

daughter(){
	while(1){
		P(apple);
		P(mutex);
		从盘中取出苹果;
		V(mutex);
		V(apple);
		吃掉苹果;
	}
}

son(){
	while(1){
		P(orange);
		P(mutex);
		从盘中取出橘子;
		V(mutex);
		V(plate);
		吃掉橘子;
	}
}

分析:

  • 刚开始,儿子,女儿进程即使上处理机运行也会被阻塞。
  • 如果刚开始是父亲进程先上处理机运行,则父亲P(plate),可以访问盘子->母亲P(plate),阻塞等待盘子->父亲放入苹果V(apple),女儿进程被唤醒,其他进程即使运行也都会阻塞,暂时不可能访问临界资源(盘子)->女儿P(apple),访问盘子,V(plate),等待盘子的母亲进程被唤醒->母亲进程访问盘子(其他进程暂时都无法进入临界区)->…
  • 如果盘子(缓冲区)容量为2,那么父亲P(plate),可以访问盘子->母亲P(plate),可以访问盘子->父亲在往盘子里放苹果,同时母亲也可以往盘子里放橘子。于是就出现了两个进程同时访问缓冲区的情况,有可能导致两个进程写入缓冲区的数据相互覆盖的情况。因此,如果缓冲区大小大于1,就必须专门设置一个互斥信号量 mutex 来保证互斥访问缓冲区。

结论:

  • 即使不设置专门的互斥变量 mutex,也不会出现多个进程同时访问盘子的现象

原因在于:

  • 本题中的缓冲区大小为1,在任何时刻,apple, orange, plate 三个同步信号量中最多只有一个是1。因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利的进入临界区。。。

知识回顾

总结:

  • 在生产者-消费者问题中,如果缓冲区大小为1,那么有可能不需要设置互斥信号量就可以实现互斥访问缓冲区的功能。当然,这不是绝对的,要具体问题具体分析

建议:

  • 在考试中如果来不及仔细分析,可以加上互斥信号量,保证各进程一定会互斥的访问缓冲区。但需要注意的是,实现互斥的P操作一定要在实现同步的P操作之后,否则可能引起“死锁”。

PV 操作题目的解题思路

  1. 关系分析,找出题目中描述的各个进程,分析它们之间的同步,互斥关系
  2. 整理思路,根据各进程的操作流程确定 P,V 操作的大致顺序。
  3. 设置信号量,设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)

17. 吸烟者问题

1. 问题描述

假设一个系统有三个抽烟者进程一个供应者进程。每个抽烟者不停的卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料,烟草,纸和胶水。三个抽烟者中,第一个拥有烟草第二个拥有纸第三个拥有胶水。供应者进程无限的提供三种材料,供应者每次将两种材料放桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉完成了,供应者就会放另外两种材料在桌上,这个过程一直重复(让三个抽烟着轮流的抽烟

2. 问题分析

本质上这题也属于“生产者-消费者”问题,更详细的说应该是”可生产多种产品的单生产者-多消费者”。

  1. 关系分析,找出题目中描述的各个进程,分析它们之间的同步,互斥关系
  2. 整理思路,根据各进程的操作流程确定 P,V 操作的大致顺序
  3. 设置信号量,设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)

桌子可以抽象为容量为1的缓冲区,要互斥访问

  • 组合一:纸+胶水
  • 组合二:烟草+胶水
  • 组合三:烟草+纸

同步关系(从事件的角度来分析):

  • 桌上有组合一:第一个抽烟者取走东西
  • 桌上有组合二:第二个抽烟者取走东西
  • 桌上有组合三:第三个抽烟者取走东西
  • 发出完成信号:供应者将下一个组合放到桌上

PV操作顺序: “前V后P”

3. 如何实现

provider(){
	while(1){
		if(i==0){
			将组合一放桌上;
			V(offer1);
		}else if(i==1){
			将组合二放桌上;
			V(offer2);
		}else if(i==2){
			将组合三放桌上;
			V(offer3);
		}
		i = (i+1)%3;
		P(finish);
	}
}

// 缓冲区大小为1,同一时刻,四个同步信号量中至少有一个的值为1
semaphore offer1 = 0; // 桌上组合一的数量
semaphore offer2 = 0; // 桌上组合二的数量
semaphore offer3 = 0; // 桌上组合三的数量
semaphore finish = 0; // 抽烟是否完成
int i = 0;			  // 用于实现“三个抽烟者轮流抽烟”

smoker1 (){
	while(1){
		P(offer1);
		从桌上拿走组合一;卷烟;抽掉;
		V(finish);
	}
}

smoker2(){
	while(1){
		P(offer2);
		从桌上拿走组合二;卷烟;抽掉;
		V(finish);
	}
}

smoker3(){
	while(1){
		P(offer3);
		从桌上拿走组合三;卷烟;抽掉;
		V(finish);
	}
}

知识回顾

  • 吸烟者问题可以为我们解决“可以生产多个产品的单生产者;问题提供一个思路
  • 值得吸取的精华是:”轮流让各个吸烟者“必然需要“轮流的在桌上放上组合一,二,三”
  • 注意体会我们是如何用一个整型变量 i 实现这个“轮流”过程的
  • 如果题目改为“每次随机的让一个吸烟者吸烟”,我们有应该如何用代码写出这个逻辑呢?

若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个V操作应该放在各自对应的“事件”发生之后的位置

18. 读者-写者问题

1. 问题描述

有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:

  • 允许多个读者可以同时对文件执行读操作
  • 值允许一个写者往文件中写信息
  • 任一写者在完成写操作之前不允许其他读者或写者工作
  • 写者执行写操作前,应让已有的读者和写者全部退出
    在这里插入图片描述

2. 问题分析

两类进程:写进程,读进程
互斥关系:写进程——写进程,写进程——读进程。读进程与读进程不存在互斥问题

  • 写者进程和任何进程都互斥,设置一个互斥信号量 rw,在写者访问共享文件前后分别执行 P,V 操作。
  • 读者进程和写者进程也要互斥,因此读者访问共享文件前后也要对 rw 执行 P,V 操作。
  • 如果所有读者进程在访问共享文件之前都执行 P(rw) 操作,那么会导致各个读进程之间也无法同时访问文件。

Key:读者写者问题的核心思想——怎么处理该问题呢?
P(rw) 和 V(rw) 其实就是对共享文件的“加锁”和“解锁”。既然各个读进程需要同时访问,而读进程与写进程又必须互斥访问,那么我们可以让第一个访问文件的读进程“加锁”,让最后一个访问完文件的读进程“解锁”。可以设置一个整数变量 count 来记录当前有几个读进程在访问文件。

3. 如何实现

semaphore rw = 1;	// 用于实现对文件的互斥访问。表示当前是否有进程在访问共享文件
int count = 0;		// 记录当前有几个读进程在访问文件
semaphore mutex = 1;// 用于保证对count变量的互斥访问
semaphore w = 1;	// 用于实现“写优先”

writer(){
	while(1){
		P(w);
		P(rw);	// 写之前“加锁”
		写文件...
		V(rw);	// 写之后“解锁”
		V(w);
	}
}

reader(){
	while(1){
		P(w);
		//(潜在的问题:只要有读进程还在读,写进程就要一直阻塞等待,可能“饿死”。因此,这种算法中,读进程是优先的)
		P(mutex);	// 各读进程互斥访问count 
		if(count == 0)
			P(rw);	//	第一个读进程负责“加锁“
		count++;	// 访问文件的读进程数+1
		V(mutex);
		V(w);
		读文件..
		P(mutex);	// 各读进程互斥访问count
		count--;	// 访问文件的读进程数-1
		if(count == 0)
			V(rw);	// 最后一个读进程负责“解锁”
		V(mutex);
	}
}

思考:

  • 若两个读进程并发执行,则两个读进程有可能先后执行P(rw),从而使第二个读进程阻塞的情况

如何解决:

  • 出现上述问题的原因在于读count变量的检查和赋值无法一气呵成,因此可以设置另一个互斥信号量来保证各读进程对count的访问是互斥的。

分析以下并发执行P(w)的情况:

  • 读者1->读者2
  • 写者1->写者2
  • 写者1->读者1
  • 读者1->写者1->读者2
  • 写者1->读者1->写者2

结论:

  • 在这种算法中,连续进入的多个读者可以同时读文件;
  • 写者和其他进程不能同时访问文件;
  • 写者不会饥饿,但也并不是真正的“写优先”,而是相对公平的先来先服务原则。(有的书上把这种算法称为“读写公平法”)

知识回顾

  • 读者-写者问题为我们解决复杂的互斥问题提供了一个参考思路。
  • 其核心思想在于设置了一个计数器count 用来记录当前正在访问共享文件的读进程数。我们可以用count 的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理
  • 另外,对count变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。
  • 最后,还要认真体会我们是如何解决“写进程饥饿“问题的。

19. 哲学家进餐问题

1. 问题描述

一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时, 才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

  1. 关系分析。系统中有5个哲学家进程,5位哲学 家与左右邻居对其中间筷子的访问是互斥关系。
  2. 整理思路。这个问题中只有互斥关系,但与之前 遇到的问题不同的事,每个哲学家进程需要同时 持有两个临界资源才能开始吃饭。如何避免临界 资源分配不当造成的死锁现象,是哲学家问题的精髓。
  3. 信号量设置。定义互斥信号量数组 chopstick[5]={l,l, 1,1,1}用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家 i 左边的筷子编号为 i ,右边的筷子编号为(i+l)%5。

2. 问题分析

  • 每个哲学家吃饭前依次拿起左、 右两支筷子
  • 每位哲学家循环等待右边的人放下筷子(阻塞),发生“死锁”

3. 如何实现

如何防止死锁的发生呢?

  • 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的
  • 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。
semaphore chopstick[5] ={1,1,1,1, 1}; 
semaphore mutex = 1;	// 互斥地取筷子
Pi ()(					// i号哲学家的进程
	while(1)(
		P(mutex);
		P (chopstick [ i] ) ;	//拿左
		P (chopstick [ (i+1) %5] ) ;	//拿右
		V(mutex);
		吃饭...
		V(chopstick[i] ) ;		// 放左
		V(chopstick[ (i+1) %5] ) ;	// 放右
		思考...
	}
}

更准确的说法应该是:

  • 各哲学家拿筷子这件事必须互斥的执行。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了

知识回顾

  • 哲学家进餐问题的关键在于解决进程死锁。
  • 这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,毎个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。

20. 管程

在这里插入图片描述

1. 为什么要引入管程

  • 信号量机制存在的问题:编写程序困难、易出错
  • 1973年,Brinch Hansen首次在程序设计语言(Pascal)中引入了 “管程”成分—— 一种高级同步机制
producer () (
	while(1)( 
		生产一个产品;
		P(mutex);	// 1
		P(empty);	// 2
		把产品放入缓冲区;
		V(mutex); 
		V(full);
	}
}

consumer () (
	while(1)(
		P(mutex);// 像这样如果写错了P操作的顺序,按1,2,3执行,就会发生死锁
		P(full);	④
		从缓冲区取出一个产品;
		V(mutex);
		V(empty); 
		使用产品;

2. 管程的定义和基本特征

管程是一种特殊的软件模块,有这些部分组成:

  1. 局部于管程的共享数据结构说明;
  2. 对该数据结构进行操作的一组过程;
  3. 对局部于管程的共享数据设置初始值的语句;
  4. 管程有一个名字。

跨考Tips: “过程”其实就是“函数”

管程的基本特征:

  • 局部于管程的数据只能被局部于管程的过程所访问;
  • 一个进程只有通过调用管程内的过程才能进入管程访问共享数据;
  • 每次仅允许一个进程在管程内执行某个内部过程。

3. 拓展: 用管程解决生产者消费者问题

monitor ProducerConsumer
	condition full, empty; // 管程中设置条件变量和等待/唤醒操作,以解决同步问题
	int count=0; // 缓冲区中的产品數W
	void insert (Item item) { // 把产品item放入缓冲区 (用编译器负贲实现各进程互斥地进入管程中的过程)
		if (count == N) 	
			wait (full);	、
		count++;
		insert_item (item);
		if (count == 1)
			signal(empty);
	}
	Item remove 0 { //从缓冲区中取出_个产品
		if (count == 0)
			wait (empty);
		count--;
		if (count == N-1)
			signal(full);
		return remove_item();
	}
end monitor;

毎次仅允许一个进程在管程内执行某个内部过程。

  • 例1:两个生产者进程并发执行,依次调用了 insert过程…
  • 例2:两个消费者进程先执行,生产者进程后执 行…

引入管程的冃的无非就是要更方便地实现进程互斥和同步。

  1. 需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
  2. 需要在管程中定义用于访问这些共享数据的“入口” 一一其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
  3. 只有通过这些特疽的“入口”才能访问共享数据
  4. 管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能比一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。
    注意:这神互斥轉性是由编译器负责实现袍, 程庁员不用关心
  5. 可在管程中设置条件变量等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出"入门”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。

程序员可以用某种特殊的语法定义一个管程(比如:monitor Producerconsumer… end monitor;), 之后其他程序员就可以使用这个管程提供的特定“入口“(“封裝”思想)很方便地使用实现进程同步/互斥了。

4. 拓展: Java中类似于管程的机制

Java中,如果用关键字 synchronized 来描述一个函数,那么这个函数同一时间段内只能被一个线程调用

static class monitor {
	private Item buffer!] = new Item[N]; 
	private int count = 0;

	public synchronized void insert(Item item){// 每次只能有一个线程进入insert函数,如果多个线程同时调用insert函数,则后来者需要排队等待
	...
	}
}

知识回顾

在这里插入图片描述

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值