Введение в параллельные вычисления. Основы программирования на языке СИ с использованием интерфейса МРI [Антон Михайлович Сальников salnikov@ipu.ru] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Учреждение Российской академии наук
Институт проблем управления
им. В.А. Трапезникова РАН

А.М. Сальников, Е.А. Ярошенко,
О.С. Гребенник, С.В. Спиридонов

ВВЕДЕНИЕ
В ПАРАЛЛЕЛЬНЫЕ ВЫЧИСЛЕНИЯ.
ОСНОВЫ ПРОГРАММИРОВАНИЯ
НА ЯЗЫКЕ СИ С ИСПОЛЬЗОВАНИЕМ
ИНТЕРФЕЙСА MPI

Москва 2010

УДК 681.3.06

Сальников А.М., Ярошенко Е.А., Гребенник О.С., Спиридонов С.В. Введение в параллельные вычисления. Основы
программирования на языке Си с использованием интерфейса MPI. – М.: ИПУ РАН, 2010. – 124 с.
Рассматривается архитектура многопроцессорных вычислительных систем. Приводится обзор современных
суперкомпьютеров. Описывается работа на суперкомпьютерах кластерного типа и особенности параллельного программирования на языке Си с использованием интерфейса
MPI. Среди рассматриваемых вопросов уделяется внимание необходимым навыкам разработки программ в операционной системе Linux. Изложенный материал адаптирован применительно к суперкомпьютеру ИПУ РАН.
Научное издание рассчитано на научных работников,
аспирантов, студентов и разработчиков прикладных программ.
Рецензенты: д.т.н. Лебедев В. Г.
д.ф.-м.н. проф. Исламов Г. Г.
Утверждено к печати Редакционным советом Института.
Текст воспроизводится в виде, утверждённом
Редакционным советом Института

ISBN 978-5-91450-065-5

ОГЛАВЛЕНИЕ
1. Архитектура многопроцессорных вычислительных
систем ................................................................................................ 4
1.1. Введение .................................................................................... 4
1.2. Традиционная классификация вычислительных систем....... 7
1.3. Классификация многопроцессорных вычислительных
систем ............................................................................................... 9
1.4. Векторно-конвейерные системы ........................................... 11
1.5. Симметричные многопроцессорные системы
(SMP и NUMA) ............................................................................... 13
1.6. Системы с массовым параллелизмом (MPP)........................ 16
1.7. Кластерные системы ............................................................... 19
2. Программирование для многопроцессорных
вычислительных систем ............................................................. 22
2.1. Программирование для систем с общей памятью ............... 22
2.2. Программирование для систем с распределенной памятью24
2.3. Параллельное программирование ......................................... 25
2.4. Оценка эффективности распараллеливания программ ....... 31
2.5. Проблемы оптимизации программ ....................................... 32
3. Суперкомпьютер ИПУ РАН ............................................... 42
3.1. Операционная система GNU/Linux ....................................... 43
3.2. Инструментарий разработчика .............................................. 50
4. Основы программирования на языке Си с
использованием интерфейса MPI.............................................. 61
4.1. Инициализация MPI ............................................................... 63
4.2. Прием/отправка сообщений с блокировкой ......................... 68
4.3. Прием/отправка сообщений без блокировки ....................... 75
4.4. Объединение запросов на прием/отправку сообщений ...... 85
4.5. Барьерная синхронизация ...................................................... 90
4.6. Группы процессов ................................................................... 92
4.7. Коммуникаторы групп ........................................................... 99
4.8. Функции коллективного взаимодействия .......................... 104
4.9. Типы данных ......................................................................... 106
Литература................................................................................... 123
3

1. АРХИТЕКТУРА МНОГОПРОЦЕССОРНЫХ
ВЫЧИСЛИТЕЛЬНЫХ СИСТЕМ
1.1. Введение
Термин «суперкомпьютер» существует со времени появления первых электронных вычислительных машин (ЭВМ) и
фактически эволюционирует вместе с термином «компьютер».
Сфера применения компьютеров охватывает абсолютно все
области человеческой деятельности, и сегодня невозможно
представить себе эффективную организацию работы без применения компьютеров. Но если компьютеры в целом развиваются
разнонаправлено и самым непостижимым образом проникают в
нашу жизнь, то суперкомпьютеры по-прежнему предназначены
для того же, для чего разрабатывались первые электронные
вычислительные машины, т.е. для решения задач, требующих
выполнения больших объемов вычислений. Развитие топливноэнергетического комплекса, авиационной, ракетно-космической
промышленности и многих других областей науки и техники
требует постоянного увеличения объема производимых расчетов
и, таким образом, способствует активному развитию суперкомпьютеров.
Считается,
что
термин
«суперкомпьютер»
(англ.
supercomputer) впервые стали использовать Джордж Майкл
(George Michael) и Сидней Фернбач (Sidney Fernbach), занимавшиеся проблемой параллельных вычислений в Ливерморской
национальной лаборатории им. Э. О. Лоуренса (англ. Lawrence
Livermore National Laboratory, LLNL) с конца пятидесятых годов
двадцатого века.
Ливерморская национальная лаборатория в настоящее время входит в структуру Калифорнийского университета (англ.
The University of California, UC) и наряду с Национальной лабораторией в Лос-Аламосе (англ. Los Alamos National Laboratory,
LANL) «является главной научно-исследовательской и опытноконструкторской организацией для решения проблем национальной безопасности США», т.е. одной из двух лабораторий,
главной задачей которых служит разработка ядерного оружия.
Также лаборатория занимается исследованиями в области наук,
4

напрямую не связанных с военными технологиями, таких как
энергетика, экология и биология (в том числе биоинженерия).
Именно в Ливерморской национальной лаборатории за многие
годы было создано и успешно эксплуатировалось абсолютное
большинство известных суперкомпьютеров, включая IBM Blue
Gene/L – самый быстрый в мире суперкомпьютер 2004-2008 гг.
В общеупотребительный лексикон термин «суперкомпьютер» вошел в восьмидесятых годах благодаря феноменальной
популярности в СМИ компьютерных систем Сеймура Крея
(Seymour Cray), таких как Cray-1, Cray-2 и др. В то время в
научно-популярной литературе суперкомпьютером назывался
«любой компьютер, который создал Сеймур Крей», хотя сам
Крей никогда не называл свои системы суперкомпьютерами,
предпочитая использовать традиционное название «компьютер».
Более того, еще при жизни Сеймура Крея его именем называли
различные суперкомпьютеры, созданные другими талантливыми
инженерами, среди которых был Стив Чен (Steve Chen), создатель самого производительного суперкомпьютера начала восьмидесятых Cray X-MP, породившего настоящий суперкомпьютерный бум в СМИ. В настоящее время имя Сеймура Крея носит
компания Cray Inc., занимающая достойное место в ряду производителей суперкомпьютеров.
На волне триумфа и популярности суперкомпьютеров Сеймура Крея в конце восьмидесятых годов появилось множество
небольших компаний, занимающихся созданием высокопроизводительных компьютеров. Однако уже к середине девяностых
большинство из них было приобретено традиционными производителями компьютерного оборудования, такими как IBM и
Hewlett-Packard.
Из-за шумихи в средствах массовой информации, созданной
при активной «помощи» журналистов, термин «суперкомпьютер» некоторое время трактовался по-разному. Например, в 1989
году знаменитый компьютерный инженер и создатель архитектуры VAX Гордон Белл (Gordon Bell) в шутку предложил считать суперкомпьютером любой компьютер, весящий более
тонны. Сегодня термин «суперкомпьютер» вернулся к истокам и
по-прежнему обозначает компьютер, способный выполнять
5

очень большие объемы вычислений, т.е. более производительный и дорогой, чем любой серийно выпускаемый компьютер.
Большинство современных суперкомпьютеров – это кластерные системы, состоящие из большого числа серийно выпускаемых компьютеров, объединенных в единую систему с помощью серийно выпускаемых сетевых интерфейсов. Границы
между специализированным программным обеспечением для
суперкомпьютеров и типовым программным обеспечением
сильно размыты. При этом суперкомпьютеры не ориентированы
на работу с типовыми приложениями и этим они принципиально
отличаются от других компьютеров с высокой общей производительностью. В отличие от серверов и мейнфреймов, призванных работать с типовыми задачами (управление базами данных,
группами пользователей и т.п.), суперкомпьютеры призваны
работать со сложными задачами (прогнозирование, моделирование и т.п.), требующими создания собственных приложений.
Иногда суперкомпьютеры работают с одним-единственным
приложением, использующим всю память и все процессоры
системы; в других случаях они обеспечивают выполнение большого числа разнообразных пользовательских программ.
Вычислительное направление применения компьютеров
всегда оставалось основным двигателем прогресса в компьютерных технологиях. Основным параметром, отличающим суперкомпьютер от обычного компьютера, является его производительность
(вычислительная
мощность)

количество
арифметических операций за единицу времени. Именно этот
показатель с наибольшей очевидностью демонстрирует масштабы прогресса, достигнутого в компьютерных технологиях. Производительность одного из первых суперкомпьютеров ABC,
созданного в 1942 году в Университете штата Айова (англ. Iowa
State University of Science and Technology, ISU) составляла всего
30 операций в секунду, тогда как пиковая производительность
самого мощного суперкомпьютера 2008 года IBM Roadrunner в
Национальной лаборатории в Лос-Аламосе составляет 1 квадриллион (1015) операций в секунду.
Таким образом, за 65 лет произошло увеличение производительности суперкомпьютеров в 30 триллионов раз. Невозможно
назвать другую сферу человеческой деятельности, где прогресс
6

был бы столь очевиден и так велик. И такой прогресс оказался
возможным не столько за счет тысячекратного увеличения
скорости работы электронных схем, сколько за счет максимального распараллеливания обработки данных.
Считается, что идея параллельной обработки данных как
мощного резерва увеличения производительности вычислительных машин была высказана еще Чарльзом Бэббиджем примерно
за сто лет до появления первого компьютера. Однако уровень
развития технологий середины девятнадцатого века не позволил
ему реализовать эту идею. С появлением первых компьютеров
идеи распараллеливания неоднократно становились отправной
точкой при разработке самых передовых и производительных
вычислительных систем. Без преувеличения можно сказать, что
вся история развития высокопроизводительных вычислений –
это история реализации идей параллельной обработки данных на
том или ином этапе развития компьютерных технологий, естественно, в сочетании с увеличением скорости работы электронных схем.
Принципиально важными решениями в повышении производительности вычислительных систем были:
− введение конвейерной организации выполнения команд;
− включение в систему команд векторных операций, позволяющих одной командой обрабатывать целые массивы
данных;
− распределение вычислений на множество процессоров.

