操作系统中进程/线程模型

进程与线程

  • 1 进程/线程模型
    • 1.1 进程的定义
    • 1.2 引入线程机制的动机和思路
    • 1.3 线程的定义
    • 1.4 线程和进程的区别
  • 2 进程状态切换
    • 2.1 java中线程的状态及状态转换
  • 3 处理器调度
    • 3.1 批处理系统中的调度算法
    • 3.2 交互式系统中的调度算法
  • 4 进程同步互斥机制
    • 4.1 进程互斥
    • 4.2 进程同步
    • 4.3 信号量及P、V操作—— 一种经典的进程同步机制
      • 4.3.1 使用PV操作解决互斥问题
      • 4.3.2 使用信号量解决经典同步问题
        • 4.3.2.1 解决生产者/消费者问题
        • 4.3.2.2 解决读者/写者问题
    • 4.4 管程monitor
      • 4.4.1 HOARE管程
      • 4.4.2 MESA管程
  • 5 进程间通信机制
    • 5.1 共享内存
    • 5.2 管道(pipe)通信
    • 5.3 消息传递
    • 5.4 套接字
    • 5.5 远程过程调用和远程方法调用

1 进程/线程模型

1.1 进程的定义

进程:进程是具有独立功能的程序关于某个数据集合的一次执行过程,也是操作系统进行资源分配和保护的基本单位。

  • 程序的一次执行过程,一个程序执行了多次是不同的进程
  • 是正在运行程序的抽象,进程是对CPU的抽象
  • 将一个物理上的CPU虚拟为多个逻辑上的虚拟CPU,供多个进程同时执行(实际上是交替执行)
  • 系统资源以进程为单位分配,如内存、文件等,每个进程具有独立的地址空间
  • 操作系统通过调度把cpu的控制权交给某个进程

通过windows下查看任务管理器,或者在linux下执行ps命令来查看有多个进程在运行。

1.2 引入线程机制的动机和思路

操作系统采用进程机制使得多任务能够并发执行,提高了资源使用和系统效率。在早期操作系统中,进程是系统进行资源分配的基本单位,也是处理器调度的基本单位,进程在任一时刻只有一个执行控制流,这种结构称为单线程进程。单线程进程调度时存在进程时空开销大、进程通信代价大、进程并发粒度粗、不适合于并发计算等问题,操作系统引入线程机制来解决这些问题。线程机制的基本思路是,把进程的两项功能——独立分配资源和被调度分派执行分离开来,后一项任务交给线程实体完成。这样,进程作为系统资源分配与保护的独立单位,不需要频繁切换;线程作为系统调度和分派的基本单位会被频繁的调度和切换。

1.3 线程的定义

线程:进程中的一个执行路径。操作系统进程中能够独立执行的实体,是处理器调度和分派的基本单位。线程是进程的组成部分,每个进程包含多个并发执行的线程,同一个进程中的线程共享进程的内存地址和资源,但是不拥有资源。

1.4 线程和进程的区别

(1)定义:进程是程序的一次执行过程,线程是进程的一个执行路径。
(2)角色:在支持线程机制的系统中,进程是系统资源分配的单位,线程是系统调度的单位。
(3)资源共享:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈,程序计数器等。
(4) 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
(5)独立性:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在。
(6)系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
(7)通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

2 进程状态切换

进程的三种基本状态:运行态、就绪态、等待态

  • 运行态(Running):占有CPU,并在CPU上运行
  • 就绪态(Ready):已经具备运行条件,但由于没有空闲CPU,而暂时不能运行
  • 等待态(Waiting/Blocked):等待资源

操作系统中进程/线程模型_第1张图片
需要注意:

  • 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
  • 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。

2.1 java中线程的状态及状态转换

Java语言中定义了5中线程状态,在任意一个时刻,一个线程只能有且只有一种状态,这五种状态分别如下:

新建(New):创建后尚未启动的线程处于这种状态
运行(Runnable):Runnable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可有可能正在执行,也有可能正在等待CPU为它分配执行时间。
无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:

  • 没有设置Timeout参数的Object.wait()方法
  • 没有设置Timeout参数的Thread.join()方法
  • LockSupport.park()方法

