读书笔记-现代操作系统-2进程与线程-2.3进程间通信

本文深入探讨了进程间通信(IPC)的各种方法和技术,包括竞争条件、临界区、互斥解决方案如忙等待、锁变量、Peterson解法、TSL指令、睡眠与唤醒、信号量等,以及管程和消息传递机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2. 进程与线程

2.3 进程间通信

进程间通信(Inter Process Communication)IPC问题:

  1. 一个进程如何把信息传递给另一个
  2. 确保两个或更多进程在关键活动中不会出现交叉
  3. 正确的顺序运行程序
    后两个对于线程也成立,通常来说线程可以通过共享地址空间实现信息传递,当然你也可以采用其他方式比如说消息传递机制等等。

2.3.1 竞争条件和临界区

两个或多个进程读写某些数据,而最后结果取决于进程的精确时序,称为竞争条件
对共享内存进行访问的程序片段称为临界区域或临界区。如果我们能安排使得两个进程不可能同时处于临界区中,就能避免竞争条件。
通常来说有4个条件:

  1. 任何两个进程不能同时处于其临界区
  2. 不应对CPU的速度和数据做任何假设
  3. 临界区外运行的进程不得阻塞其他进程
  4. 不得使得进程无限期等待进入临界区

进程间通信有很多方式根据事先的层数和思路进行分类的话:

  • 忙等待互斥
    1. 屏蔽中断
    2. 锁变量
    3. 严格轮换法
    4. peterson解法
    5. TSL指令
  • 睡眠与唤醒
  • 信号量
  • 互斥量
  • 管程
  • 消息传递
  • 屏障

2.3.2 忙等待互斥

忙等待互斥通常来说都有两个共同的问题:

首先一个是需要不停的检测标志位信号,这不仅浪费CPU,而且还可能导致优先级反正问题(就是优先级较高的CPU不停查询标志位,但是没有办法切入到低优先级来翻转标志位)。

另一个显著的问题通常都没有办法很好的解决线程切换问题,因为线程通常先检查标识位,然后设置标志位,如果在这两步之间如果有线程切换则导致信号丢失。

  1. 中断屏蔽
    使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在离开之前再打开所有中断。
    • 把屏蔽中断的权力交给用户进程是不明智的
    • 屏蔽仅对当前CPU有效,多CPU系统无效。
    • 当就绪进程队列之类的数据状态不一致时发生中断,则将导致竞争条件(不是很理解)
  2. 锁变量
    这种方案并不可行,它设置一个变量,当线程快进入临界区的时候将其设置为1,离开时设置为0。
    • 如果在检测为0-准备设置为1的过程中进行线程切换,并被另一线程切换成1,则两个线程都可访问变量
  3. 严格轮换法
    设置一个turn,线程0只有在turn为0时,才可以进入临界区,在离开临界区内turn改为1。线程1只有在turn为1时,才可以进入临界区,在离开临界区内turn改为0。
    • 问题在于必须进行忙检测。
    • 而且违反了之前的第三个条件。
  4. Peterson解法
    结合警告变量和锁变量的方式,需要两个变量,一个变量turn用来确定当前的线程是否进入临界区,一个变量用来确定进入临界区的线程序号。
    现在假设线程0将进入临界区,并检测到turn为0,将其变化为1,在这期间发送线程切换线程1,线程1也检测到turn为0,并将其变化为1,但是将进入临界区的序号变为1,应该检测这样的标志位(turn是否为1,且将进入临界区的线程号为1)则进入线程1,而线程0将不断循环直到线程1退出临界区
  5. TSL
    是一种硬件支持的方案,称为测试并加锁。用来检测是否有程序进入临界区,如果有则锁住内存总线,以禁止其他CPU在本指令结束之前访问内存。这里需要解释一下,锁足内存和屏蔽中断不同,屏蔽中断对多cpu无效,但锁足内存对所有cpu都有效。

2.3.3 睡眠与唤醒

忙等待都需要CPU消耗,和优先级反转问题。这里介绍一种新的进程间通信原语:sleep和wakeup
sleep:一个将引起进程阻塞的系统调用,即被挂起,直到另一个进程将其唤醒
wakeup:调用有一个参数,即被唤醒的进程。

当然也可以让sleep和wakeup各有一个用于匹配sleep和wakeup的内存地址。

  • 生产者消费者问题-有界缓冲器
    两个进程共享一个公共的固定大小的缓冲区,其中一个是生产者,将信息放入缓冲去,另一个是消费者,从缓冲区中取出信息。

