2.1.1 进程的概念、组成、特征
进程的引入
程序的顺序执行
早期无操作系统及单道批处理系统的计算机中,程序的执行方式都是典型的顺序执行,即先进入内存的程序先执行,在一个程序执行完毕之前不能执行其他程序。程序中的指令也是按照程序的控制流依次执行的,在一条指令执行完毕之前不能执行另一条指令。
程序顺序执行时的特点
(1)顺序性
处理机的操作严格按照程序所规定的顺序执行,即只要在前一操作结束后,才能执行后续操作。
(2)封闭性
程序是在封闭的环境下运行的,即程序在运行时独占全机资源,因而各资源的状态(除初始状态外)只有在本程序才能改变。程序一旦开始执行,其结果不受外界因素的影响。
(3)可再现性
如果程序执行时的环境和初始条件相同,那么程序多次重复执行时,其执行结果也相同。
例如:
一个求两数之和的加法程序,可再现性是指无论程序重复执行多少次,在输入的两个加数相同的情况下,和不变,即1+1永远等于2。
程序的并发执行 (失去封闭性和不可再现性)
程序的并发执行是指在同一时间间隔内运行多个程序,在一个程序执行结束之前可以运行其他程序。宏观上:用户看到多个程序同时执行,向前不间断地推进;微观上:任意时刻一个CPU上只能有一个程序在执行 。
程序并发执行的特点
(1)间断性
程序在并发执行时,由于它们共享资源,而资源的数量又往往少于并发执行的程序数量,所以系统不能保证每个程序都不受限制的占用资源。因而,每个程序在CPU上运行时都是时断时续的。当请求某种资源的程序数量大于被请求的资源数量时,就必然有因为申请不到资源而暂停执行的程序。只有当其他程序释放资源后,该程序才可能继续执行。资源的有限性使得并发执行的程序呈现执行过程的间断性。
(2)失去封闭性
程序在并发执行时,由于它们共享资源或者合作完成同一项任务,系统的状态不再只有正在执行的某一个程序可以看见和改变。
(3)不可再现性
程序在并发执行时,由于失去了封闭性,将导致其失去执行结果的可再现性。同一个程序在输入完全相同的情况下,多次运行的结果可能不唯一。
失去封闭性、结果不可再现造成的影响
下面以几个例子来说明:
例一:
- count是全局变量,初值为0
- 进程P1和进程P2都包含对count加1操作的代码
- r1和r2是两个通用寄存器
- 我们都知道高级语言表达的程序语句count=count+1经过编译以后转化为多条指令来完成其功能,但CPU执行的指令单位是编译后的指令,而不是高级语言的一条语句。
- 用r1和r2表示两个通用寄存器,执行count=count+1的过程经编译以后等价于以下形式的基本指令序列
进程P1:
- r1=count; #内存变量count对应的内存中的数据送到寄存器
- r1=r1+1; #执行累加1的操作
- count=r1; #将r1中的累加结果送到内存变量count对应的内存中
进程P2:
- r2=count; #内存变量count对应的内存中的数据送到寄存器
- r2=r2+1; #执行累加1的操作
- count=r2; #将r2中的累加结果送到内存变量count对应的内存中
假设count初始值为0,p1和p2各执行一遍,分别对count做一次加1操作。若进程并发执行时出现下面的执行序列
第一种序列:
P1执行
- r1=count; #r1=0
- r1=r1+1; #r1=1
P2执行
- r2=count; #r2=0
- r2=r2+1; #r2=1
- count=r2; #count=1
P1执行
- count=r1; #count=1
第二种序列:
P1执行
- r1=count; #r1=0
- r1=r1+1; #r1=1
- count=r1; #count=1
P2执行
- r2=count; #r2=1
- r2=r2+1; #r2=2
- count=r2; #count=2
- 第一种序列,P1和P2都执行了一遍,但是count的值等于1而不等于2的错误结果。
- 第二种序列,P1和P2都执行了一遍,但是count的值等于2而不是等于1。
例二:
- 有两个并发执行的进程P1和P2,共享初值为0的变量x。P1对x加1,P2对x减1。加1和减1操作的指令序列分别如下所示。(2011年研考题)请写出X的可能结果,并给出每种x的最后结果值对应的指令执行序列。
- // 加1操作
- load R1, x // 取x到寄存器R1中
- inc R1//加1
- store x, R1// 将R1的内容存入x
- // 减1操作
- load R2, x
- dec R2//减1
- store x, R2
P1和P2两个操作完成后,x的可能值是什么?
序列1
- load R1, x//R1=0
- inc R1//R1=1
- store x, R1//x=1
- load R2, x//R2=1
- dec R2//R2=0
- store x, R2//x=0
序列1得到结果为0
序列2
- load R1, x //R1=0
- inc R1//R1=1
- load R2, x//R2=0
- dec R2//R2=-1
- store x, R1//x=1
- store x, R2//x=-1
序列2得到结果为-1
- load R1, x//R1=0
- inc R1//R1=1
- load R2, x//R2=0
- dec R2//R2=-1
- store x, R2//x=-1
- store x, R1//x=1
序列3得到的结果为1
当允许程序并发执行时,并发执行的程序可能是同一个程序在不同数据集合上的执行,也可能是不同的程序在不同数据集合上的执行,它们共享系统资源,用程序已不能方便地描述程序的并发执行,进程的引入是为了跟踪并描述程序的并发执行。 所以引入了进程的概念。
进程的概念
定义1:
- 进程是允许并发执行的程序在某个 数据集合上的执行过程。
定义2:
- 进程是由用户数据、系统数据和正文段(程序)构成的实体
程序:是静态的,就是存放在磁盘里的可执行文件,就是一系列的指令的集合。
进程:动态的,是程序的一次执行过程
进程的组成
进程控制块是进程实体的一部分,与进程一一对应,是操作系统中最重要的记录型数据结构,PCB中记录了操作系统所需要的用于描述进程情况及控制进程运行所需的全部信息,只有内核程序可以直接访问。
当进程被创建时,操作系统会为该进程分配一个唯一的、不重复的"身份证号"--PID(Process ID,进程ID)
基本的进程描述信息,可以让操作系统区分各个进程
- 操作系统要记录PID、进程所属用户ID(UID)
可用于实现操作系统对资源的管理
- 记录给进程分配了哪些资源(如:分配了多少内存、正在使用哪些I/O设备、正在使用哪些文件)
用于实现操作系统对进程的控制、调度
- 记录进程的运行情况(如:CPU使用时间、磁盘使用情况、网络流量使用情况)
这些信息都被保存在一个数据结构PCB(Process Control Block)中,即进程控制块
PCB是进程存在的唯一标志,当进程被创建时,操作系统为其创建PCB,当进程结束时,会回收其PCB
进程的特征
程序是静态的,进程是动态的,相比于程序,进程拥有以下特征:
进程与程序比较
联系:
① 进程是程序的一次执行,进程总是对应一个特定的程序,一个进程必然对应至少一段程序(程序段可以是代码段、数据段、堆和栈)。
② 一个程序可以对应多个进程:同一个程序段可以在不同的数据集合上运行,因而构成若干个不同的进程。
区别:
① 程序是静态的概念,进程是动态的概念
② 程序是永久的,进程是暂时存在的
③ 程序与进程的存在实体不同
- 程序以文件的形式存在于磁盘上,可以是二进制文件、脚本文件或其他可执行格式。
- 进程存在于内存中,由操作系统管理和调度。
总结
2.1.2 进程的状态与转换,进程的组织
创建态、就绪态
- 创建状态:进程正在被创建时,它的状态是"创建态",在这个阶段操作系统会为进程分配资源、初始化PCB
- 就绪状态:进程一但获得CPU就可以投入运行的状态(处于就绪状态的进程已经具备运行条件,但是由于没有空闲CPU,就暂时不能运行)
执行状态
执行状态:进程获得CPU正在运行的状态。
阻塞态
阻塞状态:进程由于等待资源或某个事件的发生而暂停执行的状态。
在进程的运行过程中,可能会请求等待某个事件的发生,如:
- 等待某种系统资源的分配
- 等待其他进程的响应
在这个事件还没有发生之前,进程无法继续往下执行,此时操作系统会让这个进程下CPU,并让它进入"阻塞态"
当CPU空闲时,又会选择另一个"就绪态"进程上CPU运行
终止态
终止态:一个进程可以执行exit系统调用,请求操作系统终止该进程。此时该进程会进入"终止态",操作系统会让该进程下CPU,并回收空间等资源,最后还要回收该进程的PCB。
进程状态的转换
创建态:PCB都没有初始化,资源也未分配
就绪态:其他资源都获得了,就差处理机,PCB已初始化
运行态:处理机和其他资源都获得了,PCB已初始化
阻塞态:其他资源和处理机都没有获得,PCB已初始化
终止态:回收资源,回收PCB
状态转换:
就绪态->运行态:
- 进程被调度
运行态->就绪态:
- 时间片到
- 处理机被抢占
运行态->终止态:
- 进程运行结束
- 运行过程中遇到不可修复的错误
运行态->阻塞态:
- 进程用"系统调用"的方式申请某种系统资源,或者请求等待某个事件的方式
- 运行态->阻塞态是进程自身做出的主动行为(主动放弃了之前持有的所有资源)
阻塞态->就绪态:
- 申请的资源被分配
- 等待的事件发生
- 阻塞态->就绪态是不是进程自身能控制的,是一种被动行为
注意:
不能由阻塞态直接转换为运行态
- 阻塞态释放了所有资源,还没有进行获取
也不能由就绪态直接转换为阻塞态
- 因为进入阻塞态是进程主动请求的,必然需要进程在运行时才能发出这种请求
进程的组织
链式方式
当系统中有很多进程时,可以用队列把进程控制块组织起来,形成进程队列。
把具有相同状态的进程放在同一个队列中,具有不同状态的进程就可形成不同的进程队列。
- 处于就绪状态的进程构成的进程队列称为就绪队列
- 处于阻塞状态的进程构成的进程队列称为阻塞队列
- 把具有相同状态的PCB用其中的链接字,链接成一个队列。
根据阻塞原因不同,再分为多个阻塞队列
索引方式
2.1.3 进程控制
进程控制的主要功能是对系统中的所有进程实时有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。
如何实现进程控制?
通过原语实现
原语是一种特殊的程序,它的执行具有原子性。
也就是说,这段程序的运行必须是一气呵成,不可中断
为何进程控制(状态转换)的过程要"一气呵成"?
下面的例子中:
- 将PCB2的state设为1和把它放到就绪队列里面这两件事情必须是一气呵成的
- 否则,如果把PCB2的state设为1后,就收到了中断信号,但是它却被放到了阻塞队列里面
- 造成了把state=1的进程放在阻塞队列里面的错误
如何实现原语的原子性?
原语的执行具有原子性,即执行过程只能是一气呵成,期间不允许被中断
可以使用"关中断指令"和"开中断指令"这两个特权指令实现原子性
正常情况:CPU每执行完一条指令都会例行检查是否有中断信号需要处理,如果有,则暂停运行当前这段程序,转而执行相应的中断处理程序。
CPU执行了关中断指令之后,就不再例行检查中断信号,直到执行开中断指令之后才会恢复检查
这样,关中断、开中断之间的这些指令序列就是不可被中断的,这就实现了"原子性"
进程控制的相关原语
创建原语
作业:在外存中,还未进入内存投入运行的程序
作业调度:从外存中挑选一个程序投入运行
引起创建进程的事件:
① 用户登录:当用户输入合法登陆命令后,系统会通过创建原语建立一个新进程并插入到就绪队列中。
② 作业调度:将作业从外存调入内存,并分配必要资源,为其创建进程并插入到就绪队列中。
③ 提供服务:根据用户申请,系统创建进程为用户服务,如打印进程。
④ 应用请求:基于应用程序请求,为之创建进程。
Linux2.6.11进程创建系统调用和函数
① Clone() 系统调用,可用于线程创建
② Fork() 系统调用,用于进程创建
- 作用:
fork()
函数用于创建一个新的进程,这个新的进程被称为子进程。子进程是父进程的一个副本,它继承了父进程的所有资源(比如文件描述符),但是拥有自己独立的内存空间。- 结果:
fork()
调用一次,返回两次。在父进程中,fork()
返回子进程的PID;在子进程中,fork()
返回0。如果fork()
调用失败,则在父进程中返回-1,并且不会创建子进程。
exec()
- 作用:
exec()
系列函数(如execl()
,execlp()
,execle()
,execv()
,execvp()
,execvpe()
)用于在当前进程中加载并运行一个新的程序。这意味着当前进程的正文(代码)、数据、堆、栈等会被新程序的内容所替代。- 结果:调用
exec()
成功后,当前进程将不再执行原来的代码,而是从新程序的入口点(通常是main()
函数)开始执行。重要的是,虽然进程的内部状态被新程序取代了,但是进程的ID(PID)保持不变,因为它仍然是同一个进程,只是运行的内容不同了。好处:
- 资源隔离:子进程有自己独立的资源,因此即使新程序出错也不会影响到父进程。
- 灵活性:父进程可以继续执行其他任务,而不必等待子进程完成。
- 错误处理:如果
exec()
调用失败,那么只有子进程会受到影响,父进程可以捕获这个错误并采取相应的措施。用fork函数创建子进程后,子进程往往要调用exec函数以执行另一个程序。该程序完全替换为新程序,而新程序则从其main函数开始执行。 调用exec并不创建新进程,前后的进程ID并未改变(在子进程中调用
exec()
后,子进程的代码、数据、堆和栈等资源都会被新程序的内容替换,但这个子进程的PID不会改变。也就是说,操作系统仍然认为这是一个与之前相同的进程,只是它现在在执行不同的程序)。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。
撤销原语
引起进程终止的事件
- 正常结束:进程自己请求终止(exit系统调用)
- 异常结束:(1)整数除以0; (2)非法使用特权指令,然后被操作系统强行杀掉;(3)I/O故障;(4)运行超时;
- 外界干预:(1)Ctrl+Alt+delete,用户选择杀掉进程;(2)父进程终止子进程:父进程退出,子进程终止
撤销原语
- 从PCB集合中找到终止进程的PCB
- 若进程正在运行,立即剥夺CPU,将CPU分配给其他进程
- 终止其所有子进程
- 将该进程拥有的所有资源归还给父进程或操作系统
- 删除PCB
阻塞原语和唤醒原语
阻塞原语和唤醒原语必须成对使用
- 有阻塞原语,就应该有唤醒原语,不应该让进程一直留在阻塞队列里面(总有一个时刻等待的事件是会发生的)
引起进程阻塞的事件:
- 请求系统服务,如:打印服务。
- 启动某种操作,如:启动I/O或启动打印机。
- 新数据尚未到达,如:一个计算进程,如果新的输入数据还没有到达,则计算进程需要阻塞等待。
- 等待相互合作的其他进程完成工作
阻塞原语(运行态->阻塞态)
- 找到要阻塞的进程对应的PCB
- 保护进程运行现场,将PCB状态信息设置为"阻塞态",暂时停止进程运行
- 将PCB插入相应事件(进程的组织->按照阻塞原因不同,划分为多个队列)的等待队列
引起进程唤醒的事件:
- 等待的事件发生(因何事阻塞,就应由何事唤醒)
唤醒原语(阻塞态->就绪态)
- 在事件等待队列中找到PCB
- 将PCB从等待队列移除,设置进程为就绪态
- 将PCB插入就绪队列,等待被调度
切换原语
- 运行态->就绪态/终止态(此时正在运行的进程)
- 就绪态->运行态(就绪队列中某个进程)
- 切换原语会让两个进程的状态发生改变
引起进程切换的事件
- 当前进程时间片到
- 有更高优先级的进程到达
- 当前进程主动阻塞
- 当前进程终止(进程终止的时候会同时触发切换原语和撤销原语)
切换原语
- 将运行环境(当前进程)信息存入PCB
- PCB(当前进程)移入相应队列
- 选择另一个进程执行,并更新其PCB
- 根据PCB恢复新进程所需的运行环境
保存运行环境/恢复运行环境?
CPU中会设置很多"寄存器",用来存放程序运行过程中所需的某些数据
PSW:程序状态寄存器
PC:指令寄存器,存放当前正在执行的指令
通用寄存器:其他一些必要信息
问题
- 这些所有寄存器都是共享的,一个还没有执行完,另一个进程上CPU运行,另一个进程也会使用各个寄存器,会造成数据的覆盖
解决办法
- 在进程切换时先在PCB中保存这个进程的运行环境(保存一些必要的寄存器信息)
- 当原来的进程再次投入运行的时候,可以通过PCB恢复它的运行环境
总结
进程生命周期总结:
① fork创建新进程,但这时它只是老进程的一个克隆。
② 通过exec,新进程脱胎换骨,开始独立工作的职业生涯。
③ 进程可以是自然死亡,即运行到main函数的最后一个"}",从容地离我们而去;
④ 也可以是中途退场,退场有2种方式,一种是调用exit函数,一种是在main函数 内使用return;
⑤ 甚至它还可能被杀死,被其它进程通过另外一些方式结束它的生命。
⑥ 进程终止后,会留下一个空壳,wait站好最后一班岗,打扫战场,释放相关资源。
2.1.4 进程通信
什么是进程通信?
为什么进程通信需要操作系统支持?
共享存储
涉及到如在同一块区域写东西造成数据覆盖,所以各个进程对共享空间的访问应该是互斥的
消息传递
管道通信
管道通信与共享存储的区别:
共享存储可以在任何一块地方写,也可以从任何一个地方读
管道通信本质上是一个循环队列,只能从前面写,从后面读
总结
2.1.5 线程的概念
什么是线程,为什么要引入线程?
- 传统的进程是程序的一次执行,但这些功能显然不可能由一个程序顺序处理就能实现
- 进程同时是资源拥有者,在进程创建、撤消、切换时需要较大的时空开销,所以系统中所设置的进程数和进程切换的频率都受到了限制,影响了OS并发程度的提高
- 把进程的任务划分成更小、不能再分的、具有独立功能的单位,以线程的形式来并发执行,以提高程序并发执行的程度
进程= 一个资源+ 多个指令执行序列(即多个线程)
- 有的进程可能需要"同时"做很多事,而传统的进程只能串行地执行一系列程序。为此,引入了"线程",来增加并发度。
- 传统的进程是程序执行流的最小单位->线程成为程序执行流的最小单位(引入线程后)
- 同一个进程中的两个线程可以执行同一份程序,也可以执行不同的程序
- 可以把线程理解为"轻量级进程"
- 线程是一个基本的CPU执行单元,也是程序执行流的最小单位
- 引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各自任务(如QQ视频、文字聊天、传文件)
- 引入线程后,进程只作为除CPU之外的系统资源的分配单元(如打印机、内存地址空间都是分配给进程的)
- 线程是进程中的一个实体,是被系统独立调度和分派的基本单位
- 线程只拥有在运行中必需的资源,包括程序计数器、一组寄存器和栈
- 但它可以与同属一个进程的其他线程共享进程的全部资源,如:虚拟地址空间、文件
引入线程机制后,有什么变化?
线程的属性
2.1.6 线程的分类和多线程模型
线程的分类
用户级线程(操作系统感知不到它的存在)
历史背景:
早期的操作系统(如:早期Unix)只支持进程,不支持线程。当时的"线程"是由线程库实现的
① 用户级线程:不需要内核支持而在用户程序中实现的线程。内核不知道用户线程的存在,一 个线程阻塞将使得整个进程阻塞。时间片分配是以进程为单位。用户线程的切换无需陷入内核,故切换开销小,速度快。
很多编程语言提供了强大的线程库,可以实现线程的创建、销毁、调度等功能
1.线程的管理工作由谁来完成?
- 用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程的切换)
2.线程切换是否需要CPU变态?
- 用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预
3.操作系统是否能意识到用户级线程的存在?
- 在用户看来,是有多个线程。但是在操作系统内核看来,意识不到线程的存在,"用户级线程"就是"从用户视角看能看到的线程"
4.这种线程的实现方式有什么优点和缺点?
- 优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
- 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在处理机上并发运行
内核级线程(操作系统可以感知到它的存在)
内核级线程(Kernel-Level Thread,KLT,又称"内核支持的线程"),是由操作系统支持的线程
② 内核级线程:
- 由内核负责创建和撤销和切换;
- 一个内核线程阻塞不影响其它线程,其并发性优于用户级线程;
- 时间片分配以内核线程为单位分配。每个线程可以独享一个时间片。
- 内核级线程可以提高并发性,有效支持多核处理器。
内核级线程可以提高并发性,有效支持多核处理器。如何去理解?
这里有一台双核处理器:
➢如果有两个用户级线程。因为操作系统不认识用户级线程,所以无法调度两个用户级线程,只能挤在一个核里工作,另一个核一直空闲。
➢如果有两个进程。操作系统可以调度,把两个进程分别放在两个核上运行,表面上看可以并行,但是多核处理器的内存资源共享,不可能同时去查两个映射表(指在多核系统中,虽然每个核心可以独立执行任务,但它们共享(互斥访问)同一套物理内存。这里的“映射表”通常指的是页表(Page Table)),所以实际上两个进程间不是并行的而是并发的。这样没有发挥出多核处理器的优势。
➢如果有两个内核级线程。
- 因为创建于内核,所以操作系统可以调度;
- 因为同一个进程对应的内核级线程共享进程的内存资源,所以没有进程的瓶颈,加快硬件处理速度,实现并行。
大多数现代操作系统都实现了内核级线程,如Windows、Linux
1.线程的管理工作由谁来完成?
- 内核级线程的管理工作由操作系统内核完成。
2.线程切换是否需要CPU变态?
- 线程调度、切换工作都由内核负责,因为内核级线程的切换必然要在核心态下才能完成
3.操作系统是否能意识到内核级线程的存在?
- 操作系统会为每个内核级线程建立相应的TCB(Thread Control Block,线程控制块),通过TCB对线程进行管理。"内核级线程"就是"从操作系统内核视角看能看到的线程"
4.这种线程的实现方式有什么优点和缺点?
- 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行
- 缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大
多线程模型
一对一模型
一对一模型:一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
- 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行
- 缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大
多对一模型
多对一模型:多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。
- 优点:用户线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
- 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行
注意:
- 操作系统只"看得见"内核级线程,因此只有内核级线程才是处理机分配的单位
多对多模型
多对多模型:n用户级线程映射到m个内核级线程(n>=m)。每个用户进程对应m个内核级线程
- 克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞),又克服了一对一模型中一个用户级进程占用太多内核级线程,开销太大的缺点
可以这么理解:
- 用户级线程是"代码逻辑"的载体
- 内核级线程是"运行机会"的载体
内核级线程才是处理机分配的单位。
- 例如:多核CPU环境下,下面这个进程最多能被分配两个核
- 一段"代码逻辑"只有获得了"运行机会"才能被CPU运行
- 内核级线程中可以运行任意一个有映射关系的用户级线程代码(时间片),只有两个内核级线程中正在运行的代码逻辑都阻塞时,这个进程才会阻塞