OS复习:进程与线程

进程的所有内容都非常重要,本文尽可能详细梳理一遍

这一章包括以下内容,本文只整理进程与线程:

  • 进程与线程
  • CPU调度
  • 同步与互斥
  • 死锁

进程与线程

什么是是进程?为什么要有进程?进程如何解决我们考虑的问题

什么是进程

在多道程序下,多个程序并发执行,为描述和控制程序的并发执行,我们引入进程的概念(这样解释还是抽象)

我们让每个程序独立运行,给他配备一个专门数据结构,PCB(Process Control Block),用来描述进程的基本情况和运行状态;因此,创建进程就是创建PCB;撤销进程,就是撤销PCB

进程还可以有如下解释:

  • 正在执行程序的实例(好比你的代码是菜谱,我现在进程就是要根据菜谱正在做的菜)
  • 是程序及其数据从磁盘加载到内存,在CPU上执行的过程
  • 是具有独立功能程序在一个数据集合上运行过程

它也是进程实体的运行过程,是系统进行资源分配和调度的基本单位

这里资源是值CPU、存储器、其他设备服务某个进程的时间,强调的是时间

它有以下一些特征:

  • 动态:有一定生命周期,是动态产生、变化和消亡
  • 并发
  • 独立
  • 异步:

进程里有哪些东西

PCB

进程控制块。进程创建时,OS为它新建一个PCB,之后该结构都在内存,任何时刻都可以被存取,并在进程结束的时候删除。PCB是进程实体一部分,是它唯一标识

PCB里又有啥呢

  • 进程描述信息:PID, UID

  • 进程控制和管理信息:当前状态,作为CPU分配调度的依据;进程优先级,描述进程抢占CPU优先级;

  • 资源分配清单:关于内存地址空间或者虚拟地空间状况,所打开文件列表和IO设备信息
  • 处理机相关信息: CPU上下文,因为当CPU切换进程时,我们需要保存当前进程的现场信息,所以我们要将CPU状态信息保存在PCB中,之后重新执行能够从断点继续执行

我们组织各个进程的PCB方式有链接方式和索引方式,这两种方式也会贯穿之后也会多次出现;文件逻辑结构就会用到类似的组织结构

程序段

程序可以由多个进程共享

相当于一份菜谱,不同厨师当然可以按照同一份菜谱炒同样的菜

数据段

可以是原始数据,也可以是中间数据

进程状态转换

状态转换非常重要

进程通常有五个状态:

  • 运行态:在单CPU,同一时刻只能有一个进程处于运行
  • 就绪态:获得除CPU以外所有资源。我有菜谱,也买到菜了,菜也切好了,就差起锅了;一旦进程拿到CPU资源,就可 以立即运行,转为运行态;处于就绪态的有多个进程,我们把他们组织成就绪队列
  • 阻塞态:进程因为某一事件暂停运行,等待某个资源可用(除CPU)。我烧着发现油盐突然不够了,就停止烧菜。所以即便CPU空闲,该进程也无法运行;同样,我们也把多个处于阻塞态的进程组织成阻塞队列
  • 创建态: 指的是还未创建完,没有处于就绪态。我们来看看具体创建进程的过程:申请空白PCB,向PCB填写用于控制和管理进程的信息;然后未进程分配运行所需的资源;最后进入就绪队列;但是如果创建进程资源不够,比如内存不足,创建工作没完成,则处于创建态
  • 终止态: 进程需要结束时,先把进程设置为终止态,然后进一步处理资源释放和回收

关键点我们区分就绪和阻塞:

  • 就绪仅仅缺少CPU,拿到CPU就立刻转为运行态
  • 阻塞还缺除CPU以外其他资源

都是缺少资源,为什么还要区分就绪和阻塞?

因为基于时间片轮转机制,进程在实际运行中是频繁切换到就绪态的,而且进程得到CPU时间很短;这是我们期望的

