进程间通信

Operating system interprocess communication

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

  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;
}
Collapse

运行如图:

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

实现进程间通信(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
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!
Powered By Valine
v1.4.14