更新于 

什么是进程

进程的概念,组成,特征和组织方式

进程的概念

程序是静态的,就是存放在某个磁盘里的可执行文件,是一系列指令的集合。而进程是动态的,是程序的一次执行过程,所以同一个程序会对应多个进程。

如上图,QQ是一个程序,而图中的三个框分别对应着QQ程序三次执行过程,因此各为一个进程。仔细回想,你会注意到并发性所描述的是进程在同一个时间间隔内同时进行。

进程的组成

那么操作系统该如何区分每一个进程呢,毕竟有些进程在我们看来是一模一样的根本不好区分,操作系统则是根据PID来区分不同的进程。当一个进程被创建时,操作系统就会为这个进程分配一个唯一的且不重复的“身份证号”–PID。

而操作系统要记录PID和进程所属用户ID(UID),这个是进程最基本的描述信息,可以使操作系统区分各个进程。例如上图中的3个QQ登录进程实际上在操作系统看来并不相同,他们每个进程各对应着一个独一无二的PID号码。

并且操作系统还要记录为进程分配了那些资源(如:分配了多少内存,正在使用那些I/O设备和正在使用哪些文件),这样以便实现操作系统对于共享资源的管理和分配。

同时还要记录进程的运行情况(如:CPU使用情况,磁盘使用情况,网络流量使用情况等)以方便实现操作系统对进程的控制和协调调度。

而这些信息均被存放在一个数据结构–PCB中(Process Control Block)中,即进程控制块,操作系统需要对各个并发运行的进程进行管理,但凡是管理时所需要的的信息,都会被放在PCB中,因此PCB不仅仅是记录PID号码而已,而是进程的信息以及所运行的环境都要记录,这样放切换不同的进程时操作系统可以保证其在合适的运行环境下进行适当的工作。并且要注意每一个进程都对应一个自己的PCB存储着自己的信息,当进程结束时操作系统会回首PCB,因此可以说PCB是存储进程完整信息的最小单位。

同时进程还拥有程序段和数据段,分别用来存储程序的代码和运行过程中的各种数据。而PCB虽然也属于进程的一部分,但是他并不是提供给进程自身使用的,而只是一个个人身份信息卡,用来提供给操作系统使用,只有程序段和数据段才是进程自己使用的。

思考:程序是怎么运行的?

先看下图:

这个是一个高级语言翻译到指令然后由cpu执行的过程,那么高级语言->指令->cpu执行所对应的一个程序具体的运行过程是怎样的呢?如下图:

高级语言所编写的可执行文件.exe存放于硬盘上存储,当双击打开程序的一个进程时,首先会把程序放入内存,并且操作系统会创建一个PCB分配给这个进程,此时进程所组成的三部分PCB,程序段和数据段都被存放在了内存中,并且程序段中是二进制指令,这样cpu在执行指令时会取出相对应的指令,并将执行所产生的的数据存放在数据段,在进程运行时cpu和内存会频繁的进行信息交换。

我们在这里定义一个新名词叫做进程实体或者进程映像,就是程序段,数据段,PCB三部分的组合,那么引入进程实体后,我们可以更加准确的定义进程:进程并不是一个可见的物质实体,而是一个进程实体的运行过程即是一种连续性的状态组成的,是系统进行资源分配和调度的独立单位,所以准确来说我们之前所称呼的进程实际上是进程实体,因此对于每一个进程,他们的PCB,数据段各不相同会时刻发生着变化,而程序段就是指令集合,一般同一个程序所产生的进程程序段代码会相同。又因为PCB是记录进程的信息,所以PCB中的某些信息(如占用内存的状况等环境信息,当然PID除外)肯定也是时刻发生变化的并且PCB是进程存在的唯一标志。

进程的特征

程序是静态的而进程是动态的,因此进程拥有以下几个特征:

  1. 动态性:进程是程序的一次执行过程,是动态产生,变化和消亡的,这也是进程最基本的特征。
  2. 并发性:内存中有多个进程实体,各进程可以并发地执行。
  3. 独立性:进程是能够独立运行、独立获得资源、独立接受调度的基本单位。
  4. 异步性:各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题。
  5. 结构性:每个进程都会配置一个PCB,结构上看,进程由程序段、数据段、PCB组成。

