Использование PVM.
Введение
в программирование.


Оглавление

Введение

Данное пособие предназначено для первоначального ознакомления с программированием параллельных вычислительных процессов в рамках программной системы PVM (Parallel Virtual Machine). Параллельную виртуальную машину можно определить как часть средств реального вычислительного комплекса (процессоры, память, периферийные устройства и т.д.), предназначенную для выполнения множества задач, участвующих в получении общего результата вычислений. В общем случае число задач может превосходить число процессоров, включенных в PVM. Кроме того, в состав PVM можно включать довольно разнородные вычислительные машины, несовместимые по системам команд и форматам данных. Иначе говоря, Параллельной Виртуальной Машиной может стать как отдельно взятый ПК, так и локальная сеть, включающая в себя суперкомпьютеры с параллельной архитектурой, универсальные ЭВМ, графические рабочие станции и все те же маломощные ПК. Важно лишь, чтобы о включаемых в PVM вычислительных средствах имелась информация в используемом программном обеспечении PVM. Благодаря этому программному обеспечению пользователь может считать, что он общается с одной вычислительной машиной, в которой возможно параллельное выполнение множества задач.

Функционирование PVM существенно опирается на возможность обмена информацией между задачами, выполняемыми в ней. В этом отношении наиболее удобно реализовывать PVM в рамках многопроцессорного вычислительного комплекса, выделив виртуальной машине несколько процессоров и общее или индивидуальные (в зависимости от условий) ОЗУ. При этом, как правило, значительно упрощаются проблемы быстрого информационного обмена между задачами в PVM, а также проблемы согласования форматов представления данных между задачами, выполняемыми на разных процессорах.

Главная цель использования PVM - это повышение скорости вычислений за счет их параллельного выполнения. Верхняя граница желаемого эффекта оценивается очень просто - если для вычислений мспользовать N однотипных процессоров вместо одного, то время вычислений уменьшится не более чем в N раз. Реальный выигрыш зависит, во-первых, от специфики задачи и во-вторых, насколько полно учтены в программе вычислений специфика задачи и характеристики аппаратных и программных средств PVM (см. подраздел 1.1).

В пособии материал распределен по четырем разделам. В первом разделе кратко рассмотрены некоторые вопросы организации параллельных вычислений и приведен минимальный набор сведений, необходимых для работы с PVM. Во втором разделе рассматриваются некоторые важные функции, предназначенные для применения в пользовательских программах и обеспечивающие взаимодействие задач в PVM. Материал излагается применительно к языку программирования Си. В третьем разделе приведены 2 примера программ, иллюстрирующих применение функций, специфических для PVM. Четвертый раздел - справочный. Он содержит краткие сведения по использованию PVM на компьютере HP SPP-1600, установленном в Центре Суперкомпьютерных Технологий, и на персональных компьютерах IBM PC под управлением Windows NT или Windows 95.

Материал данного пособия основан на переводе нескольких разделов книги
"PVM: Parallel Virtual Machine. A User's Guide and Tutorial for Networked Parallel Computing" (авторы: Al Geist, Adam Beguelin, Jack Dongarra, Weicheng Jiang, Robert Manchek, Vaidy Sunderam). The MIT Press. Cambridge, Massachusetts. London, England.

Через Интернет ее можно получить в одном из двух видов: в гипертекстовом или в виде документа PostScript.
Важным источником информации о PVM является сервер Oak Ridge National Laboratory. В частности, на нем в гипертекстовом виде хранятся "Manual pages" (справочная документация) текущей версии PVM.
Кроме того, на поисковом сервере Yahoo существует раздел, специально посвященный PVM.


1. Общие сведения

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

1.1. Оптимизация параллельных вычислений

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

Пусть требуется вычислить величины

Y1 = A * B + C * D
Y2 = (A + B) * C + D.

При использовании одного процессора Y1 и Y2 будут вычислены за одинаковое время 3T. При использовании двух процессоров Y1 может быть вычислено за время 2T, т.к. операции умножения могут быть выполнены параллельно. А вычисление Y2 не может быть ускорено, т.к. сама формула не допускает параллельного выполнения операций.

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

Теперь обратим внимание на то, что в рассмотренном примере были проигнорированы затраты времени на пересылки данных между процессорами. Это отчасти правомерно, если процессоры используют общую память (следует отметить, что все процессоры имеют индивидуальную регистровую память гораздо более быстродействующую, чем ОЗУ). Если же процессоры работают с данными, хранящимися в индивидуальных ОЗУ, то обмен данными между этими ОЗУ снижает эффект от распараллеливания вычислений вплоть до того, что этот эффект может стать отрицательным. Отсюда следует, что при оптимизации распараллеливания вычислений нужно учитывать время, затрачиваемое на обмен данными между процессорами. И вообще при прочих равных условиях уменьшение числа и объемов сообщений, которыми обмениваются параллельно работающие процессоры, как правило, приводит к сокращению общего времени решения задачи. Поэтому желательно распараллеливать вычислительный процесс на уровне как можно более крупных блоков алгоритма.

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

Пусть каждые T секунд для вычисления очередного значения Y1 поступает новая порция исходных данных: A, B, C, D - и каждые T секунд процессор 1 и процессор 2 параллельно выполняют операции умножения, а процессор 3 в это время складывает произведения, полученные в предыдущие T секунд. Очевидно, что с увеличением массива исходных данных эффект от распараллеливания стремится к предельному значению. Более того, почти такой же эффект получается при вычислении Y2. Правда, здесь каждая порция исходных данных будет преобразовываться в результат уже в течение 3T секунд, зато в работе одновременно будут находиться не 2, а 3 порции исходных данных.

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

1.2. Особенности организации параллельных вычислений

Как уже отмечалось, при параллельных вычислениях необходимо программировать специальные действия по координации работы задач, а также следует четко определить "область деятельности" для каждой задачи. Рассмотрим здесь несколько возможных подходов для решения этих проблем.