当然如果直接使用sleep和wakeup同样会出现信号丢失的问题。因此这里引入了信号量的概念

  • 信号量
    使用一个整型变量来累计唤醒次数,供以后使用。信号量的取值可以为0(表示没有保存下来的唤醒操作),也可以为正值(表示有一个或多个唤醒操作)。
    这里设立两种操作

    • down:一般化的sleep,对信号量执行down操作,则是检测其值是否大于0。若该值大于0,则将其值减一,并继续,若该值为0,则进程将睡眠,而且此时若down操作并未结束。(检查数值、修改变量、可能发生的睡眠操作均是一个单一的、不可分割的原子操作),包装一个信号量操作开始,则在开始该操作完成或阻塞之前,其他进程均不允许访问该信号量。
    • up:操作对信号量的值增1,如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则由系统选择其中的一个并允许该进程完成它的down操作。如果有一个进程在其上睡眠的信号量执行一次up操作后,该信号量的值仍旧是0,但在其上睡眠的进程却少了一个。不会有进程在执行了up操作后阻塞。
    • 信号量的另一种用途用于实现同步。信号量可以用来保证某种时间的顺序发送或者不发生。
    • 使用信号量解决生产者消费者问题
      使用三个信号量,一个称为full-用来记录充满的缓存槽数目,初值为0,一个称为empty-记录空的缓冲槽总数,初值为空槽值。一个称为mutex,用来确保生产者和消费者不会同时访问缓冲区,初值为1,称为二元信号量。包装同时只有一个进程可以进入临界区。
  • 互斥量
    互斥量是信号量的简化版本。互斥量是可以出于两态之一的变量:解锁和加锁。
    当一个线程需要访问临界区的时候,它调用锁,如当前互斥量是解锁的,则调用成功进入,否则则阻塞进入睡眠状态,等待另一个线程调用解锁后才能进入。

    • 互斥量与锁变量的区别
      其实主要是在忙等待方面,互斥量在检测到加锁状态后自动放弃cpu。这样就不会导致忙等待,下次线程调用时在对他的锁进行测试。

2.3.4 共享信息问题

进程通过两种方式来共享线程:
1. 有些共享数据放到内核中,并且只能通过系统调用来访问。
2. 现代操作系统为进程提供了一些共享空间。
3. 最终还有共享文件的方式。只是比较慢

2.3.5 管程

管程是一个由过程、变量及数据结构组成的一个集合,它们组成一个特殊的模块或软件包。
管程有一个重要的特性,即任一时刻管程中只能有一个活跃进程。这一特性使得管程能有效完成互斥。
引入条件变量及相关的两个操作(wait和signal)来阻塞,它会在某个条件变量上执行wait操作。执行该操作的线程自身阻塞,并且还将另一个以前在管程之外的进程调入管程。
为了避免在两个活跃进程同时在管程之中。我们需要一个条件说明如果signal之后会怎么办?通常来说有3中方法

  • 让唤醒的进程运行,而挂起另一个进程
  • 建议执行signal的进程必须立即退出管程,signal作为管程的最后一条语句。

java实现了管程。具体请参考之前一篇java网络编程的实现。
synchronized实现了管程
wait和notiy分别等同于这里的sleep和wakeup。
这里放一个java管程实现消费者和生成者的实例。

package os.thread;

import java.util.Random;



public class ProducerConsumer {
    static final int N = 100;
    static producer p = new producer();
    static consumer c = new consumer();
    static our_monitor mon = new our_monitor();


    public static void main(String[] args) {
        p.start();
        c.start();
    }

    static class producer extends Thread{
        public void run(){
            int item;
            while(true){
                item = producer_item();
                mon.insert(item);
            }
        }
        private int producer_item() {
            Random random = new Random();
            return random.nextInt(100);
        }
    }

    static class consumer extends Thread{
        public void run(){
            int item;
            while(true){
                item = mon.remove();
                consume_item(item);
            }
        }

        private void consume_item(int item) {
            System.out.println(item);

        }
    }

    static class our_monitor{
        private int buffer[] = new int[N];
        private int count = 0,lo=0,hi=0;

        public synchronized void insert(int val){
            if(count == N) go_to_sleep();
            buffer[hi] = val;
            hi = (hi+1)%N;
            count = count +1;
            if(count==1) notify();
        }

        public synchronized int remove(){
            int val;
            if(count == 0) go_to_sleep();
            val = buffer[lo];
            lo = (lo+1)%N;
            count = count -1;
            if(count == N-1) notify();
            return val;
        }

        private void go_to_sleep() {
            try{
                wait();
            }catch(InterruptedException exc){
                System.out.println(exc.toString());
            }

        }

    }
}

2.4 消息传递机制

消息传递机制和之前的两大类方法又有本质的不同,之前的方法都是通过公共内存的一个或多个cpu的互斥问题,但是如果是分布式系统,每个系统都有自己的私有内存则共享内存的方式不可行了。

  1. 设计要点:

    • 为了防止消息丢失,接受方马上回送一条特殊的却消息、
    • 为了防止消息本身被正确的接受,如何区分新的消息和一条重发的老消息是非常重要的,采用消息中嵌入一条序号来实现
    • 还需要解决进程命名的问题,包装指定的进程没有二义性。
  2. 屏障
    如果是用于进程组的消息传递,则需要在每个阶段的结尾安置屏障来实现这种行为。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值