进程的组织方式

在一个系统中,一般会有数十至数百乃至数千个PCB,为了能够对他们有效的管理,需要适当的方式将PCB组织起来存储管理。进程的组成讨论的是一个进程内部的组成结构信息,而进程的组织讨论的是多个进程之间的组织方式。这里我们讨论两种方式。

链接方式

链接方式是按照进程的状态将PCB分为多个队列,操作系统持有各个队列的指针方便管理进程。如下:

这种方法优点很明显,各个PCB形成一个队列并且PCB指针执行下一个PCB,由指针统一指向调配,切换时更改指针指向后一个PCB即可,切换简便,但是缺点时当有对个PCB块时采用这种队列,当中途需要删除或者提高某个PCB优先级时则需要对两边的指针进行更新比较复杂。

索引方式

根据进程状态的不同,建立几张索引表用来记录PCB地址,操作系统指向各个索引表的指针。如下:

这种方法优点是建立索引表存储,更改指针执行即可切换进程,并且创建删除只需对索引表项进行操作,非常简便,但是缺点是当PCB很多时,索引表就会长可能需要建立多级索引表,同时索引表也许占用额外的空间。

进程的状态与转换

进程的状态

这是个非常重要的概念,他根据不同的进程所处信息环境将进程分为以下几个状态:

创建态

进程正在被创建时,他的状态就是创建态,这个阶段操作系统会为进程分配资源并初始化PCB。因此可能会有多个程序处于就绪态都带等待cpu的情况出现。

就绪态

当进程创建完成后并不是立刻就上cpu执行,当创建完成后便进入了就绪态,此时已经具备了运行条件,但是如果此时cpu没有空闲,则该就绪态进程则需要等待暂时不运行随时准备进入cpu执行,当然运气好的话,也可能刚创建完就上cpu。

运行态

当cpu空闲时,并且内存中有处于就绪态的进程,操作系统就会选择一个就绪态进程(这个会涉及到多种不同的算法,后面介绍)让其上处理机pu执行,此时这个进程就是处于运行态。

阻塞态

当然进程不可能会提前将所有图中的准备都一次性做好,他在运行时可能中途会请求等待另一个事件的发生后才能继续执行,或者发现某个所需的共享资源已被占用,则需等待其他进程的相应,此时操作系统会让这个进程下cpu,并让他进入阻塞态等待(毕竟cpu不只是服务这一个进程,可没时间一直等),此时操作系统就会启动另一个就绪态的进程运行。所以阻塞态不可能直接变为运行态,一定要先恢复到就绪态(毕竟要先准备好切回该进程的工作环境等工作)。并且可以看出在阻塞态之前进程一定是处于运行态。

终止态

一个进程已经执行完成或者中途被用户关掉时就会执行exit系统调用,此时会请求系统终止该程序,然后程序就会进入终止态,操作系统让该进程下cpu,并回收内存空间等资源,最后还要回首该进程的PCB,当终止进程的工作完成之后,这个进程就彻底消失了,如果是连接形式组织方式,那么这个PCB就会删除,指针指向它所指向的下一个PCB,而如果是所以方式,则PCB删除的同时,索引表的项也会删掉。注意就绪态/阻塞态/运行态的进程都可能立刻转换为终止态。

进程状态的转换

这个模型非常重要,需要牢记。

从上图可见运行态和就绪态之间是双向可以切换的,而当是外界因素例如时间片或者其他进程抢占导致的下cpu实惠直接回到就绪态,只有是通过系统调用的方式申请请求时才会切换到阻塞态,即是进程一种的自愿让出cpu的主动行为,所以可以理解为此时自愿下cpu,所以会将工作环境,工作的状态等记录下后下cpu,当可以继续执行的时候则首先需要在配置回所需的工作环境并做好之前的工作状态到达就绪态才能继续执行,而当是被动的不情愿下cpu时则会随时准备抢回cpu的使用权所以会直接切换到就绪态。当然要注意阻塞态切换到就绪态是被动地行为因为这不是进程想继续回到就绪态准备执行就可以随时自主切回就绪态的,而是需要等到某个事件发生后才能回到就绪态,由于时间无法预测所以只能被动等待。而运行态到终止态一般是调用了exit函数(不一定是正常执行完成,当遇到重大的bug如数组越界等导致进程无法继续运行也会触发),一旦转换为终止态,则只能重新创建进程了。