期限等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后他们会由系统自动唤醒。一下方法会让线程进入期限等待状态:

  • Thread.sleep()方法
  • 设置了Timeout参数的Object.wait()方法
  • 设置了Timeout参数的Thread.join()方法
  • LockSupport.parkNanos()方法
  • LockSupport.parkUntil()方法

阻塞(Blocked):线程被阻塞,“阻塞状态”与“等待状态”的区别是:阻塞状态在等待着获取一个排它锁,这个事件将在另一个线程放弃这个锁的时候发生;等待状态则是在等待一段时间或者唤醒动作的发生。在线程等待进入同步区域的时候,线程将进入这种状态。
结束(Terminated):已终止线程的线程状态,线程已经结束执行。
操作系统中进程/线程模型_第2张图片

3 处理器调度

CPU调度:控制、协调进程对CPU的竞争,即即按一定的调度算法从就绪队列中选择一个进程,把CPU的使用权交给被选中的进程;如果没有就绪进程,系统会安排一个系统空闲进程或idle进程。

调度算法总体上可以分为两类:

  • 抢占式调度:抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,线程的切换不由线程本身决定,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

  • 非抢占式调度(协同调度):协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,线程的执行时间由线程本身控制,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。

3.1 批处理系统中的调度算法

批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。

  • 先来先服务(FCFS-First Come First Serve)
    非抢占调度,按照进程就绪的先后顺序使用CPU。优点是公平实现简单,缺点是有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
  • 短作业优先(SJF-Shortest Job First)
    非抢占式调度,具有最短完成时间的进程优先执行。优点是具有最短的平均周转时间,但是不公平,如果一直有短作业到来,那么长作业永远得不到调度,产生 “饥饿”现象 (starvation)。
  • 最短剩余时间优先(SRTN-Shortest Remaining Time Next)
    SJF抢占式版本,即当一个新就绪的进程比当前运行进程具有更短的完成时间时,系统抢占当前进程,选择新就绪的进程执行。
  • 最高响应比优先(HRRN-Highest Response Ratio Next)
    是一个综合的算法调度时,首先计算每个进程的响应比R;之后,总是选择 R 最高的进程执行。其中响应比R= 周转时间 / 处理时间 =(处理时间 + 等待时间)/ 处理时间 = 1 +(等待时间 / 处理时间)。如果是短作业由于处理时间少很快就会上CPU执行,如果是长作业等待的时间越长,上CPU执行的机会就越大,从而避免长作业的饥饿。

3.2 交互式系统中的调度算法

交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应

  • 时间片轮转调度(RR-Round Robin)
    抢占式调度,将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
    对于时间片轮转算法要选择合适的时间片:如果时间片太长(大于典型的交互时间),就会降级为先来先服务算法并且延长短进程的响应时间;如果时间片太短(小于典型的交互时间),就会导致频繁的进程切换,浪费CPU时间。

  • 最高优先级调度(HPF—Highest Priority First)
    为每个进程分配一个优先级,按优先级进行调度。实现简单,但是不公平,优先级低的线程可能永远得不到调度,产生 “饥饿”现象。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级

  • 多级反馈队列(Multiple feedback queue)
    综合调度算法,设置多个就绪队列,第一级队列优先级最高,给不同就绪队列中的进程分配长度不同的时间片,第一级队列时间片最小;随着队列优先级别的降低,时间片增大。当第一级队列为空时,在第二级队列调度,以此类推;各级队列按照时间片轮转方式 进行调度。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
    操作系统中进程/线程模型_第3张图片

总结:

调度算法 占用CPU方式 吞吐量 响应时间 开销 对进程的影响 饥饿问题
FCFS 非抢占 不强调 可能很慢,特别是当进程的执行时间差别很大时 最小 对短进程不利;对I/O型的进程不利
SJF 非抢占 为短进程提供好的响应时间 可能较大 对长进程不利 可能
SRTN 抢占(到达时) 提供好的响应时间 可能较大 对长进程不利 可能
HRRN 非抢占 提供好的响应时间 可能较大 很好的平衡
Round Robin 抢占(时间片用完时) 若时间片小,吞吐量会很低 为短进程提供好的响应时间 最小 公平对待
Feedback 抢占(时间片用完时) 不强调 不强调 可能较大 对I/O型进程有利 可能