При первом варианте организации параллельных вычислений все задачи запускаются одной командой, в которой указывается имя запускаемого исполняемого файла, число запускаемых задач, а также число и тип используемых процессоров. По этой команде запускаются на указанных процессорах требуемое число копий указанного исполняемого файла. Следовательно, программные коды всех запущенных в PVM задач в данном случае одинаковы. Для того чтобы эти задачи могли выполнять разные действия, им должен быть известен признак, отличающий каждую задачу от остальных. Тогда, используя этот признак в условных операторах, легко запрограммировать выполнение задачами разных действий. Наличие такого признака предусмотрено в любой многозадачной операционной системе. Это так называемый идентификатор задачи - целое число, которое присваивается задаче при запуске. Существенно, что при запуске задача получает идентификатор, отличный от идентификаторов задач, выполняемых в данное время. Это гарантирует, что идентификаторы всех запущенных задач в PVM будут различными. Если теперь обеспечить задачи возможностью определять собственный идентификатор и обмениваться информацией с другими задачами, то ясно, что им легко распределить между собой вычислительную работу, в зависимости, например, от занимаемого места в упорядоченном наборе идентификаторов задач.

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


2. Взаимодействие задач в PVM

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

В системе PVM_3 каждая задача (которой соответствует исполняемый файл), запущенная на некотором процессоре, идентифицируется целым числом, которое называется идентификатором задачи (далее используется обозначение TID ) и по смыслу похоже на идентификатор процесса в операционной системе Unix. Конкретные значения TID несущественны, важно лишь, чтобы все задачи, запущенные в PVM, имели различные TID. Отметим здесь, что копии одного исполняемого файла, запущенные параллельно на N процессорах PVM, создают N задач с разными TID.

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

Для повышения эффективности межзадачного обмена информацией предусмотрено использование нескольких алгоритмов. В частности, можно использовать алгоритм блокированной передачи, при котором функция "Послать сообщение" возвращает значение (т.е. завершает работу) только после того как получена положительная или отрицательная квитанция от получателя сообщения. Такой алгоритм передачи с ожиданием уведомления о доставке предпочтителен в тех случаях, когда длинное сообщение передается несколькими порциями, а также при обмене командами, последовательность выполнения которых во времени дорлжна быть строго фиксированной.

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

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

Память для буферных массивов на передающей и приемной стороне выделяется динамически, следовательно, максимальный объем сообщений ограничивается только объемом доступной памяти. Если одна из задач, запущенных в PVM, не может получить требуемую память для общения с другими задачами, то она выдает пользователю соответствующее сообщение об ошибке ("cannot get memory"), но другие задачи об этом событии не извещаются и могут, например, продолжать посылать ей сообщения.

Кроме обмена сообщениями между двумя задачами в PVM предусмотрены возможность широковещательной передачи сообщений от одной задачи к нескольким другим задачам, а также возможности синхронизации действий в группе задач и совместное выполнение задачами группы некоторых операций над распределенными данными (фрагменты обрабатываемых данных распределены между задачами - членами группы).

2.1. Управление задачами

Здесь приводится краткое описание функций, которые позволяют определять идентификатор задачи ( TID ), а также запускать задачи и завершать их выполнение.

   int  tid = pvm_mytid ( void );

- возвращает идентификатор задачи tid >= 0. Эта функция может вызываться более одного раза, но в первый раз она, как правило, вызывается в самом начале программы, т.к. значением tid может определяться выбор для выполнения той или иной части программы.

   int  numt = pvm_spawn (
      char *task,  /* имя исполняемого файла                   */
      char **argv, /* аргументы командной строки               */
      int  flag,   /* опции запуска                            */
      char *where, /* указывает место запуска                  */
      int  ntask,  /* число запускаемых копий программы        */
      int  *tids   /* массив значений tid для запущенных задач */
   );

- запускает в PVM ntask копий исполняемого файла с именем "task" с одинаковыми аргументами командной строки в массиве argv и возвращает число запущенных задач numt а также последовательность идентификаторов для запущенных задач. Причем, если numt < ntask, то в последних ntask - numt элементах массива tids записаны отрицательные коды ошибок, объясняющие срыв запуска задачи.

Замечание 1: исполняемый файл для функции pvm_spawn() должен находиться в строго определенном каталоге. Под Юниксом задача ищется в каталогах $PVM_ROOT/bin/$PVM_ARCH/ и $HOME/pvm3/bin/$PVM_ARCH. Под Windows алгоритм поиска более замысловатый, читайте о нем в разделе 4.1. Задавать имя каталога в параметре "task" недопустимо (или, по крайней мере, нежелательно).

Замечание 2: исполняемый файл ищется (и запускается) не только на том компьютере, на котором работает вызвавшая pvm_spawn() задача, но в зависимости от параметров flag и where, на любом входящем в состав PVM. Например, если flag==0, то PVM сам выбирает, на какой из машин запускать новые задачи (главное, чтобы приложение было скомпилировано на этих машинах); а если flag & PvmMppFront > 0, то местом запуска будет выбран самый быстрый компьютер.

Значением параметра flag задается набор опций для запускаемых задач. Каждой опции сответствует целое неотрицательное число - вес опции, и значение flag равно сумме весов выбранных опций. Ниже перечисляются опции запуска задач, кратко раскрывается их содержание и в скобках указаны их веса.

   PvmTaskDefault ( 0 ) - Pvm выбирает по умолчанию, где запустить задачи;
   PvmTaskHost    ( 1 ) - задачи запускаются на машине, тип которой
                          указан в параметре where;
   PvmTaskArch    ( 2 ) - задачи запускаются на комплексе, архитектура
                          которого указана в параметре where;
   PvmTaskDebug   ( 4 ) - задачи стартуют под отладчиком;
   PvmTaskTrace   ( 8 ) - при выполнении генерируются результаты трассировки;
   PvmMppFront   ( 16 ) - задачи стартуют на MPP системе;
   PvmHostCompl  ( 32 ) - дополнение к информации в параметре where.