在进程PCB中,会有一个state变量来记录当下进程的状态,1代表创建态,2代表就绪态,3代表运行态…为了对同一个状态下的各个进程进行统一的管理,操作系统会将各个进程的PCB以链接形式或者索引形式统一存储管理。

进程控制

进程控制就是对系统中的进程实施有效的管理,它具有创建新进程,撤销已有进程,实现进程状态转换等功能,其中最重要的就是实现进程的状态转换。

进程控制的实现

实现进程的控制肯定会也是需要程序来实现的,而这个程序就是内核中的原语部分(重点:原语是一个程序),原语是一种特殊的程序,他的执行具有原子性,即这段程序的运行必须是一气呵成无中断的。

思考:原语为什么不能有中断?

因为如果不能一气呵成,那么就有可能导致操作系统在某些关键数据结构信息处不统一,从而影响操作系统进行个别的管理工作。如果可以中断,就会出现重大的bug,如:

这是一个以连接形式组织的PCB队列,此时假设系统要执行将PCB2(此时他在队列头部,该轮到他了)所对应的进程2等待的进程已经发生了,则此时需要从阻塞态转换到就绪态,即放到就绪队列中,此时原语程序需要进行以下两个步骤:

  1. 将PCB的state变量设为1(假设1表示就绪态,2表示阻塞态)
  2. 将PCB2从阻塞队列放到就绪队列

那么如果原语可以被打断的话,此时刚刚执行完第一个步骤后收到了中断信号停止执行原语程序,就会出现state=1但是却在阻塞队列的bug。所以原语必须具有一气呵成的特点。

思考:如何实现原语的“原子性”?

我们可以使用“关中断指令”和“开中断指令”两个特权指令实现原子性。

我们首先知道cpu在每执行完一个指令后都会例行检查是否有中断信号需要处理,如果有,就会暂停运行当前的这段程序,转而执行相应的中断处理程序。如下:

那么显然在执行原语时我们需要cpu不在根据中断信号而停止运行原语程序,因此就有了关中断指令和开中断指令两个特权指令(此时其他的指令如指令1,2和a,b还是非特权指令),那么可以这样实现:当cpu执行了关中断指令后,就不在例行检查中断信号,知道执行到开中断指令之后在恢复检查。这样关中断和开中断之间的这些指令序列(指令a,b)就是不可被中断的了,当然在这期间cpu还是会受到中断信号,但是此时不检查,就可视为忽略了,知道开中断以后在执行中断信息。如下图:

显然关开中断指令必须是特权指令,否则用户可以修改就会造成原语程序被打断的情况出现。

进程控制相关的原语

首先我们看有关进程创建的原语:

有关进程终止的原语:

这里面将该进程拥有的所有资源归还给父进程或操作系统要特别注意。撤销原语是指由就绪态/阻塞态/运行态切换到终止态再到释放时所执行的原语程序。

有关进程的阻塞和唤醒的原语

有关进程的切换的原语:

我们可以看出大部分原语都会有许多步骤,并且引起原语的事件也各不相同。

程序是如何运行的

学完进程转换后,我们在更加详细的讨论一下程序在运行时切换进程在切回进程的具体步骤。首先我们需要了解一个新的概念–寄存器,就是用来存储信息的,这里寄存器可以分为许多类(机组原理有讲),如下图:

我们可以看出PC和IR的作用分别是存储下一条指令和现在正在执行的指令的特殊功能寄存器,并且回忆PSW寄存器是用来记录当前cpu处于管态还是目态的,通用寄存器就是用来存储中间的某些计算数据结果的,所以PC和IR肯定是经常与内存中进程处的程序段进行交流的,而通用寄存器就是与数据段进行频繁的信息交换。