Windows 基于优先级的抢占式多任务调度,Linux使用抢占式调度。

4 进程同步互斥机制

并发环境下进程的特征
这一部分的主要问题都是由并发引起的,并发是所有问题产生的基础,也是操作系统设计的基础。从进程的特征出发,看并发环境下进程的执行会带来什么问题。在并发环境下进程的执行是间断性的,由于间断性,使得进程的相对执行速度不可预测;在并发环境下多个进程/线程之间共享某些资源,在资源的使用中会产生进程之间的一种制约性,比如一个进程想用打印机这个资源,另一个进程在这个进程没有释放资源的前提下是不能使用该资源;在不同的执行顺序下,进程的执行结果也是不确定的。

4.1 进程互斥

进程互斥:由于各进程要求使用共享资源(变量、文件等),而这些资源需要排他性使用。各进程之间竞争使用这些资源,这一关系称为进程互斥。

其中共享资源也叫临界资源:系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量。当多个进程需要使用临界资源的时候,需要在代码中进行相应的操作,这些代码就叫临界区。

临界区(互斥区):各个进程中对某个临界资源(共享变量)实施操作的程序片段。这些程序片段分散在不同的进程/线程里面,共同的特点是对同一个临界资源进行一些操作,这一段代码和另一个进程的这段代码之间互为临界区。

互斥区的使用规则:

  • 没有进程在临界区时,想进入临界区的进程可进入
  • 不允许两个进程同时处于其临界区中
  • 临界区外运行的进程不得阻塞其他进程进入临界区
  • 不得使进程无限期等待进入临界区
    操作系统中进程/线程模型_第4张图片

进程互斥解决方案:
软件方案:Dekker解法、Peterson解法(编程实现)
硬件方案:屏蔽中断、TSL(XCHG)指令(用特定的指令完成进程互斥保护)

在软件和硬件的解法中都有忙等待,进程在得到临界区访问权之前,持续测试而不做其他事情,在单处理上降低CPU的执行效率。在多处理器上,某个内存单元的值由1变成0可以由另一个处理器CPU来完成这项操作,某个进程上的一个CPU想要进入临界区想要加锁,在这个锁或者临界区的访问权没有得到之前可以一直处于忙等待状态,直到其他的CPU上的进程释放临时取资源,因为进程之间切换的代价可能要大于忙等待的代价,这也叫做自旋锁

4.2 进程同步

进程的互斥是进程之间具有竞争关系,进程的同步是指多个进程之间的协作关系。

进程同步:指系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同完成一项任务。具体地说,一个进程运行到某一点时,要求另一伙伴进程为它提供消息,在未获得消息之前,该进程进入阻塞态,获得消息后被唤醒进入就绪态。

同步:多个进程按一定顺序执行;互斥:多个进程在同一时刻只有一个进程能进入临界区。

互斥和同步的区别和联系
互斥:间接制约关系,是指系统中的某些共享资源,一次只允许一个线程访问。当一个线程正在访问该临界资源时,其它线程必须等待。
同步:直接制约关系,是指多个线程(或进程)为了合作完成任务,必须严格按照规定的某种先后次序来运行。
互斥是指某一资源同时只允许一个进程对其进行访问,具有唯一性和排它性。但互斥无法限制进程对资源的访问顺序,即访问是无序的。
同步是指在互斥的基础上(大多数情况),通过其它机制实现进程对资源的有序访问。同步其实已经实现了互斥,所以同步是一种更为复杂的互斥。
互斥是一种特殊的同步。

4.3 信号量及P、V操作—— 一种经典的进程同步机制

进程的互斥可以看做是特殊的同步,信号量及P、V操作既可以解决同步问题也可以解决互斥问题。

