【《现代操作系统 第4版》】2、进程与线程

进程

为什么要有进程

早期,OS上只能跑一个程序,随着计算机的发展,可以在内存中放入多个可运行的程序。这个时候再用程序这个概念来描述在OS上运行的各个程序就不太合适了。比如一个程序在OS上跑多份,这个程序的多个实例在内存中如何表示呢?这就引出了进程的概念。

进程描述

进程描述部分介绍了进程的一些静态特点

进程的定义

进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。
进程
简单地说,一个进程就是一个正在执行程序的实例,它是对正在运行程序的一个抽象。
同一个程序的多次运对应不同的进程。

进程的组成

进程包含了正在运行的一个程序的所有状态信息:

  • 代码
  • 数据
  • 状态寄存器
    • CPU状态CR0、指令指针IP
  • 通过寄存器(GR)
    • AX、BX、CX
  • 进程占用系统资源
    • 打开文件、已分配内存等

进程与程序

经典case

进程和程序间的区别是很微妙的,但非常重要。用一个比喻可以更容易理解这一点。想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法),计算机科学家就是处理器(CPU),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。

现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。

两者的区别
  • 进程是动态的,程序是静态的
    • 程序是有序代码的集合
    • 进程是程序的执行,进程有核心态/用户态
  • 进程是暂时的,程序的永久的
    • 进程是一个状态变化的过程
    • 程序可长久保存
  • 进程与程序的组成不同
    • 进程的组成包括程序、数据和进程控制块

进程的特点

  • 动态性:可动态地创建、结束进程
  • 并发性:进程可以被独立调度并占用处理机运行
  • 独立性:不同进程的工作不相互影响
  • 制约性:因访问共享数据/资源或进程间同步而产生制约

进程控制块(process control block, PCB)

如何来描述进程呢?OS通过PCB来描述进程,PCB是操作系统管理控制进程运行所用的信息集合:

  • 操作系统用PCB来描述进程的基本情况以及运行变化的过程
  • PCB是进程存在的唯一标志,每个进程都在操作系统中有一个对应的PCB

进程创建时需要生成它的PCB,进程终止时需要回收它的PCB,进程的组织关系也需要PCB来管理。

一个典型的进程控制块所包含的信息如下:

进程表表项

PCB的组织方式:

  • 链表:由于进程是动态的,一会儿创建一会儿删除,所以可以使用链表来维护
    • 同一状态的进程其PCB成一链表,多个状态对应多个不同的链表
    • 各状态的进行形成不同的链表:就绪链表、阻塞链表
  • 索引表:如果进程个数比较固定,没有频繁的创建和删除操作,索引也是个不错的方式
    • 同一状态的进程归入一个索引表(由索引指向PCB),多个状态对应多个不同的索引表
    • 各状态的进行形成不同的索引表:就绪索引表、阻塞索引表
      PCB的实现

进程管理

该部分描述进程的动态特点

进程的生命周期

进程创建

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

  1. 系统初始化
  2. 正在运行的程序执行了创建进程的系统调用(如linux的系统调用fork)
  3. 用户请求创建一个新进程(比如输入命令行或双击图标启动程序)
  4. 一个批处理作业的初始化(仅在大型机的批处理系统中应用)

从技术上看,在所有这些情形中,新进程都是由于一个已存在的进程执行了一个用于创建进程的系统调用而创建的。这个进程可以是一个运行的用户进程、一个由键盘或鼠标启动的系统进程或者一个批处理管理进程。这个进程所做的工作是,执行一个用来创建新进程的系统调用。这个系统调用通知操作系统创建一个新进程,即创建一个PCB,完成一些初始化操作。

进程创建后,处于就绪状态,被加入到就绪队列中。

进程运行

内核根据调度算法从就绪队列中选择一个就绪进程,让它占用cpu并得到执行。此时进程就会处于运行状态。

进程抢占

进程会被抢占的情况:

  • 高优先级进程就绪
  • 进程执行当前时间片用完

一旦进程被抢占,就会从运行态转为就绪态。

进程等待

