进程间通信

实现进程/线程间通信的方法有:

  1. 进程间通信方法有:文件映射、共享内存、匿名管道、命名管道、邮件槽、剪切板、动态数据交换、对象连接与嵌入、动态连接库、远程过程调用等
  2. 线程同步的方法有:事件、临界区、互斥量、信号量

其实通过管道共享内存等等都是可以实现进程间同步的,比如使用管道就非常方便:

1
2
// 创建一个命名管道
mkfifo fifo

然后具有以下代码实现一个简单的进程间通信的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// writefifo.cc
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
using namespace std;

int main(int argc,char* argv[]){
int len=0;
char buf[100];
memset(buf,0,sizeof(buf));
int fd=open("fifo",O_WRONLY);
while(1){
cin>>buf;
if(buf[0]=='0'){
break;
}
write(fd, buf,sizeof(buf));
}
close(fd);
return 0;
}
//readfifo
// 碍于篇幅readfifo的头文件忽略,但是编译时不可忽略。
int main(int argc,char* argv[])
{
int len=0;
char buf[100];
memset(buf,0,sizeof(buf));
int fd=open("fifo",O_RDONLY);
while((len=read(fd,buf,sizeof(buf))>0)){
cout<<buf<<endl;
}
close(fd);
return 0;
}

运行如图:

下面我们从操作系统的角度来描述一下实现进程间同步的要求。

实现进程间通信(Inter Process Communication,IPC)具有三个问题。

  • 一个进程如何把信息传递给另一个进程
  • 确保两个进程或更多的进城之间在关键活动中不会出现交叉。比如飞机订票系统上两个不同的进程为不同的客户争夺最后一个座位。
  • 正确的顺序,比如使用进程A产生数据而进程B需要使用这些数据,所以在A在产生数据完成之前,进程B必须等待(阻塞)

竞争条件

在一些操作系统中,协作的进程可能共享一些彼此都能读写的公用存储区域。

两个或者多个进程读写某些共享数据,而最后的结果取决于进程运行时的精确时间,称为竞态条件(race condition)

举个栗子:
进程A和进程B之间共享一个变量X,有可能发生的情况是,进程A向X中赋值了一个变量,但是恰好(Murphy定理,有可能的就一定会发生)此时操作系统的调度程序将其中断,进程A进入就绪态,进程B进入运行态。然后选择并运行了进程B,进程B也向X中存入了一个变量, 并在下次调度程序选择进程A之前都使用X,当调度程序再度运行进程A时,A并不知道X已经被进程B修改过了,所以会造成未定义的错误。

原因在于,在进程A使用X完毕之前进程B就对X进行了操作,所以我们会得到错误的结果,这就为竞态条件。

如何避免竞态条件?

实际上在凡是涉及共享内存、共享文件以及共享任何资源的情况都有可能引发竞态条件。要避免竞态条件的发生关键是要找出某种途径来阻止多个进程同时读写共享的数据。

我们需要的是**互斥(mutual exclusion)**,即以某种手段确保当一个进程在使用一个共享的数据时,其他的文件不能进行同样的操作。

一个进程的一部分时间做内部计算或另外一个不会引发竞态条件的操作。在某些进程可能需要访问共享内存或共享文件或执行一些会导致竞态条件的操作。

对共享内存进行访问的操作程序片段(代码)称为临界区(criticalsection)

正像上面所说的原因,引发竞态条件的原因就是有两个进程同时处于临界区,所以避免引发竞态条件就需要我们适当的安排使两个进程不能同时处于临界区。

对于一个好的避免进程同时处于临界区的四个条件:

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

临界区互斥的几种方案

单处理系统

在单处理系统中实现互斥最简单的方法就是使每个进程在刚刚进入临界区之后立即屏蔽所有中断,在离开临界区之前再打开中断。这样就可以保证在当前进入临界区的程序完成之前系统不会调度其他的进程,从而保证进入临界区的只有一个进程。

但是把屏蔽中断的权利交给用户进程是一个非常不好的选择,如果一个用户进程关闭了中断而忘记了打开,有可能会造成系统的终止(单处理系统)。

如果是多处理操作系统,则屏蔽终端仅仅对执行关闭的那个CPU有效,其他的CPU仍继续运行,并意味着其上运行的进程也可以进入临界区。

锁变量

另一种防止两个进程同时进入临界区软件解决方案是设定一个共享变量——锁变量,其初始值为0,当一个进程想要进入临界区时会先检验这把锁是否锁上(为1),如果该锁的初始值为0,则该进程将其设置为1,并进入临界区。若这把锁的值已经为1则该进程等待知道锁值变为0再进入临界区。

注意:这种方案看起来很好但是也会引发竞态条件。

如下图:

严格轮换法

定义一个变量turn初始值为0,用于记录轮到那个进程进入临界区,并检查或更新共享内存。开始时进程0检查turn发现其值为0时于是进入临界区。进程1也发现其值为0,所以在一个循环中不停的测试turn,看其值何时变为1.

连续测试一个变量直到某个值出现为止称为忙等待(busy waiting),这种方式非常浪费CPU时间,通常应该避免。

1
2
3
4
5
6
7
8
// 进程0
while(true){
while(turn!=0){
critical_region();
}
trun=1;
noncritical_region();
}
1
2
3
4
5
6
7
8
// 进程1
while(true){
while(turn!=1){
critical_region();
}
trun=1;
noncritical_region();
}

只有在有理由认为是非常短的情形下,才使用忙等待。

用于忙等待的锁称为自旋锁(spin lock)

但是这种解法也会出现几个问题:

  1. 当进程0进入临界区之后,很快退出了临界区并将turn设为1,此时进程0和进程1都在临界区之外执行,但是,如果现在进程0突然结束了临界区外的操作回到循环的开始,此时进程0是无法进入临界区的(因为turn已经被进程0修改为1),但是此时进程1在进行非临界区的操作,并没有进入临界区,所以turn并不会修改为0,所以进程0只有继续循环,直到进程1将turn设置为0(进程1进入并退出临界区)
  2. 由1可得,在一个进程比另一个慢了很多的情况下,轮流进入临界区不是一个好办法
  3. 上面的问题造成的后果为:进程0被临界区外运行的进程阻塞了

虽然使用严格轮换法能够实现避免所有的竞态条件,但是它违反了上面一个好的避免进程同时处于临界区的四个条件之条件三。

Peterson算法

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:进程间通信
文章作者:查利鹏
发布时间:2016年05月17日 19时48分
本文字数:本文一共有2.1k字
原始链接:https://imzlp.com/posts/58483/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!