信号量是一个特殊变量,用于进程间传递信息的一个整数值。信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。信号量的值仅能由PV操作来改变
操作系统中进程/线程模型_第5张图片
P操作:信号量S减1,如果S>=0该进程继续执行,如果S<0则该进程进入阻塞队列。执行一次P操作其实就是意味请求分配一个资源,当信号量的值小于0,那么就表示没有可用资源,那么进程就只能进行等待其他拥有该资源的进程释放资源之后,才能进行执行;当信号量大于0的时候,那么表示还有足够的资源,所以,当前进程就可以继续执行;
V操作:信号量S加1,如果S>0该进程继续执行,如果S<=0则释放阻塞队列中的第一个等待信号量的进程。执行一次V操作其实就是意味释放一个资源。
其中P操作也为down操作或者semWait;V操作也为up操作或者semSianal。
对于PV操作的理解可以参考博客:信号量和PV操作专题

P、V操作是原语操作(在执行过程中不允许被中断),首次提出信号量概念是二元信号量(S只取两个值0或者1),用于解决互斥问题。将其推广到一般信号量(多值)或计数信号量,就可以解决同步问题。现在不区分二元信号量和多值信号量,直接就叫信号量,根据S的取值的不同可以解决互斥问题,也可以解决同步问题。

4.3.1 使用PV操作解决互斥问题

  • 分析并发进程的关键活动,划定临界区
  • 设置信号量 mutex,初值为1
  • 在临界区前实施 P(mutex)
  • 在临界区之后实施 V(mutex)

如下图所示,假定有三个进程P1,P2,P3都对共享资源进行操作(临界区),假设P1进程获得CPU的执行权进入临界区,则mutex-1=0(不小于0),因此P1进程继续执行。如果此时P2进程获得CPU的执行权进入临界区,则mutex-1=-1<0,因此P2进程进入阻塞队列,P3获得CPU执行权想进入临界区,则mutex-1=-2<0,因此P3进程进入阻塞队列。
当CPU执行权继续切换到P1进程时,P1执行完临界区代码之后,执行V操作,则mutex+1=-1<0,此时P1唤醒P2进程,P2进程得到CPU执行权之后可以运行临界区代码(P3还在阻塞状态)。P2出临界区的时候执行V操作,则mutex+1=0<=0,唤醒P3.
操作系统中进程/线程模型_第6张图片

4.3.2 使用信号量解决经典同步问题

4.3.2.1 解决生产者/消费者问题

生产者不能往满的缓冲区放东西,设置信号量empty表示缓冲区中空缓冲区的个数,初始值是N;
消费者不能从一个空的缓冲区取东西,设置信号量full表示缓冲区有值的缓冲区个数,初始值为0;
不允许生产者和消费者同时操作缓冲区,因此设置信号量mutex=1保护临界资源。
操作系统中进程/线程模型_第7张图片
生产者生产商品的时候,执行P(empty)操作,empty-1看能不能生产商品,当empty减为0的时候,这时已经生产了N件商品,缓冲区已经满了,再生产商品的时候empty-1=-1<0,因此生产者进程进入阻塞状态,生产者什么时候被唤醒就需要看消费者的操作。消费者执行P(full)操作消费一件商品后执行V(empty)操作,empty+1=0<=0因此唤醒生产者进程,当生产者进程获得CPU执行权的时候就可以生成商品。消费者进程与之类似,full的初值为0,如果一开始获得CPU执行权,full-1=-1<0会进入阻塞状态,除非生产者生产商品执行V(full)操作唤醒消费者进程。由此可见解决同步问题的时候,P操作和V操作分散在两个不同的进程里面

由于缓冲区是临界资源,因此需要互斥访问,使用P(mutex)和V(mutex)实现对临界区的互斥访问。由此可见,互斥的PV操作是在同一个进程里面

问题:
同步和互斥的P操作可以改变吗?
不能。会出现死锁问题,假设颠倒了p(mutex)和p(full),假设消费者获得CPU执行权,进入临界区,执行p(full),full-1=-1消费者进入阻塞状态;此时生产者获得cpu执行权,进入生产者的临界区时,mutex-1=-1,生产者进入阻塞状态,等待消费者释放临界资源。生产者和消费者都进入阻塞状态,等待对方释放资源,造成死锁。