При выборе значений параметров flag и "where" пользователю целесообразно проконсультироваться с программистом, которому известны особенности установки системы PVM на используемом вычислительном комплексе.

Пример 1.

   numt = pvm_spawn ( "host", 0, PvmTaskHost, "sparky", 2, &tid[0] );

- запускаются 2 копии исполняемого файла "host" без параметров в командной строке на вычислительном комплексе типа "sparky"; возвращаемые идентификаторы задач записаны в массиве tid.

Пример 2.

   numt = pvm_spawn ( "node", argv, PvmTaskArch + PvmTaskDebug,
                      "RIOS", 2, &tid[5] );

- запускаются 2 копии исполняемого файла "node" с параметрами командной строки в массиве argv на любом включенном в состав PVM вычислительном комплексе с архитектурой типа "RIOS" с использованием отладчика; возвращаемые идентификаторы задач записаны в tid[5], tid[6].

Пример 3.

   numt = pvm_spawn ( "node", 0, 0, 0, 4, tids );

- запускаются 4 копии исполняемого файла "node" без параметров командной строки с автоматическим выбором используемых вычислительных средств для выполнения задач; возвращаемые идентификаторы задач записаны в массиве tid.

   int  info = pvm_kill ( int  tid );

- завершает выполнение задачи с идентификатором tid; при ошибке возвращает код ошибки info < 0. Отметим, что задача не может "убить" себя, поэтому один из возможных сценариев завершения многозадачной работы PVM заключается в том, что одна из задач "убивает" все остальные, после чего вызывает функцию

   int  info = pvm_exit ( void );

которая завершает работу PVM, запущенной пользователем, но при этом сама задача продолжает выполняться уже как объект локальной операционной системы и завершает работу, как обычно.

2.2. Обмен сообщениями между задачами

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

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

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

2.2.1. Функции для работы с буферами

   int  bufid = pvm_initsend ( int encoding );

- возвращает значение, равное либо идентификатору буфера (bufid > 0), либо коду ошибки (bufid < 0). Параметр encoding принимает следующие значения:

Функцию pvm_initsend() следует вызывать каждый раз перед упаковкой и передачей нового сообщения.

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

   int  bufid = pvm_mkbuf ( int encoding );

- создает новый пустой передающий буфер с идентификатором bufid и со способом кодирования-упаковки данных, определяемым значением параметра encoding. При ошибке bufid < 0 - код ошибки.

   int  info = pvm_freebuf ( int bufid );

- уничтожает буфер с идентификатором bufid. При ошибке info < 0.

   int  bufid = getsbuf ( void );

- возвращает идентификатор активного передающего буфера или код ошибки при bufid < 0.

   int  bufid = getrbuf ( void );

- возвращает идентификатор активного приемного буфера или код ошибки при bufid < 0.

   int  oldbuf = pvm_setsbuf ( int bufid );

- переключает активность с передающего буфера oldbuf на передающий буфер bufid, сохраняя состояние буфера oldbuf.

   int  oldbuf = pvm_setrbuf ( int bufid );

- переключает активность с приемного буфера oldbuf на приемный буфер bufid, сохраняя состояние буфера oldbuf.

Если в функциях pvm_setsbuf(...) и pvm_setrbuf(...) параметр bufid = 0, то состояние активного буфера сохраняется, а активность с него снимается, но не передается другому буферу. Это позволяет защитить временно неиспользуемый буфер от сообщений, которые могут порождаться некоторыми системными и библиотечными модулями, использующими тот же механизм обмена сообщениями.

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

2.2.2. Функции для упаковки сообщений

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

   int  info = pvm_pkbyte   ( char   *xp, int nitem, int stride );
   int  info = pvm_pkcplx   ( float  *cp, int nitem, int stride );
   int  info = pvm_pkdcplx  ( double *zp, int nitem, int stride );
   int  info = pvm_pkdouble ( double *dp, int nitem, int stride );
   int  info = pvm_pkfloat  ( float  *fp, int nitem, int stride );
   int  info = pvm_pkint    ( int    *ip, int nitem, int stride );
   int  info = pvm_pkuint   ( unsigned int  *ip, int nitem, int stride );
   int  info = pvm_pkshort  ( short  *ip, int nitem, int stride );
   int  info = pvm_pkushort ( unsigned short  *ip, int nitem, int stride );
   int  info = pvm_pklong   ( long   *ip, int nitem, int stride );
   int  info = pvm_pkulong  ( unsigned long *ip, int nitem, int stride );
   int  info = pvm_pkstr    ( char   *cp, int nitem, int stride );

Для перечисленных функций возвращаемое значение info либо равно 0, либо ( при info < 0 ) является кодом ошибки. Функция pvmpkstr(...) упаковывает строку символов, начиная с адреса cp и кончая первым встреченным символом NULL ('\0').

Последовательное применение указанных функций позволяет упаковывать в передающем буфере сообщение, содержащее несколько массивов разных типов. Для упаковки данных сложного типа (например, типа struct) нужно последовательно упаковать все фрагменты, принадлежащие к указанным типам. К сожалению, функции типа sprintf(), которая принимала бы строковый параметр с описанием упаковываемых типов, и позволяла бы за один прием упаковывать структуры и юнионы, в 3 редакции PVM нет.

2.2.3. Функции для передачи и приема сообщений

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

   int  info = pvm_send ( int tid, int msgtag );

- присваивает сообщению идентификатор msgtag и посылает сообщение задаче с идентификатором tid.

   int  info = pvm_mcast ( int *tids, int ntask, int msgtag );