1.2. Традиционная классификация
вычислительных систем
Большое разнообразие вычислительных систем породило
естественное желание ввести для них какую-то классификацию.
Эта классификация должна однозначно относить ту или иную
вычислительную систему к некоторому классу, который, в свою
очередь, должен достаточно полно ее характеризовать. Попыток
такой классификации в разное время предпринималось множество. Одна из первых классификаций, ссылки на которую наиболее часто встречаются в литературе, была предложена М. Флинном в конце 60-х годов прошлого века. Она базируется на
7

понятиях потоков команд и потоков данных. На основе числа
этих потоков выделяется четыре класса архитектур:
− SISD (англ. Single Instruction Single Data) – единственный
поток команд и единственный поток данных. По сути дела это классическая машина фон Неймана. К этому классу
относятся все однопроцессорные системы.
− SIMD (Single Instruction Multiple Data) – единственный
поток команд и множественный поток данных. Типичными представителями являются матричные компьютеры, в
которых все процессорные элементы выполняют одну и
ту же программу, применяемую к своим (различным для
каждого ПЭ) локальным данным. Некоторые авторы к
этому классу относят и векторно-конвейерные компьютеры, если каждый элемент вектора рассматривать как отдельный элемент потока данных.
− MISD (Multiple Instruction Single Date) – множественный
поток команд и единственный поток данных. М. Флинн
не смог привести ни одного примера реально существующей системы, работающей на этом принципе. Некоторые авторы в качестве представителей такой архитектуры
называют векторно-конвейерные компьютеры, однако такая точка зрения не получила широкой поддержки.
− MIMD (Multiple Instruction Multiple Date) – множественный поток команд и множественный поток данных. К
этому классу относится большинство современных многопроцессорных систем.
Поскольку в этой классификации почти все современные
многопроцессорные системы принадлежат одному классу, то
вряд ли такая классификация представляет сегодня какую-либо
практическую ценность. Тем не менее, используемые в ней
термины достаточно часто упоминаются в литературе по параллельным вычислениям.
Эффективность использования современных компьютеров в
решающей степени зависит от состава и качества программного
обеспечения, установленного на них. В первую очередь это
касается программного обеспечения, предназначенного для
разработки прикладных программ. Так, например, недостаточ8

ная развитость средств разработки для систем с распределенной
памятью долгое время сдерживала их широкое использование. В
настоящее время ситуация изменилась, и благодаря кластерным
технологиям такие системы стали самой распространенной и
доступной разновидностью высокопроизводительных вычислительных систем.
Основной характеристикой при классификации многопроцессорных систем является наличие общей или распределенной
памяти. Это различие является важнейшим фактором, определяющим способы параллельного программирования и, соответственно, структуру программного обеспечения.

1.3. Классификация многопроцессорных
вычислительных систем
В процессе развития суперкомпьютерных технологий идею
повышения производительности вычислительной системы за
счет увеличения числа процессоров использовали неоднократно.
Если не вдаваться в исторический экскурс и обсуждение всех
таких попыток, то можно следующим образом вкратце описать
развитие событий.
Экспериментальные разработки по созданию многопроцессорных вычислительных систем (МВС) начались в семидесятых
годах двадцатого века. Одной из первых таких систем стала
разработанная в 1976 году в Университете Иллинойса в УрбанеШампэйн (англ. University of Illinois at Urbana-Champaign, UIUC)
МВС ILLIAC IV, которая включала 64 (в проекте до 256) процессорных элемента (ПЭ), работающих по единой программе,
применяемой к содержимому собственной оперативной памяти
каждого ПЭ. Обмен данными между процессорами осуществлялся через специальную матрицу коммуникационных каналов.
Указанная особенность коммуникационной системы дала название «матричные суперкомпьютеры» соответствующему классу
МВС. Более широкий класс МВС с распределенной памятью и с
произвольной коммуникационной системой получил впоследствии название «многопроцессорные системы с массовым параллелизмом», или МВС с MPP-архитектурой (англ. Massive
Parallel Processing, MPP). При этом, как правило, каждый из ПЭ
9

системы с MPP-архитектурой является универсальным процессором, действующим по своей собственной программе (в отличие от общей программы для всех ПЭ матричной МВС).
Первые матричные МВС выпускались с конца семидесятых
годов буквально поштучно, поэтому их стоимость была фантастически высокой. Серийные образцы подобных систем, такие
как первая коммерческая матричная МВС ICL DAP (англ.
Distributed Array Processor), включавшая до 8192 ПЭ, появились
примерно в это же время, однако не получили широкого распространения ввиду сложности программирования МВС с одним
потоком управления (с одной программой, общей для всех ПЭ).
Первые промышленные образцы многопроцессорных систем появились на базе векторно-конвейерных компьютеров в
середине восьмидесятых годов. Наиболее распространенными
МВС такого типа были суперкомпьютеры Сеймура Крея. Однако такие системы были чрезвычайно дорогими и производились
небольшими сериями. Как правило, в подобных компьютерах
объединялось от 2 до 16 процессоров, которые имели равноправный (симметричный) доступ к общей оперативной памяти.
В связи с этим они стали называться симметричными многопроцессорными системами (англ. Symmetric Multiprocessing, SMP).
Как альтернатива дорогим многопроцессорным системам на
базе векторно-конвейерных процессоров была предложена идея
строить эквивалентные по мощности многопроцессорные системы из большого числа дешевых серийно выпускаемых микропроцессоров. Однако очень скоро обнаружилось, что архитектура SMP обладает весьма ограниченными возможностями по
наращиванию числа процессоров в системе из-за резкого увеличения числа конфликтов при обращении к общей шине памяти.
В связи с этим оправданной представлялась идея снабдить каждый процессор собственной оперативной памятью, превращая
компьютер в объединение независимых вычислительных узлов.
Такой подход значительно увеличил масштабируемость многопроцессорных систем, но в свою очередь потребовал разработки
специального способа обмена данными между вычислительными узлами, реализуемого обычно в виде механизма передачи
сообщений (англ. Message Passing). Компьютеры с такой архитектурой являются наиболее яркими представителями совре10

менных MPP-систем. В настоящее время оба этих направления
(или их комбинации) являются доминирующими в развитии
суперкомпьютерных технологий.
Нечто среднее между SMP и MPP представляют собой
NUMA-архитектуры (англ. Non-Uniform Memory Access), в
которых память физически разделена, но логически общедоступна. При этом время доступа к различным блокам памяти
становится неодинаковым. В одной из первых NUMA-систем
Cray T3D, выпущенной в 1993 году, время доступа к памяти
соседнего процессора было в шесть раз больше, чем к памяти
своего собственного процессора.
В настоящее время развитие суперкомпьютерных технологий идет по четырем основным направлениям:
− векторно-конвейерные системы;
− SMP- и NUMA-системы;
− MPP-системы;
− кластерные системы.

1.4. Векторно-конвейерные системы
Первый векторно-конвейерный компьютер Cray-1 появился
в 1976 году. Архитектура его оказалась настолько удачной, что
он положил начало целому семейству компьютеров. Название
этому семейству компьютеров дали два принципа, заложенные в
архитектуре процессоров:
− конвейерная организация обработки потока команд;
− введение в систему команд набора векторных операций,
которые позволяют оперировать целыми массивами данных.
Длина одновременно обрабатываемых векторов в современных векторных системах составляет, как правило, 128 или 256
элементов. Очевидно, что векторные процессоры должны иметь
гораздо более сложную структуру и по сути дела содержать
множество арифметических устройств. Основное назначение
векторных операций состоит в распараллеливании выполнения
операторов цикла, в которых в основном и сосредоточена большая часть вычислительной работы. Для этого циклы подвергаются процедуре векторизации с тем, чтобы они могли реализо11

вываться с использованием векторных команд. Как правило, это
выполняется автоматически компиляторами при создании исполняемого кода программы. Поэтому векторно-конвейерные
компьютеры не требуют какой-то специальной технологии
программирования, что и явилось решающим фактором в их
успехе на компьютерном рынке. Тем не менее, требуется соблюдение некоторых правил при написании циклов с тем, чтобы
компилятор мог их эффективно векторизовать.
Исторически это были первые компьютеры, к которым в
полной мере было применимо понятие суперкомпьютер. Как
правило, несколько векторно-конвейерных процессоров (от двух
до шестнадцати) работают в режиме с общей памятью (SMP),
образуя вычислительный узел, а несколько таких узлов объединяются с помощью коммутаторов, образуя NUMA- или MPPсистему. Типичными представителями такой архитектуры являются компьютеры серий Cray J90 (1994 год), Cray T90 (1995
год), NEC SX-4 (1995 год), Cray SV1 (1998 год), NEC SX-5 (1999
год). Уровень развития микроэлектронных технологий долгое
время не позволял производить однокристальные векторные
процессоры, поэтому эти системы были довольно громоздки и
чрезвычайно дороги. В связи с этим, начиная с середины 90-х
годов, когда появились достаточно мощные суперскалярные
микропроцессоры, интерес к этому направлению значительно
ослабел.
Суперкомпьютеры с векторно-конвейерной архитектурой
стали проигрывать системам с массовым параллелизмом. Однако в марте 2002 г. компания NEC представила систему Earth
Simulator (ES) из 5120 векторно-конвейерных процессоров,
которая в 5 раз превысила производительность предыдущего
обладателя рекорда, очередной MPP-системы из Ливерморской
национальной лаборатории ASCI White (2000 год), состоящей из
8192 суперскалярных микропроцессоров. Это заставило многих
по-новому взглянуть на перспективы векторно-конвейерных
систем. Производительность суперкомпьютера NEC ES оставалась самой высокой в мире до момента запуска в 2004 году
первой версии суперкомпьютера IBM Blue Gene/L с MPPархитектурой.
12

1.5. Симметричные многопроцессорные системы
(SMP и NUMA)
Характерной чертой симметричных многопроцессорных систем является то, что все процессоры имеют прямой и равноправный доступ к любой точке общей памяти. Первые SMPсистемы состояли из нескольких однородных процессоров и
массива общей памяти, к которой процессоры подключались
через общую системную шину. Однако очень скоро обнаружилось, что такая архитектура непригодна для создания каких-либо
масштабных систем.
Первая возникшая проблема – это большое число конфликтов при обращении к общей шине. Остроту проблемы удалось
частично снять разделением памяти на блоки, подключение к
которым с помощью коммутаторов позволило распараллелить
обращения от различных процессоров. Однако и в таком подходе неприемлемо большими казались накладные расходы для
систем более чем с 32 процессорами.
Современные системы архитектуры SMP состоят, как правило, из нескольких однородных серийно выпускаемых микропроцессоров и массива общей памяти, подключение к которой
производится либо с помощью общей шины, либо с помощью
коммутатора.
CPU
Кэш

CPU
Кэш

CPU
Кэш

Шина или коммутатор

Общая память
Рис. 1. Архитектура SMP-системы

13