同步和互斥的V操作可以改变吗?
可以。V操作不会使调用V操作的进程进入等待状态,不会造成死锁,但是由于临界区边变大,性能会下降。

4.3.2.2 解决读者/写者问题

多个进程共享一个数据区,这些进程分为两组:读者进程只读数据区中的数据;写者进程:只往数据区写数据。要求满足下述条件:允许多个读者同时执行读操作;不允许多个写者同时操作;不允许读者、写者同时操作

下面使用信号量及PV操作解决第一类读者写着问题:读者优先
如果读者执行:

  • 无其他读者、写者,该读者可以读
  • 若已有写者等,但有其他读者正在读,则该读者也可以读
  • 若有写者正在写,该读者必须等

如果写者执行:

  • 无其他读者、写者,该写者可以写
  • 若有读者正在读,该写者等待
  • 若有其他写者正在写,该写者等待

    操作系统中进程/线程模型_第8张图片
    在读者进程里面引入一个计数器,表示现在的读者数量,对临界区的操作不需要每个读者都进行PV操作,第一个读者进行P操作,最后一个读者进行V操作释放临界区资源即可,由于多个读者对计数器进行操作,因此需要对临界资源计数器rc添加互斥访问。

4.4 管程monitor

在引入了信号量及PV操作之后,为什么出现了新的同步机制——管程?主要是因为信号量存在一些缺点:程序编写困难、易出错。因此引入管程的概念,在程序设计语言中引入管程成分,是一种高级同步机制。

管程是一个特殊的模块,有一个名字,由关于共享资源的数据结构及在其上操作的一组过程组成。进程只能通过调用管程中的过程来间接地访问管程中的数据结构。
操作系统中进程/线程模型_第9张图片
管程作为一种同步机制,需要解决两个问题?
互斥管程管理着共享资源,因此凡是使用管程的进程中只能有一个进程对管程进行操作,即管程是互斥进入的,管程中数据结构的数据完整性。管程的互斥性是由编译器负责保证的。(管程是一种程序设计语言,因此编译器可以保证其是互斥的)
同步管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时应先释放管程的使用权),也可以通过发送信号将等待在条件变量上的进程或线程唤醒。

应用管程时遇到的问题?
当一个进入管程的进程执行等待操作时,它应当释放管程的互斥权。当后面进入管程的进程执行唤醒操作时(例如P唤醒Q),管程中便存在两个同时处于活动状态的进程,这时候如何解决?

  • P等待Q执行(HOARE管程)
  • Q等待P继续执行(MESA管程)
  • 规定唤醒操作为管程中最后一个可执行的操作

4.4.1 HOARE管程

管程是互斥进入的,如果有进程P1进入管程,则其他进程只能等待(管程像有个门,门外有入口等待队列,只有里面的进程释放管程的使用权才能进入)。进入管程的进程在操作过程中发现条件不成熟就通过wait操作等待在某个条件变量上,这时释放管程的使用权,新的进程P2进入管程,对资源进行操作,在操作过程中发现条件成熟了,就通过singal操作唤醒等待在某个环境变量上的进程P1,这时候管程里面就有两个进程存在。根据HOARE管程的解决思路,P2唤醒了P1,则P2等待P1执行,P2进入紧急等待队列。
操作系统中进程/线程模型_第10张图片
因为管程是互斥进入的,所以当一个进程试图进入一个已被占用的管程时,应当在管程的入口处等待,为此,管程的入口处设置一个进程等待队列,称作入口等待队列
如果进程P唤醒进程Q,则P等待Q执行;如果进程Q执行中又唤醒进程R,则Q等待R执行;……,如此,在管程内部可能会出现多个等待进程,在管程内需要设置一个进程等待队列,称为紧急等待队列,紧急等待队列的优先级高于入口等待队列的优先级。

