现代操作系统:第二章 进程和线程

操作系统中最核心的概念就是进程,这是对正在运行的程序的抽象。

进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。

2.1 进程

操作系统最核心的概念就是进程,它是对正在运行的程序的一个抽象,也可以理解为对处理器的抽象。即使可用的CPU可用,但是依然可以支持多进程(伪)并发操作。

2.1.1 进程模型

在进程模型中,计算机上所有的可运行的软件,通常也包括操作系统,被组织成若干顺序进程,简称进程。一个进程就是一个正在执行程序的实例。

在这里插入图片描述

一个进程就是某种类型的活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。

2.1.2 进程的创建

四种主要事件会导致进程创建:

  1. 系统初始化
  2. 正在运行的程序执行了创建进程的系统调用
  3. 用户请求创建一个进程
  4. 一个屁处理作业的初始化

在UNIX系统中,只有一个系统调用可以用来创建新的进程:fork。这个系统调用会创建一个与调用进程相同的副本。进程创建之后,父进程和子进程有各自不同的地址空间。如果其中某个进程在地址空间中修改了一个字,这个修改对其他进程而言是不可见的。

2.1.3 进程的终止

四种主要事件会导致进程的终止:

  1. 正常退出(自愿的)
  2. 出错退出(自愿的)
  3. 严重错误(非自愿)
  4. 被其他进程杀死(非自愿)

2.1.4 进程的层次结构

UNIX系统中,进程 创建另一个进程后,父进程和子进程就以某种形式继续保持联系,进程只有一个父进程,但可以有多个子进程。

Windows中没有进程层次的概念,所有进程都是地位相等的,但父进程创建爱你子进程后会得到一个令牌(称为句柄),该句柄可以用来控制子进程,但父进程有权把这个令牌传递给其他进程,这样,就不存在层次概念了。

2.1.5 进程的状态

进程有三种状态:

  1. 运行态(该时刻进程实际占用CPU)
  2. 就绪态(可运行,但是因为其他进程而运行而暂时停止)
  3. 阻塞态(除非某种外部事件发生,否则进程不能运行)

进程总是在这三种状态见切换:

在这里插入图片描述

进程准备好后就可以运行了,但是这个时候其他程序在运行,因此进入就绪状态,当调度选择了这个进程时,它就可以运行了。而正在运行的进程,由于CPU给的时间用完了,但还没执行完毕,这时就会进入就绪转态,等待下次调度进入CPU。如果正在运行的进程由于需要I/O操作或者其他因素,需要等待一段时间才可以继续运行,就会进入阻塞状态,如果I/O执行完毕,就会进入就绪状态,再次等待运行。

操作系统的最底层是调度程序,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的所有具体细节都隐藏在调度程序中。

2.1.6 进程的实现

为了实现进程模型,操作系统维护一张表格,即进程表,每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所在文件的状态、账号和调度信息。

在这里插入图片描述

与每个I/O类关联的是一个称为中断向量的位置(靠近内存底部的固定区域)。它包含中断服程序的入口地址。进程的切换都是一次中断,所有的中断都是从保存寄存器开始,对于当前进程而言,通常是保存在进程表项中, 中断的处理和调度过程如图:

在这里插入图片描述

2.1.7 多道程序的设计模型

CPU利用率 = 1 - p^n. p为一个进程等待I/O操作的时间与其在内存中时间的比值,n为程序的数量。

2.2 线程

在传统的操作系统中,每个进程有一个地址空间和一个控制线程。

2.2.1 线程的使用

线程:线程是一种并行实体拥有共享一个地址空间和所有可用数据的能力。

多线程是在多进程的基础上继续的演化来的, 都是为了提升CPU的利用率,因为CPU是一种很宝贵的资源,因减少浪费。假设一个进程要执行三道数学题,如果没有线程的概念,那这个进程只能一道一道的去完成,执行第一道产生I/O操作时,就会阻塞,知道阻塞完成了才会继续执行。这样效率就比较低,而如果有了多个线程,每个线程执行一道数学题,互不干扰,这个线程就可以一直处于运行状态,抢到时间片的几率就大了,就可不断执行。并且,如果当前运行进程少或者其他进程也在阻塞,那没人用CPU,就是一种浪费,所以多线程可以尽量减少进程的阻塞,从而可以让CPU一直有事可做。当然,这只是一种比喻。 还有就是,进程切换的开销远大于线程切换的开销,毕竟,线程切换都是在同一个进程中。

2.2. 经典的线程模型

进程模型有两种独立的概念:资源分组处理和执行

进程拥有一个执行的线程,通常称为线程。在线程中有一个程序计数器,用来记录接着要执行哪条指令。线程拥有寄存器,用来保存线程当前的工作变量。线程还有一个堆栈,用来记录执行历史,其中每个帧保存了一个已经调用的但是还没有从中返回的过程。

用一句简单的话说:进程用于把资源集中在一起,而线程则是在CPU上被调度执行的实体

在这里插入图片描述

进程中的线程不同线程不像不同进程之间有很大的独立性。所有线程有完全一样的地址空间,共享同样的全局变量。

在这里插入图片描述