然而,其他资源使用和分配相对时间就很长,转换到阻塞态次数很少;这不是我们期望的

所以,阻塞态和就绪态是生命周期完全不同的两个状态

518194f9f4e07eb83f166ba4a9465f99

注意:运行态到阻塞态是主动的,而进程从阻塞态到就绪态需要其他进行协助

进程控制

进程控制就是对系统所有进程管理,包括创建、撤销、实现进程状态转换。用于进程转换的程序段一般是原语

创建

我们允许一个父进程创建一个子进程;类似java 中的类,子进程可以继承父进程的资源;当子进程被撤销,它的资源会还给父进程。

在撤销父进程时,也会撤销其所有的子进程

什么时候会要创建进程?

终端登录系统、作业调度、系统提供服务、用户程序的应用请求

创建过程?

  • 为新进程申请唯一PID,并申请PCB;
  • 分配其运行所需的资源,从OS或者父进程那儿拿
  • 初始化PCB,包括初始化标志信息、CPU状态信息、CPU控制信息,设置优先级
  • 当就绪队列还能接纳,那么放入新进程,等待被调度.

终止

什么时候发生终止?

  • 正常结束,进程完成任务
  • 异常错误,比如存储区越界、保护错、非法指令、特权指令错、运行超时、算数运算错、IO故障
  • 外界干预

过程:

  • 根据被终止进程的标识符,检索进程的PCB,读取进程状态
  • 进程处于运行的话,终止其执行,将CPU资源分配给其他进程
  • 若进程还有子进程,则将所有子进程终止
  • 将进程所有的资源归还给父进程或者OS
  • 将PCB从队列/链表中删除

阻塞和唤醒

当进程期待的事情未发生,比如请求系统资源失败、等待某种操作完成,新数据未到达,都可能调用阻塞原语。

阻塞是进程的主动行为,所以只有处于运行态的进程才能主动进入阻塞态;

阻塞具体发生过程如下:

  • 找到被阻塞进程PID,对应的PCB
  • 若进程在运行态,保护现场,将其转为阻塞态,停止运行
  • 将该PCB插入相应事件的等待队列,将CPU资源调度给其他就绪队列

那阻塞了的进程怎么被唤醒呢?

之前我们提到阻塞态到就绪态是由其他进程协助自身完成的,比如当IO操作完成后,由释放该IO设备的进程唤醒原来的进程

唤醒原语执行过程如下:

  • 在该事件的等待队列找到相应进程的PCB
  • 将其从等待队列移出,设置其状态为就绪态
  • 将PCB插入就绪态,等待调度程序调度

Block原语和Wakeup原语是一对作用相反的原语,要成对使用。

也就是说,我们在A进程调了Block原语,必须在相关的进程B安排一条Wakeup原语;否则,A进程就似了(永远处于阻塞态)

进程间的通信

有些进程间需要交换数据,我们就需要考虑进程间的通信问题

PV操作是低级通信,高级通信方式是有以较高效率传输大量数据的通信方式,有三种:共享存储、消息传递、管道通信

共享存储

进程间有一块直接访问的共享空间,那么进程们就对共享空间进行写/读操作就行;那我们关注的重点就是怎么实现同步互斥让进程安全的对共享空间读写,之后章节后具体讲述;

低级方式的共享存储基于数据结构;而高级共享基于存储区的共享。OS指负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换要由用户自己安排读写

进程内的空间是独立的,当然进程内的线程是共享其进程空间的;

因此这种方式下,两个进程只能从共享空间交换物品,但不能直接去对方家里;

消息传递

没有共享空间的情况下,进程必须用OS提供的消息传递方法实现通信

进程间的数据交换以格式化的信息(Message)为单位,调用OS的发送、接收原语

在微内核与服务器通信就采用了消息传递机制,这样能够较好支持多CPU系统、分布式系统和计算机网络

