操作系统之进程和线程

366 阅读34分钟

进程与线程

image-20210602170022811.png

进程

进程的定义

我们都知道计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,统领整个计算机硬件;应用程序是具有某种功能的程序,程序是运行于操作系统之上的。

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志

进程与程序的区别

  1. 进程是动态的,程序是静态的

    • 程序是有序代码的集合

    • 进程是程序的执行,进程有核心态/用户态

  2. 进程是暂时的,程序的永久的

    • 进程是一个状态变化的过程
    • 程序可长久保存
  3. 进程与程序的组成不同

    进程的组成包括程序、数据和进程控制块

进程的特征

动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;

并发性:任何进程都可以同其他进行一起并发执行;

独立性:进程是系统进行资源分配和调度的一个独立单位;

结构性:进程由程序,数据和进程控制块三部分组成

进程的组成

由程序、数据和进程控制块组成。

进程控制块

概念
  1. 操作系统管理控制进程运行所用的信息集合
  2. 操作系统用PCB来描述进程的基本情况以及运行变化的过程
  3. PCB是进程存在的唯一标志。
内容
  1. 进程标识信息
  2. 处理机现场保存
  3. 进程控制信息
  • 调度和状态信息
    • 调度进程和处理机使用情况
  • 进程间通信信息
    • 进程间通信相关的各种标识
  • 存储管理信息
    • 指向进程映像存储空间数据结构
  • 进程所用资源
    • 进程使用的系统资源,如打开文件等
  • 有关数据结构的连接信息
    • 与PCB相关的进程队列
    • 进程状态的变化体现于其进程控制块所在的链表,通过进程队列实现。

进程的状态与转换

进程的生命周期划分

  1. 进程创建
  2. 进程执行
  3. 进程等待
  4. 进程抢占
  5. 进程唤醒
  6. 进程结束

导致进程创建的情况

  1. 系统初始化时
  2. 用户请求创建一个新进程
  3. 正在运行的进程执行了创建进程的系统调用

进程执行

内核选择一个就绪的进程,让它占用处理机并运行

如何选择?处理机调度算法

进程进入等待(阻塞)的情况

只有进程本身才知道何时需要等待某种事件的发生,即导致其进入等待状态的一定是进程本身内部原因所导致的,不是外部原因所导致的。

  1. 请求并等待系统服务,无法马上完成
  2. 启动某种操作,无法马上完成
  3. 需要的数据没有到达

进程被抢占的情况

  1. 高优先级的进程变成就绪状态
  2. 调度算法为每个进程设置的时间片,进程执行的时间片用完了,操作系统会抢先让下一个进程投入运行。

唤醒进程的情况

进程只能被别的进程或者操作系统给唤醒。

  1. 被阻塞进程需要的资源可被满足
  2. 被阻塞进程等待的事件到达

进程结束的情况

  1. 正常退出(自愿的)
  2. 错误退出(自愿的)
  3. 致命错误(强制性的)
  4. 被其他进程所杀(强制性的)
  5. 进程退出了,但还没被父进程回收,此时进程处于zombie态

进程的状态

1.运行状态(Running)
  • 进程正在处理机上运行
2.就绪状态(Ready)
  • 进程获得了除了处理机之外的所有所需的资源,得到处理机即可运行
3.等待状态(有成阻塞状态Blocked)
  • 进程正在等待某一事件的出现而暂停运行
4.创建状态(New)
  • 一个进程正在被创建,还没被转到就绪状态之前的状态,是一个过渡状态。也就是在分配资源和相应的数据结构
5.退出状态(Exit)
  • 一个进程反正在从系统中消失时的状态,这是因为进程结束或者由于其他原因所致(也就是系统正在回收资源)

20190110200051140.png

各种状态的变迁

1.NULL->创建(启动)

一个新进程被产生出来执行一个程序

2.创建->就绪(进入就绪队列)

当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态

3.就绪->运行(被调度)

处于就绪状态的进程被进程调度程序选中后,就分配到处理机上运行

4.运行->结束(结束)

当进程表示它已经完成或者因出错,当前运行进程会由操作系统作结束处理(回收资源)

5.运行->就绪(时间片完或者被抢先)

处于运行状态的进程在其运行期间,由于分配给它的处理时间片用完而让出处理机

6.运行->等待(等待事件)

当进程请求某资源且必须等待时

7.等待(阻塞)->就绪(事件发生)

当进程要等待某事件到来时,它从阻塞状态变到就绪状态。

image-20210601093347956.png

image-20210601093537094.png

进程挂起

image-20210601094212087.png

1作用

处在挂起状态的进程映像在磁盘上,目的是减少进程占用内存

每种状态的含义

1.等待挂起状态

进程在外存并等待某事件的出现(多加了一个关于进程的位置信息)

2.就绪挂起状态

进程在外存,但只要进入内存,即可运行