当进程等待某种事情的发生时就会放弃CPU的执行权,从运行状态进入阻塞状态。比如执行I/O操作,等待I/O操作执行完毕。

进程只能自己阻塞自己。因为只有进程自身才能知道何时需要等待某种事件的发生。

进程唤醒

唤醒进程的情况:

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

进程只能被别的进程或操作系统唤醒。唤醒后,进程从阻塞态变为就绪态。

进程终止

进程的终止通常由以下条件引起:

  • 正常退出
    • 自愿的,大多数进程都是完成了它们的工作后正常退出
    • 比如linux中调用系统函数exit
  • 出错退出
    • 自愿的
    • 比如编译器读取文件时,文件不存在,于是编译器就会推出
  • 严重错误
    • 强制性的
    • 程序自身引起的错误。比如除数是零。
  • 被其他进程杀死
    • 强制性的
    • 比如linux中的某个进程执行系统调用 kill 通知操作系统杀死其他进程

进程的状态变化模型

进程在整个生命周期分为三种基本状态:

  • 运行态(该时刻进程实际占用CPU)
  • 就绪态(可运行,但因为没有分配到CPU资源而暂时停止)
  • 阻塞态(除非某种外部事件发生,否则进程不能运行,即使CPU空闲)

如果加上创建和退出的话,会有如下的状态转换:
进程的状态

  • NULL -> 创建状态(new):一个新进程被产生出来执行一个程序
  • 创建状态(new) -> 就绪状态(ready):当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态
  • 就绪状态(ready) -> 运行状态(running):处于就绪状态的进程被进程调度程序选中后,就分配到CPU上来运行
  • 运行状态(running) -> 结束(exit):当进程表示它已经完成或者因出错,当前运行进程会由操作系统作结束处理
  • 运行状态(running) -> 就绪状态(ready) :处于运行状态的进程在其运行过程中,由于分配给它的处理机时间片用完而让出CPU
  • 运行状态(running) -> 阻塞状态(blocked) :当进程请求某资源且必须等待时
  • 阻塞状态(blocked) -> 就绪状态(ready) :当进程要等待某事件到来时,它从阻塞状态变到就绪状态

进程的切换

从概念上说,每个进程拥有它自己的虚拟CPU。当然,真正的CPU在各进程间来回切换,这种快速切换称作多道程序设计。

由于CPU在各进程之间来回快速切换,所以每个进程执行其运算的速度是不确定的。而且当同一进程再次运行时,其运算速度通常也不可再现。所以,在对进程编程时决不能对时序做任何想当然的假设。

所有的 I/O 操作都与中断向量表关联。中断向量表是一段靠近内存底部的固定区域的程序。它包含中断服务程序的入口地址。假设用户进程3正在运行,当一个磁盘中断发生时,中断硬件会将程序计数器(PC)、程序状态字(PSW)、有时还有一个或多个寄存器压入堆栈,计算机随即跳转到中断向量所指示的地址。这些是硬件完成的所有操作。完成跳转后由中断服务例程接管一切剩余的工作。

所有的中断都是才能够保存寄存器开始,对于当前进程而言,通常是保存在进程表项中。底层执行如下:

  1. 硬件将当前进程的PC、SP、PSW等压入堆栈
  2. 根据中断类型硬件从中断向量表中装入新的PC
  3. 执行一段汇编代码来保存寄存器值
  4. 执行一段汇编代码来设置新的堆栈
  5. 运行C中断服务例程来处理某个特定的中断类型剩下的工作
  6. 调度程序决定下一个将运行的进程
  7. C过程返回将控制权转给一段汇编代码
  8. 汇编代码将当前的进程转入寄存器值以及内存映射并启动该进程运行

一个进程在执行过程中可能被中断数千次,但由于中断所提供的保护现场和恢复现场机制,使得每次中断后,被中断的进程都能返回到与中断发生前完全相同的状态。

进程切换

进程挂起

当进程没有占用内存空间时称为进程挂起。
进程状态模型主要讨论的是进程和CPU相关的状态,进程挂起模型主要讨论的是进程和存储相关的状态。
进程挂起

进程挂起状态

