进程管理及相关概念

进程概述

进程(Process)是计算机进行系统分配和调度的基本单位,为使程序能并发执行,且为了对并发执行的程序加以描述和控制,人们引入了“进程”的概念。而实现进程并发和调度的关键是进程控制块-PCB(Process Control Block),那PCB是什么呢,而其工作原理是什么样的呢?

进程实体是由程序段、相关的数据段和PCB三部分构成。PCB主要是为了描述和控制进程的运行的,由系统为每个进程定义的数据结构,PCB 中记录了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息。所以说,PCB是操作系统实现进程并发、控制、调度的关键。例如,当OS要调度某进程执行时,要从该进程的PCB中查出其现行状态及优先级;在调度到某进程后,要根据其PCB 中所保存的处理机状态信息,设置该进程恢复运行的现场,并根据其PCB 中的程序和数据的内存始址,找到其程序和数据;进程在执行过程中,当需要和与之合作的进程实现同步、通信或访问文件时,也都需要访问PCB;当进程由于某种原因而暂停执行时,又须将其断点的处理机环境保存在PCB中。可见,在进程的整个生命期中,系统是根据进程的PCB来感知到该进程的存在的。所以说,PCB是进程存在的惟一标志。

PCB块主要内容:

  • 标识进程身份的进程标识符,PID
  • 进程最基本数据:进程状态(CPU),进程标志(内存)。
  • CPU调度信息:进程的CPU使用时间,用户设置的进程优先级
  • 进程的内存映像:正文段的地址,数据段的地址
  • 进程的组织隶属关系
  • 进程分配的资源
  • 进程会计统计信息

进程的状态

进程的执行状况总是“走走停停”的,而不是“一气呵成”的,这就决定了进程应该具有多种状态。包括如下:

就绪状态(Ready):当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。

执行状态(Running):进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态;在多处理机系统中,则有多个进程处于执行状态。

阻塞状态(Block):正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态或封锁状态。致使进程阻塞的典型事件有:请求I/O,申请缓冲空间等。

创建状态:通常创建一个进程先要为其创建PCB,并填写相应的进程标识信息,然后,把该进程转入就绪状态并插入就绪队列之中。所以,在转入就绪队列之前就是创建状态。

终止状态:进程的终止要通过两个步骤:首先等待操作系统进行善后处理,然后将其PCB清零,并将PCB 空间返还系统。当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止状态。

几个关键概念

原语

进程控制一般是由OS的内核中的原语来实现的。原语(Primitive)是由若干条指令组成的,用于完成一定功能的一个过程。它与一般过程的区别在于:它们是“原子操作(Action Operation)”。所谓原子操作,是指一个操作中的所有动作要么全做,要么全不做。换言之,它是一个不可分割的基本单位,因此,在执行过程中不允许被中断。包括进程的建立、进程的撤销、进程的等待和进程的唤醒。

创建原语:创建一个就绪状态的进程,使进程从创建状态变迁为就绪状态;

撤销原语:使进程从执行状态变迁为完成状态;

阻塞原语:使进程从运行状态变迁为阻塞状态,如:block;

唤醒原语:使进程从阻塞状态变迁为就绪状态,如:wakeup;

中断

中断(Interrupt)是指处理器接收到来自硬件或软件的信号,提示发生了某个事件,应该被注意,这种情况就称为中断。

中断是用以提高计算机工作效率、增强计算机功能的一项重要技术。如果计算机系统没有中断,则处理器与外部设备通信时,它必须在向该设备发出指令后进行忙等待(Busy waiting),反复轮询该设备是否完成了动作并返回结果。这就造成了大量处理器周期被浪费。引入中断以后,当处理器发出设备请求后就可以立即返回以处理其他任务,而当设备完成动作后,发送中断信号给处理器,后者就可以再回过头获取处理结果。这样,在设备进行处理的周期内,处理器可以执行其他一些有意义的工作,而只付出一些很小的切换所引发的时间代价。执行过程如下图:

中断通常分为同步中断和异步中断,同步中断是当指令执行时由CPU控制单元产生的,之所以称之为同步,是因为只有在一条指令终止执行后CPU才会发出中断;异步中断是由其他硬件设备依照CPU时钟信号随机产生的。异步中断叫做中断(Interrupt),是由间隔定时器和I/O设备产生的;而同步中断也叫做异常(Exception),分为错误(fault)、陷阱(trap)和终止(abort)三种。三者的区别主要在于返回的位置不同。fault在返回后依然会重新执行引起故障的指令(缺页异常就是fault,要保证连贯性);trap返回后执行下一条指令(调试器断点);abort则不会返回,这往往意味着发生了一个严重错误,异常中止处理程序会终止引起abort的进程。

外部设备不能直接发出中断,而必须通过中断控制器的标准组件来请求中断,这种请求叫做IRQ,或中断请求(Interrupt Request)。每个能够发出中断请求的硬件设备控制器都有一条名为IRQ(Interrupt ReQuest)的输出线,所有IRQ线都与一个可编程中断控制器(PIC)的硬件电路的输入引脚相连。

进程上下文与中断上下文

首先,上下文就像我们常用的Spring框架的ApplicationContext,包含了Spring容器在运行时的各种Bean信息。

而对于进程而言,进程上下文就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈上的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

而对于中断来讲,硬件通过触发信号,向CPU发送中断信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核, 内核通过这些参数进行中断处理。 所以,“中断上下文”就可以理解为硬件传递过来的这些参数和内核需要保存的一些被中断的进程的环境。

在现代操作系统中,CPU都具有不同的操作模式,它们代表不同的级别,对系统资源具有不同的访问权限。如:最高级别(内核态),可以使用处理器的所有资源;而应用程序运行在较低级别(用户态),这个级别下不能对硬件进行直接访问以及对内存的非授权访问。进程既可以在用户空间运行,又可以在内核空间运行。在用户空间运行时被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。当工作在用户态的进程想访问某些内核才能访问的资源时,必须通过系统调用或者中断切换到内核态,由内核代替其执行。如查看文件时,执行多次系统调用:open、read、write、close等,就是通过系统调用完成的从用户态到内核态的转变。进程上下文和中断上下文就是完成这两种状态切换所进行的操作总称。

处理器总处于以下状态中的一种:

用户态,运行于用户空间;

内核态,运行于进程上下文,内核代表进程运行于内核空间;

内核态,运行于中断上下文,内核代表硬件运行于内核空间。

临界资源

许多硬件资源如打印机、磁带机等,都属于临界资源(Critical Resouce),诸进程间应采取互斥方式,实现对这种资源的共享。

临界区

人们把在每个进程中访问临界资源的那段代码称为临界区(critical section)。通常使用进程互斥地进入自己的临界区,来实现诸进程对临界资源的互斥访问。因此,必须在临界区前面增加一段用于进行上述检查的代码,把这段代码称为进入区(entry section)。相应地,在临界区后面也要加上一段称为退出区(exit section)的代码,用于将临界区正被访问的标志恢复为未被访问的标志。

进程的同步机制

操作系统为了实现程序的并发性,引入了进程。而为了实现多个进程对操作系统的某些资源的访问,尤其是面对竞争临界资源的时候,需要遵循一定的规则或顺序,这就是同步。正是有了同步机制,才使得操作系统可以高效稳定运行,提高了资源的利用率和系统的吞吐量。

通常同步机制要遵循四条准则:

  • 空闲让进。当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入自己的临界区,以有效地利用临界资源。
  • 忙则等待。当已有进程进入临界区时,表明临界资源正在被访问,因而其它试图进入临界区的进程必须等待,以保证对临界资源的互斥访问。
  • 有限等待。对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态。
  • 让权等待。当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。

信号量机制

进程间对共享资源的互斥访问是通过信号量机制来实现的。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。为使多个进程能互斥地访问某临界资源,只须为该资源设置一互斥信号量mutex,并设其初始值为1,然后将各进程访问该资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。这样,每个欲访问该临界资源的进程在进入临界区之前,都要先对mutex 执行wait操作,若该资源此刻未被访问,本次wait操作必然成功,进程便可进入自己的临界区,这时若再有其他进程也欲进入自己的临界区,此时由于对mutex 执行wait 操作定会失败,因而该进程阻塞,从而保证了该临界资源能被互斥地访问。

管程机制

管程(Monitors)是一个资源管理模块,其中包含了共享资源的数据结构,以及由对该共享数据操作的一组接口,组成的资源管理程序。进程可以通过调用管程来实现进程级别的互斥访问,而不用进程去关系临界资源的互斥问题。当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。