(无法进入内存原因:内存空间不够或者进程本身优先级不够高)

3从内存到外存的变迁

0.挂起:把一个进程从内存转到外存

1.等待->等待挂起:

没有进程处于就绪状态或者就绪进程要求更多内存资源

2.就绪->就绪挂起:

当有高优先级等待(系统认为会很快就绪的)进程和低优先级就绪进程

3.运行->就绪挂起:

对抢先式分时系统,当有高优先级等待挂起进程因为事件出现而进入就绪挂起(比如内存不够)

4在外存时的状态变迁

1.等待挂起->就绪挂起

当有等待挂起进程因为相关事件出现

5激活:把一个进程从外存转到内存

1.就绪挂起->就绪

没有就绪进程或者挂起就绪进程优先级高于就绪进程

2.等待挂起->等待

当一个进程释放足够内存,并有高优先级等待挂起进程

状态队列

  1. 由操作系统来维护一组队列,表示系统中所有进程的当前状态

  2. 不同队列表示不同状态

    就绪队列、各种等待队列

  3. 根据进程状态不同,进程PCB加入相应队列

    进程状态变化时,它所在的PCB会从一个队列换到另一个

进程通信

  1. 管道/匿名管道(Pipes) :⽤于具有亲缘关系的⽗⼦进程间或者兄弟进程之间的通信。
  2. 有名管道(Names Pipes) : 匿名管道由于没有名字,只能⽤于亲缘关系的进程间通信。为了克服 这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘⽂件的⽅式存在,可以实现本机任意两个进程通信。
  3. 信号(Signal) :信号是⼀种⽐较复杂的通信⽅式,⽤于通知接收进程某个事件已经发⽣;
  4. 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(⽆名管道:只存在于内存中的⽂件;命名管道:存在于实际的磁盘介质或者⽂件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除⼀个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不⼀定要以先进先出的次序读取,也可以按 消息的类型读取.⽐ FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 信号量(Semaphores) :信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。
  6. 共享内存(Shared memory) :使得多个进程可以访问同⼀块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种⽅式需要依靠某种同步操作,如互斥锁和信号量等。可 以说这是最有用的进程间通信⽅式。
  7. 套接字(Sockets) : 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持 TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程

基本概念

进程通信是进程之间的信息交换,是进程进行通信和同步的机制。

消息传递系统

进程不借助任何共享存储区或数据结构,而是以格式化的消息(message)为单位,将数据封装在消息中,并利用操作系统提供一组通信命令(原语)完成信息传递和数据交换。

消息传递系统中实现方式

1.直接消息传递系统(消息缓冲队列(教材中))

发送进程利用OS所提供的通信指令,直接把消息放到目标进程

直接通信原语

(1)对称寻址方式;该方式要求发送和接受进程必须以显示方式提供对方的标识符。

系统提供命令:

send(receiver,message);//发送一个消息给接受进程receiver
receive(sender,message);//接受进程sender发来的消息

(2)非对称寻址方式;在接受程序原语中,不需要命名发送进程。

系统提供命令:

send(P,message);//发送一个消息给接受进程P
receive(id,message);//接受来自任何进程的消息,id变量可以设置为发送进程的id或者名字

消息格式

(1)定长(消息长度)

(2)变长(消息长度)

进程的同步方式(同步机制,进程之间)

(1)发送阻塞,接收阻塞

(2)发送不阻塞,接收阻塞

(3)发送不阻塞,接收不阻塞

对应通信链路的属性

建立方式:(1)显示建立链接命令;(2)发送命令自动建立链路

通信方式(1)单向(2)双向

2.直接消息传递系统的实例--消息缓冲队列
[1-1]数据结构--消息缓冲区

image.png

[1-2]数据结构--PCB中关于通信的数据项

(增加了消息队列的队首指针,互斥和资源信号量)

image.png

[2]发送原语

发送原语首先根据发送区a中的消息长度a.size来申请一个缓冲区i,接着把a中的信息复制到缓冲区i中。获得接受进程内部标识符j,然后将i挂在j.mq上,由于该队列属于临界资源,所以执行insert前后都要执行wait和signal操作。

20190110195815890.jpg 其中

mq//消息队列
mutex//对消息队列的互斥访问
sm//消息的资源信号量

在这里插入图片描述

[3]接受原语

调用接受原语receive(b),从自己的消息缓冲队列mq中摘下第一个消息缓冲区i,并将其中的数据复制到以b为首地址的指定消息接收区内。

img

3.间接消息传递系统(信箱)

发送和接收进程,通过共享中间实体(邮箱 )完成通信。该实体建立在随机存储器的公用缓冲区上,用来暂存信息

[1]信箱结构--数据结构

信箱头:用于存放信箱的描述信息,如信箱标识符等

信箱体:由若干个可以存放信息的信箱格组成,信箱格数目和大小是在创建信箱时确定的。

在这里插入图片描述

[2]信箱通信原语

(1)邮箱的创建和撤销

(2)消息的发送和接收

Send(mailbox,message);//将一个消息发送到指定邮箱
Receive(mailbox,message);//从指定邮箱中接受一个消息
[3]邮箱的类型

(1)私用邮箱:只有创建者才能接收消息

(2)公用邮箱:操作系统创建

(3)共享邮箱:创建进程指明接收进程

[4]使用邮箱通讯时,发送进程和接收进程之间的关系:

(1)一对一:专用通信链路

(2)多对一:客户/服务器

(3)一对多:广播

(4)多对多:公共邮箱

管道通信

1.概念
  • 管道是进程间基于内存文件的通信机制;
    • 管道在父进程创建子进程过程中继承文件描述符;
    • 缺省文件描述符:0 stdin, 1 stdout, 2 stderr;
  • 进程不关心另一端
    • 创建一个管道时,只关心通信的管道是谁,不关心另一端是谁放入的数据
    • 数据可能从键盘、文件、程序读取
    • 数据可能写入到终端、文件、程序
2.与管道相关的系统调用
  • 读管道:read(fd,buffer, nbytes)
    • scanf()是基于它实现的
  • 写管道:write(fd,buffer, nbytes)
    • printf()是基于它实现的
  • 创建管道:pipe(rgfd)
    • 结果生成的rgfd是2个文件描述符组成的数组
    • rgfd [0]是读文件描述符
    • rgfd [1]是写文件描述符

共享存储器

1.共享内存概念

共享内存是把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制

2.在进程里时

每个进程都有私有内存地址空间

每个进程的内存地址空间需明确设置共享内存段

3.在线程里时

同一进程中的线程总是共享相同的内存地址空间

4.优点
  • 快速、方便地共享数据;
  • 最快的通信方法;
  • 一个进程写另外一个进程立即可见;
  • 没有系统调用干预;
  • 没有数据复制;
5.不足
  • 不提供同步,必须用额外的同步机制来协调数据访问,比如由程序员提供同步

嵌套字

1.概念
  • 一个嵌套字就是一个通信标识类型的数据结构,包含通信目的的地址,端口号,传输层协议等
2.分类
  • 基于文件型:两个进程都运行在同一台机器上,嵌套字基于本地文件系统支持
  • 基于网络型:非对称通信方式,需要发送者提供接收者的命名
3.优点
  • 不仅使用与同一台计算机内部的进程通信,而且适用于网络环境中不同计算机之间的进程通信。

线程

image-20210601105956474.png

image-20210601110827639.png

为什么引入进程和线程?

在OS中引入进程是为了让多个程序能并发执行,来提高资源利用率和系统吞吐量。

在OS中映入线程是为了减少程序在并发执行时所付出的时空开销,使得OS有更高的并发性。

线程的概念

线程是进程的一部分,描述指令流执行状态,它是进程中的指令执行流的最小单元,是CPU调度的基本单位。

PCB变化
  • 进程的资源分配角色:进程由一组相关资源构成,包括地址空间(代码段、数据段)、打开的文件等各种资源
  • 线程的处理机调度角色:线程描述在进程资源环境中的指令流执行状态

image-20210601111403066.png

image-20210601112939266.png

image-20210601112954228.png

image-20210601113330896.png

image-20210601113407143.png

image-20210601113646663.png

线程 = 进程 - 共享资源

  • 线程的优点:
    • 一个进程中可以同时存在多个线程
    • 各个线程之间可以并发地执行
    • 各个线程之间可以共享地址空间和文件等资源
  • 线程的缺点:
    • 一个线程崩溃,会导致其所属进程的所有线程都崩溃

线程的三种实现方式

  • 用户线程:在用户空间实现(函数库实现,不依赖内核)
    • POSIX Pthreads,Mach C-threads,Solaris threads
  • 内核线程:在内核中实现(通过系统调用实现,由内核维护内核线程的线程控制块)
    • Windows,Solaris,Linux
  • 轻量级进程:在内核中实现,支持用户线程
    • Solaris (LightWeight Process)

在用户空间实现线程

image-20210601114301689.png 在用户空间实现线程的优势

  • 保存线程的状态和调度程序都是本地过程,所以启动他们比进行内核调用效率更高。因而不需要切换内核,也就不需要上下文切换,也不需要等于内存的告诉缓存进行刷新,因为线程调度非常便捷,因此效率比较高。
  • 允许每个进程又在自己的调度算法。

在用户空间实现线程的劣势

  • 线程不能进行阻塞调用,被阻塞的线程会影响其他线程。还有缺页中断问题,如果一个线程引起页面故障,内核由于不知道有线程存在,通常会把整个进程阻塞直到磁盘io完成为止,尽管其他的线程是可以运行的。
  • 如果一个线程开始运行,该线程所在的进程中的其他线程都不能运行,除非第一个线程自愿放弃cpu。

在内核中实现线程

image-20210601142810400.png

image-20210601143017953.png

混合实现

image-20210601143546018.png

进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;

  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

线程和进程关系示意图

image-20210601143546018.png

image-20210601144320659.png

同步与互斥

进程同步的基本概念

进程同步

对多个进程在执行顺序上进行调节,使并发执行的诸程序之间能按照一定的规则(时序)共享系统资源,并能够很好的相互合作,从而使程序的执行具有可再现性

两种形式的制约

间接相互制约关系(互斥):由于共享系统资源导致

直接相互制约关系(同步):为完成同一任务而合作

临界资源(Critical resource, 进程需要互斥访问的资源)

比如打印机,磁带机,producer-consumer问题

临界区(Critical Section,进程中访问临界资源的代码)

enter section		//进入区
	critical section //临界区
exit section		//退出区
	remainder seciton//剩余区
  • 临界区
    • 进程中访问临界资源的一段需要互斥执行代码
  • 进入区(entry section)
    • 检查可否进入临界区的一段代码
    • 如可进入,设置相应"正在访问临界区"标志
  • 退出区(exit section)
    • 清除“正在访问临界区”标志
  • 剩余区(remainder section)
    • 代码中的其余部分,与同步互斥无关的代码

image-20210601153927361.png

同步机制规则(临界区的访问规则)

空闲让进:无进程时,任何进程可进去

忙则等待:有进程在临界区时,其他进程均不能进入临界区

有限等待:有点等待时间,不能无限等待

让权等待(可选):不能进入临界区的进程,应释放CPU

实现临界区互斥的基本方法

软件实现方法,硬件实现方法。

方法一:禁用硬件中断
  • 没有中断,没有上下文切换,因此没有并发
    • 硬件将中断处理延迟到中断被启用之后
    • 现代计算机体系结构都提供指令来实现禁用中断
local_irq_save(unsigned long flags); //关中断
critical section					//临界区
local_irq_restore(unsigned long flags); //使能中断
  • 进入临界区
    • 禁止所有中断,并保存标志
  • 离开临界区
    • 使能所有中断,并恢复标志
  • 缺点
    • 关中断后,进程无法被停止,可能导致其他进程饥饿或者系统停止
    • 临界区可能很长,因为无法确定响应中断所需要的时间,并且可能存在硬件影响
    • 不适用于多CPU系统,因为一个处理器上关中断不影响其他处理器执行临界区代码
方法二:软件方法-Peterson算法

满足线程TiTi和TjTj之间互斥的经典的基于软件的方法

  • 共享变量
int turn;	//表示允许进入临界区的线程ID
boolean flag[];	//表示进程请求进入临界区
  • 进入区代码
flag[i]=true;	
turn=j;
while(flag[j] && turn == j);	//有一个条件不满足就进入临界区,否则一直等
/*
*此时如果同时有两个进程进入临界区
*那么先写的那个进程能进入(后一个不满足),后的不能(都满足)
*/
  • 退出区代码
flag[i]=false;

线程TiTi的代码

do{
    flag[i]=true; //线程i请求进入临界区
    turn=j;		
    while(flag[j] && turn == j);
        CRITICAL SECTION	//临界区
    flag[i]=false;
    	REMAINDER SECTION 	//退出区
}while(true);
方法二:软件方法-Dekkers算法(两个进程)
flag[0]=false;
flag[1]=false;
turn=0;
do{
    flag[i]=true; //线程i请求进入临界区
    while(flag[j]==true){
        if(turn!=i){
            flag[i]=false;
            while(turn!=i){}
            flag=true;
        }
    }
    CRITICAL SECTION //临界区
    turn=j;
    falsg[i]=false;
    REMAINDER SECTION //退出区
}while(true);
方法三:更高级的抽象方法
1.概念

基于硬件提供了一些同步原语,比如中断禁用,原子操作指令等

操作系统提供更高级的编程抽象来简化进程同步,例如:锁、信号量,用硬件原语来构建

image-20210601155311937.png

image-20210601155503088.png

2.例如使用TS指令实现自旋锁(spinlock)
class Lock{
    int value=0;
}
//忙等待锁
Lock::Acquire(){
    while(test-and-set(value))
        ;//spin
}

Lock::Release(){
    value=0;
}
3.原子操作指令锁的特征
  • 优点
    • 适用于单处理器或者共享主存的多处理器中任意数量的进程同步
    • 简单并且容易证明
    • 支持多临界区
  • 缺点
    • 忙等待消耗处理器时间
  • 可能导致饥饿
    • 进程离开临界区时有多个等待进程的情况
  • 死锁
    • 有一个拥有临界区的低优先级进程
    • 同时有一个请求访问临界区的高优先级进程获得处理器并等待临界区

image-20210601155552732.png

信号量(semaphore)

概念

信号量是操作系统提供的一种协调共享资源访问的方法

image-20210601160335370.png

image-20210601160348684.png

1.信号量是一种抽象数据类型

由一个整形变量sem(共享资源的数目)和两个原子操作组成

//P操作--申请使用资源
P()(Prolaag (荷兰语尝试减少)) -》wait
sem--;//可用资源减少一
if sem<0,进入等待,否则继续  //可用资源用完了,需要等待其他线程释放资源

//V操作--释放可用资源
V()(Verhoog (荷兰语增加)) -》signal
sem++;
if sem<=0,唤醒一个等待进程
2.信号量的特性
  • 信号量(sem)是被保护的整数变量
    • 初始化完成后,只能通过P()和V()操作修改
  • 操作系统保证,PV操作是原子操作(无法被打断)。
  • P() 可能阻塞(由于没有资源进入等待状态),V()不会阻塞(V操作只会释放资源)
  • 通常假定信号量是“公平的”
    • 线程不会被无限期阻塞在P()操作
    • 假定信号量等待按先进先出排队(等待队列按照FCFS排列)
  • 信号量不能避免死锁问题
3.自旋锁能否实现先进先出?

不能。因为自旋锁需要占用CPU,随时检查,有可能临界区的使用者退出时刚修改完,下一个进入者进入时资源才变成有效,就无法实现先进先出。

信号量的实现

classSemaphore{
    int sem;	//共享资源数目
    WaitQueue q;  //等待队列
}

Semaphore::P(){
    sem--;
    if(sem<0){
        //资源用完了
		Add this thread t to q;
        block(p);  //阻塞
    }
}

Semaphore::V() {
    sem++; 
    if (sem<=0) {
        //此时前面仍有等待线程
        //从对应的等待队列里把相应的线程放入就绪队列
        Remove a thread t from q;
        wakeup(t);        
    }
}

信号量的使用

1.信号量分类
  • 可分为两种信号量
    • 二进制信号量(AND型):资源数目为0或1
    • 资源信号量(记录型):资源数目为任何非负值
    • 两者等价
      • 基于一个可以实现另一个
2.信号量的使用
  • 互斥访问
    • 临界区的互斥访问控制
  • 条件同步
    • 线程间的事件等待
3.用信号量实现临界区的互斥访问

每个临界区设置一个信号量,其初值为1

mutex = new Semaphore(1); //信号量初始化为1

//控制临界区的访问
mutex->P();		//信号量计数--
Critical Section;
mutex->V();		//释放资源,信号量计数++

注意:

初始化如果是同步互斥,看资源数目,如果是条件同步,为0或者1

必须成对使用P()操作和V()操作

P()操作保证互斥访问临界资源

PV操作不能次序错误、重复或遗漏(但不要求P在V之前或者之后)

执行时不可中断

问题:

不申请直接释放,出现多个线程进入临界区的情况

只申请不释放,缓冲区没有线程,但是谁也进不去临界区

4.用信号量实现条件同步
//此时的条件同步设置一个信号量,初始化为0
condition =new Semaphore(0);

//实现一个条件等待,线程A要等到线程B执行完X模块后才能执行N模块


//线程A
---M---
    condition->P();
---N---
    
//线程B
---x---
	condition->V();
---Y---
    
    
    //在B里释放信号量,使其0->1,如果B先执行完X模块,则A可以直接往下执行;
    //如果A先执行完就等待

生产者-消费者问题(信号量)

20190110200307291.png

  • 有界缓冲区的生产者-消费者问题描述

    • 一个或多个生产者在生成数据后在一个缓冲区里
    • 一个或多个消费者从缓冲区出数据处理
    • 任何时刻只能有一个生产者或消费者可访问缓冲区
  • 问题分析

    • 任何时刻只能有一个线程操作缓冲区(互斥访问
    • 缓冲区空时,消费者必须等待生产者(条件同步
    • 缓冲区满时,生产者必须等待消费者(条件同步
  • 用信号量描述每个约束

    • 二进制信号量mutex
    • 资源信号量fullBuffers--等待有数据
    • 资源信号量emptyBuffers--缓冲区有空闲
    • 两个资源相加=缓冲区总大小
  • 实现

    Class BoundedBuffer {
        mutex = new Semaphore(1);
        fullBuffers = new Semaphore(0);//一开始缓冲区中没有数据
        emptyBuffers = new Semaphore(n);//全都是空缓冲区
    }
    
    BoundedBuffer::Deposit(c) {//生产者
        emptyBuffers->P(); //检查是否有空缓冲区
        mutex->P(); //申请缓冲区
        Add c to the buffer;
        mutex->V();
        fullBuffers->V();//释放该资源,相等于缓冲区中多了一个数据
    }
    
    BoundedBuffer::Remove(c) {//消费者
        fullBuffers->P();//检查缓冲区中是否有数据
        mutex->P();
        Remove c from buffer;
        mutex->V();
        emptyBuffers->V();//释放空缓冲区资源
    }
    

    两次P操作的顺序有影响吗?

    交换顺序会出现死锁,原因在于自我检查空和满

image-20210601160716144.png

互斥量

image-20210601162116113.png

image-20210601162314739.png

image-20210601162714577.png

image-20210601162724156.png

image-20210601164336715.png

image-20210601164447018.png

管程

概念

管程是一种用于多线程互斥访问共享资源的程序结构

  • 采用面向对象方法,简化了线程间的同步控制
  • 任一时刻最多只有一个线程执行管程代码
  • 正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复

管程的使用

  • 在对象/模块中,收集相关共享数据
  • 定义访问共享数据的方法

管程的组成

1.(在入口队列加)一个锁

控制管程代码的互斥访问

2.0或者多个条件变量

管理共享数据的并发访问

如果是0个,就等同与一个临界区,如果是多个就是管程所特有的

image.png

3.条件变量
  • 条件变量是管程内的等待机制

    • 进入管程的线程因资源被占用而进入等待状态
    • 每个条件变量表示一种等待原因对应一个等待队列
  • Wait()操作

    --

    等待操作

    • 将自己阻塞在等待队列中
    • 同时唤醒一个等待者或释放管程的互斥访问(即允许另外一个线程进入管程)
  • Signal()操作

    --

    释放操作

    • 将等待队列中的一个线程唤醒
    • 如果等待队列为空,则等同空操作
4.条件变量的实现
Class Condition{
    int numWaiting=0;
    //条件变量初值为0,如果在信号量里和资源数目一致
    WaitQueue q;
}
Condition::Wait(lock){
    numWaiting++;  //等待数目++
    Add this thread t to q;//将自己放入等待队列当中
    release(lock);	//释放管程的互斥访问权限
    shedule();//执行调度,切换线程need mutex
    require(lock);//请求访问权限
}
Condition::Signal(){
    if(numWaiting>0){//等待队列不为空,即有另外的线程等待这个条件变量上,每个变量对应一个队列
        Remove a thread t from q;//将此线程从等待队列移动到就绪队列中
        wakeup(t);//唤醒进程need mutex
        numWaiting--;//等待数目--
    }
}

4生产者-消费者问题

classBoundedBuffer {
    …
    Lock lock;//一个入口等待队列
    int count = 0; //写入缓冲区的数据的数目
    Condition notFull, notEmpty;//两个条件变量
}
BoundedBuffer::Deposit(c) {//生产者
    lock->Acquire();	//管程进入权申请
    while (count == n)	//n个缓冲区中都有数据了--对应**1
        notFull.Wait(&lock);//就放弃管程使用权,等在notfull条件变量上
    Add c to the buffer;
    count++;
    notEmpty.Signal();
    lock->Release();	//管程进入权释放
}
BoundedBuffer::Remove(c) {//消费者
    lock->Acquire();	//管程进入权申请
    while (count == 0)    //如果没数据
      notEmpty.Wait(&lock);//就放弃管程使用权,等在非空条件上
    Remove c from buffer;
    count--;
    notFull.Signal();	//读出一个数据后就释放notfull--对应**1
    lock->Release();	//管程进入权释放
}
做法:

1.初始化

2.管程中写法

  • 先写管程进去权申请和释放lock

image-20210601170510844.png

image-20210601171141662.png

image-20210601171150733.png

消息传递

image-20210601183830697.png

image-20210601183855682.png

image-20210601183909744.png

image-20210601184004093.png

屏障

image-20210601184041064.png

同步问题

哲学家就餐问题

1.问题描述:

5个哲学家围绕一张圆桌而坐,桌子上放着5支叉子,每两个哲学家之间放一支

哲学家的动作包括思考和进餐,进餐时需同时拿到左右两边的叉子,思考时将两支叉子放回原处

如何保证哲学家们的动作有序进行?如:不出现有人永远拿不到叉子

2.信号量解决
#define   N   5                     // 哲学家个数
semaphore fork[5];                  // 信号量初值为1
void   philosopher(int   i)         // 哲学家编号:0 - 4
    while(TRUE)
    {
        think( );                   // 哲学家在思考
        if (i%2 == 0) {
            P(fork[i]);	      // 去拿左边的叉子
            P(fork[(i + 1) % N]);   // 去拿右边的叉子
        } else {
            P(fork[(i + 1) % N]);   // 去拿右边的叉子
            P(fork[i]);             // 去拿左边的叉子 
        }      
        eat( );                     // 吃面条中….
        V(fork[i]);		      // 放下左边的叉子
        V(fork[(i + 1) % N]);	      // 放下右边的叉子
    }
//没有死锁,可有多人同时就餐

读者-写者问题(信号量)

1.问题描述
  • 共享数据的两类使用者
    • 读者:只读取数据,不修改
    • 写者:读取和修改数据
  • 读者-写者问题描述:对共享数据的读写
    • “读-读”允许
      • 同一时刻,允许有多个读者同时读
    • “读-写”互斥
      • 没有写者时读者才能读
      • 没有读者时写者才能写
    • “写-写”互斥
      • 没有其他写者时写者才能写
2.解决

用信号量描述每个约束

  • 信号量WriteMutex
    • 控制读写操作的互斥
    • 初始化为1
  • 读者计数Rcount
    • 正在进行读操作的读者数目
    • 初始化为0
  • 信号量CountMutex
    • 控制对读者计数的互斥修改
    • 初始化为1,同一时间只有一个可以写
semphoare WriteMutex=1;
int Rcount=0;
semphoare CountMutex=1;
void Writer(){
    P(WriteMutex);
    write;
    V(WriteMutex);
}
void Reader(){
    P(CountMutex);
    if (Rcount == 0)
		P(WriteMutex);
	++Rcount;
    V(CountMutex);
	read;
	P(CountMutex);
	--Rcount;
	if (Rcount == 0)
		V(WriteMutex);
	V(CountMutex)
}
//此实现中,读者优先
3.优先策略

读者优先策略

只要有读者正在读状态,后来的读者都能直接进入

如读者持续不断进入,则写者就处于饥饿

写者优先策略

只要有写者就绪,写者应尽快执行写操作

如写者持续不断就绪,则读者就处于饥饿

如何实现?

读者-写者问题(写者优先)

用信号量描述每个约束

  • 信号量WriteMutex
    • 控制读写操作的互斥
    • 初始化为1
  • 读者计数Rcount
    • 正在进行读操作的读者数目
    • 初始化为0
  • 信号量CountMutex
    • 控制对计数变量Rcount的互斥修改
    • 初始化为1,同一时间只有一个可以写
  • 信号量ReadMutex与x
    • 控制读者进入
    • x防止大量读者被阻塞
  • 写者计数WRcount
    • 正在进行写操作的写者数目
    • 初始化为0
  • 信号量WRMutex
    • 控制计数变量WRcount的互斥修改
    • 初始化为1,同一时间只有一个可以修改
semaphore WriteMutex=1,ReadMutex=1,x=1;
int Rcount=0,WRcount=0;
semaphore CountMutex=1,WRMutex=1;
void Reader(){
    P(x);
    P(ReadMutex);
    P(CountMutex);
    if(Rcount==0)
        P(WriteMutex);
    ++Rcount;
    V(CountMutex);
    V(ReadMutex);
    V(x);
    read;
    P(CountMutex);
    Rcount--;
    if(Rcount==0)
       V(WriteMutex);
    V(CountMutex);
}
void Writer(){
    P(WRMutex);
    if(WRcount==0)
        P(ReadMutex);
    WRcount++;
    V(WRMutex);
    P(WriteMutex);
    Write;
    V(WriteMutex);
    P(WRMutex);
    WRcount--;
    if(WRcount==0)
        V(ReadMutex);
    V(WRMutex);
}

处理机调度

调度的基本概念

处理机调度

调度的实质是一种资源分配,处理机调度是对处理机资源进行分配。

处理机调度决定系统运行时的性能:系统吞入量、资源利用率、作业周转时间、作业响应时间等….

处理机调度的层次

高级调度(又称长程调度或者作业调度)->> 作业级

低级调度(又称短程调度或者进程调度)->> 进程(线程)级

中级调度(内存调度)->> 内存

处理机调度算法功能--CPU的时分复用

  • 处理机调度算法
    • 就绪队列中挑选下一个占用CPU运行的进程
    • 从多个可用CPU中挑选就绪进程可使用的CPU资源
  • 调度程序:挑选就绪进程的内核函数
    • 调度策略:依据什么原则挑选进程/线程?
    • 调度时机:什么时候进行调度?

调度时机、切换与过程

在进程/线程的生命周期中的什么时候进行调度?

20190110200051140.png

内核运行调度程序的条件

  • 进程从运行状态切换到等待状态
  • 进程被终结(退出)了

非抢占系统

  • 当前进程主动放弃CPU时

可抢占系统

  • 中断请求被服务例程响应完成时
  • 当前进程被抢占
    • 进程时间片用完
    • (高优先级)进程从等待切换到就绪

调度的基本准则

处理机调度算法的共同目标

  • 资源利用率:CPU处于忙状态的时间百分比(包括I/O)
    • (CPU利用率=) (\frac{CPU有效工作时间}{CPU有效工作时间+CPU空闲等待时间})
  • 公平性:各个进程都能合理的使用CPU,不会发送进程饥饿状态
  • 平衡性:使各个资源经常处于繁忙状态
  • 策略强制执行

批处理系统的目标

  • 平均周转时间

    T短(周转时间是指作业到达时间开始一直到作业完成的时间)

    • [T={\frac{1}{n}}\left [\sum_{i=0}^n T_i \right] ]
  • 带权的平均周转时间

  • [T={\frac{1}{n}}\left [\sum_{i=0}^n \frac{T_i}{T_s} \right] ]

  • 其中(T_i)为作业周转时间,(T_s)为系统为其提供服务的时间

  • 系统吞吐量高

  • 处理机利用率高

image-20210602101130178.png

分时系统的目标

  • 响应时间快
  • 均衡性

实时系统的目标

  • 截止时间的保证
  • 可预测性
  • 哪些系统是实时系统?
    • 硬实时操作系统的代表:VxWorks
    • 软实时操作系统的代表:各种实时Linux

硬实时:必须满足绝对的截止时间

软实时:虽然不希望偶尔措施截止时间,但是可以容忍。

image-20210602094011413.png

image-20210602094041496.png

image-20210602094125801.png

典型调度算法

批处理中的调度

先来先服务调度算法(FCFS)

image-20210602101443934.png

1.依据进程进入就绪状态的先后顺序排列
2.周转时间(从到达时间~作业结束时间)

比如3个进程,计算时间为12,3,3,到达顺序为P1,P2,P3(假设同一时刻到达)

1165691-20190114215036408-2002816900.png

则周转时间=(12+15+18)/3=15

如果到达顺序为P2,P3,P1

1165691-20190114215044346-675409244.png 则测试周转时间=(3+6+18)/3=9

3.优点

简单

4.缺点

平均等待时间波动比较大,比如短进程可能排在长进程后面

短进程(短作业、短线程)优先调度算法(SPN,SJF)

1.概念

选择就绪队列中执行时间最短的进程占用CPU进入运行状态

2.排序

就绪队列按照预期的执行时间长度来排序

3.SPN的可抢占改进--SRT(短剩余时间优先算法)

新进程所需要的执行时间比当前正在执行的进程剩余的执行时间还要短,那么允许它抢先。

4.SPN具有最优平均周转时间

SPN算法中一组进程的平均周转时间

20190110200155944.png 此时周转时间(=(r_1+r_2+r_3+r_4+r_5+r_6)/6)

修改进程执行顺序可能减少平均等待时间吗?

20190110200201784.png **

即按照R值来排序 R=(w+s)/s w: 等待时间(waiting time) s: 执行时间(service time)

2.优点
  • 在短进程优先算法的基础上改进
  • 不可抢占
  • 关注进程的等待时间
  • 防止无限期推迟(w越大,优先级越高)

时间片轮转调度算法(依靠时钟中断)

1.思想

时间片结束时,按照FCFS算法切换到下一个就绪进程

每隔(n-1)个时间片进程,进程执行一个时间片q

image-20210602102027948.png 2.时间片为20的RR算法示例

20190110200210220.png

3.进程切换时机

进程在一个时间片内已执行完,立刻调度,并将该进程从就绪队列中删除

进程在一个时间片内未执行完,立刻调度,并将该进程放入就绪队列末尾

4.RR的时间片长度
  • RR开销主要在于额外的上下文切换
  • 如果时间片太大
    • 那么等待时间过长
    • 极限情况下RR就退化为FCFS
  • 如果时间片太小
    • 反应迅速,会产生大量的上下文切换
    • 从而增加了系统的开销,影响系统吞吐量
  • 时间片长度选择目标
    • 选择合适的时间片长度
    • 经验规则:维持上下文开销处于1%

比较FCFS和RR例子

0.等待时间(=)周转时间(-)执行时间

20190110200216884.png

1.最好FCFS相当于短进程优先
2.最坏FCFS相等于长进程优先

多级队列调度算法(MQ)

1.就绪队列被划分成多个独立的子队列

如:前台(交互)、后台(批处理)

2.每个队列拥有自己的调度策略(进程不能在队列间移动)

如:前台–RR、后台–FCFS

3.队列间的调度
  • 如果固定优先级
    • 先处理前台,然后处理后台
    • 可能导致饥饿
  • 如果时间片轮转
    • 每个队列都得到一个确定的能够调度其进程的CPU总时间
    • 如:80%CPU时间用于前台,20%CPU时间用于后台

多级反馈队列调度算法(MLFQ)

1.调度机制

设置多个就绪队列,为每个队列赋予不同的优先级,每个队列采用FCFS算法,按队列优先级调度

  • 进程可在不同队列间移动的多级队列算法
    • 队列时间片大小随优先级级别增加而增加(倍增),即时间片越小队列优先级越高
    • 如进程在当前的时间片没有完成,则降到下一个优先级队列
2.MLFQ算法的特征
  • CPU密集型进程的优先级下降很快,并且时间片会分得很大
  • I/O密集型进程停留在高优先级

image-20210602093842097.png 彩票调度

image-20210602110310307.png 公平分享调度

image-20210602110453739.png

image-20210602110503241.png 线程调度

image-20210602112136840.png

image-20210602112151350.png

image-20210602112213438.png