线程概念试图实现的是:共享一组资源的多个线程的执行能力,以便这些线程可以为完成某一个共同任务而相同工作

和传统进程一样,线程也可以处于若干状态中的任何一个:运行、阻塞、就绪。

  1. 正在运行的线程拥有CPU并且是活跃的。
  2. 被阻塞的线程正在等待某个释放它的事件。
  3. 就绪的线程是可以被调度运行的。
  • 有两种方式可以实现线程包:在用户空间和在内核中

2.2.4 在用户空间中实现线程

线程在一个运行时系统的上层运行,该运行时系统是一个管理线程的过程的集合。在用户管理线程时,每个进程需要有其专用的线程表(TCB),用来跟踪该进程中的线程。

在这里插入图片描述

在这里插入图片描述

优点

在这里插入图片描述

缺点

在这里插入图片描述

2.2.5 在内核空间中实现线程

此时不需要运行时系统了,另外,每个进程中也没有线程表(TCB)。相反在内核中有用来记录系统中的所有线程的表。当某个线程希望创建或者撤销一个已有的线程的时候,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程的创建或者撤销工作。

内核的线程保存了每个线程的寄存器、状态和其他信息。

在这里插入图片描述

在这里插入图片描述

优点

在这里插入图片描述

缺点:切换线程需要从用户态切换到内核状态。

2.2.6 混合实现

内核只识别内核级别的线程,并对其进行调度。其中一些内核级别的线程会被多个用户级别的线程多路复用。如果同在没有多线程能力操作系统中某个进程中的用户级别线程一样,可以创建、撤销和调度这些用户级别的线程。

在这里插入图片描述

2.3 进程间通信

进程间通信是很重要也很常用的的一个概念,主要围绕三个问题:

  1. 如何把信息传递给另一个进程
  2. 如何确保两个或多个进程不会交叉(例如多个用户在飞机订票系统同时取买票,该给谁)
  3. 如何确保按正确的顺序执行:例如B进程要打印A进程的结果,那么肯定是先执行完A,才能执行B。

2.3.1 竞争条件

类似这样的情况,即两个或者多个进程读写某个共享数据,而最后的结果取决于进程运行的精确时序,这就称为竞争条件。

2.3.2 临界区

互斥就是指通过某种手段确保当一个进程在使用一个共享变量或者文件的时候,其他进程不能做同样的操作。

临界区域在某些时候可能需要访问共享内存或共享文件,或执行另外一些会导致竞争的操作时,我们把对共享区域进行访问的程序片段称为临界区域。

为了避免竞争条件,引入互斥的概念,设计的方案应该满足以下四个条件:

  1. 任何两个进程不能同时处于临界区
  2. 与CPU的速度和数量无关
  3. 临界区外的进程不得阻塞其他进程
  4. 进程不能无限期等待

使临界区保持互斥的效果图如下:

在这里插入图片描述

2.3.3 忙等待的互斥

1. 内存屏障

在单处理器系统中,最简单的方法就是使每个进程在刚刚进入临界区后立即屏蔽所有的中断,并将在要离开的时候再打开中断。屏蔽中断后,时钟也会被屏蔽。这样,在屏蔽中断之后CPU将不会被切换到其他进程。

2. 锁变量

设想有一个共享变量,将其初始值设置为0。当一个进程要进入其临界区的时候,它首先测试有没有这把锁,如果该锁的值为0,则将其设置为1并进入临界区。若这把锁的值已经为1,则该进程将等待其值变为0。于是,0就表示该临界区没有进程,1就表示已经有某个进程进入到临界区。

3. 严格轮换法

需要用到忙等待,非常浪费CPU。导致其他进程可能被阻塞。 连续测试一个值直到某个值出现,称为忙等待, java示例代码如下:

while(true) {
    while(a != 0) {
        break;
    }
}

4. Peterson解法

一个不需要严格轮换的软件互斥算法。

5. TSL指令

硬件支持的方案,需要用到TSL指令:

TSL RX, LOCK

TSL指令将 内存自LOCK读到寄存器RX中,然后在该内存地址上存在一个非零值。读字和写字由操作系统保证是不可分割的(原子性),CPU将锁住内存总线,以禁止其他CPU在本指令结束前访问内存。

一个可替代TSL的指令是XCHG,本质和TSL解决办法一致。

2.3.4 睡眠和唤醒

前面的解决方法都是采用忙等待的做法。这些做法的本质是:当一个进程想进入临界区,先检查是否允许进入,若不允许则原地等待,直到允许为止。 但很浪费时间。引入生产者消费者,但没从根本解决。这种做法不仅浪费了CPU资源,而且还可能产生意想不到的结果。

2.3.5 信号量 (Semaphore)

信号量有两个操作,一个是up操作一个是down操作。对一个信号量执行down操作,则是检查其值是否大于0,若该值大于0,则将其值减1(即用掉一个保存的唤醒信号)并继续。若改值为0,则将进程睡眠,并且此时的down操作还没有结束。将检查值、修改值以及可能发生的睡眠操作作为一个单一的、不可分割的原子操作完成,原子性由操作系统保证。在完成前,其他进程不允许访问信号量。