为了描述进程在外存中的状态,引入了如下两种状态:

  • 阻塞挂起(blocked-suspend)状态:进程在外存并等待某事件的出现。此时的进程相对于内存来说处于阻塞状态。
  • 就绪挂起(ready-suspend)状态:进程在外存,但只要进入内存,即可运行。此时的进程相对于内存来说处于就绪状态。
挂起状态转换

挂起:把一个进程从内存转到外存,从而减少进程占用内存。

  • 阻塞到阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时触发
  • 就绪到就绪挂起:当有高优先级阻塞(系统认为会很快就绪的)进程和低优先级就绪进程时,系统会挂起低优先级就绪进程
  • 运行到就绪挂起:对抢先式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,可能会触发
  • 阻塞挂起到就绪挂起:当有阻塞挂起进程因相关事件出现时触发,同内存中的阻塞到就绪转换,只是这个转换发生在外存中

激活:把一个进程从外存转到内存,叫做进程激活。

  • 就绪挂起到就绪:没有就绪进程或挂起就绪进程优先级高于就绪进程
  • 阻塞挂起到阻塞:当一个进程释放足够内存,并有高优先级等待挂起进程

状态队列

OS怎么通过PCB和定义的进程状态来管理PCB,从而帮组完成进程的调度过程呢?

对任何一种状态而言,它都会存在多个进程。OS通过维护一组状态队列,来表示OS中所有进程的当前状态。
不同状态用不同的队列来表示,比如就绪队列、各种类型的阻塞队列、挂起队列等。
每个进程的PCB会根据它的状态加入到相应的队列中,当一个进程的状态发生变化时,它所在的PCB会从一个状态队列换到另一个队列。

如下存在多个阻塞队列(优先级不同),当等待事件1触发时,如果阻塞队列1中的一个或全部进程得到了满足,则它或它们会出队,而入队到就绪队列中。
进程队列

线程

为什么需要线程?

比如在处理极大量数据的应用中,通常的处理方式是,读进一块数据,对其处理,然后再写出数据。这里的问题是,如果只能使用阻塞系统调用,那么在数据进入和数据输出时,会阻塞进程。在有大量计算需要处理的时候,让CPU空转显然是浪费,应该尽可能避免。如果可以将这些应用程序分解成可以准并行运行的多个顺序实体,且它们拥有共享同一个地址空间和所有可用数据的能力。则可以提高程序运行的效率。这写实体就是线程。

引入线程后可以对上面的问题提供一种解决方案,有关的进程可以用一个输入线程、一个处理线程和一个输出线程构造。输入线程把数据读入到输入缓冲区中;处理线程从输入缓冲区中取出数据,处理数据,并把结果放到输出缓冲区中,输出线程把这些结果写到磁盘上。按照这种工作方式,输入、处理和输出可以全部同时进行。当然,这种模型只有当系统调用只阻塞调用线程而不是阻塞整个进程时,才能正常工作。

很显然,在这里用三个不同的进程是不能工作的,这是因为它们都需要对同一个数据进行操作。由于多个线程可以共享公共内存,而进程不能,所以通过用三个线程替换三个进程,从而使得它们可以访问同一个正在编辑的文件。

线程描述

线程的定义

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

在同一个进程中并行运行多个线程,是对在同一台计算机上并行运行多个进程的模拟。在前一种情形下,多个线程共享同一个地址空间和其他资源。而在后一种情形中,多个进程共享物理内存、磁盘、打印机和其他资源。

线程的组成

每个线程都有自己的PC、堆栈、寄存器、状态寄存器,它们共享同一个进程中的代码段、数据段、文件等资源。
在这里插入图片描述
在多道程序系统中,通过在多个进程之间来回切换,系统制造了不同的顺序进程并行运行的假象。
多线程的工作方式也是类型的,CPU在线程之间快速切换,制造了线程并行运行的假象。

线程的优缺点

-优点:
- 一个进程中可以同时存在多个线程
- 各个线程之间可以并发地执行
- 各个线程之间可以共享地址空间和文件等资源

  • 缺点
    • 一个线程崩溃,会导致其所属进程的所有线程崩溃