- присваивает сообщению идентификатор msgtag и посылает его задачам, идентификаторы которых перечислены в ntask элементах массива tids.

   int  info = pvm_psend ( int tid, int msgtag, void *vp, int cnt, int type )

- упаковывает cnt элементов массива vd типа type в сообщение, присваивает ему идентификатор msgtag и посылает его задаче с идентификатором tid. Эта функция позволяет обходиться без функций упаковки, если сообщение содержит лишь один массив простого типа. Для значений параметра type используются следующие символические имена: PVM_STR, PVM_BYTE, PVM_SHORT, PVM_INT, PVM_LONG, PVM_USHORT, PVM_UINT, PVM_ULONG, PVM_FLOAT, PVM_DOUBLE, PVM_CPLX, PVM_DCPLX.

Перечисленные функции возвращают либо значение info = 0, либо код ошибки info < 0.

Для функций приема сообщений возможны следующие вариации значений параметров. Если tid = -1, то принимается любое сообщение с идентификатором msgtag, направленное к данной задаче. Если msgtag = -1, то принимается любое сообщение, направленное к данной задаче задачей с идентификатором tid. Если tid = msgtag = -1, то принимается любое сообщение от любой задачи, направленное к данной задаче.

   int  bufid = pvm_recv ( int tid, int msgtag );

- осуществляет блокированный прием, т.е. ожидает поступления сообщения с идентификатором msgtag от задачи с идентификатором tid, после чего создает новый приемный буфер с идентификатором bufid, помещает в него принятое сообщение и завершает работу.

   int  bufid = pvm_nrecv ( int tid, int msgtag );

- осуществляет неблокированный прием, т.е. при отсутствии сообщения завершает работу, возвращая значение bufid = 0. Если сообщение с заданными значениями tid и msgtag поступило, то оно помещается в созданный для него приемный буфер, идентификатор которого является возвращаемым значением. В процессе ожидания сообщения эту функцию можно вызывать несколько раз, заполняя промежутки времени между вызовами какой-либо полезной работой.

   int  bufid = pvm_trecv ( int tid, int msgtag, struct timeval *tmout );

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

Когда заданное время ожидания tmout = 0, функция pvm_trecv(...) осуществляет неблокированный прием, а когда указатель на tmout равен NULL (соответствует бесконечному времени ожидания), осуществляется блокированный прием, в остальных же случаях реализуется промежуточный алгоритм приема сообщений.

   int  bufid = pvm_probe ( int tid, int msgtag );

- проверяет, поступило ли ожидаемое сообщение. Если оно поступило, то возвращается идентификатор приемного буфера bufid > 0, если сообщение еще не поступило, то возвращается значение bufid = 0.

   int  info = pvm_bufinfo ( int bufid, int nbytes, int msgtag, int tid );

- возвращает значения tid, msgtag и длину в байтах nbytes для сообщения в буфере с идентификатором bufid. Эту функцию целесообразно использовать в сочетании с приемом сообщений при неопределенных значениях tid = -1 и/или msgtag = -1.

2.2.4. Распаковка данных из принятого сообщения

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

   char cp[5];  int np[3];  double  dp[7];  string  s[];

Тогда сообщение упаковывается следующим образом:

   info = pvm_pkbyte   ( cp, 5, 1 );
   info = pvm_pkint    ( np, 3, 1 );
   info = pvm_pkdouble ( dp, 7, 1 );
   info = pvm_pkstr    ( s );

и после приема в другой задаче делается следующая распаковка

   char cp[];  int np[];  double  dp[];  string  s[];

   info = pvm_upkbyte   ( cp, 5, 1 );
   info = pvm_upkint    ( np, 3, 1 );
   info = pvm_upkdouble ( dp, 7, 1 );
   info = pvm_upkstr    ( s );

при условии, что для массивов cp, np, dp, s выделена память.

Перечень функций для распаковки данных в принятом сообщении:

   int  info = pvm_upkbyte   ( char   *xp, int nitem, int stride );
   int  info = pvm_upkcplx   ( float  *cp, int nitem, int stride );
   int  info = pvm_upkdcplx  ( double *zp, int nitem, int stride );
   int  info = pvm_upkdouble ( double *dp, int nitem, int stride );
   int  info = pvm_upkfloat  ( float  *fp, int nitem, int stride );
   int  info = pvm_upkint    ( int    *ip, int nitem, int stride );
   int  info = pvm_upkshort  ( short  *ip, int nitem, int stride );
   int  info = pvm_upklong   ( long   *ip, int nitem, int stride );
   int  info = pvm_upkstr    ( char   *cp, int nitem, int stride );

Перечисленные функции возвращают значение либо info = 0, либо код ошибки info < 0.

2.2.5. Групповые функции

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

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

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

У всех групповых функций имеется параметр char *group, который либо указывает на строку с именем группы, либо является строковой константой.

   int  inum = pvm_joingroup ( char *group );

- присоединяет задачу к группе "group" и присваивает ей порядковый номер в этой группе, равный inum >= 0. При ошибке inum < 0.

   int  info = pvm_lvgroup ( char *group );

- выводит задачу из группы "group". При ошибке info < 0.

   int  tid = pvm_gettid ( char *group, int inum );

- возвращает идентификатор задачи, которая в группе "group" имеет номер inum. При ошибке tid < 0.

   int  inum = pvm_getinst ( char *group, int tid );

- возвращает для задачи с идентификатором tid ее порядковый номер в группе "group". При ошибке inum < 0.

   int  size = pvm_gsize ( char *group );

- возвращает число задач в группе "group". При ошибке size < 0. Отметим, что последние три функции могут вызываться любыми задачами в PVM, а не только членами группы "group".