Наличие общей памяти значительно упрощает организацию
взаимодействия процессоров между собой и упрощает программирование, поскольку параллельная программа работает в едином адресном пространстве. Однако за этой кажущейся простотой скрываются проблемы, присущие системам этого типа. Все
они, так или иначе, связаны с оперативной памятью. Дело в том,
что в настоящее время даже в однопроцессорных системах
самым узким местом является оперативная память, скорость
работы которой отстает от скорости работы процессора. Для
того чтобы сгладить этот разрыв, современные процессоры
снабжаются скоростной буферной памятью (кэш-памятью),
скорость работы которой значительно выше, чем скорость работы основной памяти.
В качестве примера приведем данные измерения пропускной способности кэш-памяти и основной памяти для персонального компьютера образца 1999 года на базе процессора Pentium
III. В данном процессоре кэш-память имела два уровня:
− L1 (буферная память команд) – объем 32 Кб, скорость
обмена 9976 Мб/с;
− L2 (буферная память данных) – объем 256 Кб, скорость
обмена 4446 Мб/с.
В то же время скорость обмена с основной памятью составляла всего 255 Мб/с. Это означало, что для полной согласованности со скоростью работы процессора скорость работы основной памяти должна была быть минимум в 40 раз выше. Для
сравнения, скорость обмена 10 Гбайт/с обеспечивает контроллер
памяти (впервые интегрированный непосредственно в кристалл
CPU) процессора Intel Core i7 образца 2009 года.
Очевидно, что при проектировании многопроцессорных систем эти проблемы еще более обостряются. Помимо хорошо
известной проблемы конфликтов при обращении к общей шине
памяти возникла и новая проблема, связанная с иерархической
структурой организации памяти современных компьютеров. В
многопроцессорных системах, построенных на базе микропроцессоров со встроенной кэш-памятью, нарушается принцип
равноправного доступа к любой точке памяти. Данные, находящиеся в кэш-памяти некоторого процессора, недоступны для
14

других процессоров. Это означает, что после каждой модификации копии некоторой переменной, находящейся в кэш-памяти
какого-либо процессора, необходимо производить синхронную
модификацию самой этой переменной, расположенной в основной памяти.
С большим или меньшим успехом эти проблемы решаются
в рамках общепринятой в настоящее время архитектуры
ccNUMA (англ. Cache coherent Non-Uniform Memory Access). В
такой архитектуре память физически распределена, но логически общедоступна. Это с одной стороны позволяет работать с
единым адресным пространством, а с другой – увеличивает
масштабируемость систем.
CPU
Кэш

CPU
Кэш

CPU
Кэш

CPU
Кэш

Шина

Шина

Память

Память

Коммуникационная среда
Рис. 2. Архитектура NUMA-системы
Когерентность кэш-памяти поддерживается на аппаратном
уровне, что не избавляет, однако, от накладных расходов на ее
поддержание. В отличие от классических SMP-систем память
становится трехуровневой:
− кэш-память процессора;
− локальная оперативная память;
− удаленная оперативная память.
15

Время обращения к различным уровням может отличаться
на порядок, что сильно усложняет написание эффективных
параллельных программ для таких систем.
Перечисленные обстоятельства значительно ограничивают
возможности по наращиванию производительности ccNUMA
систем путем простого увеличения числа процессоров. Тем не
менее, эта технология позволяет создавать системы, содержащие
до 256 процессоров с общей производительностью порядка 200
млрд. операций в секунду. Системы этого типа серийно производятся многими компьютерными фирмами как многопроцессорные серверы с числом процессоров от 2 до 128 и до недавнего времени они прочно удерживали лидерство в классе малых
суперкомпьютеров. Типичными представителями данного класса суперкомпьютеров являются компьютеры на базе процессоров AMD Opteron или Intel Itanium 2, например, HP Integrity
Superdome (2002 год).
С конца 2008 года архитектура ccNUMA также поддерживается микроархитектурой Nehalem, преемником Core 2, первыми представителями которой стали процессоры Intel Core i7.
Неприятным свойством всех симметричных многопроцессорных систем является то, что их стоимость растет быстрее,
чем производительность при увеличении числа процессоров в
системе. Кроме того, из-за задержек при обращении к общей
памяти неизбежно взаимное торможение при параллельном
выполнении даже независимых программ.

1.6. Системы с массовым параллелизмом (MPP)
Проблемы, присущие многопроцессорным системам с общей памятью, простым и естественным образом устраняются в
системах с массовым параллелизмом. Компьютеры этого типа
представляют собой многопроцессорные системы с распределенной памятью, в которых с помощью некоторой коммуникационной среды объединяются однородные вычислительные
узлы.
Каждый из узлов состоит из одного или нескольких процессоров, собственной оперативной памяти, коммуникационного
оборудования, подсистемы ввода/вывода, т.е. обладает всем
16

необходимым для независимого функционирования. При этом
на каждом узле может функционировать либо полноценная
операционная система, либо урезанный вариант, поддерживающий только базовые функции ядра, а полноценная операционная
система работает на специальном управляющем компьютере.
Память

Память

Память

Кэш
CPU

Кэш
CPU

Кэш
CPU

Коммуникационная среда
Рис. 3. Архитектура MPP-системы
Процессоры в таких системах имеют прямой доступ только
к своей локальной памяти. Доступ к памяти других узлов реализуется обычно с помощью механизма передачи сообщений.
Такая архитектура вычислительной системы устраняет одновременно как проблему конфликтов при обращении к памяти, так и
проблему когерентности кэш-памяти. Это дает возможность
практически неограниченно наращивать число процессоров в
системе, увеличивая тем самым ее производительность. Успешно функционируют MPP-системы с сотнями и тысячами процессоров, производительность наиболее мощных из них достигает
нескольких сотен триллионов операций в секунду. Важным
свойством MPP-систем является их высокая масштабируемость.
В зависимости от вычислительных потребностей для достижения необходимой производительности требуется просто собрать
систему с нужным числом узлов.
Однако на практике устранение одних проблем, как это
обычно бывает, порождает другие. Для MPP-систем на первый
план выходит проблема эффективности коммуникационной
17

среды. Самым простым и наиболее эффективным было бы соединение каждого процессора с каждым. Но тогда на каждом
узле суперкомпьютера, содержащего N процессоров, потребовалось бы N – 1 коммуникационных каналов, желательно двунаправленных. Различные производители MPP-систем использовали разные топологии. В суперкомпьютерах Intel Paragon (1992
год) процессоры образовывали прямоугольную двумерную
сетку. Для этого на каждом узле достаточно было иметь четыре
коммуникационных канала. В суперкомпьютерах Cray T3D
(1993 год) и Cray T3E (1995 год) использовалась топология
трехмерного тора. Соответственно, на каждом узле этого компьютера было по шесть коммуникационных каналов. Фирма
nCUBE в начале девяностых годов использовала в своих компьютерах топологию n-мерного гиперкуба.
Каждая из упомянутых топологий имела свои преимущества и недостатки. Отметим, что при обмене данными между
процессорами, не являющимися ближайшими соседями, происходит трансляция данных через промежуточные узлы. Очевидно, что в узлах должны быть предусмотрены какие-то аппаратные средства, которые освобождали бы центральный процессор
от участия в трансляции данных. В последнее время для соединения вычислительных узлов чаще используется иерархическая
система высокоскоростных коммутаторов, как это впервые было
реализовано в компьютерах IBM SP2 (1994 год). Такая топология дает возможность прямого обмена данными между любыми
узлами, без участия в этом промежуточных узлов.
Системы с распределенной памятью идеально подходят для
параллельного выполнения независимых программ, поскольку
при этом каждая программа выполняется на своем узле и никаким образом не влияет на выполнение других программ. Однако
при разработке параллельных программ приходится учитывать
более сложную, чем в SMP-системах, организацию памяти.
Оперативная память в MPP-системах имеет трехуровневую
структуру:
− кэш-память процессора;
− локальная оперативная память узла;
− оперативная память других узлов.
18

При этом отсутствует возможность прямого доступа к данным, расположенным на других узлах. Такие данные должны
быть предварительно переданы в тот узел, который в них нуждается. Это значительно усложняет программирование. Кроме
того, обмены данными между узлами выполняются значительно
медленнее, чем обработка данных в локальной оперативной
памяти узлов. Поэтому написание эффективных параллельных
программ для таких компьютеров представляет собой более
сложную задачу, чем для SMP-систем.

1.7. Кластерные системы
Кластерные технологии стали логическим продолжением
развития идей, заложенных в архитектуре MPP. Если процессорный модуль в MPP-системе представляет собой законченную
вычислительную систему, то следующий шаг напрашивается
сам собой: почему бы в качестве таких вычислительных узлов не
использовать обычные серийно выпускаемые компьютеры.
Развитие коммуникационных технологий, а именно, появление
высокоскоростного сетевого оборудования и специального
программного обеспечения, такого как интерфейс MPI, реализующего механизм передачи сообщений над стандартными
сетевыми протоколами, сделали кластерные технологии общедоступными. Сегодня не составляет большого труда создать
небольшую кластерную систему, объединив вычислительные
мощности нескольких компьютеров лаборатории.
Привлекательной чертой кластерных технологий является
то, что они позволяют для достижения необходимой производительности объединять в единые вычислительные системы компьютеры разного типа, от персональных компьютеров до промышленных блейд-серверов.
Широкое распространение кластерные технологии получили как средство создания систем суперкомпьютерного класса из
составных частей массового производства, значительно удешевляющих стоимость готовой системы. Одним из первых современных кластеров можно считать созданный в 1998 году в Университете штата Пенсильвания (англ. Pennsylvania State
University) суперкомпьютер COCOA (англ. The Cost Effective
19

Computing Array), в котором на базе 25 двухпроцессорных персональных компьютеров общей стоимостью порядка 100 тыс.
долл. США была создана система с производительностью, эквивалентной производительности 48-процессорного Cray T3D
стоимостью несколько млн. долл. США.
Конечно, о полной эквивалентности упомянутых систем говорить не приходилось, потому что производительность систем
с распределенной памятью очень сильно зависит от производительности коммуникационной среды. Коммуникационную среду
можно достаточно полно охарактеризовать двумя параметрами:
латентностью (временем задержки при посылке сообщения) и
пропускной способностью (скоростью передачи информации).
Для компьютера Cray T3D эти параметры составляли 1 мкс и 480
Мб/с соответственно. Для кластера COCOA, в котором в качестве коммуникационной среды использовалась стандартная на
тот момент сеть Fast Ethernet, латентность и пропускная способность составляли 100 мкс и 10 Мб/с соответственно. При таких
параметрах найдется не так много задач, эффективно решаемых
на достаточно большом числе процессоров. Это обстоятельство
долгое время отчасти объясняло чрезмерную стоимость суперкомпьютеров. Для сравнения, интерфейс InfiniBand 4X SDR,
используемый в кластере ИПУ РАН, обеспечивает латентность
200 нс и пропускную способность 8 Гб/с (на уровне MPI – 1 мкс
и 800 Мб/с соответственно).
Если говорить кратко, то кластер – это связанный набор
полноценных компьютеров, используемый в качестве единого
вычислительного ресурса. Преимущества кластерной системы
перед набором независимых компьютеров очевидны. Во-первых,
разработано множество диспетчерских систем пакетной обработки заданий, позволяющих послать задание на обработку
кластеру в целом, а не какому-то отдельному компьютеру. Эти
диспетчерские системы автоматически распределяют задания по
свободным вычислительным узлам или буферизуют их при
отсутствии таковых, что позволяет обеспечить более равномерную и эффективную загрузку компьютеров. Во-вторых, появляется возможность совместного использования вычислительных
ресурсов нескольких компьютеров для решения одной задачи.
20