消息传递也有两种方式

  • 直接通信: A进程将消息挂在B进程的缓冲队列上
  • 简介通信: 进程将消息发送到一个中间实体,“信箱”

管道通信

管道pipe 文件,数据在管道先进先出。

管道让两个进程按生产者-消费者进行通信,只要管道不满,写进程就能向管道另一端写入数据

为协调通信,管道必须提高三种协调能力:

  • 互斥,一个进程对管道读/写,其他进程必须等待
  • 同步,写进程向管道写入一定数量数据后,写进程阻塞,直到读进程取走数据后才将写进程唤醒
  • 读进程将管道数据读空后,读进程阻塞,同理,需要写进程写入数据才能被唤醒
  • 要确定读写双方是存在的

Linux里频繁使用Pipe,管道就是一种文件,它比一般的通信方式有些好处,克服了两个问题:

  • 限制管道大小。管道文件时固定大小的缓冲区,LInux中缓冲区大小为4KB;使用单个缓冲区也会带来问题,再写管道时可能变满;(这里没懂)
  • 读进程也可能比写进程的快,读写是异步的;当管道的数据被读取时,管道变空。这种情况下,一个随后的read调用将被阻塞,等待某些数据写入

管道只能由创建进程访问,当父进程创建一个管道后,管道是种特殊文件,子进程会继承父进程的打开文件,会因此子进程也进程父进程的管道,可以与父进程通信

从管道读数据是一次性操作,数据一旦被读取,就释放空间以便些更多数据;普通管道只允许单向通信,若有两个进程相互通信,要起两个pipe

:sweat: 管道通信还不是很理解,它对比消息传递,共享存储到底优势在哪?更小,更轻量?它是要走内核的缓冲区实现的,而共享存储区不需要走内核

线程和多线程

进程引入为了实现多道程序并发;

引入线程为了减小程序在并发所付出的时空开销,提高OS并发性能

线程可以理解为轻量级进程 ,是基本的CPU执行单元,也是程序执行流最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成

线程自己没有系统资源,但它可与同属一个进程的其他线程共享进程所有资源

一个线程可以创建撤销另一个线程,同一个进程下的线程可以并发执行;线程间相互制约,拥有就绪、阻塞、运行态

引入线程后,进程是除CPU外系统资源的分配单元,线程是CPU分配单元

线程的特点是不拥有系统资源,只有少量的必要资源,但它可以访问其进程资源;这样以来,我们在同一进程下切换线程就只要开销极小的时空

线程的属性

多线程OS中,进程不是基本的执行实体,这里进程处于运行态,指其线程处于允许:

  • 线程是个轻量的实体,有唯一的标识符和线程控制块,记录其执行的寄存器和栈等现场状态
  • 不同线程可以执行相同程序,同个服务程序被不同用户调用,OS将他们创建为不同线程
  • 同个进程下线程间共享资源

线程池技术

  • 通过线程池,操作系统可以高效地管理线程的生命周期。在传统的进程模型中,创建和销毁进程需要较大的开销,而线程池则通过复用线程来减少线程创建和销毁的成本。
  • 线程池可以让线程在任务间共享,避免了频繁创建和销毁线程的性能开销,并且能够控制同时运行的线程数,从而在不同负载下优化资源使用。

线程状态转换类似进程,不赘述

线程组织和控制

TCB,线程控制块。TCB包括:

  • 线程标识符
  • 一组寄存器,成程序计数器、状态寄存器、通用寄存器
  • 线程运行状态
  • 优先级、
  • 线程专有存储区,用于保护线程切换现场
  • 堆栈指针,用于过程调用保存局部变量以及返回地址