Рассмотренные функции позволяют организовывать обмен сообщениями между задачами, которые априори не знают идентификаторов друг друга (напоминаем, что при передаче сообщения нужно указывать, как правило, идентификатор задачи получателя). Пусть, например, N процессоров решают одну и ту же математическую математическую задачу разными способами, и процессор, оказавшийся вторым призером, должен послать результаты своей работы ( для конкретности примем, что это массив из N элементов массива results типа float, причем значение N априори неизвестно получателю ) процессору, который решил задачу первым, для сверки и дальнейшей обработки. В этой ситуации удобно использовать группу с названием "финал", в которой первый и второй призеры должны будут получить номера соответственно 0 и 1. При этом, в текст программы (которая является общей для всех задач) следует после основной части включить следующие инструкции:


   /* присоединиться к группе "final", получив номер в группе inum */
   inum = pvm_joingroup ( "final" );

   if ( inum == 0 )
      /* принять сообщение с идентификатором msgtag */
      bufid = pvm_recv ( (-1), msgtag );
   if ( inum == 1 ) {
      /* инициализировать передающий буфер */
      bufid = pvm_initsend ( PvmDataRaw );
      /* упаковать число передаваемых элементов */
      info  = pvm_pkshort  ( N, 1, 1 );
      /* упаковать результаты работы */
      info  = pvm_pkfloat  ( results, 5, 1 );
      /* определить tid задачи - получателя */
      tid   = pvm_gettid   ( "final", 0 );
      /* послать сообщение с идентификатором msgtag
         задаче с идентификатором tid   */
      info  = pvm_send     ( tid, msgtag );
   }

Функция для синхронизации задач

   int  info = pvm_barrier ( char *group, int count );

- задача блокируется до того момента, когда функция pvm_barrier() будет вызвана ровно count участниками группы "group" ( включая данную задачу ). Эта функция позволяет синхронизировать действия членов группы следующим образом. Пусть, например, требуется, чтобы из нескольких задач, выполняющих индивидуальные задания, первые три освободившиеся начали бы одновременно выполнять коллективное задание. В этом случае образовать группу "collectiv", введя в программу после выполнения индивидуальных заданий следующие инструкции:

   if ((inum = pvm_joingroup( "collectiv" )) < 3) {
        info = pvm_barrier ( "collectiv", 3 );
        выполнение коллективного задания ;
   }
   else  exit();

Отметим, что функция pvm_barrier() может быть вызвана только членом группы, в противном случае будет зафиксирована ошибка. При ошибке возвращаемое значение info < 0 является кодом ошибки.

Функция передачи сообщений в группе

   int  info = pvm_bcast ( char *group, int msgtag );

- посылает всем членам группы "group" (кроме самой себя) сообщение с идентификатором msgtag. Если для приема этого сообщения использовать функции с блокировкой выполнения задачи, то это сообщение будет принято всеми ожидающими его задачами (которые не выполняют в это время другую работу) одновременно, что означает синхронизацию действий свободных от работы членов группы.

Функция для распределенных вычислений

   int  info = pvm_reduce ( void (*func)(), void *data, int item,
                   int datatype, int msgtag, char *group, int root );

- выполняет глобальную операцию, определяемую функцией func(), с участием членов группы "group" и помещает результат операции в задачу, для которой номер в группе равен root. Глобальная операция совершается над данными в массиве data, тип которого указан в параметре datatype (символические имена значений параметра datatype: PVM_BYTE, PVM_SHORT, PVM_INT, PVM_LONG, PVM_FLOAT, PVM_DOUBLE, PVM_CPLX, PVM_DCPLX). Длина массива data равна item, и результат глобальной операции является массивом длины item, в котором j-й элемент получен применением глобальной операции к j-м элементам массивов data, размещенных в задачах, входящих в группу "group".

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

В качестве первого параметра функции pvm_reduce() может быть либо указана пользовательская функция, либо подставлено имя одной из уже реализованных функций:

   PvmMin      - вычисление минимального элемента,
   PvmMax      - вычисление максимального элемента,
   PvmSum      - вычисление суммы,
   PvmProduct  - вычисление произведения.

Как видно из рассмотрения групповых функций, они предоставляют широкие возможности для организации взаимодействия задач, поэтому имеет смысл использовать группу с именем "All_Tasks", к которой должна присоединяться каждая задача после запуска, в результате чего снимаются ограничения на применение групповых функций.

Заключая рассмотрение групповых функций, отметим некоторые особенности их применения.

В процессе решения коллективной задачи состав группы может изменяться. При выходе любой задачи из группы ее номер в группе (целое неотрицательное число) освобождается, а при включении задачи в группу ей присваивается наименьший свободный групповой номер. Таким образом, в группе из 10 задач необязательно их номера являются числами от 0 до 9 и отражают порядок вступления задач в группу. Более того, задача, покинувшая группу и через некоторое время вернувшаяся в нее, может получить уже другой групповой номер.

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

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

2.3. Задачи PVM и отладочный вывод.

По умолчанию только текст, выводимый родительской задачей (то есть той, которую Вы сами запустили с терминала) окажется на экране. Стандартный вывод задач, запускаемых функцией pvm_spawn(), по умолчанию перенаправляется в LOG-файл исполняющей системы PVM ($PVM_TMP/pvml.*). Функция pvm_catchout(FILE *) позволяет перенаправить его в любой другой открытый для записи файл, например, фрагмент

   pvm_catchout (stdout);
   info = pvm_spawn (... 
в родительской задаче ВЕСЬ вывод от ВСЕХ запускаемых под-задач перенаправит на экран. При этом PVM гарантирует, что строки от разных задач не будут "налезать" одна на другую, и каждая строка будет предваряться идентификатором той задачи, которая ее вывела. Использование pvm_catchout() имеет два недостатка: а) между посылкой строки в файл или на экран из под-задачи и ее фактическим там появлением может быть задержка неизвестной заранее длительности, и, б) если объем выводимой диагностики от разных задач очень велик, очень трудно разобраться в поведении какой-то одной конкретной задачи.