进程中的线程共享同一地址空间,它们之间是没有保护的。因为线程概念试图实现的是,共享一组资源的多个线程可以为完成某一任务而共同工作。由于线程之间没有保护,所以在进行多线程编程时需要人为去保证安全问题。

进程与线程

引入线程的概念后,进程=共享资源+线程
线程

  • 进程是资源分配单位,线程是CPU调度单位
  • 进程拥有一个完整的资源平台,而线程只独享指令流执行的必要资源,如寄存器和栈
  • 进程和线程具有相同的状态变化模型
  • 线程能减少并发执行的时间和空间开销
    • 线程比进程更轻量级,线程的创建和销毁要比进程快的多(直接使用所属进程已管理好的资源,如内存、文件资源等)
    • 同一进程内的线程切换时间比进程短(线程无需切换页表,线程切换时的数据传递无需通过内核直接使用内存地址即可找到)
    • 由于同一进程的各线程间共享内存和文件资源,可不通过内核进行直接通信

常见的线程调用

为实现可移植的线程程序,IEEE在IEEE标准1003.1c中定义了线程的标准。它定义的线程包叫作pthread。

所有pthread线程都有某些特性。每一个都含有一个标识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性。这些属性包括堆栈大小、调度参数以及其他线程需要的项目。

线程的创建

在多线程的情况下,进程通常会从当前的单个线程开始。这个线程有能力通过调用一个库函数
(如thread_create)创建新的线程。thread_create的参数专门指定了新线程要运行的过程名。

新线程会自动在创建线程的地址空间中运行。创建线程时通常会返回一个线程标识符(PID)作为新线程的名字。

线程的终止

当一个线程完成分配给它的工作后,可以通过调用一个库过程(如thread_exit)退出。
调用后线程终止并释放它的栈。

等待其他线程

通过调用一个过程,例如thread_join, 一个线程可以等待一个(特定)线程退出。这个过程会阻塞调用线程直到那个(特定)线程退出。

线程的让步

线程调用 thread-yield 允许线程自动放弃CPU从而让另一个线程运行。
由于无法利用时钟中断强制线程让出CPU,所以设法使线程自动交出CPU以便让其他线程有机会运行就变得非常重要。

POSIX线程

为实现可移植的线程程序,IEEE在IEEE标准1003.1c中定义了线程的标准。它定义的线程包叫作 pthread。
大部分UNIX系统都支持该标准。这个标准定义了超过60个函数调用。下图中列出常见的线程调用:
POSIX线程

所有pthread线程都有某些特性。每一个都含有一个标识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性。这些属性包括堆栈大小、调度参数以及其他线程需要的项目。

线程的三种实现

用户线程——在用户空间中实现

用户线程

如图,由一组用户级的线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。

在用户空间管理线程时,毎个进程需要有其专用的线程控制块(TCB)用来跟踪该进程中的线程。这些表和内核中的PCB类似,不过它仅仅记录各个线程的属性,如每个线程的程序计数器、堆栈指针、寄存器和状态等。该线程表由线程库函数运行时系统管理。当一个线程转换到就绪状态或阻塞状态时,在该线程的TCB中存放重新启动该线程所需的信息,与内核在PCB中存放进程的信息完全一样。

  • 优点
    • 不依赖于操作系统的内核
      • 用户级线程可以在不支持线程的OS上实现
      • 内核不了解用户线程的存在
    • 同一进程内的用户线程切换速度快
      • 无需陷入内核,无需进行上下文切换,也无需对cache进行刷新,仅需要在本地线程表中切换即可
    • 允许每个进程有自己定制的线程调度算法,那些有GC线程的应用程序就不用担心线程会在不适合的时刻停止
    • 由于不占用内核内存,所以具有较好的可扩展性
  • 缺点
    • 线程发起系统调用而阻塞时,则整个进程进入等待
      • 由于内核不知道线程的存在,如果线程引起缺页中断,将导致整个进程阻塞直到磁盘I/O完成为止
    • 不支持基于线程的处理机抢占
      • 如果一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程自动放弃CPU
    • 只能按进程分配CPU时间
      • 在一个单独的进程内部,没有时钟中断,所以不可能用轮转调度(轮流)的方式调度线程。
    • 并不是所有应用都用到多线程,通常在经常发生线程阻塞的应用中才希望使用多线程。比如web服务器。