用管程实现生产者/消费者问题
操作系统中进程/线程模型_第11张图片
java中也有类似的机制:比如synchronized的底层实现以及Object中的wait()和notify()方法实现,可以参考博客 Java中synchronized的底层实现原理

4.4.2 MESA管程

针对于HOARE管程中存在的问题:两次额外的进程切换(P进程唤醒Q进程,Q进程执行P进程等待,Q进程执行完成之后P进程在继续执行会造成两次进程切换)。使用notify代替signal,notify的含义是当一个正在管程中的进程执行notify(x)时,它使得x条件队列得到通知,发信号的进程继续执行。

使用notify的注意事项:
使用notify之后,位于条件队列头的进程在将来合适的时候且当处理器可用时恢复执行,由于不能保证在它之前没有其他进程进入管程,因而这个进程必须重新检查条件(用while循环取代if语句)。因此会导致对条件变量至少多一次额外的检测(但不再有额外的进程切换),并且对等待进程在notify之后何时运行没有任何限制相比于HOARE管程更加简单,效率更高。

使用MESA管程来实现生产者/消费者问题:
操作系统中进程/线程模型_第12张图片
对于这个过程的理解,对于了解java的同学可以参考我的另一篇博客多线程总结,其中的多生产/多消费者部分的代码及解决问题的过程中遇到的新问题来扩展思路。这也说明了管程在java中的应用。同时在MESA管程的演进过程中,notify的改进引入了broadcast的概念,也可以参考java中的notifyAll方法。

管程总结
是抽象数据类型,有一个明确定义的操作集合,通过它且只有通过它才能操纵该数据类型的实例。只能通过管程的某个过程才能访问资源;管程是互斥的,某个时刻只能有一个进程或线程调用管程中的过程。由于管程是一种程序语言设计,是通过编译器来实现互斥访问;实现同步是通过条件变量以及在条件变量上的若干操作来实现的。(操作包含:wait/signal 或 wait/notify 或 wait/broadcast)

5 进程间通信机制

有了信号量和管程之后,进程之间为什么还需要新的通信机制?主要原因是信号量和管程只能传递简单的信息,不能传递大量的信息,比如说把一个大的数组传递给另外一个进程,信号量和管程时做不到的,另外管程不适合用于多处理器的情况。因此在传递大量信息的时候就需要引入一种新的机制,即进程间通信机制,其中一个典型的形式就是消息传递(send / receive原语操作),当一个进程想把消息发送给另一个进程的时候就执行send,另一个进程想接收消息就执行receive。由于进程的互斥和同步也需要在进程间交换一定的信息,不少学者也将其归为进程通信,但只能把它们称为低级进程通信。

进程间的通信机制适用于:分布式系统、基于共享内存的多处理机系统、单处理机系统,可以解决进程间的同步问题、通信问题。进程间的通信机制可以分为以下五类:共享内存、管道通信、消息传递、套接字和远程过程调用。

5.1 共享内存

在共享内存系统中,多个通信的进程共享某些数据结构或存储区,进程之间能够通过这些空间进行通信。可分为两种类型:

  • 基于共享数据结构的通信方式。多个进程共用某些数据结构,实现进程之间的信息交换,例如生产者-消费者问题中的有界缓冲区。操作系统仅提供共享存储器,由程序负责共有数据结构的设置和进程同步的处理。这种方式仅适用于少量的数据,通信效率低下。
  • 基于共享内存的通信方式。在内存中划分出一块共享内存,进程可通过对共享内存的读或者写交换信息。需要通信的进程在通信前,先向系统申请共享内存中的一个分区,并将其附加到自己的地址空间中,便可对其中的数据进行读写操作。

操作系统中进程/线程模型_第13张图片

5.2 管道(pipe)通信

管道(Pipe)是指用于连接一个读进程和一个写进程以实现进程间通信的缓冲传输介质(内存或者文件)。发送进程以字符形式将数据送入管道,而接收进程则从管道中接收数据。
管道机制提供了三方面的协调能力:

  • 互斥:当一个进程对管道执行读或写操作时,其他进程必须等待;
  • 同步:当写进程把一定数量的数据写入管道,便睡眠等待,直到读进程取走数据后再把它唤醒;
  • 确定对方是否存在,只有确定对方存在才能通信。
    在这里插入图片描述

