本篇通过一个小例子来总结下阻塞通信中的标准通信模式

阻塞通信

阻塞通信是指消息发送发的send调用需要接收方的recv调用的配合才可完成。即在发送的消息信封和数据被安全的“保存”起来之前,send函数的调用不会返回。
所谓的安全保存,就是指send调用可再次被执行而不会对上一次发送的数据带来任何破坏。
不同模式下所要求的“配合”程度也不同,尤其有的MPI环境下还要结合运行时环境本身提供的缓存机制综合考虑实际效果。

标准通信模式

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "mpi.h"
#include <stdio.h>

#define BUF_SIZE 10

int main(int argc, char ** argv)
{
int myid, nprocs, other;

// Data sent and received.
int sb[BUF_SIZE], rb[BUF_SIZE];

MPI_Status status;

// Initialize MPI env.
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid); // Get process id.
MPI_Comm_size(MPI_COMM_WORLD, &nprocs); // Get total process number.

// Fill the data array for different process.
for (int i = 0; i < BUF_SIZE; ++i)
{
sb[i] = myid + i;
}

// Set another process number.
if (myid == 0)
{
other = 1;
}

else if (myid == 1)
{
other = 0;
}

// Send and receive data in proc 0.
if (myid == 0)
{
fprintf(stderr, "process %d of %d sending data...\n", myid, nprocs);
MPI_Send(sb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD);

fprintf(stderr, "process %d of %d receiving data...\n", myid, nprocs);
MPI_Recv(rb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD, &status);
}

// Send and receive data in proc 1.
if (myid == 1)
{
fprintf(stderr, "process %d of %d sending data...\n", myid, nprocs);
MPI_Send(sb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD);

fprintf(stderr, "process %d of %d receiving data...\n", myid, nprocs);
MPI_Recv(rb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD, &status);
}

// Print something.
fprintf(stderr, "Hello world, process %d of %d \n", myid, nprocs);

// Finalize MPI env.
MPI_Finalize();

return 0;
}

使用mpicc脚本编译,执行:

1
2
3
4
5
6
7
8
[zjshao@master ch02]$ mpicc -std=c99 std_send_recv.c -o std_send_recv.x
[zjshao@master ch02]$ mpiexec -n 2 std_send_recv.x
process 1 of 2 sending data...
process 1 of 2 receiving data...
process 0 of 2 sending data...
process 0 of 2 receiving data...
Hello world, process 1 of 2
Hello world, process 0 of 2

从输出的可以看出,进程1的发送和接受操作要早于进程0,因此进程0发送数据时,进程1可立即进行接收,立即实施传输操作。而进程1在调用recv的时候才开始进行消息传输,之前的进程1的数据存在MPI的缓冲区中。

缓冲区

我觉得到这里可以总结下缓冲区了。
MPI环境定义了3中缓冲区:应用缓冲区、系统缓冲区、用户向系统注册的缓冲区(通信缓冲区)

应用缓冲区

就是在程序中开辟的内存空间的地址,也就上面代码中的sbrb分别代表发送数据和接收数据的缓冲区地址,本质上呢就是一个数组的首地址。

系统缓冲区

是MPI环境为通信所准备的存储空间。类似于一个数据中转站,应用缓冲区的数据在系统缓冲区中复制入和出,即发送数据的时候从系统缓冲区将数据复制到系统缓冲区,接收数据的时候从系统缓冲区复制到应用缓冲区。

用户向系统注册的缓冲区

是指用户使用某些API(如MPI_Bsend)时,在程序中显式申请的存储空间,然后注册到MPI环境中共通讯所用。

接收和发送信息的几种情况测试

只有接收没有发送

将上面的程序进程0的发送消息的部分注释掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Send and receive data in proc 0.
if (myid == 0)
{
//fprintf(stderr, "process %d of %d sending data...\n", myid, nprocs);
//MPI_Send(sb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD);

fprintf(stderr, "process %d of %d receiving data...\n", myid, nprocs);
MPI_Recv(rb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD, &status);
}

// Send and receive data in proc 1.
if (myid == 1)
{
fprintf(stderr, "process %d of %d sending data...\n", myid, nprocs);
MPI_Send(sb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD);

fprintf(stderr, "process %d of %d receiving data...\n", myid, nprocs);
MPI_Recv(rb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD, &status);
}

则输出的结果是:
1
2
3
4
5
6
7
8
[zjshao@master ch02]$ mpiexec -n 2 std_send_recv_2.x
process 1 of 2 sending data...
process 0 of 2 receiving data...
process 1 of 2 receiving data...
Hello world, process 0 of 2
[mpiexec@master.cluster] Sending Ctrl-C to processes as requested
[mpiexec@master.cluster] Press Ctrl-C again to force abort
[zjshao@master ch02]$

程序卡在了进程1接收来自进程0消息的地方无法返回,而进程0这个时候已经将程序执行完毕,输出了HelloWorld。最终只能向进程发送CTRL C来终止MPI进程。

只有发动没有接收

将上面的程序进程1的接收消息的部分注释掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Send and receive data in proc 0.
if (myid == 0)
{
fprintf(stderr, "process %d of %d sending data...\n", myid, nprocs);
MPI_Send(sb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD);

fprintf(stderr, "process %d of %d receiving data...\n", myid, nprocs);
MPI_Recv(rb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD, &status);
}

// Send and receive data in proc 1.
if (myid == 1)
{
fprintf(stderr, "process %d of %d sending data...\n", myid, nprocs);
MPI_Send(sb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD);

//fprintf(stderr, "process %d of %d receiving data...\n", myid, nprocs);
//MPI_Recv(rb, BUF_SIZE, MPI_INT, other, 1, MPI_COMM_WORLD, &status);
}

则输出的结果是:
1
2
3
4
5
6
7
[zjshao@master ch02]$ mpiexec -n 2 std_send_recv_2.x
process 1 of 2 sending data...
process 0 of 2 sending data...
Hello world, process 1 of 2
process 0 of 2 receiving data...
Hello world, process 0 of 2
[zjshao@master ch02]$

这个时候两个进程都自行结束了,这个应该就想到于发送动作遭遇接收动作,这个时候消息大小小于MPI的缓冲区大小,会将数据传输到MPI缓冲区中进行缓存,然后函数立即返回,这样进程就不会开在发送数据部分无法将程序的拥有权交给主程序。

但如果我将应用缓冲区大小增大,使其大于系统缓冲区大小,会出现什么情况呢?

1
#define BUF_SIZE 100000

修改好编译执行:
1
2
3
4
5
6
7
[zjshao@master ch02]$ mpicc -std=c99 std_send_recv.c -o std_send_recv_2.x
[zjshao@master ch02]$ mpiexec -host node01 -n 2 std_send_recv_2.x
process 1 of 2 sending data...
process 0 of 2 sending data...
[mpiexec@master.cluster] Sending Ctrl-C to processes as requested
[mpiexec@master.cluster] Press Ctrl-C again to force abort
[zjshao@master ch02]$

果然,如果发送的数据大于系统缓冲区大小,则进程要等待接收方进程接收动作启动后才开始数据传输。但是在这里,进程1先发送了数据,数据不能进入系统缓冲区,要等待进程0的接收动作,这时候进程0也发送了数据,同理,要等待进程1的接收动作,但是进程0要等进程1,进程1又要等进程0,这个时候就卡死了,也就是发生了死锁。

以上就是我对阻塞通信中的标准通信模式的总结了。

Comments