进程通信

进程通信,是指进程之间的信息交换,进程通信根据交换信息量的多少和效率的高低,分为低级通信(只能传递状态和整数值)和高级通信(提高信号通信的效率,传递大量数据,减轻程序编制的复杂度)。

低级通信

由于进程的互斥和同步,需要在进程间交换一定的信息,所以,将进程的同步也归为进程通信。只能传递状态和整数值。

高级通信

进程之间可直接利用操作系统所提供的一组通信命令高效地传送大量数据的一种通信方式。

高级通信机制可归结为三大类:共享存储器系统、消息传递系统以及管道通信系统。

共享存储:在共享存储器系统(Shared-Memory System)中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。

消息传递:消息传递系统(Message passing system)是当前应用最为广泛的一种进程间的通信机制。在该机制中,进程间的数据交换是以格式化的消息(message)为单位的;在计算机网络中,又把message称为报文。程序员直接利用操作系统提供的一组通信命令(原语),不仅能实现大量数据的传递,而且还隐藏了通信的实现细节,使通信过程对用户是透明的,从而大大减化了通信程序编制的复杂性,因而获得了广泛的应用。

管道通信:所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。

经典进程问题

生产者-消费者问题

问题描述:一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。

问题分析:生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,他们也是同步关系。

解决方案:

  • 让生产者在缓冲区满时休眠,等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。
  • 让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。

注:Java的阻塞队列(ArrayBlockingQueue)的实现

哲学家进餐问题

问题描述:一张圆桌上坐着五名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿的时候,才试图拿起左、 右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

问题分析:五名哲学家与左右邻居对其中间筷子的访问是互斥关系。死锁情况,每个哲学家都拿着左手的筷子,永远都在等右边的筷子(或者相反);资源耗尽情况,增加超时机制,规定当哲学家等待另一只筷子超过五分钟后就放下自己手里的那一只筷子,并且再等五分钟后进行下一次尝试。这个策略消除了死锁,但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的筷子,那么这些哲学家就会等待五分钟,同时放下手中的筷子,再等五分钟,又同时拿起这些筷子。

解决思路:关键是如何让一个哲学家拿到左右两个筷子而不造成死锁或者资源耗尽现象。一种方案是:让哲学家同时拿到两个筷子,而不是一个一个去拿,不能同时拿起两个筷子的哲学家必须等待,等待可以同时拿起两个筷子的情况出现,否则一个筷子都不能拿。这种解决方案比较直接,也有很多升级的解决方案,增加了哲学家拿起筷子的限制条件,比如:

  • 最多只能允许四位哲学家拿起左手边的筷子;
  • 规定奇数号哲学家先拿左筷子再拿右筷子,而偶数号哲学家相反;
  • 让先拿到一个筷子的人,优先去获取另外一只筷子,引入优先级概念,让并发更合理;

读者-写者问题

问题描述:有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此有以下情况:

  1. 允许多个读者可以同时对文件执行读操作;
  2. 只允许一个写者往文件中写信息;
  3. 任一写者在完成写操作之前不允许其他读者或写者工作;
  4. 写者执行写操作前,应让已有的读者和写者全部退出。

问题分析:读者和写者是互斥的,写者和写者也是互斥的,而读者和读者不存在互斥问题。

解决方案:写者是比较简单的,它和任何进程互斥,当有进程进行写操作时,不允许有其他线程在继续操作,其他进程只能阻塞,等待写者释放资源。读者的问题比较复杂,它必须实现与写者互斥的同时还要实现与其他读者的同步,这里需要使用一个读者的计数器,用来计算读者的个数。当有读者占用文件的时候,其他读操作进程可以同时进行读,读者计数需要加1;这时,如果有写者需要进行写文件时,写操作必须阻塞等待,当读者释放文件时,需要减1,直到为0,也就是没有读者,写者才可以进行操作。这里对于读者计数的加减操作也应该是互斥的。

注:Java中读写锁(ReentrantReadWriteLock)的实现

线程

操作系统中引入进程的目的,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使OS具有更好的并发性。由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销。正因如此,在系统中所设置的进程,其数目不宜过多,进程切换的频率也不宜过高,这也就限制了并发程度的进一步提高。