Для создания современных кластеров уже не используются
однопроцессорные персональные компьютеры – сегодня их
место заняли многопроцессорные, многоядерные и зачастую
сами по себе высокопроизводительные SMP-серверы. При этом
не накладывается никаких ограничений на состав и архитектуру
узлов. Каждый из узлов может функционировать под управлением своей собственной операционной системы. Чаще всего используются UNIX-подобные ОС, причем как коммерческие
(Solaris, Tru64 Unix), так и свободные (GNU/Linux, FreeBSD). В
тех случаях, когда узлы кластера неоднородны, итоговая система называется гетерогенным кластером.
При создании кластера можно выделить два подхода:
1. В кластер объединяются полнофункциональные компьютеры, которые продолжают работать как самостоятельные
единицы, например, рабочие станции лаборатории. Такой
подход применяется при создании небольших кластерных
систем или в целях тестирования технологий параллельной обработки данных.
2. Системные блоки компьютеров компактно размещаются
в стандартных серверных стойках, а для управления системой и для запуска задач выделяется один или несколько полнофункциональных компьютеров. Такой подход
применяется при целенаправленном создании мощных
вычислительных ресурсов.
За последние годы разработаны специальные технологии
соединения компьютеров в кластер. Наиболее широко в настоящее время применяется высокоскоростная коммутируемая последовательная шина InfiniBand. Это обусловлено простотой ее
использования и относительно низкой стоимостью коммуникационного оборудования, которое обеспечивает приемлемую
скорость обмена между узлами от 2,5 Гбит/с до 96 Гбит/с, в
зависимости от типа установленного оборудования.
Разработчики пакета подпрограмм ScaLAPACK, предназначенного для решения задач линейной алгебры на многопроцессорных системах, в которых велика доля коммуникационных
операций, сформулировали следующим образом требование к
многопроцессорной системе: «Скорость межпроцессорных
обменов между двумя узлами, измеренная в Мб/с, должна быть
21

не менее 1/10 пиковой производительности вычислительного
узла, измеренной в мегафлопс». Таким образом, если в качестве
вычислительных узлов использовать рабочие станции образца
2006 года на базе Intel Core 2 Duo с пиковой производительностью 15 гигафлопс, то аппаратура InfiniBand будет обеспечивать
необходимый теоретический минимум, а все еще популярная
Gigabit Ethernet – уже нет. В целом можно утверждать, что
InfiniBand в настоящее время становится новым сетевым стандартом, потому что соответствует потребностям не только суперкомпьютеров кластерного типа, но и любых других высокопроизводительных серверных систем.

2. ПРОГРАММИРОВАНИЕ
ДЛЯ МНОГОПРОЦЕССОРНЫХ
ВЫЧИСЛИТЕЛЬНЫХ СИСТЕМ
2.1. Программирование для систем с общей памятью
К системам с общей памятью относятся компьютеры с SMPархитектурой, различные разновидности NUMA-систем и многопроцессорные векторно-конвейерные компьютеры. Характерным словом для этих компьютеров является «единый»: единая
оперативная память, единая операционная система, единая
подсистема ввода-вывода. Только процессоры образуют множество. Единая UNIX-подобная операционная система, управляющая работой всего компьютера, функционирует в виде множества процессов. Каждая пользовательская программа также
запускается как отдельный процесс.
Операционная система сама каким-то образом распределяет
процессы по процессорам. В принципе, для распараллеливания
программ можно использовать механизм порождения процессов.
Однако этот механизм не очень удобен, поскольку каждый
процесс функционирует в своем адресном пространстве, и основное достоинство этих систем – общая память – не может
быть использована простым и естественным образом.
Для распараллеливания программ используется механизм
порождения нитей (англ. threads) – легковесных процессов, для
которых не создается отдельного адресного пространства, но
22

которые на многопроцессорных системах также распределяются
по процессорам. В языке программирования Си возможно прямое использование этого механизма для распараллеливания
программ посредством вызова соответствующих системных
функций, а в компиляторах с языка Fortran этот механизм используется либо для автоматического распараллеливания, либо в
режиме задания распараллеливающих директив компилятору
(такой подход поддерживают и компиляторы языка Си).
Все производители симметричных многопроцессорных систем в той или иной мере поддерживают стандарт PThreads
(POSIX Threads) и включают в программное обеспечение распараллеливающие компиляторы для популярных языков программирования или предоставляют набор директив компилятору для
распараллеливания программ. В частности, многие поставщики
компьютеров SMP-архитектуры (Sun, HP, SGI) в своих компиляторах предоставляют специальные директивы для распараллеливания циклов. Однако эти наборы директив, во-первых, весьма
ограничены и, во-вторых, несовместимы между собой. В результате этого разработчикам приходится распараллеливать прикладные программы отдельно для каждой платформы.
В последние годы все более популярной, особенно на платформах Sun Solaris и GNU/Linux, становится система программирования OpenMP (англ. Open Multi-Processing), являющаяся
во многом обобщением и расширением этих наборов директив.
Интерфейс OpenMP задумывался как стандарт для программирования в модели общей памяти. В OpenMP входят спецификации набора директив компилятору, процедур и переменных
среды. По сути дела, он реализует идею «инкрементального
распараллеливания, когда разработчик не создает новую параллельную программу, а просто добавляет в текст последовательной программы OpenMP-директивы. При этом система программирования OpenMP предоставляет разработчику большие
возможности по контролю над поведением параллельного приложения.
Вся программа разбивается на последовательные и параллельные области. Все последовательные области выполняет
главная нить, порождаемая при запуске программы, а при входе
в параллельную область главная нить порождает дополнитель23

ные нити. Предполагается, что OpenMP-программа без какойлибо модификации должна работать как на многопроцессорных
системах, так и на однопроцессорных. В последнем случае
директивы OpenMP просто игнорируются.
Следует отметить, что наличие общей памяти не препятствует использованию технологий программирования, разработанных для систем с распределенной памятью. Многие производители SMP-систем предоставляют также такие технологии
программирования, как MPI и PVM. В этом случае в качестве
коммуникационной среды выступает распределенная память.

2.2. Программирование для систем с распределенной
памятью
В системах с распределенной памятью на каждом вычислительном узле функционируют собственные копии операционной
системы, под управлением которых выполняются независимые
программы. Это могут быть как действительно независимые
программы, так и параллельные ветви одной программы. Вэтом
случае единственно возможным механизмом взаимодействия
между ними является механизм передачи сообщений.
Стремление добиться максимальной производительности
заставляет разработчиков при реализации механизма передачи
сообщений учитывать особенности архитектуры многопроцессорной системы. Это способствует написанию более эффективных, но ориентированных на конкретный компьютер программ.
Вместе с тем независимыми разработчиками программного
обеспечения было предложено множество реализаций механизма передачи сообщений, независимых от конкретной платформы.
В 1995 г. был принят стандарт механизма передачи сообщений MPI (англ. Message Passing Interface). Он готовился с 1992
по 1994 гг. группой Message Passing Interface Forum, в которую
вошли представители более чем сорока организаций из США и
Европы. Основная цель, которую ставили перед собой разработчики MPI – это обеспечение полной независимости приложений,
написанных с использованием MPI, от архитектуры многопроцессорной системы, без какой-либо существенной потери произ24

водительности. По замыслу авторов это должно было стать
мощным стимулом для разработки прикладного программного
обеспечения и стандартизованных библиотек подпрограмм для
многопроцессорных систем с распределенной памятью. Подтверждением того, что эта цель была достигнута, служит тот
факт, что в настоящее время этот стандарт поддерживается
всеми производителями многопроцессорных систем. Реализации
MPI успешно работают не только на классических MPPсистемах, но также на SMP-системах и в сетях рабочих станций
(в том числе неоднородных).
Реализация MPI – это библиотека функций, обеспечивающая взаимодействие параллельных процессов с помощью механизма передачи сообщений. Большинство реализаций MPI поддерживают интерфейсы для языков Си, C++ и Fortran.
Библиотека MPI включает в себя множество функций передачи
сообщений типа точка-точка, развитый набор функций для
выполнения коллективных операций и управления процессами
параллельного приложения.
Основное отличие MPI от предшественников в том, что явно вводятся понятия групп процессов, с которыми можно оперировать как с конечными множествами, а также областей связи и
коммуникаторов, описывающих эти области связи. Это предоставляет программисту очень гибкие средства для написания
эффективных параллельных программ. В настоящее время MPI
является основной технологией программирования для многопроцессорных систем с распределенной памятью.
Несмотря на значительные успехи в развитии технологии
программирования с использованием механизма передачи сообщений, трудоемкость программирования с использованием этой
технологии по-прежнему велика.

2.3. Параллельное программирование
Разработка параллельных программ является весьма трудоемким процессом, особенно для MPP-систем, поэтому, прежде
чем приступать к этой работе, важно правильно оценить как
ожидаемый эффект от распараллеливания, так и трудоемкость
выполнения этой работы.
25

Решение на компьютере любой вычислительной задачи для
выбранного алгоритма решения предполагает выполнение некоторого фиксированного объема арифметических операций.
Ускорить решение задачи можно одним из трех способов:
− использовать более производительную вычислительную
систему с более быстрым процессором и более скоростной системной шиной;
− оптимизировать программу, например, в плане более эффективного использования скоростной кэш-памяти;
− распределить вычислительную работу между несколькими процессорами, т.е. перейти на параллельные технологии.
Очевидно, что без распараллеливания не обойтись при программировании алгоритмов решения тех задач, которые в принципе не могут быть решены на однопроцессорных системах. Это
может проявиться в двух случаях: либо когда для решения
задачи требуется слишком много времени, либо когда для программы недостаточно оперативной памяти на однопроцессорной
системе.
Для небольших задач зачастую оказывается, что параллельная версия работает медленнее, чем однопроцессорная. Заметный эффект от распараллеливания начинает наблюдаться при
решении систем уравнений с большим количеством неизвестных. На кластерных системах ситуация еще хуже. Разработчики
уже упоминавшегося ранее пакета ScaLAPACK для многопроцессорных систем с приемлемым соотношением между производительностью узла и скоростью обмена дают следующую
формулу для количества процессоров P, которое рекомендуется
использовать при решении задач линейной алгебры
(1) P = m × n / 106,
где m × n – размерность матрицы.
Таким образом, на один процессор должен приходиться
блок матрицы размером примерно 1000 × 1000. Формула (1)
носит рекомендательный характер (особенно для процессоров
последних поколений), при этом наглядно иллюстрируя масштаб
задач, решаемых пакетами типа ScaLAPACK.