内核线程——在内核中实现

内核线程

如上图,由内核通过系统调用实现的线程机制,由内核完成线程的创建、终止和管理。

当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对TCB的更新完成线程创建或撤销工作。

所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统过程相比,代价是相当可观的。
当一个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程(若有一个就绪线程)或者运行另一个进程中的线程。而在用户级线程中,运行时系统始终运行自己进程中的銭程,直到内核剥夺它的CPU (或者没有可运行的线程存在了)为止。

由于在内核中创建或撤销线程的代价比较大,某些系统采取“环保”的处理方式,回收其线程。当某个线程被撤销时,就把它标志为不可运行的,但是其内核数据结构没有受到影响。稍后,在必须创建一个新线程时,就重新启动某个旧线程,从而节省了一些开销。在用户级线程中线程回收也是可能的,但是由于其线程管理的代价很小,所以没有必要进行这项工作。

内核线程不需要任何新的、非阻塞系统调用。另外,如果某个进程中的线程引起了页面故障,内核可以很方便地检査该进程是否有任何其他可运行的线程,如果有,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的主要缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止等)比较多,就会带来很大的开销。

轻量级线程——在内核中实现,支持用户线程

混合实现将用户级线程的优点和内核级线程的优点结合了起来。
混合实现

如上图的一种实现方法,它将用户级线程与某些或全部内核线程多路复用起来。

采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。如同在没有多线程能力操作系统中某个进程中的用户级线程一样,可以创建、撒销和调度这些用户级线程。在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。

调度程序激活机制

调度程序激活机制工作的目标是模拟内核线程的功能,但是为线程包提供通常在用户空间才能实现的更好的性能和更大的灵活性。

当使用调度程序激活机制时,内核给每个进程安排一定数量的虚拟的处理机,并且让运行时系统将线程分配到处理机上。这一机制可以用于多处理机上,此时虚拟处理器可以成为真实的CPU。

该机制工作的基本思路是,当内核了解到一个线程被阻塞后,内核通知该线程的运行时系统,并且在堆栈中以参数形式传递有问题的现成的编号和所发生时间的一个描述。内核通过在一个已知的起始地址启动运行时系统,从而发出了通知,这是对Unix中信号的一种粗略模拟,这个机制称为上下调用(upcall)

一旦如此激活,运行时系统就重新调度其线程,这个过程通常是这样的:把当前线程标记为阻塞并从就绪表中取出另一线程,设置其寄存器,然后再启动之,稍后,当内核知道原来的线程又可运行时,内核就又一次上下调用运行时系统,通知它这一事件。此时运行时系统按照自己的判断,或者立即重新启动被阻塞的线程或者把它放入就绪表中稍侯继续。

当某个用户线程运行的同时发生一个硬件中断,被中断的CPU切换进和心态。如果被中断的进程也对引起该中断的事件不感兴趣,就把中断的线程恢复到中断之前的状态。如果对中断的事件感兴趣,那么被中断的线程就不再启动,代之为刮起被中断的线程,而运行时系统则启动对应的虚拟CPU。此时被中断的线程的状态保存在堆栈中。随后,运行时系统决定在该CPU上调度哪个线程。

调度程序激活机制的一个目标是作为上行调用的信赖基础,这是一种违反分层系统内在结构的概念。通常n曾提供n+1层可调用的特定服务,但是n层不能调用n+1层中的过程。上行调度并不遵守这个基本原理

弹出式线程

如何处理到来的消息?例如服务请求。

传统的方法是将进程或线程阻塞在一个receive系统调用上,等待消息到来。当消息到达时,该系统调用接收
消息,并打开消息检査其内容,然后进行处理。

另一种方法是,在一个消息的到达时系统创建一个处理该消息的线程,这种线程称为弹出式线程。

弹出式线程

这种线程没有历史信息,每个线程都是从新开始,彼此完全一样。所以可以快速地创建这类线程,从而使得消息到达与处理开始之间的时间非常短。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值