5.3 消息传递

进程以格式化的消息(message)为单位,将通信的信息封装在消息中,并利用操作系统提供的一组通信命令(send/receive原语),在进程间进行消息传递,完成进程间的数据交换。

如下图所示,进程S向进程R发送消息,由于发送进程S在自己的进程空间,不能到接收进程R的地址空间进行任何操作,因此发送消息的过程必须由操作系统来完成。发送进程执行send原语,陷入内核,将消息复制到消息缓冲区,消息进入接收进程消息对列中,将消息复制到接收进程的地址空间。
操作系统中进程/线程模型_第14张图片
该方式隐藏了通信的实现细节,使通信过程对用户透明化,降低了通信程序设计的复杂性和错误率,称为当前应用最为广泛的一类进程间通信的机制。例如,在计算机网络中,消息又称为报文;在微内核操作系统中,微内核和服务器之间也是通过消息传递机制。基于消息传递的通信机制能很好的支持多处理机系统、分布式系统和计算机网络,称为这些领域的主要通信方式。

5.4 套接字

套接字是一个通信标识类型的数据结构,包含了通信目的地址、通信使用的端口号、通信网络的传输层协议、进程所在的网络地址,以及针对客户端或服务器提供的不同系统调用(或函数API)等,是进程通信和网络通信的基本构件。套接字是为客户端/服务器模型而设计的,包含以下两类:

  • 基于文件类型:通信进程都运行在同一机器中,套接字是基于本地文件系统支持的,一个套接字关联到特殊的文件,通信双方对这个特殊文件的读写实现通信,其原理类似于前面提到的管道。
  • 基于网络型:通信双方的进程运行在不同主机的网络环境下,被分配一对套接字,一个属于接收进程(或服务器端),一个属于发送进程(或客户端)。发送进程(或客户端)发送连接请求时,随机申请一个套接字,主机分配一个端口与该套接字绑定;接收进程(或服务端)使用全局公认的套接字和指定端口(如ftp为21,http为80),并通过监听端口等待客户请求。进程之间通过建立连接完成数据传输。

Socket是面向客户/服务器模型而设计的,通过Socket通信机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。网络通信就是主机中的应用进程相互通信,通过应用层协议如HTTP规定数据的传输格式,应用进程通过传输层协议传送数据,而Socket就相当于是传输层的编程接口。应用通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。为了区别不同的应用程序进程和连接,计算机操作系统就可以为应用程序与TCP/IP协议交互提供套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务(通过使用协议端口号解决运输层的复用和分用,使得不同的应用进程都可以通过使用同一个传输层协议传输数据)。

对套接字的理解可以参考博客:套接字

5.5 远程过程调用和远程方法调用

远程过程调用

远程过程(函数)调用RPC(Remote Procedure Call),是一个通信协议,用于通过网络连接的系统。该协议允许运行于一台主机(本地)系统上的进程调用另一台主机(远程)系统上的进程,处理远程过程调用的进程有两个,一个是本地客户进程,另一个是远程服务器进程,这两个进程通常也被称为网络守护进程,主要负责在网络间的消息传递,一般情况下,这两个进程都处于阻塞状态,等待消息。如果涉及到的软件采用面向对象编程,那么远程过程调用也可称为远程方法调用。

为了使远程调用看上去和本地过程调用一样(即实现RPC的透明性),RPC引入一个存根(stub)的概念:在本地客户端,每个能够独立运行的远程过程都拥有一个客户存根(client stubborn),本地进程调用远程过程实际是调用该过程关联的存根;与此类似,在每个远程进程所在的服务端,其对应的实际可执行进程也存在一个服务器存根与其关联。本地客户存根与对应的远程服务器存根也是处于阻塞状态,等待消息。