线程又称为轻量级进程,线程在调度、并发、系统开销和拥有资源等方面,都要在进程的基础上更优化,更加提升系统的并发程度。

调度:在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位,把传统进程的两个属性分开,使线程基本上不拥有资源,这样线程便能轻装前进,从而可显著地提高系统的并发程度。

并发:在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量。

系统开销:在进程创建、撤销时,操作系统要创建和回收PCB,分配和回收各种资源;在进程切换时,涉及当前进程CPU环境的保存而新进程CPU环境的设置。无论是创建线程,还是切换线程,对于系统的代价都远小于进程。更细粒度的是,有些操作系统中,线程的切换、同步和通信都无须操作系统内核的干预。更加减少内核的压力,让内核可以去做更多的事情。

拥有资源:进程是操作系统中拥有资源的基本单位,一般而言,线程自己不拥有系统资源,但它可以访问其隶属进程的资源。

线程的实现方式

内核支持线程KST(Kernel Supported Threads)

进程创建、撤销、切换,以及要求由系统设备完成的I/O 操作,都是用内核的相应处理程序完成的,所以,进程都是在操作系统内核的支持下运行的。而这里的KST,也同样是在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等也是依靠内核,在内核空间实现的。

用户级线程ULT(User Level Threads):

ULT仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。由于这些线程的任务控制块(TCB)都是设置在用户空间,而线程所执行的操作也无须内核的帮助,因而内核完全不知道用户级线程的存在。

组合方式

把用户级线程和内核支持线程两种方式进行组合,提供了组合方式ULT/KST 线程。在组合方式线程系统中,内核支持多KST线程的建立、调度和管理,同时,也允许用户应用程序建立、调度和管理用户级线程。

用户级线程是在用户空间实现的,所有的用户级线程都运行在一个中间系统上,当前有两种方案实现中间系统,即运行时系统和内核控制线程:

运行时系统,实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。

内核控制线程,这种线程又称为轻型进程LWP(Light Weight Process)。每一个进程都可拥有多个LWP,同用户级线程一样,每个LWP都有自己的数据结构(如TCB),。LWP 可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。

在一个系统中的用户级线程数量可能很大,为了节省系统开销,不可能设置太多的LWP,而把这些LWP 做成一个缓冲池,称为“线程池”。用户进程中的任一用户线程都可以连接到LWP池中的任何一个LWP上。为使每一用户级线程都能利用LWP与内核通信,可以使多个用户级线程多路复用一个LWP,但只有当前连接到LWP上的线程才能与内核通信,其余进程或者阻塞,或者等待LWP。而每一个LWP都要连接到一个内核级线程上,这样,通过LWP可把用户级线程与内核线程连接起来,用户级线程可通过LWP来访问内核,但内核所看到的总是多个LWP 而看不到用户级线程。

线程的同步和通信

为使系统中的多线程能有条不紊地运行,在系统中必须提供用于实现线程间同步和通信的机制。为了支持不同频率的交互操作和不同程度的并行性,在多线程OS中通常提供锁机制和信号量机制等同步机制。

锁机制

通过锁机制实现多线程之间的互斥与同步机制,包括互斥锁、条件变量、以及多读、单写锁(读写锁):

  • 互斥锁提供了以排他方式防止数据结构被并发修改的方法。
  • 读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
  • 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的,条件变量始终与互斥锁一起使用。

信号量机制

用于实现进程同步的最常用工具——信号量机制,也可用于多线程OS中,实现诸线程或进程之间的同步。锁机制使用是有限制的,锁只有两种状态,即加锁和解锁。对于互斥的访问一个临界资源,锁机制比较容易满足。但对于多个临界资源来说,信号量机制更适合一些。


  1. http://www.kerneltravel.net/journal/viii/01.htm
  2. https://r00tk1ts.github.io/2017/12/21/Linux%E4%B8%AD%E6%96%AD%E5%86%85%E5%B9%95/
  3. 《中断——维基百科》
  4. 《深入理解Linux内核》
  5. http://www.kerneltravel.net/journal/vi/syn.htm
  6. http://c.biancheng.net/view/1220.html

原文发布于微信公众号 - BanzClub(banz-club)

原文发表时间:2019-06-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券