26

Рост эффективности распараллеливания при увеличении
размера решаемой системы уравнений объясняется ростом
объема вычислительной работы пропорционально n3, а количества обменов между процессорами пропорционально n2. Это
снижает относительную долю коммуникационных затрат при
увеличении размерности системы уравнений. Однако на эффективность параллельного приложения влияют не только коммуникационные издержки.
Параллельные технологии в MPP-системах допускают две
модели программирования, похожие на традиционную классификацию вычислительных систем М. Флинна:
− SPMD (англ. Single Program Multiple Date) – на всех процессорах выполняются копии одной программы, обрабатывающие разные блоки данных;
− MPMD (англ. Multiple Program Multiple Date) – на всех
процессорах выполняются разные программы, обрабатывающие разные данные.
Второй вариант иногда называют функциональным распараллеливанием. Такой подход, в частности, используется в
системах обработки видеоинформации, когда множество квантов данных должны проходить несколько этапов обработки. В
этом случае вполне оправданной будет конвейерная организация
вычислений, при которой каждый этап обработки выполняется
на отдельном процессоре.
Однако функциональное распараллеливание имеет весьма
ограниченное применение, поскольку организовать достаточно
длинный конвейер, да еще с равномерной загрузкой всех процессоров, весьма сложно. Наиболее распространенным режимом
работы на системах с распределенной памятью является загрузка
на некотором числе процессоров копий одной и той же программы.
Разработка параллельной программы подразумевает разбиение задачи на P подзадач, каждая из которых решается на
отдельном процессоре.
В параллельном программировании термин «подзадача»
имеет весьма широкий смысл. В MPMD-модели подзадачей
называется функционально выделенный фрагмент программы.
27

В SPMD-модели подзадачей чаще называется обработка некоторого блока данных.
Таким образом, схему параллельной программы, использующей механизм передачи сообщений, можно упрощенно записать на языке Си следующим образом:
if (proc_id == 1) task1 ();
if (proc_id == 2) task2 ();

result = reduce (result1, result2, …);