当执行完指令1后PC和IR会立刻更新,并且如果需要通用寄存器会存储信息。那么鸡舍现在进程A执行到了指令3以后需要开始执行另一个就绪进程B,则此时进程A需要切换到阻塞态,因为通用寄存器和PC,IR都是共享资源,那么进程A的信息肯定就不能在占用了,那么之后执行完进程B切换回进程A时如何能够恢复到之前的运行环境呢?如下图:

上图是执行进程B到指令x,此时需要切换回进程A的运行环境并开始执行进程A。这时我们就需要PCB来保存之前进程A运行态时的环境信息(一些必要的寄存器信息),这样在切换回进程A(当然这之前进程A肯定是要先到达就绪态)后可以保证其正常运行。如下图:

因此PCB会存储一些进程必要的环境信息,所以我之前说道PCB会随时发生小部分变化。但是要注意到这个信息是提供给操作系统,然后操作系统进行恢复cpu到之前进程A的运行环境的任务。所以PCB存储的环境信息也是提供给操作系统的,进程自身不使用。

进程通信

顾名思义,进程之间也是需要信息交换的,进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。为了保证安全,一个进程不能直接访问另一个进程的地址空间。但是进程之间的信息交换又是必须实现的,所以为了进程之间的通信安全,操作系统提供了一些方法。

共享存储

即有一个共享空间可以来实现两个进程之间的信息交换,但是需要满足两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统提供的工具实现)。操作系统只负责提供共享空间和同步互斥工作,具体的信息编写和读入是由进程之间完成的。

思考:为什么共享空间的访问要互斥?

访问互斥即意味着每次只能有一个进程进入共享空间进行读写,原因很简单,如果可以有多个进程同时进入共享空间进行信息的编写,那么就会出现冲突,即两个进程可能同时对某一个变量更改,这种冲突应该避免。

思考:共享空间的实现方式

共享存储有基于数据结构的共享和基于存储区的共享两种方式来实现,两种不同的共享空间会对共享速度产生影响。

基于数据结构的共享:比如共享空间里只能放入一个长度为10的数组,这种共享方式速度慢并且限制多,是一种低级的通信方式。

基于存储区的共享:在内存中划出一块共享存储区,数据的形式,存放位置都由进程控制,而不是操作系统,相比之下,这种共享方式速度更快是一种高级的通信方式。

消息传递

进程间的数据交换格式与共享一个空间不同,而是“信书传递”,数据交换以格式化的消息为单位,进程通过操作系统的“发送消息/传递消息”两个原语(不可打断)进行数据交换。一般一个消息由两部分组成:

而消息传递又有两种方式,一种是直接通信方式,类似于邮递员链接,消息直接挂到接受进程的消息缓冲队列上,另一种是先发送到中间实体类似于信箱,然后另一个进程从中间实体收取,因此也称为“信箱通信方式”。如下图:

上半部分是直接通信,下半部分是简介通信方式,无好坏之分。

管道通信

如下图:

管道实际上是一个用于连接读写进程的一个共享文件,又名pipe文件,其实就是在内存中开辟一个大小固定的缓冲区。那么他和共享空间又有什么本质区别呢?

  1. 管道采用的是半双工通信,某一个时间段内只能实现单向的传输,即一个时间段只能我传给你或者你传给我,当然方向可以任选只是一个时间段只能一个方向,如果需要双向同时通信,则只能在设置一个管道即两个管道才能同时双向通信。因此管道在一个时间段内永远只有一端是可以写数据的口,另一端是读数据的口,且不能同时打开。
  2. 各进程要互斥的访问管道(即读写不同时)。
  3. 数据以字符流的形式写入管道,当管道满时,写进程的write()系统调用就会被阻塞即使没有写完,等待读进程将数据取走。当读进程将数据全部取走后,管道变空后,此时读进程的read()系统调用会被阻塞,此时才能继续write()。
  4. 如果没写满,就不允许读,如果没读空,就不允许写。
  5. 数据一旦被读出,就从管道被抛弃,这就意味着读进程最多只有一个,否则可能会有读错误数据的情况,但是写进程可以有多个。
小测试:项目实战

如果你对管道通信了解透彻了,尝试完成以下这个大作业吧😬:作业大礼包