目录
通行子、MPI_Comm_size和MPI_Comm_rank
Linux编译c文件的基本命令
编译程序
$ mpicc -g -Wall -o mpi_hello mpi_hello.c
启动程序
$ mpicc -n <number of processes> ./mpi_hello
$ mpicc -n 1 ./mpi_hello
$ mpicc -n 4 ./mpi_hello
简单的程序(打印来自进程问候语句的MPI程序)
#include <stdio.h>
#include <string.h>
#include <mpi.h>
const int MAX_STRING = 100;
int main(void){
char greeting[MAX_STRING];
int comm_sz;
int my_rank;
MPI_Init(NULL,NULL);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
if(my_rank != 0){
sprintf(greeting,"Greetings from process %d of %d!",my_rank,comm_sz);
MPI_Send(greeting,strlen(greeting)+1,MPI_CHAR,0,0,MPI_COMM_WORLD);
} else {
printf("Greetings from process %d of %d!\n",my_rank,comm_sz);
for(int q=1;q<comm_sz;q++){
MPI_Recv(greeting,MAX_STRING,MPI_CHAR,q,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
printf("%s\n",greeting);
}
}
MPI_Finalize();
return 0;
}
相关代码注解
MPI程序
头文件中包含了mpi.h的头文件,该头文件包括了MPI函数的原形、宏定义、类型定义等。
MPI定义的标识符都由字符串MPI_开始。下划线后的第一个字母大写,表示函数名和MPI定义的类型。MPI定义的宏和常量的所有字母都是大写的,这样可以区分MPI或者用户程序定义的。
MPI_Init 和 MPI_Finalize
MPI_Init是为了告知MPI系统进行所有必要的初始化设置。
例如,系统可能需要为消息缓冲区分配存储空间,为进程指定进程号等。从经验上看,在程序调用MPI_Init前,不应该调用其他MPI函数。它的语法结构为:
int MPI_Init(
int* argc_p,
char*** argv_p
);
参数argc_p和argv_p是指向参数argc和argv的指针。然而,当程序不使用这些参数时,可以只是将他们设置成NULL。MPI_Init返回一个int型错误码,在大部分情况下,我们忽略这些错误码。
调用MPI_Finalize是为了告知MPI系统MPI已经使用完毕,为MPI分配的任何资源都可以释放了。它的语法结构为:
int MPI_Finalize(void);
通行子、MPI_Comm_size和MPI_Comm_rank
在MPI中,通信子指的是一组可以互相发送消息的进程集合。MPI_Init的其中一个目的,是在用户启动程序时,定义由用户启动的所有进程所组成的通信子。这个通信子称为MPI_COMM_WORLD。关于获取MPI_COMM_WORLD的语法结构为:
int MPI_Comm_size(
MPI_Comm comm,
int* comm_sz_p
);
int MPI_Comm_rank(
MPI_Comm comm,
int* my_rank_p
);
这两个参数中,第一个参数是一个通信子,它所属的类型是MPI为通信子定义的特殊类型:MPI_Comm。MPI_Comm_size函数在它的第二个参数里返回通信子的进程数;MPI_Comm_rank函数在她的第二个参数里返回正在调用进程在通信子中的进程号。在MPI_COMM_WORLD中经常用参数comm_sz表示进程的数量,用参数my_rank来表示进程号。
SPMD程序
让进程按照它们的进程号来匹配程序分支。这一方法称为单程序多数据流。上述程序中的if-else语句使得程序时SPMD的。
通信
上述程序中,除了0号进程外,每个进程都生成了一个要发送给0号进程的消息(sprintf函数和printf函数类似,它不是输出到标准输出stdout,而是输出到一个字符串中)。然后将消息发送给0号进程。另一方面,0号进程只是用printf函数简单的将消息打印出来,然后用一个for循环接收并打印由1、2、...、comm_sz-1号进程发送来的消息。
MPI_Send
每个发送都是由调用MPI_Send函数来实现的,其语法结构为:
int MPI_Send(
void* msg_buf_p,
int msg_size,
MPI_Datatype msg_type,
int dest,
int tag,
MPI_Comm communicator
);
前三个参数定义了消息的内容,剩下的参数定义了消息的目的地。
第一个参数msg_buf_p是一个指向包含信息内容的内存块的指针(可以大致理解为字符串本身)。第二个和第三个参数,msg_size和msg_type,指定了要发送的数据量。在上述程序中,参数msg_size是消息字符串加上C语言中字符串结束符'\0'所占的字符数量。参数msg_type的值是MPI_CHAR。这两个参数一起告知系统整个消息含有strlen(greeting)+1个字符。
因为C语言中的类型(int、char等)不能作为参数传递给函数,所以MPI定义了一个特殊类型:MPI_Datatype,用于参数msg_type。MPI也为这个类型定义了一些常量,如下图:
字符串greeting的大小与msg_size和msg_type所指定的消息的大小并不相同。当然,发送消息的大小必须小于或等于缓冲区的大小。
第四个参数dest指定了要接收消息的进程的进程号。第五个参数tag是个非负int型,用于区分看上去完全一样的消息。比如说,tag为0表示消息是要打印的,为1就表示消息是用于计算的。
MPI_Send的最后一个参数是一个通信子。所有涉及通信的MPI函数都有一个通信子参数。通信子最重要的目的之一是指定通信范围。通信子指的是一组可以互相发送消息的进程的集合。反过来,一个通信子中的进程所发送的消息不能被另一个通信子中的进程所接收。
MPI_Recv
MPI_Recv的前六个参数对应了MPI_Send的前六个参数:
int MPI_Recv(
void* msg_buf_p,
int buf_size,
MPI_Datatype buf_type,
int source,
int tag,
MPI_Comm communicator,
MPI_Status* status_p
);
前三个参数指定了用于接收消息的内存:msg_buf_p指向内存块,buf_size指定了内存中要存储对象的数量,buf_type说明了对象的类型。后面的三个参数用来识别消息。参数source指定了接收的消息应该从哪个进程发送而来,参数tag要与发送消息的参数tag相匹配,参数communicator必须与发送进程所用的通信 子想匹配。在大部分情况下,调用函数并不适用status_p这个参数,就像上述程序那样,赋予其特殊的MPI常量MPI_STATUS_IGNORE就行了。
消息匹配
假定q号进程调用了MPI_Send函数:
MPI_Send(send_buf_p, send_buf_sz, send_type, dest, send_tag, send_comm);
假定r号进程调用了MPI_Recv函数:
MPI_Recv(recv_buf_p, recv_buf_sz, recv_type, src, recv_tag, recv_comm, &status);
则q号进程调用MPI_Send函数所发送的消息可以被r号进程调用MPI_Recv函数接收,如果:
-
recv_comm = send_comm
-
recv_tag = send_tag
-
dest = r
-
src = q
-
如果recv_type = send_type,同时recv_buf_sz >= send_buf_sz,那么由q号进程发送的消息就可以被r号进程成功地接收。
一个进程可以接收多个进程发来的消息,接收进程并不知道其他进程发送消息的顺序。为了避免“顺序等待”这个问题,MPI提供了一个特殊的常量MPI_ANY_SOURCE,可以传递给MPI_Recv。这样,如果0号进程执行下列代码,那么它可以按照进程完成工作的顺序来接收结果:
for(i=1; i<comm_sz; i++){
MPI_Recv(result, result_sz, result_type, MPI_ANY_SOURCE, result_tag, comm, MPI_STATUS_IGNORE);
Process_result(result);
}
类似的,一个进程也有可能接收多条来自另一个进程的有着不同标签的消息,并且接收进程并不知道消息发送的顺序。在这种情况下,MPI提供了特殊的常量MPI_ANY_TAG,可以将它传给MPI_Recv的参数tag。
当使用这些“通配符”参数时,有几点需要强调:
1)只有接收者可以使用这些通配符参数。发送者必须指定一个进程号与一个非负整数标签。因此。MPI使用的是所谓的“推”通信机制,而不是“拉”通信机制。
2)通信子参数没有通配符。发送者和接收者都必须指定通信子。
status_p参数
在上述所说的这些规则中,你会发现接收者可以在不知道以下信息的情况下接收消息:
-
消息中的数据量
-
消息的发送者,或
-
消息的标签
接收者是通过MPI_Recv中最后一个参数(类型为MPI_Status*)来获取的。MPI类型MPI_Status是一个至少有三个成员的结构,MPI_SOURCE、MPI_TAG和MPI_ERROR。
假定程序含有如下定义:
MPI_Status status;
那么,将&status
作为最后一个参数传递给MPI_Recv函数并调用它之后,可以通过检查以下两个成员来确定发送者和标签:
status.MPI_SOURCE
status.MPI_TAG
接收到的数据量不是存储在应用程序可以直接访问到的域中,但用户可以调用MPI_Get_count函数找回这个值。例如,假设对MPI_Recv的调用中,接收缓冲区的类型为recv_type,再次传递&status参数,则以下调用:
MPI_Get_count(&status, recv_type, &count);
会返回count参数接收到的元素数量。一般而言,MPI_Get_count的语法结构为:
int MPI_Get_count(
MPI_Status* status_p,
MPI_Datatype type,
int* count_p
);
注意,count值并不能简单地作为MPI_Status变量的成员直接访问,因为它取决于接收数据的类型。因此,确定该值的过程需要以此计算。如果这个信息不是必须的,那么我们没必要为了得到该值浪费一次计算。