Здесь proc_id – идентификатор процессора, а функция
reduce формирует некий глобальный результат на основе полученных на каждом процессоре локальных результатов работы
функций task1, task2 и т. д. В этом случае одна и та же копия
программы будет выполняться на P процессорах, но каждый
процессор будет решать только свою подзадачу. Если разбиение
на подзадачи достаточно равномерное, а накладные расходы на
обмены не слишком велики, то можно ожидать близкого к P
коэффициента ускорения решения задачи.
На практике процедура распараллеливания чаще всего применяется к циклам. Тогда в качестве отдельных подзадач могут
выступать экземпляры тела цикла, выполняемые для различных
значений переменной цикла. Рассмотрим простейший пример:
for (i=1, i help echo

Оболочка Bash обеспечивает выполнение запросов пользователя: находит и вызывает исполняемые файлы, организует
ввод/вывод, отвечает за работу с переменными окружения,
выполняет некоторые преобразования (подстановки) аргументов
при запуске программ и т.п. Главное свойство оболочки, которое
делает ее мощным инструментом пользователя, – это наличие
собственного языка программирования. Оболочка также использует все программы, доступные пользователю, как базовые
операции поддерживаемого ею языка, обеспечивает передачу им
аргументов, а также передачу результатов их работы другим
программам и пользователю.
Ниже приведен список основных сочетаний клавиш, позволяющих эффективно работать с командной строкой Bash:
− или + – перемещение вправо
по командной строке в пределах уже набранной цепочки
символов плюс один символ справа (место для ввода следующего символа);
− или + – перемещение на один
символ влево;
− + – перемещение на одно слово вправо;
47




















+ – перемещение на одно слово влево;
или + – перемещение в начало
набранной цепочки символов;
или + – перемещение в начало/конец
набранной цепочки символов;
или + – удаление символа, на который
указывает курсор;
– удаление символа, находящегося слева от
курсора;
+ – удаление правой части строки, начиная с
символа, на который указывает курсор;
+ – удаление левой части строки, включая символ, находящийся слева от курсор;
или + – запуск на выполнение набранной цепочки символов;
+ – очищение экрана и помещение набранной
цепочки символов в верхней строке экрана;
+ – замена местами символа, на который указывает курсор, и символа, находящегося слева от курсора,
с последующим перемещением курсора на один символ
вправо;
+ – замена местами слова, на которое указывает
курсор, и слова, находящегося слева от курсора;
+ – вырезание и сохранение в буфере части
набранной цепочки символов, находящейся справа от
курсора;
+ – вырезание и сохранение в буфере правой
части слова, на которое указывает курсор;
+ – вырезание и сохранение в буфере левой
части слова, на которое указывает курсор;
+ – вырезание и сохранение в буфере части
набранной цепочки символов, находящейся слева от курсора до ближайшего пробела;
+ – вставка из буфера строки;
+ – замена строчного символа, на который указывает курсор, на прописной символ и перемещение курсора на ближайший пробел справа;
48

+ – замена строчных символов слова, на которое указывает курсор, прописными символами и перемещение курсора на ближайший пробел справа;
− + – замена прописных символов слова, на которое указывает курсор, на строчные и перемещение курсора на ближайший пробел справа;
− + или + – просмотр
страниц экранного вывода (количество страниц зависит
от размера видеопамяти);
− + – прерывание выполнения программы;
− + – выход из оболочки (завершение сеанса).
Оболочка Bash имеет встроенную подпрограмму, предназначенную для облегчения ввода команд в командной строке.
Эта подпрограмма вызывается по клавише после того, как
уже введено некоторое число символов. Если эти символы
являются первыми символами в названии одной из стандартных
команд, известных оболочке, то возможны разные варианты
дальнейшего развития событий:
− если по введенным первым символам команда определяется однозначно, оболочка добавит окончание названия
команды в командную строку;
− если однозначно восстановить имя команды по введенным первым символам невозможно, оболочка выдаст
список всех подходящих вариантов продолжения для того, чтобы пользователь мог ввести оставшиеся символы
самостоятельно, пользуясь выведенным списком как подсказкой;
− если среди введенных символов уже есть пробел, оболочка решит, что вы ищете имя файла, который должен передаваться как параметр, и выдаст в качестве подсказки
список файлов текущего каталога, начинающихся с символов, следующих за пробелом.
Аналогичным образом можно получить список переменных
окружения, если вместо клавиши нажать комбинацию
клавиш +.
Оболочка Bash запоминает крайнюю тысячу набранных последовательностей символов и позволяет вызывать их путем


49

выбора из списка, называемого историей ввода. Историю ввода
можно просмотреть с помощью команды history (и воспользоваться
упомянутыми
ранее
комбинациями
клавиш
+ и + для просмотра результата). История ввода сохраняется в файле, полное имя которого
хранится в переменной окружения HISTFILE, (обычно
~/.bash_history). Для работы с историей ввода в оболочке Bash
используются следующие комбинации клавиш:
− или + – переход к предыдущей последовательности символов в списке;
− или + – переход к следующей
последовательности символов в списке;
− – переход к самой первой последовательности
символов в списке;
− , – выполнение (без нажатия клавиши ) Nой последовательности символов из списка, считая от
начала;
− , , – выполнение (без нажатия клавиши
) N-ой последовательности символов из списка,
считая от конца;
− , – выполнение первой найденной последовательности символов из списка, начинающейся на введенную строку (движение по списку осуществляется от
конца к началу);
− + – аналогично нажатию клавиши , но с
последующим отображением очередной последовательности символов из списка.

3.2. Инструментарий разработчика
Инструментарий разработчика, созданный в рамках проекта
GNU (англ. GNU Toolchain), является стандартным средством
разработки программ в любой операционной системе на базе
ядра Linux. Основу этого инструментария составляет набор
GNU-компиляторов (англ. GCC, GNU Compiler Collection, ранее
GNU C Compiler) для различных языков программирования, в
том числе Си, C++ и Fortran. В набор GNU-компиляторов входят
также компиляторы Java, Objective-C и Ada, однако эти языки
50

программирования не используются для высокопроизводительных вычислений.
Компиляция программ осуществляется с помощью утилиты
gcc, по традиции называемой компилятором, но фактически
являющейся интерфейсом целой системы компиляции, созданной проектом GNU. Выбор того или иного языка программирования обусловлен скорее личными предпочтениями разработчика и особенностями реализуемого алгоритма.
Инструментарий разработчика суперкомпьютера ИПУ РАН
поддерживает языки программирования Си, C++ и Fortran,
используемые во всех современных реализациях MPI для создания высокопроизводительных параллельных программ.
Создадим и запустим простую программу на Си. По сложившейся традиции первая программа будет выводить в консоль
приветствие «Здравствуй, мир!».
Файлы с кодами программ – это обычные текстовые файлы,
создавать их можно с помощью любого текстового редактора,
в т.ч. с помощью традиционного для GNU/Linux консольного
текстового редактора vi.
Создадим в домашней папке текстовый файл hello.c, содержащий следующий код на языке Си
#include
int main (int argc, char *argv[])
{
printf ("Здравствуй, мир!\n");
return (0);
}

Для компиляции программы наберите в консоли
> gcc hello.c

Если все сделано правильно, в домашней папке появится
новый исполняемый файл под названием a.out. Чтобы запустить
его наберите в консоли
> ./a.out

В ответ вы увидите следующее
51

Здравствуй, мир!

Компилятор gcc по умолчанию присваивает всем исполняемым файлам имя a.out. С помощью флага -o можно указать имя
исполняемого файла самостоятельно. Наберите в консоли
> gcc hello.c -o hello

В результате в текущей папке появится исполняемый файл с
именем hello. Чтобы запустить его наберите в консоли
> ./hello

Точка и слеш перед именем исполняемого файла означают,
что исполняемый файл размещен в текущей папке. Без указания
пути к исполняемому файлу запускаются только программы,
размещенные в системных папках и являющиеся частью операционной системы. Для запуска всех остальных программ, в т.ч.
пользовательских, необходимо указывать путь к исполняемому
файлу (абсолютный или относительно текущей папки), например
> /home/nfs/username/hello

или
> ~/hello

Работа системы компиляции состоит из четырех последовательно выполняемых этапов:
1. Препроцессинг (англ. preprocessing).
2. Компиляция (англ. compilation).
3. Ассемблирование (англ. assembly).
4. Компоновка (англ. linking).
Препроцессинг подключает к программному коду содержимое заголовочных файлов, указанных в директивах #include, и
заменяет макросы, определенные директивами #define, соответствующим программным кодом.
Во время компиляции происходит превращение исходного
кода программы на языке Си в промежуточный код на Ассемблере. Этот промежуточный шаг очень важен для дальнейшей
работы компилятора, потому что именно во время компиляции
происходит детальный разбор исходного кода и поиск синтакси52

ческих ошибок. Возможно, поэтому, а также из-за некоторой
исторически сложившейся терминологической путаницы, некоторые ошибочно полагают, что компиляция – это единственное,
чем занимается компилятор (система компиляции).
Во время ассемблирования происходит превращение ассемблерного кода программы в объектный код, очень близкий к
набору машинных команд. Результат сохраняется в объектных
файлах.
На этапе компоновки происходит создание исполняемого
файла путем связывания вызовов пользовательских и библиотечных функций с их объектным кодом, хранящимся в различных объектных файлах.
Для дальнейшей демонстрации работы инструментария разработчика GNU напишем небольшую программу-калькулятор.
Создадим в домашней папке текстовый файл calc.c, содержащий следующий код на языке Си
#include
int main (int argc, char *argv[])
{
float x, y, z;
char o;
printf ("X = ");
scanf ("%f", &x);
printf ("Y = ");
scanf ("%f", &y);
printf ("Операция ( + – * / ): ");
while ((o = getchar ()) != EOF)
if (o == '+')
{
z = x + y; break;
}
else if (o == '-')
{
z = x - y; break;
}
else if (o == '*')

53

{
z = x * y; break;
}
else if (o == '/')
{
z = x / y; break;
}
printf ("X %c Y = %f\n", o, z);
return (0);
}

Программа запрашивает у пользователя два числа и арифметическое действие, после чего печатает в консоль результат
вычислений.
Флаг -E прерывает работу системы компиляции после завершения препроцессинга. Наберите в консоли
> gcc -E calc.c -o calc.i

Название конечного файла calc.i желательно указывать, потому что результаты препроцессинга по умолчанию печатаются
в консоль. В файл calc.i будет добавлен код заголовочного файла
stdio.h, указанного в директиве #include. Файл stdio.h содержит
объявления встречающихся в программе функций ввода-вывода
printf, scanf и getchar. В файл calc.i также будут добавлены
некоторые теги, указывающие компилятору на способ связи с
объявленными функциями. Код программы из файла calc.c без
изменений будет добавлен в конец файла calc.i.
Описания стандартных функций и заголовочных файлов для
всех языков программирования из инструментария разработчика, точно также как и описания всех стандартных программ
операционной системы, можно получить с помощью команд man
и info. Наберите в консоли
> man printf

или
> info stdio.h

54

Флаг -S прерывает работу системы компиляции после завершения компиляции программы в ассемблерный код. Наберите в консоли
> gcc -S calc.c

Название конечного файла можно не указывать, потому что
компилятор gcc самостоятельно создаст объектный файл calc.s,
т.е. назовет его также как и файл с исходным кодом, но поставит
стандартное расширение для файла на Ассемблере.
Флаг -c прерывает работу системы компиляции после завершения ассемблирования. В результате получается объектный
файл, состоящий из набора машинных команд, но без связи
вызываемых функций с их определениями в подключаемых
библиотеках. Наберите в консоли
> gcc -c calc.c

Название конечного файла можно не указывать, потому что
компилятор gcc самостоятельно создаст объектный файл calc.o,
т.е. назовет его также как и файл с исходным кодом, но поставит
стандартное расширение для объектного файла.
Флаг -x сообщает компилятору с какого этапа следует
начать обработку файла. Например, для файла calc.i (полученного ранее с помощью флага -E), наберите в консоли
> gcc -x cpp-output -c calc.i

В данном случае параметры -x cpp-output сообщает компилятору, что файл calc.i содержит обработанный препроцессором
исходный код на языке Си (если указаны параметры -x c++ cpp
output – на языке C++), поэтому компилятор начнет обработку
файла сразу с этапа компиляции. Следует отметить, что расширение .i сообщает компилятору ту же информацию, что и параметры -x cpp output.
Компилятор gcc самостоятельно распознает языки программирования и этапы компиляции программы, основываясь на
расширениях имен файлов, указанных пользователем. В частности, по умолчанию считается, что файлы с расширениями .c
содержат исходный код на языке Си, файлы с расширениями .C,
.cc, .cp или .cpp содержат исходный код на языке C++, файлы с
55

расширениями .i и .ii содержат обработанный препроцессором
исходный код на языках Си и C++ соответственно и т. д. Файлы
с неизвестными расширениями считаются объектными файлами
и сразу попадают на этап компоновки.
Компилятор gcc позволяет проводить частичную обработку
файла путем указания начального этапа с помощью флага -x и
конечного этапа с помощью флагов -c, -S, и -E. Однако таким
образом нельзя изменить последовательность прохождения
этапов препроцессинга, компиляции, ассемблирования и компоновки. В частности, одновременное использование опций
-x -cpp-output -E не запустит ни один из этапов.
К сожалению, реальные программы очень редко состоят из
одного файла. Как правило, исходных файлов всегда несколько,
причем в некоторых случаях программу приходится компоновать из нескольких частей, написанных на разных языках программирования. В таких случаях принято говорить о программе
как о проекте. Компилятор gcc используется для последовательной обработки файлов проекта и последующей компоновки
объектных файлов в исполняемый файл программы.
Добавим в программу-калькулятор операцию возведения в
степень и разобьем код программы на несколько файлов:
− calculator.c – главное тело программы;
− calculate.c – расчетная функция;
− calculate.h – заголовочный файл.
Создадим в домашней папке текстовый файл calculator.c,
содержащий следующий код на языке Си
#include
#include "calculate.h"
int main (int argc, char *argv[])
{
float x, y, z;
char o;
printf ("X =
scanf ("%f",
printf ("Y =
scanf ("%f",

");
&x);
");
&y);

56

printf ("Операция ( + – * / ^ ): ");
while ((o = getchar ()) != EOF)
if (o == '+' || o == '-' ||
o == '*' || o == '/' || o == '^')
{
z = calculate (x, y, o); break;
}
printf ("X %c Y = %f\n", o, z);
return (0);
}

Создадим в домашней папке текстовый файл calculate.c, содержащий следующий код на языке Си
#include
#include "calculate.h"
float calculate (float x, float y, char o)
{
float z;
if (o == '+')
z = x + y;
else if (o == '-')
z = x – y;
else if (o == '*')
z = x * y;
else if (o == '/')
z = x / y;
else if (o == '^')
z = powf (x, y);
return (z);
}

Создадим в домашней папке текстовый файл calculate.h, содержащий следующий код на языке Си
float calculate (float x, float y, char o);

Создадим объектные файлы
calculate.o. Наберите в консоли
57

проекта

calculator.o

и

> gcc -c calculator.c
> gcc -c calculate.c

Наконец, скомпонуем исполняемый файл проекта calc.
Наберите в консоли
> gcc calculator.o calculate.o -lm -o calc

Флаг -lm подключает в проект на этапе компоновки стандартную библиотеку математических функций libm.so, содержащую объектный код функции возведения в степень powf,
вызываемой пользовательской функцией calculate () и объявленной в заголовочном файле math.h. Без подключения этой библиотеки исполняемый файл не будет скомпонован и программа
не заработает.
На суперкомпьютере ИПУ РАН объектные файлы стандартных библиотек компилятора располагаются в папке
/usr/lib64. Объектные файлы библиотеки ATLAS располагаются
в папке /opt/atlas/lib (соответствующие заголовочные файлы – в
папке /opt/atlas/include). Объектные файлы пробной версии
библиотеки
Intel
MKL
располагаются
в
папке
/opt/intel/mkl/10.2.5.035/lib/em64t (соответствующие заголовочные файлы – в папке /opt/intel/mkl/10.2.5.035/include).
Файлы библиотек с расширением .a называются статическими библиотеками. При компоновке объектный код функций
из статических библиотек включается в код исполняемого файла. Файлы библиотек с расширением .so называются динамическими библиотеками. При компоновке в исполняемом файле
размещаются только ссылки на динамические библиотеки,
реального включения объектного кода вызываемых функций не
происходит. Использование динамических библиотек ухудшает
переносимость исполняемого кода, но экономит ресурсы системы, позволяя нескольким программам использовать одну и ту же
библиотеку, загруженную в память.
Стандартные имена файлов библиотек состоят из префикса
lib, названия библиотеки и расширения .a или .so. В параметрах
запуска компилятора gcc префикс lib заменяется префиксом -l, а
расширение файла не указывается. Таким образом, файл libm.so
в параметрах запуска компилятора значится как -lm.
58

В системе имеются статические и динамические версии всех
стандартных библиотек. По умолчанию компилятор gcc подключает к исполняемому файлу динамические библиотеки. Для
компоновки исполняемого файла с использованием только
статических библиотек используется параметр -static. Однако
следует помнить, что некоторые библиотеки сами используют
функции из других библиотек и при статической компоновке
требуют явного указания всех используемых библиотек в параметрах запуска компилятора, что может представлять определенную сложность для неопытного разработчика.
Стандартные функции языка Си, такие как printf, находятся
в библиотеке libc.so, которая автоматически участвует в компоновке всех программ, написанных на языке Си, и не требует
упоминания в параметрах компилятора.
Для управления большим количеством файлов и компиляции больших программных проектов используется утилита make
из инструментария разработчика GNU.
Создадим в домашней папке текстовый файл Makefile, содержащий следующие строки
calc: calculator.o calculate.o
gcc calculator.o calculate.o -lm -o calc
calculator.o: calculator.c calculate.h
gcc -c calculator.c
calculate.o: calculate.c calculate.h
gcc -c calculate.c
clean:
rm -f calc calculator.o calculate.o
install:
cp calc ~/calc
uninstall:
rm -f ~/calc

Обратите внимание на отступ слева, который должен быть
сделан только с помощью символа табуляции.
Файл Makefile – это список действий (макросов, правил),
которые утилита make может проделывать над многочисленными файлами программного проекта.
Для компиляции проекта наберите в консоли
59

> make

или
> make calc

Для копирования исполняемого файла в домашний каталог
наберите в консоли
> make install

Для удаления исполняемого файла из домашнего каталога
наберите в консоли
> make uninstall

Утилита make самостоятельно определяет, какие из файлов
проекта требуют компиляции и, в случае необходимости, производит над ними действия, указанные в файле Makefile.
Для компиляции параллельных программ, использующих
реализацию интерфейса MPI MVAPICH2, используется утилита
mpicc. Утилита mpicc не является самостоятельным компилятором – это всего лишь небольшой скрипт оболочки Bash, запускающий компилятор gcc с параметрами подключения библиотеки функций MPI. Таким образом, компилятор mpicc использует
тот же набор флагов и опций, что и система компиляторов gcc.
Чтобы скомпилировать параллельную программу program.c
наберите в консоли
> mpicc program.c
Для запуска параллельных программ используется менеджер управления очередями и вычислительными ресурсами
SLURM. Параллельная программа может быть запущена на
суперкомпьютере ИПУ РАН только после постановки в очередь
с помощью утилиты srun. Наберите в консоли
> srun -n2 a.out

Параметр -n2 указывает менеджеру управления очередями,
что программа должна быть запущена на двух вычислительных
узлах, в результате чего будет создано два параллельных процесса. Количество вычислительных узлов для запуска параллельных программ на суперкомпьютере ИПУ РАН может изменяться в пределах от 2 до 96.
60

После постановки в очередь программа ждет освобождения
вычислительных узлов, которые могут быть заняты другими
программами. При наличии необходимого количества свободных вычислительных узлов программа, стоящая в очереди первой, немедленно уходит на исполнение.
Для постановки программы в очередь на исполнение и немедленный возврат в командную строку наберите в консоли
> srun -n2 a.out &

Для просмотра списка очередей используется утилита sinfo.
По умолчанию все программы ставятся в очередь debug.
Для просмотра выполняемых программ (этапов программы)
в очереди используется утилита squeue.
Утилита srun по умолчанию запускает экземпляр программы на каждом вычислительном узле (ядре процессора).

4. ОСНОВЫ ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ СИ
С ИСПОЛЬЗОВАНИЕМ ИНТЕРФЕЙСА MPI
Данный раздел посвящен написанию параллельных программ с использованием программного интерфейса передачи
сообщений MPI (англ. Message Passing Interface). На сегодняшний день MPI является наиболее распространенной технологией
параллельного программирования для суперкомпьютеров.
Большинство реализаций этого интерфейса поддерживают
стандарты MPI 1.1 и MPI-2.0. Стандарт MPI-2.0 появился в 1997
году и существенно расширил возможности стандарта 1.1, но в
течение долгого времени не был широко распространен. Тем не
менее, для написания параллельных программ на суперкомпьютере ИПУ РАН был выбран именно стандарт MPI-2.0. Большая
часть аспектов, которые будут описаны в данном разделе, является идентичной для обоих стандартов, поэтому далее будет
предполагаться, что мы имеем дело со стандартом MPI-2.0. Если
будет необходимо привести какие-то исключения для стандарта
1.1, то это будет оговорено дополнительно.
Существуют реализации MPI для языков Си, C++ и Fortran.
Примеры и описания всех функций даны с использованием
языка Си. Однако пользователи суперкомпьютера ИПУ РАН
61

имеют возможность компилировать и запускать программы
также на языках C++ и Fortran. Основные идеи и правила создания программ с использованием интерфейса MPI схожи для всех
языков. Дополнительную информацию о стандарте MPI можно
получить в Интернете на сайте MPI Forum.
Главное отличие параллельной программы, использующей
интерфейс MPI, от традиционной непараллельной программы
состоит в том, что в параллельной программе в один момент
времени могут выполняться несколько различных операций на
разных процессорах/ядрах. Разработчик должен создать программный код для каждого ядра. Стандарт MPI предполагает
возможность разработки приложений с уникальным кодом для
каждой из параллельных ветвей алгоритма, т.е. со своим кодом
для каждого ядра. Однако в большинстве случаев для всех ядер,
участвующих в работе приложения, используется один и тот же
программный код.
Таким образом, для создания параллельной программы достаточно написать программный код, в который будет включена
библиотека MPI и реализованы все необходимые механизмы
взаимодействия параллельных ветвей программы, откомпилировать этот программный код и обеспечить запуск исполняемого
файла на нескольких ядрах.
Прежде чем перейти к описанию принципов написания программ введем некоторые обозначения:
− процесс – это исполнение программы на одном процессоре, на котором установлен MPI, безотносительно к тому,
содержит ли эта программа внутри параллельные ветви
или операции ввода/вывода или просто последовательный программный код;
− группа – это совокупность процессов, каждый из которых
имеет внутри группы уникальное имя, используемое для
взаимодействия с другими процессами группы посредством коммуникатора группы;
− коммуникатор группы – интерфейс синхронизации и обмена данными между процессами.
Коммуникатор выступает для прикладной группы как коммуникационная среда взаимодействия. Коммуникаторы бывают
62

внутригрупповыми (intra) и межгрупповыми (inter). Коммуникатор определяет контекст передачи сообщений. Сообщения,
использующие разные коммуникаторы, не оказывают влияния
друг на друга и не взаимодействуют друг с другом. Каждая
группа процессов использует отдельный коммуникатор. Если в
группе n процессов, то процессы внутри группы имеют номера
от 0 до n – 1.
С точки зрения операционной системы процесс рассматривается как отдельное приложение, не взаимодействующее с
другими процессами (приложениями). С точки зрения разработчика процесс – это одна из ветвей параллельной программы,
которой необходимо обеспечить коммуникацию с другими
ветвями (процессами) параллельного приложения. Поскольку
зачастую все процессы одного приложения выполняют один и
тот же код, то приходится реализовывать взаимодействие этого
кода с самим собой с учетом его выполнения в разных процессах. Коммуникатор группы как раз и является той абстракцией,
которая обеспечивает взаимодействие процессов между собой.
При этом разработчика не должно интересовать, каким способом передается информация между процессами: заботу о передаче сообщений между процессами берет на себя интерфейс
MPI.

4.1. Инициализация MPI
Любая программа на языке Си, использующая MPI, должна
включать в себя заголовочный файл, в котором определены все
функции, переменные и константы MPI. Для подключения
библиотеки MPI необходимо внести в программу следующую
строку
#include "mpi.h"

Директива #include подключает заголовочный файл mpi.h
библиотеки MPI. Следует отметить, что при написании программ на языке C++ и использовании стандарта MPI-2.0 у пользователей могут возникнуть проблемы с подключением библиотеки MPI. На этапе компиляции программы компилятор может
вывести следующее сообщение об ошибке
63

SEEK_SET is #defined but must not be for the C++
binding of MPI

Причиной этого является то, что в заголовочном файле
стандартной библиотеки ввода/вывода stdio.h и в заголовочном
файле mpi.h для C++ объявлены одни и те же константы
SEEK_SET, SEEK_CUR и SEEK_END, что является ошибкой
реализации MPI-2.0.
Для решения проблемы необходимо при запуске компилятора использовать флаг DMPICH_IGNORE_CXX_SEEK или
подключать библиотеку MPI следующим образом
#undef SEEK_SET
#undef SEEK_END
#undef SEEK_CUR
#include "mpi.h"

После запуска параллельная программа должна инициализировать свою параллельную часть, т.е. подготовиться к работе с
другими ветвями параллельного приложения. Для этого используется функция MPI_Init.
int MPI_Init (
int *argc,
char ***argv
);

Параметры функции:
argc – число аргументов в командной строке, вызвавшей
программу;
− argv – указатель на массив символьных строк, содержащих эти аргументы.
Параметры обычно соответствуют аналогичным параметрам
исходной программы, но не обязательно.
Функция MPI_Init обеспечивает инициализацию параллельной части приложения. Реальная инициализация для каждого
приложения выполняется не более одного раза, а если интерфейс
MPI уже был инициализирован, то никакие действия не выполняются, а происходит немедленный выход из функции. Все
оставшиеся функции MPI могут быть вызваны только после
вызова MPI_Init.


64

Для успешного выполнения функции MPI_Init необходимо,
чтобы приложение имело информацию, необходимую для создания группы процессоров, которые будут участвовать в выполнении приложения. Обычно такая информация передается в
приложение при помощи специальных средств для запуска
программы, и разработчику не нужно заботиться об этом.
Для завершения параллельной части приложения используется функция MPI_Finalize.
int MPI_Finalize (void);

Функция MPI_Finalize обеспечивает завершение параллельной части приложения. Все последующие обращения к любым
функциям MPI, в том числе к MPI_Init, запрещены. К моменту
вызова MPI_Finalize некоторым процессом все действия, требующие его участия в обмене сообщениями, должны быть завершены.
Общая структура программы, использующей интерфейс
MPI, на языке Си выглядит следующим образом
int main (int argc, char *argv[])
{
MPI_Init (&argc, &argv);

MPI_Finalize ();
}

Иногда приложению необходимо выяснить, была ли уже
вызвана функция MPI_Init. Для этого используется функция
MPI_Initialized.
int MPI_Initialized
int *flag
);

(

Функция MPI_Initialized возвращает значение в переменную
flag. Если MPI_Init уже была вызвана, то flag имеет ненулевое
значение, в противном случае flag равна нулю. В стандарте MPI
1.1 это единственная функция, которую можно вызывать до
MPI_Init. Вызов функции MPI_Finalize не влияет на поведение
MPI_Initialized.
65

Приведенная ниже программа демонстрирует работу функций инициализации интерфейса MPI
#include
#include "mpi.h"
int main (int argc, char *argv[])
{
int flag;
MPI_Initialized (&flag);
printf ("MPI_Initialized вернула %d\n", flag);
MPI_Init (&argc, &argv);
MPI_Initialized (&flag);
printf ("MPI_Initialized вернула %d\n", flag);
MPI_Finalize ();
return (0);
}

Каждый из процессов этой программы печатает в консоль
сначала информацию о том, что функция MPI_Initialized вернула
значение 0, а затем 1. Обратите внимание, что сообщения каждый раз выводятся в хаотичном порядке. Это связано с тем, что
без использования функций синхронизации процессы работают
с разной скоростью.
Для работы над решением общей вычислительной задачи
группе процессов необходимо выяснить, над какой частью
задачи должен работать каждый процесс. Для этого каждый
процесс должен уметь идентифицировать себя в группе. Работа с
группами процессов будет рассмотрена далее, поэтому пока
ограничимся лишь общим представлением о группах процессов.
Каждый процесс характеризуется уникальной парой: группа
и номер процесса в группе. Каждый из процессов может принадлежать одной или нескольким группам, и в каждой из них
иметь свой номер. После выполнения функции MPI_Init создается базовая группа с коммуникатором MPI_COMM_WORLD,
содержащая все процессы приложения. Затем в рамках этой
группы могут создаваться новые группы, которые будут включать часть процессов. Процессы могут общаться в рамках одной
66

группы. Для самоидентификации процесса в группе служат
функции MPI_Comm_size и MPI_Comm_rank.
int MPI_Comm_size
MPI_Comm comm,
int *size
);

(

Параметры функции:
comm – коммуникатор группы;
size – размер группы.
Функция MPI_Comm_size определяет число процессов в
группе.



int MPI_Comm_rank (
MPI_Comm comm,
int *rank
);

Параметры функции:
comm – коммуникатор группы;
rank – номер вызывающего процесса в группе.
Функция MPI_Comm_rank определяет номер вызывающего
процесса в группе. Значение, возвращаемое по адресу &rank,
лежит в диапазоне от 0 до size – 1.
Во время выполнения параллельной программы может возникнуть необходимость экстренно завершить работу всей группы процессов. Для этих целей используется функция MPI_Abort.



int MPI_Abort (
MPI_Comm comm,
int errorcode
);

Параметры функции:
comm – коммуникатор группы;
errorcode – код ошибки, с которой завершится процесс.
Функция MPI_Abort прерывает процессы, ассоциированные
с коммуникатором comm, и возвращает управление внешней
среде. Прерванный процесс всегда завершает свою работу с
ошибкой, номер которой указан в параметре errorcode. Процесс,



67

вызывающий функцию MPI_Abort, должен быть членом группы
с указанным коммуникатором. Если указанного коммуникатора
группы не существует или вызывающий процесс не является ее
членом, то прерывается вся базовая группа процессов
MPI_COMM_WORLD.
Приведенная ниже программа демонстрирует работу функций идентификации и прерывания параллельных процессов
#include
#include "mpi.h"
int main (int argc, char *argv[])
{
int size, me;
MPI_Init (&argc, &argv);
MPI_Comm_size (MPI_COMM_WORLD, &size);
MPI_Comm_rank (MPI_COMM_WORLD, &me);
printf ("Размер группы %d, мой номер %d\n",
size, me);
printf ("Вызываю MPI_Abort\n");
MPI_Abort (MPI_COMM_WORLD, 911);
printf ("MPI_Abort была вызвана\n");
MPI_Finalize();
return (0);
}

Во время выполнения этой программы каждый из процессов
печатает в консоль размер своей группы и свой номер в группе.
Обратите внимание, что процессы выводят сообщения не по
порядку своих номеров. Ни один из процессов не сообщает о
завершении вызова функции MPI_Abort.

4.2. Прием/отправка сообщений с блокировкой
Помимо инициализации и идентификации все параллельные
процессы должны обмениваться информацией друг с другом.
При этом обмен информацией должен происходить не только до
начала вычислений, но и в процессе вычислений, и после их
завершения.
68

Обеспечение информационного обмена между процессами –
основная задача интерфейса MPI. Для разных видов коммуникации MPI предлагает множество различных функций.
Во время коммуникации типа «точка-точка» с блокировкой
соответствующие функции останавливают вызывающий процесс
до тех пор, пока не будет завершена передача сообщения.
Для отправки сообщений используется функция MPI_Send.
int MPI_Send (
void *buf,
int count,
MPI_Datatype datatype,
int dest,
int msgtag,
MPI_Comm comm
);

Параметры функции:
− buf – отправляемое сообщение;
− count – число элементов в отправляемом сообщении;
− datatype – тип элементов;
− dest – номер процесса-получателя;
− msgtag – идентификатор сообщения;
− comm – коммуникатор группы.
Функция MPI_Send осуществляет блокирующую отправку
сообщения с идентификатором msgtag, состоящего из count
элементов типа datatype, процессу с номером dest в группе с
коммуникатором comm. Все элементы сообщения содержатся в
буфере buf. Значение count может быть нулевым. Тип передаваемых элементов datatype должен указываться с помощью предопределенных констант из библиотеки типов.
Например, вызов функции MPI_Send, отправляющей три
элемента массива целого типа int нулевому процессу в группе с
коммуникатором MPI_COMM_WORLD выглядит следующим
образом
MPI_Send (&buf, 3, MPI_INT, 0, 1, MPI_COMM_WORLD);

В примере использован тип данных MPI_INT. Он соответствует типу int в языке Си (подробнее типы данных рассмотрены
69

далее). Значение идентификатора сообщения выбрано произвольным образом. Процессу разрешается отправлять сообщения
самому себе.
Блокировка гарантирует корректность повторного использования всех параметров после возврата из функции MPI_Send.
Выбор способа реализации такой гарантии: копирование сообщения в промежуточный буфер или непосредственная передача
сообщения процессу dest, остается за конкретной реализацией
интерфейса MPI.
Следует отметить, что возврат из функции MPI_Send не
означает, что сообщение передано процессу dest или уже покинуло вычислительный узел, на котором выполняется процессотправитель, вызвавший функцию MPI_Send.
Для приема сообщения используется функция MPI_Recv:
int MPI_Recv (
void *buf,
int count,
MPI_Datatype datatype,
int source,
int msgtag,
MPI_Comm comm,
MPI_Status *status
);

Параметры функции:
− buf – принимаемое сообщение;
− count – максимальное число элементов в принимаемом
сообщении;
− datatype – тип элементов;
− source – номер процесса-отправителя;
− msgtag – идентификатор сообщения;
− comm – коммуникатор группы;
− status – параметры принятогосообщения.
Функция MPI_Recv осуществляет блокирующий прием сообщения с идентификатором msgtag, состоящего максимум из
count элементов типа datatype, от процесса с номером dest в
группе с коммуникатором comm.
70

Число элементов в принимаемом сообщении не должно
превышать значения count. Если число элементов в принимаемом сообщении меньше значения count, то гарантируется, что в
буфере buf будут размещены только принимаемые элементы, т.е.
функция MPI_Recv не очищает буфер. Для определения точного
числа элементов в принимаемом сообщении можно воспользоваться функцией MPI_Probe.
Блокировка гарантирует, что после возврата из функции
MPI_Recv все элементы сообщения уже приняты и размещены в
буфере buf.
В качестве номера процесса-отправителя можно указать
MPI_ANY_SOURCE – признак готовности принять сообщение от
любого процесса. В качестве идентификатора принимаемого
сообщения можно указать MPI_ANY_TAG – признак готовности
принять сообщение с любым идентификатором.
Если процесс-отправитель успел отправить несколько сообщений процессу-получателю, то однократный вызов функции
MPI_Recv примет только одно сообщение, отправленное раньше
других.
Например, вызов функции MPI_Recv, принимающей три
элемента массива целого типа int от первого процесса в группе с
коммуникатором MPI_COMM_WORLD выглядит следующим
образом
MPI_Recv (&buf, 3, MPI_INT, 1,
MPI_ANY_TAG, MPI_COMM_WORLD, &status);

Структура status типа MPI_Status (подробно рассматривается далее) содержит параметры принятого сообщения.
Пользуясь функциями отправки/получения сообщений с
блокировкой, следует соблюдать осторожность. Как отмечалось
ранее, в зависимости от реализации MPI сообщения отправляются напрямую или записываются в промежуточный буфер.
Поэтому в некоторых реализациях MPI процессы могут блокировать друг друга. Если двум процессам необходимо одновременно отправить друг другу некоторый объем данных, и при
этом сообщения пересылаются напрямую, минуя буфер, то оба
процесса одновременно дойдут до функции отправки сообщения
и заблокируются на ней, ожидая пока получатель дойдет до
71

функции получения сообщения. Однако ни один из процессов
никогда не дойдет до функции получения сообщения, поскольку
каждый процесс заблокирован на этапе отправки, программа
остановится. В некоторых реализациях MPI подобные ситуации
отслеживаются с помощью промежуточного буфера, но разработчик должен самостоятельно исключать взаимную блокировку
в коде программы для обеспечения полноценной переносимости
кода.
Функция MPI_Sendrecv объединяет функционал приема и
отправки сообщений.
int MPI_Sendrecv (
void *sbuf,
int scount,
MPI_Datatype stype,
int dest,
int stag,
void *rbuf,
int rcount,
MPI_Datatype rtype,
int source,
MPI_DAtatype rtag,
MPI_Comm comm,
MPI_Status *status
);

Параметры функции:
sbuf – отправляемое сообщение;
scount – число элементов в отправляемом сообщении;
stype – тип элементов в отправляемом сообщении;
dest – номер процесса-получателя;
stag – идентификатор отправляемого сообщения;
rbuf – принимаемое сообщение;
rcount – максимальное число элементов в принимаемом
сообщении;
− rtype – тип элементов в принимаемом сообщении;
− source – номер процесса-отправителя;
− rtag – идентификатор принимаемого сообщения;
− comm – коммуникатор группы;








72

status – параметры принятого сообщения.
Функция MPI_Sendrecv отправляет и принимает сообщения.
С помощью этой функции процессы могут отправлять сообщения самим себе. Сообщение, отправленное функцией
MPI_Sendrecv, может быть принято функцией MPI_Recv, и точно
также функция MPI_Sendrecv может принять сообщение, отправленное функцией MPI_Send. Буферы приема и отправки
сообщений обязательно должны быть различными.
Если процесс ожидает получения сообщения, но не знает
его параметров, можно воспользоваться функцией MPI_Probe.


int MPI_Probe (
int source,
int msgtag,
MPI_Comm comm,
MPI_Status *status
);

Параметры функции:
source

номер
процесса-отправителя
или
MPI_ANY_SOURCE;
− msgtag – идентификатор принимаемого сообщения или
MPI_ANY_TAG;
− comm – коммуникатор группы;
− status – параметры принимаемого сообщения.
Функция MPI_Probe получает параметры принимаемого сообщения с блокировкой процесса. Возврата из функции не происходит до тех пор, пока сообщение с подходящим идентификатором и номером процесса-отправителя не будет доступно для
приема. Параметры принимаемого сообщения определяются
обычным образом с помощью параметра status. Функция
MPI_Probe отмечает только факт получения сообщения, но
реально его не принимает.
Для определения количества уже полученных сообщений
используется функция MPI_Get_Count.


int MPI_Get_Count (
MPI_Status *status,
MPI_Datatype datatype,

73

int *count
);

Параметры функции:
status – параметры сообщения;
datatype – тип элементов;
count – число элементов в сообщении.
Функция MPI_Get_Count определяет по значению параметра status число уже принятых (после обращения к MPI_Recv) или
принимаемых (после обращения к MPI_Probe или MPI_IProbe)
элементов сообщения типа datatype.
Приведенная ниже программа, где каждый процесс n считает сумму чисел от n × 10 + 1 до (n + 1) × 10, иллюстрирует
работу функций приема/отправки сообщений с блокировкой




#include
#include "mpi.h"
int main (int argc, char *argv[])
{
int i;
int size, me;
int sum;
MPI_Status status;
MPI_Init (&argc, &argv);
MPI_Comm_size (MPI_COMM_WORLD, &size);
MPI_Comm_rank (MPI_COMM_WORLD, &me);
sum = 0;
for (i=me*10+1; i 1)
{
printf ("Процесс 0, сумма = %d\n", sum);
for (i=1; i 1)
{
if (me == 0)
{
MPI_Send_init (buffer, 3, MPI_INT, 1,
1, MPI_COMM_WORLD, &request);
for (i=0; i