2. 进程与线程
2.3 进程间通信
进程间通信(Inter Process Communication)IPC问题:
- 一个进程如何把信息传递给另一个
- 确保两个或更多进程在关键活动中不会出现交叉
- 正确的顺序运行程序
后两个对于线程也成立,通常来说线程可以通过共享地址空间实现信息传递,当然你也可以采用其他方式比如说消息传递机制等等。
2.3.1 竞争条件和临界区
两个或多个进程读写某些数据,而最后结果取决于进程的精确时序,称为竞争条件
对共享内存进行访问的程序片段称为临界区域或临界区。如果我们能安排使得两个进程不可能同时处于临界区中,就能避免竞争条件。
通常来说有4个条件:
- 任何两个进程不能同时处于其临界区
- 不应对CPU的速度和数据做任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使得进程无限期等待进入临界区
进程间通信有很多方式根据事先的层数和思路进行分类的话:
- 忙等待互斥
- 屏蔽中断
- 锁变量
- 严格轮换法
- peterson解法
- TSL指令
- 睡眠与唤醒
- 信号量
- 互斥量
- 管程
- 消息传递
- 屏障
2.3.2 忙等待互斥
忙等待互斥通常来说都有两个共同的问题:
首先一个是需要不停的检测标志位信号,这不仅浪费CPU,而且还可能导致优先级反正问题(就是优先级较高的CPU不停查询标志位,但是没有办法切入到低优先级来翻转标志位)。
另一个显著的问题通常都没有办法很好的解决线程切换问题,因为线程通常先检查标识位,然后设置标志位,如果在这两步之间如果有线程切换则导致信号丢失。
- 中断屏蔽
使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在离开之前再打开所有中断。
- 把屏蔽中断的权力交给用户进程是不明智的
- 屏蔽仅对当前CPU有效,多CPU系统无效。
- 当就绪进程队列之类的数据状态不一致时发生中断,则将导致竞争条件(不是很理解)
- 锁变量
这种方案并不可行,它设置一个变量,当线程快进入临界区的时候将其设置为1,离开时设置为0。
- 如果在检测为0-准备设置为1的过程中进行线程切换,并被另一线程切换成1,则两个线程都可访问变量
- 严格轮换法
设置一个turn,线程0只有在turn为0时,才可以进入临界区,在离开临界区内turn改为1。线程1只有在turn为1时,才可以进入临界区,在离开临界区内turn改为0。
- 问题在于必须进行忙检测。
- 而且违反了之前的第三个条件。
- Peterson解法
结合警告变量和锁变量的方式,需要两个变量,一个变量turn用来确定当前的线程是否进入临界区,一个变量用来确定进入临界区的线程序号。
现在假设线程0将进入临界区,并检测到turn为0,将其变化为1,在这期间发送线程切换线程1,线程1也检测到turn为0,并将其变化为1,但是将进入临界区的序号变为1,应该检测这样的标志位(turn是否为1,且将进入临界区的线程号为1)则进入线程1,而线程0将不断循环直到线程1退出临界区 - 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的互斥问题,但是如果是分布式系统,每个系统都有自己的私有内存则共享内存的方式不可行了。
设计要点:
- 为了防止消息丢失,接受方马上回送一条特殊的却消息、
- 为了防止消息本身被正确的接受,如何区分新的消息和一条重发的老消息是非常重要的,采用消息中嵌入一条序号来实现
- 还需要解决进程命名的问题,包装指定的进程没有二义性。
屏障
如果是用于进程组的消息传递,则需要在每个阶段的结尾安置屏障来实现这种行为。