Предлагаемая здесь функция log_printf() предназначена для того, чтобы обойти все связанные со стандартным выводом в PVM проблемы. Благодаря ей при написании программ для PVM становится возможной так называемая "отладка через контрольные выводы" ;) Каждая задача, использующая log_printf(), ведет в текущем каталоге свой собственный LOG-файл с именем pvmll.<tid>. Формат вызова log_printf() точно такой же, как у стандартной функции printf(). Эта функция использовалась при отладке второго примера

Простой пример использования log_printf()

Два замечания:

2.4. Повышение скорости.

Вообще говоря, PVM автоматически выбирает для передачи сообщений наиболее быстрый способ: если передающая и принимающая задача выполняются на одном процессоре, или на многопроцессорной ЭВМ с разделяемой памятью, то для передачи сообщений в качестве вырожденного "канала" передачи используется разделяемая память; на компьютере с архитектурой MPI используется быстрая внутримашинная сеть; и лишь в оставшихся случаях используется связь через гнезда (sockets) над протоколом TCP/IP. Однако:


3. Примеры программ

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

Не ждите от приведенных примеров слишком многого. На сегодняшний день реализации PVM не обеспечивают еще стабильной работы некоторых, казалось бы, совершенно правильных программ. В работе PVM3 замечены и такие явные сбои, как, например, невозможность запустить очередную задачу, если суммарное число уже запущенных демоном задач (в т.ч. и тех, которые успели завершиться) превысило 45-50 штук. За время работы над пособием успели увидеть свет две очередные редакции PVM3 для Windows: более простые в установке, с исправленными старыми ошибками, и непредсказуемыми новыми.

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

3.1. Запуск и синхронизация задач

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

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

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

Текст первого примера

3.2. Умножение матриц

В этом примере рассматриваются так называемые распределенные вычисления, в которых участвует множество процессоров, снабженных индивидуальными оперативными запоминающими устройствами (ОЗУ). В этих устройствах перед решением задачи записаны разные части массива исходных данных. В ходе решения процессоры обмениваются как исходными данными, так и промежуточными результатами. По окончании работы в индивидуальных ОЗУ записаны разные части данных, составляющих решение задачи.

Пусть A, B - квадратные матрицы порядка (size * size), причем size = m * blk, и требуется вычислить матрицу C = A * B, используя для этого m * m процессоров с индивидуальными ОЗУ. Так как программное обеспечение PVM предоставляет только средства для реализации параллельных вычислений, то разработка алгоритма работы коллектива процессоров полностью ложится на программиста. Результатом этой работы может быть либо модификация известного алгоритма решения задачи, либо даже переформулировка самой задачи.

3.2.1. Алгоритм перемножения матриц коллективом процессоров

Исходные матрицы A и B можно считать составленными из m * m квадратных блоков размера (blk * blk). Положение этих блоков в исходных матрицах естественно характеризовать парой индексов, каждый из которых изменяется в пределах от 0 до (m-1). Тогда для блоков матрицы A естественно ввести обозначение A[i,j] (i,j = 0...(m-1)), а элементы матрицы A обозначать a[i,j] (i,j = 0...(size-1)). Пример 1. Пусть size = 4, m = 2, blk = 2, матрица A:

                      a[0,0]  a[0,1]  a[0,2]  a[0,3]

                      a[1,0]  a[1,1]  a[1,2]  a[1,3]
                A =
                      a[2,0]  a[2,1]  a[2,2]  a[2,3]

                      a[3,0]  a[3,1]  a[3,2]  a[3,3]
Тогда
                 a[0,0]  a[0,1]                   a[0,2]  a[0,3]
       A[0,0] =                 ;       A[0,1] =                 ;
                 a[1,0]  a[1,1]                   a[1,2]  a[1,3]

                 a[2,0]  a[2,1]                   a[2,2]  a[2,3]
       A[1,0] =                 ;       A[1,1] =                 .
                 a[3,0]  a[3,1]                   a[3,2]  a[3,3]
и матрицу A можно представить в виде
                      A[0,0]  A[0,1]
                A =
                      A[1,0]  A[1,1]
Легко доказывается, что в общем случае вычисление элементов матрицы C по формуле
    c[i,j] = a[i,0]*b[0,j] + a[i,1]*b[1,j] + ... + a[i,size-1]*b[size-1,j]
                 ( i,j = 0...(size-1) )
можно заменить вычислением блоков матрицы C по формуле
    C[i,j] = A[i,0]*B[0,j] + A[i,1]*B[1,j] + ... + A[i,m-1]*B[m-1,j]
                 ( i,j = 0...(m-1) );
в которой операции над блоками являются матричными операциями.

Основная идея алгоритма заключается в том, чтобы (m * m) процессоров параллельно вычисляли (m * m) блоков матрицы C. При этом удобно ввести обозначение p[i,j] для процессора (для задачи в PVM), вычисляющего блок C[i,j] матрицы C. Будем предполагать, что в ОЗУ процессора p[i,j] в качестве исходных данных загружены матрицы (блоки) A[i,j], B[i,j] и проинициализирован блок матрицы C (C[i,j]=0).

Вычисление реализуется за m шагов. На шаге s (s = 0...(m-1))выполняются следующие действия:

  1. Процессоры с номерами вида p[i,(i+s)%m] (i=0...(m-1)) передают свои блоки A[i,i+s] процессорам, у которых тот же индекс i.
  2. Каждый процессор p[i,j] делает вычисление
    C[i,j] = C[i,j] + A[i,i+s] * B_current,
    где начальное значение B_current равно B[i,j].
  3. Каждый процессор p[i,j] передает свой блок B_current процессору с номером p[(i-1)%m,j] и принимает новое значение B_current от процессора с номером p[(i+1)%m,j].
  4. Переход к следующему шагу, если s < m - 1.