2.3.6 互斥量(mutex)

信号量的简化版本,不需要计数能力,只需要两种状态:解锁和加锁。一个二进制位就可表示它,不过通常用整型。

互斥量使用两个过程。当一个线程需要访问临界区的时候,他调用mutex_lock。如果该互斥量当前是解锁的(即临界区是可用),此调用成功,调用线程可以自由进入该临界区。另一方面,如果该互斥量是已经加锁的,调用线程被阻塞,知道临界区中的线程完成并调用mutex_unlock。如果多个线程被阻塞在互斥量上,将随机选择一个线程并允许它获得锁。

2.3.7 管程

使用信号量的问题:一处很小的错误将会导致很大的麻烦。这就像用汇编语言一样,甚至更糟,因为这里出现的错误都是竞争条件、死锁、以及其他一些不可预测和不可再出现的错误。

产生目的:为了更加易于编写正确的程序,我们提出了一种更加高级的同步原语,称为管程。

一个管程是由过程、变量以及数据结构等组成的一个集合,他们组成了一个特殊的模块或者软件包。

管程是编程语言的组成部分,编译器知道他们的特殊性。任意时刻管程中只有一个活跃进程。 管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。管程具有面向对象编程的特点。

额外概念,同步原语:保证同步执行的代码语句。大致理解为只有将同步的代码执行完毕,才能顺序执行下一段代码。

2.3.8 消息传递

消息传递:这种进程间通信的方法采用两条原语:send和receive

send(destinction,& message)
receive(source,& message)

send调用向另一个给定的目标发送一条消息。receive调用从一个给定的源(或者是任意的源)接受一条消息。如果没有消息可用,则接受者可能被阻塞。

通常在并行程序设计系统中使用消息传递。如著名的消息传递通信框架:Message Passing Interface,MPI

2.3.9 屏障

通常用于进程组。在有些应用中划分了若干的阶段,并且规定,除非所有的进程都就绪准备着手进入到下一个阶段,否则任何进程都不能进入到下一个阶段。屏障把他们的执行划分了不同阶段,每个阶段末尾设置一个屏障,只有所有进程都到达屏障,才能继续运行下一个阶段。

在这里插入图片描述

2.4 调度

在操作系统中,完成选择工作的这一部分称为调度程序,该程序使用的算法称为调度算法。

2.4.1 调度算法简介

1. 进程行为

几乎所有进程的I/O请求和计算都是交替突发的。

在这里插入图片描述

某些进程花费了绝大多数时间在计算上,而其他进程在等待I/O上花费了绝大多数时间。前者称为计算密集性,后者称为I/O密集型。

2. 何时调度

  1. 在创建一个新的进程后,需要决定是运行父进程还是运行子进程。
  2. 在一个进程退出的时候必须做出调度决策。
  3. 当一个进程阻塞在I/O和信号量上或者由于其他原因阻塞的时候,必须选择另一个进程运行。
  4. 在一个I/O中断发生的时候,必须做出调度决策。

根据如何处理时钟中断,可以把调度算法分为两类。非抢占式调度算法和抢占式调度算法。

  • 非抢占式系统:调度算法挑一个去运行,直到该进程阻塞或自动释放CPU。
  • 抢占式系统:雕塑算法挑一个进程,让其运行固定的最大时间周期,如果时间到了还在运行,则挂起等待下一次运行,然后切换下一个进程。

3. 调度算法的分类

  1. 批处理
  2. 交互式
  3. 实时

4. 调度算法的目标

在这里插入图片描述

2.4.2 批处理系统中的调度

  1. 先来先服务:按照请求CPU的顺序使用CPU。
  2. 最短作业优先:谁的运行时间短,谁先执行,好处是每个作业的平均等待时间短。如两个进程,A需要运行20分钟,B两分钟,如果先运行A,则B等待20分钟,总的等待20分钟,平均等待10分钟;如果先运行B,在运行A,则A等待2分钟,总的等待2分钟,平均等待一分钟。
  3. 最短剩余时间优先:也是抢占式的,每次找到剩余执行最短的程序执行,给其固定的运行时间,如果到期还没运行完毕,则进入就绪队列等待。

2.4.3 交互式系统的调度算法

  1. 轮转调度:也就是大家轮流来,一个进程分配固定时间片,时间到了还没执行完,则移动到就绪队列队尾,下一个进程接着来。
  2. 优先级调度:优先级高的进程先运行,统一优先级的则按照轮转调度。如图所示:

在这里插入图片描述

  1. 多级队列:举个例子,高优先级的进程先运行一个时间片,然后是次高级队列每个进程运行2个时间片,然后再次一级运行四个时间片。每个进程运行一次后,优先级降低一级。
  2. 最短进程优先
  3. 保证调度
  4. 彩票调度
  5. 公平分享调度

2.4.4 实时系统中的调度

硬实时调度:在绝对截止时间前完成,软实时调度:在某个时间前后完成调度。

2.4.5 策略与机制

将调度算法以某种形式参数化,具体参数由用户进程写入。调度机制位于内核,调度策略可由用户进程决定。

2.4.6 线程调度

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_21125183/article/details/83828557