远程过程调用的主要步骤如下:
(1)本地过程调用者以一般方式调用远程过程在本地关联的客户存根,传递相应的参数,然后将控制权转移给客户存根;
(2)客户存根执行,完成包括过程名和调用参数等信息的消息建立,将控制权转移给本地客户进程;
(3)本地客户进程完成与服务器的消息传递,将消息发送给远程服务器进程;
(4)远程服务进程接到消息后转入执行,并根据其中的远程过程名找到对应的服务器存根,将消息转给该存根;
(5)该服务器存根接到消息后,由阻塞态转入执行态,拆开消息从中取出过程调用的参数,然后以一般方式调用服务器上关联的过程;
(6)在服务器端的远程过程执行完毕后,将结果返回给与之关联的服务器存根;
(7)该服务器存根获得控制权运行,将结果打包为消息,并将控制权转移给远程服务器进程;
(8)远程服务器进程将消息发送给客户端;
(9)本地客户进程接收到消息后,根据其中的过程名将消息存入关联的客户存根,再将控制权转移给客户存根;
(10)客户存根从消息中取出结果,返回给本地调用者进程,并完成控制权的转移。
本地调用者再次获得控制权,并且获得了所需的数据,继续运行。上述步骤的作用在于:将客户过程的本地调用转化为客户存根,再转化为服务器过程的本地调用,对客户与服务器来说,调用时透明的。
操作系统中进程/线程模型_第15张图片
针对于RPC原理,可以参考博客:你应该知道的RPC原理

远程方法调用

RMI(Remote Method Invocation)远程方法调用。能够让在客户端Java虚拟机上的对象像调用本地对象一样调用服务端java 虚拟机中的对象上的方法。为了实现RMI调用的透明性,引入了Stub(存根)和Skeleton(骨架)的概念。RMI框架采用代理来负责客户与远程对象之间通过Socket进行通信的细节。RMI框架为远程对象分别生成了客户端代理和服务器端代理。位于客户端的代理类称为存根(Stub),位于服务器端的代理类称为骨架(Skeleton)。

RMI远程方法调用过程:
(1)客户调用客户端辅助对象stub上的方法
(2)客户端辅助对象stub打包调用信息(变量、方法名),通过网络发送给服务端辅助对象skeleton
(3)服务端辅助对象skeleton将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象
(4)调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象skeleton
(5)服务端辅助对象将结果打包,发送给客户端辅助对象stub
(6)客户端辅助对象将返回值解包,返回给调用者
(7)客户获得返回值
操作系统中进程/线程模型_第16张图片

注意:RMI只用于Java,支持传输对象;RPC是是网络服务协议,与操作系统和语言无关。可以理解为RMI为RPC的在java中的一种实现。

6 总结
典型操作系统中的IPC机制

  • linux:管道、消息队列、共享内存、信号量、信号、套接字。内核同步机制:原子操作、自旋锁、读写锁、信号量、屏障
  • windows:同步对象包括互斥对象、事件对象、信号量对象、临界区对象、互锁变量。通信机制包括套接字、文件映射、管道、命名管道、邮件槽、剪贴板、动态数据交换、对象连接与嵌入、动态链接库、远程过程调用。

原子操作是指该操作不可分割,在执行完之前不会被其他任务或事件中断。
屏障(barrier)是一种同步机制(又称为栅栏、开关)。用于对一组线程进行协调,应用场景为一组线程协同完成一项任务,需要所有线程都到达一个汇合点后再一起向前推进。可以参考java的J.U.C中的CyclicBarrier类的应用。

现将进程之间的同步机制和通信机制总结如下:
进程间的同步机制:互斥量、信号量以及PV操作、管程(条件变量+wait/notify操作)、原子操作和锁机制,在内核也涉及到屏障、原子操作、锁机制等。
进程间的通信机制:共享内存、管道、消息传递、套接字、远程过程调用。
这些同步/通信机制的具体说明在本篇博客都可以找到。

参考资料:
视频资料:操作系统原理(北京大学 )
书籍:《计算机操作系统》 第四版 汤小丹 ;《现代操作系统》陈向群译
博客:CyC2018/CS-Notes/计算机操作系统

你可能感兴趣的:(操作系统)