同一进程下的线程可以完全共享进程的地址空间和全局变量,各个线程都可以访问进程的地中间每个单元;所以一个线程可以读写、清除另一个线程

  • 线程创建:OS有创建线程和终止线程的函数,用户程序启动时,通常仅有一个成为初始化线程的线程正在执行,只要功能是用于创建新线程。创建进程函数,提供指向主程序的入口指针、堆栈大小和优先级,返回一个线程标识符
  • 进程终止:由线程终止函数完成,但是系统线程一旦被建立,就不会被终止。线程被终止后并不立即释放它的资源,只有进程其他线程执行了分离函数,被终止线程与资源分离。被终止但尚未释放资源的线程任然可以被其他线程调用,使其重新恢复运行

线程实现方式

线程实现分两类:用户级和内核级

用户级线程(ULT)

线程管理所有工作都在用户态完成,无需请求OS;内核对ULT是没有意识的,我们可以通过线程库实现多线程程序

对于ULT的系统,调度单位是进程;也就是说A进程100个线程,B进程1个线程,那么B的一个线程资源是A的100倍

所以优缺点显而易见:

优势:

  • 线程切换无需到内核空间,减少时空开销
  • 调度算法是进程专用,不同进程可以对自己的线程选择不同调度算法
  • ULT与OS平台无关,用户来管理线程

劣势:

  • 当线程执行一个系统调用,不仅该线程被阻塞,所有线程都被阻塞(如上文所说,OS只能意识到是一个进程在请求,而非其中一个线程,所以进程下的所有线程被阻塞了)
  • 无法发挥多CPU优势,内核每次分配给一个进程只有一个CPU,进程一个时刻只能有一个线程能执行

所以可见,ULT主要问题在于和OS交互时,OS无法意识到进程里的线程

内核级别(KLT)

OS 为每个内核级线程设置一个TCB,内核根据TCB感知线程的存在

优势:

  • 发挥多CPU,内核能在同一时刻调度同一进程多个线程
  • 一个线程的阻塞不会影响其他线程
  • 内核支持线程具有很小的数据结构和堆栈,线程切换开销小、快
  • 内核可采用多线程技术

劣势:

  • 同一进程的线程切换,需要从用户态到核心态进行,系统开销较大(因为用户进程的线程在用户态执行,线程调度管理都在内核完成)
组合:ULT+KLT

我们尝试将一些内核线程对应多个用户级别线程,这是ULT通过时分多路复用内核级实现的(啥玩意儿?)

线程库的实现(为用户提供创建和管理线程的API):

  • 在用户空间提供一个没有内核支持的库;
  • 实现由OS直接支持的内核级库

Windows API是KLT库

Java线程API允许线程在JAVA程序中直接创建管理,由于JVM实例允许在宿主OS上,java线程API通常采用宿主系统的线程库来实现。

多模型线程

根据ULT和KLT连接方式不同,我们分为

  • 多对一 : 多个ULT映射到一个KLT, 每个进程只分配一个内核级线程,线程调度和管理由用户空间完成;仅仅当ULT需要访问内核时,才需要映射到一个KLT,但每次只允许一个线程进行映射

优点: 线程管理在用户空间,无需切换到内核,效率高

缺点: 一个线程发生阻塞,进程内的所有线程都阻塞;因为任何时刻都只有一个线程访问内核

  • 一对一: 每个ULT映射到一个KLT,线程切换由内核完成,要切换到内核态

优点: 一个线程阻塞不会影响其他线程,因为一对一的关系

缺点:每创建一个ULT,都要创建与之对应的KLT,开销较大

  • 多对多: 将n个ULT映射到m个KLT,不是一一对应的关系;要求n>=m,那自然既克服了多对一并发不高的缺点,也克服了一对一创建KLT开销太大的缺点,算是一种折中?

小结

  • 为什么要引入进程:为描述程序动态执行过程,支持和管理多道程序的并发执行

  • 进程由什么组成?:程序段、数据段、PCB

  • 进程是怎么解决问题的:将识别程序运行状态的一些变量存放在PCB,通过系统变量了解程序状况,并在适当时机进行进程的转换,避免资源浪费;以及可以划分到更小的调度单位:线程