Пример 2. Если m = 5, то процессор с номером p[2,3] выполняет вычисления в следующем порядке.
      step = 0      C[2,3] += A[2,2] * B[2,3];
      step = 1      C[2,3] += A[2,3] * B[3,3];
      step = 2      C[2,3] += A[2,4] * B[4,3];
      step = 3      C[2,3] += A[2,0] * B[0,3];
      step = 4      C[2,3] += A[2,1] * B[1,3].
В предлагаемой ниже программе 'mmult' каждый процессор p[i,j] сам формирует исходные данные A[i,j] и B[i,j]. Чтобы было проще контролировать правильность решения, матрица B является единичной, т.е. должно быть C = A * B = A. Элементы же матрицы A порождаются датчиком псевдослучайных чисел.

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

В комментариях к программе для упрощения изложения p[i,j] обозначает не только процессор, но и задачу, решаемую процессором.

В программе широко используются групповые функции. Все запущенные задачи включаются в одну группу "mmult". Это позволяет каждой задаче p[i,j] легко определить собственные значения i и j по номеру mygid в группе "mmult" : i = mygid / m; j = mygid % m. Использовать в этих формулах вместо mygid идентификатор задачи нельзя, т.к. идентификаторы для ntask запущенных задач не обязательно образуют множество целых чисел от 0 до (ntask - 1).

Текст второго примера


4. Использование PVM

4.1. PVM3 для Windows95/NT

Этот раздел рассматривает запуск примеров на персональном компьютере IBM PC, работающем под управлением Windows-95 или Windows NT. Компьютер должен быть подключен к сети через протокол TCP/IP.

Для компиляции примеров использовался Visual C++. Убедитесь, что он установлен на Вашем компьютере.

Скачайте и установите PVM для Win32 Это архив, для распаковки которого Вам потребуется WinZip или UnZip.NT, так как в нем содержатся длинные файловые имена.

Некоторые имена файлов в архиве уже повреждены, в частности, это относится ко всем файлам справочной системы (manual pages). За исключением manual pages, имена можно восстановить навскидку:

    Где находится         После распаковки     Исправленный
    =============         ================     ============
    \PVM3\Gexamples\      MAKEFI~1.AIM         MAKEFILE.AIM
    \PVM3\TASKER\         MAKEFI~1.AIM         MAKEFILE.AIM
    \PVM3\XEP\            MAKEFI~1.AIM         MAKEFILE.AIM
    \PVM3\Gexamples\      JOINLE~1.C           JoinLeave.c
    \PVM3\DOC\            EXAMPL~1.PVM         Example.pvm

Скачайте и разверните в своем каталоге примеры. Их можно развернуть PKUNZIP'ом: pkunzip -d pvmt_w32

Теперь перейдите в каталог <Ваш каталог>\PVM3\TUTOR. Прочитайте и исправьте файл SET_ENV.BAT, следуя написанным в нем инструкциям.

По умолчанию примеры все сообщения выводят по-русски. Если Ваша система не русифицирована, отредактируйте файл MAKEFILE, стерев из него фрагмент /D "RUSSIAN"

Откройте "MS-DOS command prompt" (он же "Сеанс MS-DOS") и из него запустите SET_ENV. Если SET_ENV завершает свою работу с сообщением "Out of environment space", попробуйте запустить его так: %comspec% /e:2048 /k set_env

Если SET_ENV отработает без ошибок, переходите в <Ваш каталог>\PVM3\BIN\WIN32\ и запускайте примеры. В момент первого запуска примера на экране может появиться нижеследующее окошко. Выберите "Пропустить" ("Ignore").

В пробовавшейся версии (3.3.1) демон PVM делает текущим каталог %PVM_TMP%, кроме того, текущий диск так же меняется на диск %PVM_TMP% !!! Это означает, что временный и пользовательский каталог ОБ÷ЗАТЕЛЬНО должны находиться на одном и том же диске. Этот факт упоминается в комментариях SET_ENV.BAT.

Примечание: в последней версии PVM for Win32 эта ошибка устранена.

Если Вы точно прошли все эти шаги, у Вас должны нормально отработать все поставляемые примеры.

Запускайте SET_ENV.BAT всякий раз, когда Вам необходимо запустить демона PVM и/или перекомпилировать примеры.

4.2. PVM3 для супер-ЭВМ Convex SPP-1600

На SPP-1600 (spp.csa.ru) уже установлено мат.обеспечение PVM3 в каталоге /usr/convex/pvm. Скачайте в свой HOME-каталог архив с примерами и распакуйте его: tar xf pvmt_spp.tar.

Для построения примеров Вам нужен интерпретатор C-Shell (csh или tcsh). Запустите его и наберите "source set_env". Скрипт set_env запустит демона PVM и сгенерирует исполняемые файлы примеров в $HOME/pvm3/bin/CSPP/

Теперь Вы можете их запустить: ../bin/CSPP/ex1 и так далее.

Запускайте "source set_env" всякий раз, когда Вам необходимо запустить демона PVM и/или перекомпилировать примеры.

Обратите внимание, что в Makefile библиотека libgpvm3.a указывается в командной строке Си перед libpvm3.a. Обратная перестановка вызовет ошибку компоновщика.

Перед выходом из системы НЕ ЗАБУДЬТЕ запустить файл $HOME/pvm3/tutor/kill_pvm! set_env генерирует его автоматически всякий раз, когда загружает демона в память. kill_pvm, в свою очередь, выгружает демона из памяти и стирает себя с диска. Демон не выгружается из памяти автоматически по завершении сессии!

4.3. Перенос примеров на другие машины под управлением Юникса

