4.17кластерные системы.Отдельным подклассом систем с распределенной памятью являются кластерные системы, которые представляют собой некоторый аналог массивно-параллельных систем, в котором в качестве ВУ выступают обычные рабочие станции общего назначения, причем иногда узлы кластера могут даже одновременно использоваться в качестве пользовательских рабочих станций. Кластер, объединяющий компьютеры разной мощности или разной архитектуры, называют гетерогенным (неоднородным). Для связи узлов используется одна из стандартных сетевых технологий, например, Fast Ethernet. Главными преимуществами кластерных систем, благодаря которым они приобретают все большую популярность, являются их относительная дешевизна, возможность масштабирования и возможность использования при построении кластера тех вычислительных мощностей, которые уже имеются в распоряжении той или иной организации. При программировании для кластерных систем, как и для других систем с распределенной памятью, используется модель передачи сообщений. Модель программирования MPI. Как мы видим, при написании программ для параллельных архитектур выбор модели программирования сильно зависит от конкретной архитектуры, на которой предполагается выполнять программу. Например, если целевой архитектурой является система с общей памятью, то для обмена данными между процессами целесообразно использовать механизм разделяемой памяти, если же программа пишется для работы на системе с распределенной памятью, то необходимо организовывать обмен с помощью сообщений. Таким образом, если программист имеет возможность доступа к системе с общей памятью и с распределенной памятью, ему придется создавать отдельную версии своей программы для работы на каждой из этих систем, осваивая при этом различные модели программирования. В то же время, хотелось бы иметь некоторый единый механизм взаимодействия, который был бы реализован, и притом эффективно, для большинства или хотя бы для многих конкретных параллельных систем. В таком случае для перенесения программы с одной архитектуры на другую было бы достаточно простой перекомпиляции, а программист, освоивший данное средство, получил бы возможность создавать эффективные программы для широкого класса параллельных архитектур. Одним из таких широко распространенных средств параллельного программирования является MPI. MPI представляет собой стандарт, описывающий некоторое множество функций для обмена сообщениями между параллельными процессами. Существует множество реализаций MPI для различных параллельных архитектур, как с распределенной, так и с общей памятью. Как правило, эти реализации оформлены в виде набора библиотечных функций, которые можно использовать при программировании на языках Фортран и Си. В модели программирования MPI приложение представляет собой совокупность процессов или нитей (иначе называемых ветвями), общающихся друг с другом путем передачи сообщений. При этом для организации обмена не является важным, будут ли процессы исполняться на одном процессоре или вычислительном узле или на разных – механизм обмена данными в обоих случаях одинаков. Во всем, что не касается передачи и приема сообщений, ветви являются независимыми и изолированными друг от друга. Отметим, что ветви приложения могут обмениваться сообщениями в среде MPI только между собой, но не с процессами других приложений, исполняющимися в той же вычислительной системе. Помимо функций для обмена сообщениями, MPI предоставляет возможности для взаимной синхронизации процессов и для решения ряда других вспомогательных задач. Количество ветвей в данном приложении задается в момент его запуска, т.е. не существует возможности порождать ветви динамически во время исполнения приложения[18]. Запуск MPI-приложения осуществляется с помощью специальной программы (чаще всего она называется mpirun), которой обычно указывается количество ветвей, которые необходимо породить, имя исполняемого файла, а также входные параметры приложения. При написании программы с использованием MPI ее исходный текст должен содержать код для всех ветвей сразу, однако во время исполнения у каждой ветви имеется возможность определить свой собственный порядковый номер и общее количество ветвей и в зависимости от этого исполнять ту или иную часть алгоритма (данный подход в чем-то аналогичен использованию системного вызова fork()) Функции общего назначения. Общая структура программы. Все без исключения функции Си-библиотеки MPI возвращают целое значение, описывающее код завершения операции. Существует специальная константа MPI_SUCCESS, соответствующая успешному завершению функции. В случае неудачного завершения возвращаемое значение будет отлично от MPI_SUCCESS, и равно одному из кодов ошибок, определенных в MPI и описывающих различные исключительные ситуации. Все типы данных, константы и функции, относящиеся к Си-библиотеке MPI, описаны в заголовочном файле <mpi.h>. Коммуникаторы и группы. Важными понятиями MPI являются группы ветвей и коммуникаторы. Группа ветвей представляет собой упорядоченное множество ветвей. Каждой ветви в группе назначается уникальный порядковый номер – целое число в диапазоне [0, N-1], где N – количество ветвей в группе. При запуске приложения все его порожденные ветви образуют начальную группу. Библиотека MPI предоставляет функции, позволяющие создавать новые группы на основе существующих путем их разбиения, объединения, исключения ветвей из групп и т.п. операций. С каждой группой ветвей MPI связан так называемый «коммуникационный контекст», или «коммуникационное поле», задающее множество всех возможных участников операций обмена данными и уникальным образом описывающее каждого участника. Кроме того, коммуникационный контекст может использоваться для хранения некоторых общих для всех ветвей данной группы данных. Для описания коммуникационного контекста в MPI служат объекты специального типа – коммуникаторы. Коммуникатор, или описатель коммуникационного контекста, присутствует в качестве параметра во всех вызовах MPI, связанных с обменом данными. Подчеркнем, что любая операция обмена данными может быть осуществлена только внутри определенного коммуникационного контекста, поэтому, например, сообщение, отправленное с помощью какого-либо коммуникатора, может быть получено только с помощью этого же самого коммуникатора. Коммуникаторы MPI описываются специальным типом данных MPI_Comm. При запуске приложения сразу после инициализации среды выполнения MPI каждой ветви становятся доступны два предопределенных коммуникатора, обозначаемых константами MPI_COMM_WORLD и MPI_COMM_SELF. MPI_COMM_WORLD представляет собой коммуникатор, описывающий коммуникационный контекст начальной группы ветвей, т.е. всех ветвей, порожденных при запуске приложения. MPI_COMM_SELF – это коммуникатор, включающий одну-единственную ветвь – текущую, и соответственно, для каждой ветви он будет иметь свое значение. Каждая ветвь приложения имеет возможность узнать общее количество ветвей в своей группе и свой собственный уникальный номер в ней. Для этого служат функции MPI_Comm_size() и MPI_Comm_rank(): #include <mpi.h> int MPI_Comm_size(MPI_Comm comm, int *size); int MPI_Comm_rank(MPI_Comm comm, int *rank); В первом параметре каждой из этих функций передается коммуникатор, описывающий коммуникационный контекст интересующей группы ветвей, во втором – указатель на целочисленную переменную, в которую будет записан результат: для функции MPI_Comm_size() – количество ветвей в группе; для функции MPI_Comm_rank() – уникальный номер текущей ветви в группе. Отметим, что в MPI предусмотрена также возможность обмена сообщениями между ветвями, принадлежащими разным группам. Для этого существует возможность создать так называемый интер-коммуникатор, объединяющий коммуникационные контексты, заданные двумя различными коммуникаторами, каждый из которых описывает коммуникационный контекст некоторой группы. После создания такого коммуникатора его можно использовать для организации обмена данными между ветвями этих групп. Обрамляющие функции. Инициализация и завершение. Самым первым вызовом MPI в любой программе, использующей эту библиотеку, должен быть вызов функции MPI_Init() – инициализация среды выполнения MPI. #include <mpi.h> int MPI_Init(int *argc, char ***argv); Этой функции передаются в качестве параметров указатели на параметры, полученные функцией main(), описывающие аргументы командной строки. Смысл этого заключается в том, что при загрузке MPI-приложения mpirun может добавлять к его аргументам командной строки некоторые дополнительные параметры, необходимые для инициализации среды выполнения MPI. В этом случае вызов MPI_Init() также изменяет массив аргументов командной строки и их количество, удаляя эти дополнительные аргументы. Еще раз отметим, что функция MPI_Init() должна быть вызвана до любой другой функции MPI, кроме того, в любой программе она должна вызываться не более одного раза. По завершении работы с MPI в каждой ветви необходимо вызвать функцию закрытия библиотеки – MPI_Finalize(): #include <mpi.h> int MPI_Finalise(void); После вызова этой функции невозможен вызов ни одной функции библиотеки MPI (в том числе и повторный вызов MPI_Init()) Для аварийного завершения работы с библиотекой MPI служит функция MPI_Abort(). #include <mpi.h> int MPI_Abort(MPI_Comm comm, int errorcode); Эта функция немедленно завершает все ветви приложения, входящие в коммуникационный контекст, который описывает коммуникатор comm. Код завершения приложения передается в параметре errorcode. Отметим, что MPI_Abort() вызывает завершения всех ветвей данного коммуникатора даже в том случае, если она была вызвана только в какой-либо одной ветви. Синхронизация: барьеры. Для непосредственной синхронизации ветвей в MPI предусмотрена одна специальная функция – MPI_Barrier(). #include <mpi.h> int MPI_Barrier(MPI_Comm comm); С помощью этой функции все ветви, входящие в определенный коммуникационный контекст, могут осуществить так называемую барьерную синхронизацию. Суть ее в следующем: в программе выделяется некоторая точка синхронизации, в которой каждая из ветвей вызывает MPI_Barrier(). Эта функция блокирует вызвавшую ее ветвь до тех пор, пока все остальные ветви из данного коммуникационного контекста также не вызовут MPI_Barrier(). После этого все ветви одновременно разблокируются. Таким образом, возврат управления из MPI_Barrier() осуществляется во всех ветвях одновременно. Барьерная синхронизация используется обычно в тех случаях, когда необходимо гарантировать завершение работы над некоторой подзадачей во всех ветвях перед переходом к выполнению следующей подзадачи. Важно понимать, что функция MPI_Barrier(), как и другие коллективные функции, которые мы подробно рассмотрим ниже, должна быть обязательно вызвана во всех ветвях, входящих в данный коммуникационный контекст. Из семантики функции MPI_Barrier() следует, что если хотя бы в одной ветви из заданного коммуникационного контекста MPI_Barrier() вызвана не будет, это приведет к «зависанию» всех ветвей, вызвавших MPI_Barrier(). Использование барьерной синхронизации. В данном примере иллюстрируется общая схема построения программы с использованием библиотеки MPI, а также использование функций общего назначения и барьерной синхронизации. На экран выводится общее количество ветвей в приложении, затем каждая ветвь выводит свой уникальный номер. После этого ветвь с номером 0 печатает на экран аргументы командной строки. Барьерная синхронизация необходима для того, чтобы сообщения о порядковых номерах ветвей не смешивались с сообщением об общем количестве ветвей и с выводом аргументов командной строки. #include <mpi.h> #include <stdio.h>
int main(int argc, char **argv) { int size, rank, i;
MPI_Init(&argc, &argv); /* Инициализируем библиотеку */ MPI_Comm_size(MPI_COMM_WORLD, &size); /* Узнаем количество задач в запущенном приложении... */ MPI_Comm_rank (MPI_COMM_WORLD, &rank); /* ...и свой собственный номер: от 0 до (size-1) */
/* задача с номером 0 сообщает пользователю размер группы,коммуникационный контекст которой описывает коммуникатор MPI_COMM_WORLD, т.е. число ветвей в приложении */ if (rank == 0) { printf("Total processes count = \%d ", size ); } /* Осуществляется барьерная синхронизация */ MPI_Barrier(MPI_COMM_WORLD);
/* Теперь каждая задача выводит на экран свой номер */ printf("Hello! My rank in MPI_COMM_WORLD = \%d ", rank);
/* Осуществляется барьерная синхронизация */ MPI_Barrier(MPI_COMM_WORLD); /* затем ветвь c номером 0 печатает аргументы командной строки. */ if (rank == 0) { printf(“Command line of process 0: "); for(i = 0; i < argc; i++) { printf("\%d: \"\%s\" ", i, argv[i]); } } /* Все задачи завершают выполнение */ MPI_Finalize(); return 0; } Прием и передача данных. Общие замечания. Прежде чем перейти к рассмотрению функций, непосредственно относящихся к организации обмена сообщениями между ветвями, остановимся на некоторых важных общих моментах, таких как атрибуты сообщения и поддержка типизации данных в MPI. |
|