Существуют ли какие-то препятствия, мешающие скомпилировать и запустить примеры на другой машине с установленным PVM и работающей под управлением Юникса? Чисто теоретически, нет. Изменения должны быть минимальными и затрагивают только самое начало скрипта set_env:

  1. Переменная окружения PVM_ROOT должна указывать на тот каталог, в котором развернуто программное обеспечение PVM
  2. Переменная окружения PVM_ARCH должна содержать аббревиатуру платформы. Для Windows это "WIN32", для SPP - "CSPP". Попробуйте посмотреть имена подкаталогов в $PVM_ROOT/bin, если аббревиатуру Вам не сообщили. По идее, подкаталог должен быть всего один, и он обозван этой аббревиатурой.


5. Что не испробовано

Ниже кратко перечислены те возможности и компоненты PVM, которые могут представлять интерес для разработчика, но не были описаны в данном пособии по трем причинам:

Вместе с тем, это наиболее вероятные кандидаты для дальнейшего изучения (и описания в следующих редакциях пособия). Тому, что написано о них здесь и сейчас, особой веры придавать не следует, так как это на собственной шкуре не опробовано, а значит, почитай что вилами на воде писано.

Еще одним таким кандидатом (ну очень большим) является какая-нибудь более поздняя, чем та, которая использовалась для написания примеров, версия PVM (чем свежее, тем лучше):

5.1. Сигналы

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

А теперь на примере Юникса рассмотрим пример, когда задача может получить некоторое сообщение в неизвестный заранее момент времени, или не получить вообще. Нажатие Ctrl-C на клавиатуре и последующий вызов Юниксом функции kill() является хорошим примером такого сообщения. Неужели, чтобы правильно его обработать, задача все время должна проверять состояние клавиатуры? Конечно нет - ведь в Юниксе реализован механизм сигналов.

В рамках этого механизма задача может приказать операционной системе: вызови такую-то мою функцию, когда произойдет то или иное внешнее событие. Механизм сигналов позволяет задаче тратить на обработку внешнего события ровно столько времени, сколько необходимо (без потерь, связанных с проверками типа "не произошло ли событие?" - этим займется система); и точно тогда, когда нужно:

    #include <signal.h>
    ...
    void kill_handler (int sig)
    {
      puts ("i\'m killed : Ctrl-C pressed or other reason");
      ...
    }
    ...
    signal (SIGKILL, kill_handler);  // Задаем обработку принуд.завершения
    ...
Это слишком общий пример, но он должен сгодиться для того, чтобы объяснить суть.

Возвращаемся к PVM. Сейчас, если время поступления сообщения является неизвестным, задача-приемник может поступить одним из двух способов:

Механизм сигналов в PVM все-таки есть, однако даже документация не рекомендует его использовать - или использовать с сугубой осторожностью. Используя его, посылку "блуждающих" сообщений можно организовать так:
    Задача-отправитель:
    ===================
    ...
    pvm_send (target_tid, msg_id_1);
    pvm_sendsig (target_tid, SIGUSR1);
    ...

    Задача-получатель (target_tid):
    ===============================
    #include <signal.h>
    ...
    void recv_handler (int sig)
    {
      int bufid = pvm_recv (-1, msg_id_1);
      ...
    }
    ...
    signal (SIGUSR1, recv_handler);
Однако следует еще раз оговориться, что для того класса задач, для которого создавался PVM, механизм сигналов не является жизненно необходимым. Во всяком случае, нет подтверждающих это примеров.

5.2. Многомашинность

Все построение и выполнение примеров производилось на одной машине: либо это была IBM PC, либо SPP-1600, но не виртуальная ЭВМ, объединявшая в себя сразу две физических машины. Чтобы это организовать (предположим, что системный администратор уже развернул PVM на нескольких машинах), пользователь должен на каждой машине создать свой личный конфигурационный файл PVM (возможно, на базе шаблона?). В этом файле указываются:

ВНИМАНИЕ: Первое, что следует сделать после выхода из редактора - набрать команду chmod 600 hostfile.pvm !!! В противном случае указанные в нем пароли могут стать известны любому желающему.

Скорее всего, потребуется создать и файл .rhosts, поскольку использующий его интерпретатор rsh привлекается PVM для автозагрузки демонов: с консоли надо загрузить демона только на одной машине, а он, пользуясь данными из свего хост-файла, загрузит своих собратьев на всех остальных физических машинах.

Чтобы приложение могло быть запущено на каком-то компьютере, его (приложение) надо сначала скомпилировать на нем (компьютере) и поместить там в каталог $HOME/pvm3/bin/$PVM_ARCH ! Только тогда pvm_spawn() сможет выбирать наиболее подходящее место выполнения для запускаемой задачи, не ограничиваясь текущей физической машиной. Стадию переноса можно автоматизировать, воспользовавшись утилитами rdist и rsh. Их описание приведено в документе "Сетевые службы Интернет".

5.3. Интерпретатор PVM

Интепретатор команд PVM позволяет пользоваться в рамках виртуальной машины аналогами команд, хорошо знакомых пользователям Юникса. Без него с легкостью можно обойтись, если физическая машина в PVM всего одна: такие команды Юникса как ps -edl | grep " $uid " для поиска PVM-приложений и kill ... для выгрузки демона работают весьма надежно. Но что если PVM охватывает несколько физических машин? В этом случае команда ps интерпретатора PVM выведет (теоретически) список всех PVM-приложений, а ps -edl | grep " $uid " - только выполняемых на данной машине. Если интерпретатор грузится ДО демона, как это рекомендуют разработчики PVM, то загрузка и выгрузка демонов на ВСЕХ машинах будут им производиться автоматически. И хотя этот процесс можно автоматизировать, но автоматизацию-то придется проводить вручную... ;)

В интерпретатор введены несколько сервисных команд общего назначения, таких как setenv. Это, в-принципе, позволяет всю работу с приложением PVM (после того как оно скомпилировано) проводить так:

Таким образом, работа с PVM через специализированный интерпретатор становится сходной с работой на обычной ЭВМ через интерпретатор *sh со служебными утилитами, что, конечно же, делает работу с PVM более привычной.


Автор: Илья Евсеев