Версия для печати

Архив документации на OpenNet.ru / Раздел "Программирование в Linux" (Многостраничная версия)
next up previous contents
Next: Contents   Contents






Средства параллельного программирования для ОС Linux

Оригинал: skif.bas-net.by




Учебное пособие



























Минск 2004
УДК 004.451.2, 004.272









Под редакцией доктора технических наук, профессора

Р.Х.Садыхова

Рекомендовано к изданию на заседании

кафедры ЭВМ БГУИР

(протокол 42 от 21.06.2004 г.)




Рецензент:

заведующий кафедрой математического обеспечения ЭВМ

Белорусского государственного университета

доктор технических наук, профессор М.К. Буза

Садыхов Р.Х., Поденок Л.П., Отвагин А.В., Глецевич И.И., Пынькин Д.А.

Средства параллельного программирования в ОС Linux: Учеб. пособие / Под ред. Р.Х. Садыхова. - Мн.: ЕГУ, 2004. - 475 с.


ISBN 000-0000-00-0.


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

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





2004-06-22

next up previous contents
Next: Contents   Contents






Средства параллельного программирования для ОС Linux




Учебное пособие



























Минск 2004
УДК 004.451.2, 004.272









Под редакцией доктора технических наук, профессора

Р.Х.Садыхова

Рекомендовано к изданию на заседании

кафедры ЭВМ БГУИР

(протокол 42 от 21.06.2004 г.)




Рецензент:

заведующий кафедрой математического обеспечения ЭВМ

Белорусского государственного университета

доктор технических наук, профессор М.К. Буза

Садыхов Р.Х., Поденок Л.П., Отвагин А.В., Глецевич И.И., Пынькин Д.А.

Средства параллельного программирования в ОС Linux: Учеб. пособие / Под ред. Р.Х. Садыхова. - Мн.: ЕГУ, 2004. - 475 с.


ISBN 000-0000-00-0.


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

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





2004-06-22

next up previous
Next: Введение Up: mainfile Previous: mainfile


Contents



Subsections

2004-06-22

next up previous contents
Next: Посылка сигналов с помощью Up: Сигналы Previous: Сигналы   Contents

Понятие о сигналах

Сигналы являются программными прерываниями, которые посылаются процессу, когда случается некоторое событие. Сигналы могут возникать синхронно с ошибкой в приложении, например SIGFPE (ошибка вычислений с плавающей запятой) и SIGSEGV (ошибка адресации), но большинство сигналов является асинхронными. Сигналы могут посылаться процессу, если система обнаруживает программное событие, например, когда пользователь дает команду прервать или остановить выполнение, или получен сигнал на завершение от другого процесса. Сигналы могут прийти непосредственно от ядра ОС, когда возникает сбой аппаратных средств ЭВМ. Система определяет набор сигналов, которые могут быть отправлены процессу. В Linux применяется около 30 различных сигналов. При этом каждый сигнал имеет целочисленное значение и приводит к строго определенным действиям.

Механизм передачи сигналов состоит из следующих частей:

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

Известно три варианта реакции на сигналы:

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

#include <signal.h>

void(*signal(int signr, void(*sighandler)(int)))(int);

Такой прототип очень сложен для понимания. Следует упростить его, определив тип для функции обработки:

typedef void signalfunction(int);
После этого прототип функции примет вид:
signalfunction *signal(int signr,

      signalfunction *sighandler);

signr устанавливает номер сигнала, для которого устанавливается обработчик. В заголовочном файле <signal.h> определены следующие сигналы (табл. 1).

Таблица 1. Сигналы ОС Linux.


Номер Значение Реакция программы по умолчанию
SIGABRT Ненормальное завершение (abort()) Завершение
SIGALRM Окончание кванта времени Завершение
SIGBUS Аппаратная ошибка Завершение
SIGCHLD Изменение состояния потомка Игнорирование
SIGCONT Продолжение прерванной программы Продолжение / игнорирование
SIGEMT Аппаратная ошибка Завершение
SIGFPE Ошибка вычислений с плавающей запятой Завершение
SIGILL Неразрешенная аппаратная команда Завершение
SIGINT Прерывание с терминала Завершение
SIGIO Асинхронный ввод/вывод Игнорирование
SIGKILL Завершение программы Завершение
SIGPIPE Запись в канал без чтения Завершение
SIGPWR Сбой питания Игнорирование
SIGQUIT Прерывание с клавиатуры Завершение
SIGSEGV Ошибка адресации Завершение
SIGSTOP Остановка процесса Остановка
SIGTTIN Попытка чтения из фонового процесса Остановка
SIGTTOU Попытка записи в фоновый процесс Остановка
SIGUSR1 Пользовательский сигнал Завершение
SIGUSR2 Пользовательский сигнал Завершение
SIGXCPU Превышение лимита времени CPU Завершение
SIGXFSZ Превышение пространства памяти (4GB) Завершение
SIGURG Срочное событие Игнорирование
SIGWINCH Изменение размера окна Игнорирование

Переменная sighandler определяет функцию обработки сигнала. В заголовочном файле <signal.h> определены две константы SIG_DFL и SIG_IGN. SIG_DFL означает выполнение действий по умолчанию - в большинстве случаев - окончание процесса. Например, определение signal(SIGINT, SIG_DFL); приведет к тому, что при нажатии на комбинацию клавиш CTRL+C во время выполнения сработает реакция по умолчанию на сигнал SIGINT и программа завершится. С другой стороны, можно определить
signal(SIGINT, SIG_IGN);

Если теперь нажать на комбинацию клавиш CTRL+C, ничего не произойдет, так как сигнал SIGINT игнорируется. Третьим способом является перехват сигнала SIGINT и передача управления на адрес собственной функции, которая должна выполнять действия, если была нажата комбинация клавиш CTRL+C, например
signal(SIGINT, function);

Пример использования обработчика сигнала приведен ниже:

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

 

void sigfunc(int sig) {

  char c;

  if(sig != SIGINT)

  return;

  else {

    printf("\nХотите завершить программу (y/n) : ");

    while((c=getchar()) != 'n')

    return;

    exit (0);

  }

}

int main() {

  int i;

  signal(SIGINT,sigfunc);

  while(1)

  {

    printf(" Вы можете завершить программу с помощью

            CTRL+C ");

    for(i=0;i<=48;i++)

    printf("\b");

  }

  return 0;

}


next up previous contents
Next: Посылка сигналов с помощью Up: Сигналы Previous: Сигналы   Contents
2004-06-22

next up previous contents
Next: Директивы препроцессора Up: Удаленный вызов процедур Previous: Преобразование локальных процедур в   Contents

Передача сложных структур данных

rpcgen можно использовать для создания процедур XDR, которые будут преобразовывать локальные структуры данных в формат XDR и наоборот.

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

Файл описания протокола RPC dir.x имеет следующий вид:

/*

* dir.x: Протокол вывода удаленного каталога

*/

 /*максимальная длина элемента каталога */

const MAXNAMELEN = 255;

/* элемент каталога */

typedef string nametype<MAXNAMELEN>; 

/* ссылка в списке */

typedef struct namenode *namelist; 

/* Узел в списке каталога */

struct namenode {

  nametype name; /* имя элемента каталога */

  namelist next; /* следующий элемент */

};

union readdir_res switch (int errno) {

       case 0:

            namelist list; /* нет ошибок:

                возвращает оглавление каталога */

       default:

            void; /*возникла ошибка: возвращать нечего*/

};

 

/* Определение программы каталога */

program DIRPROG {

    version DIRVERS {

        readdir_res

        READDIR(nametype) = 1;

    } = 1;

} = 0x20000076;

Имеется возможность переопределить типы (например, readdir_res в примере выше) с использованием ключевых слов языка RPC
struct, union, и enum. Эти ключевые слова не используются в последующих декларациях переменных таких типов. Например, если определяется объединение, my_un, то объявляется использование только my_un, а не union my_un. rpcgen собирает объединения RPC в структуры C. Не следует объявлять объединения C, используя ключевое слово union.

При запуске rpcgen для dir.x создаются четыре файла:

Последний файл содержит процедуры XDR для преобразования объявленных типов данных из представления конкретной аппаратной платформы в формат XDR, и наоборот. rpcgen предполагает, что libnsl содержит процедуру для каждого типа данных RPCL, используемого в файле .x. Имя процедуры - это имя типа данных, расширенное префиксом XDR xdr_ (например, xdr_int). Если тип данных определен в .x файле, rpcgen создает нужную процедуру xdr_. Если в исходном файле .x нет никакого определения типов данных (например, msg.x, выше), то файл _xdr.c не создается. Можно написать исходный файл .x, который использует тип данных, не поддерживаемый libnsl, и намеренно опустить определение типа (в файле .x). При этом нужно написать и процедуру xdr_. Это способ определения собственных процедур xdr_.

Серверная часть процедуры READDIR, в файле dir_proc.c:

/*

* dir_proc.c: удаленная реализация readdir

*/

#include <dirent.h>

#include "dir.h" /* Создается rpcgen */

 

extern int errno;

extern char *malloc();

extern char *strdup();

 

readdir_res *

readdir_1(nametype *dirname, struct svc_req *req)

{

  DIR *dirp;

  struct dirent *d;

  namelist nl;

  namelist *nlp;

  static readdir_res res; /* должен быть static! */

 

  /* Открыть каталог */

  dirp = opendir(*dirname);

  if (dirp == (DIR *)NULL) {

      res.errno = errno;

      return (&res);

  }

 

  /* Очистить предыдущий результат */

  xdr_free(xdr_readdir_res, &res);

  /*

  * Собрать элементы каталога.

  */

  nlp = &res.readdir_res_u.list;

  while (d = readdir(dirp)) {

      nl = *nlp = (namenode *)

      malloc(sizeof(namenode));

      if (nl == (namenode *) NULL) {

         res.errno = EAGAIN;

         closedir(dirp);

         return(&res);

      }

      nl->name = strdup(d->d_name);

      nlp = &nl->next;

  }

  *nlp = (namelist)NULL;

  /* Вывести результат */

  res.errno = 0;

  closedir(dirp);

  return (&res);

}

Клиентская часть процедуры READDIR, файл rls.c:

/*

* rls.c: Клиент для удаленного чтения каталогов

*/

#include <stdio.h>

#include "dir.h" /* создается rpcgen */

 

extern int errno;

 

main(int argc, char *argv[])

{

  CLIENT *clnt;

  char *server;

  char *dir;

  readdir_res *result;

  namelist nl;

  if (argc != 3) {

     fprintf(stderr, "usage: %s host

        directory\n",argv[0]);

     exit(1);

  }

  server = argv[1];

  dir = argv[2];

 

  /*

  * Создает обработчик клиента,

  * вызывающий MESSAGEPROG на сервере

  */

  cl = clnt_create(server, DIRPROG, DIRVERS, "tcp");

  if (clnt == (CLIENT *)NULL) {

      clnt_pcreateerror(server);

      exit(1);

  }

  result = readdir_1(&dir, clnt);

  if (result == (readdir_res *)NULL) {

      clnt_perror(clnt, server);

      exit(1);

  }

 

  /* Успешный вызов удаленной процедуры. */

  if (result->errno != 0) {

  /* Ошибка на удаленной системе.

  */

      errno = result->errno;

      perror(dir);

      exit(1);

  }

 

  /* Оглавление каталога получено.

  * Вывод на экран.

  */

  for (nl = result->readdir_res_u.list;

       nl != NULL;

       nl = nl->next) {

       printf("%s\n", nl->name);

  }

  xdr_free(xdr_readdir_res, result);

  clnt_destroy(cl);

  exit(0);

}

Код клиента, создаваемый rpcgen, не освобождает память, выделенную для результатов запроса RPC. Поэтому следует вызывать xdr_free(), чтобы освободить память после завершения работы. Это похоже на вызов free(), за исключением того, что здесь для получения результата передается процедура XDR.


next up previous contents
Next: Директивы препроцессора Up: Удаленный вызов процедур Previous: Преобразование локальных процедур в   Contents
2004-06-22

next up previous contents
Next: Высокоуровневые средства межпроцессного взаимодействия Up: Удаленный вызов процедур Previous: Передача сложных структур данных   Contents

Директивы препроцессора

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

Чтобы создать файл определенного вида, можно использовать следующие символы:

Пример, иллюстрирующий использование возможностей препроцессинга rpcgen:

/*

* time.x: Удаленный протокол времени

*/

program TIMEPROG {

   version TIMEVERS {

      unsigned int TIMEGET() = 1;

   } = 1;

} = 0x20000044;

 

#ifdef RPC_SVC

%int *

%timeget_1()

%{

% static int thetime;

%

% thetime = time(0);

% return (&thetime);

%}

#endif



2004-06-22

next up previous contents
Next: DIPC - Распределенные межпроцессные Up: mainfile Previous: Директивы препроцессора   Contents

Высокоуровневые средства межпроцессного взаимодействия



Subsections

2004-06-22

next up previous contents
Next: Введение Up: Высокоуровневые средства межпроцессного взаимодействия Previous: Высокоуровневые средства межпроцессного взаимодействия   Contents

DIPC - Распределенные межпроцессные коммуникации



Subsections

2004-06-22

next up previous contents
Next: Кластеры Up: DIPC - Распределенные межпроцессные Previous: DIPC - Распределенные межпроцессные   Contents

Введение

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

Часть DIPC, относящаяся к ядру, ``заглушена'', когда dipcd не выполняется. При отсутствии dipcd, вызовы DIPC в программе должны происходить так же, как если бы они были нормальными вызовами IPC System V. Сам dipcd использует обычные средства для получения доступа к механизмам IPC System V. Эти механизмы изменчивы - так, они предполагают отличие dipcd от других пользовательских процессов. Например, dipcd может получить доступ к сегменту разделяемой памяти с помощью smget(), даже в том случае, если он был удален (shmctl() - вследствие команды IPC_RMID), - но не окончательно из ядра. Все манипуляции dipcd со структурами IPC делаются локально, без проявления вне данной машины. Это противоречит подходам к нормальным пользовательским процессам, когда действия с распределенными структурами IPC могут затронуть другие компьютеры в сети.

DIPC затрагиваются только при передаче данных в распределенной среде. Запуск соответствующих программ на различных компьютерах - это дело пользователя/программиста DIPC. Это значит, что программы для исполнения могут нуждаться в размещении на компьютере, который предназначен для их исполнения. Программы могут быть перенесены на различные компьютеры один раз и использованы множество раз после этого. Это не вызывает перегрузок при передаче кода по сети во всех случаях, когда программа запускается. Считается, что код программы остается без изменений в течение относительно долгого периода, в то время как используемые им данные изменяются часто (может быть, от одного запуска к другому), что в большинстве случаев должно считаться плюсом.

Важно учитывать, что DIPC - это набор механизмов. Речь идет не о выборе стратегии: как распараллеливается программа, где должны запускаться процессы и т.д. - решает пользователь/программист. Известно несколько иных доступных средств для решения данного класса задач, хотя в полной мере удовлетворительного решения пока не известно.

Выделяют два вида активности DIPC:

  1. Синхронная: программист использует системные вызовы для поддержания вычислительного процесса. Примерами могут служить использование xxxget() - для получения доступа к структуре IPC, или системного вызова msgrcv() - для приема сообщения. Программа делает вызов и ожидает его завершения перед тем, как продолжить работу. Далее, в процессе рассмотрения, DIPC всегда будет прибегать к таким действиям.
  2. Асинхронная: чтение разделяемой памяти и запись в нее может привести к возникновению асинхронного действия. Программист не может предугадать, где и когда может возникнуть подобная ситуация. Одним из примеров является чтение разделяемой памяти, когда затрагиваемые страницы на запрашиваемой машине подвергаются свопингу (IPC) или не подвергаются (DIPC).



2004-06-22

next up previous contents
Next: Сбои работы при использовании Up: DIPC - Распределенные межпроцессные Previous: Введение   Contents

Кластеры

Система состоит из определенного количества компьютеров, соединенных посредством TCP/IP или UDP/IP сети. Некоторые (или все) машины могут быть в одном ``кластере''. В сети может быть более одного кластера, но каждая машина будет принадлежать только одному из них. Кластеры являются логическими объектами: они могут быть созданы или удалены, членство в них подвергается изменениям без необходимости изменения любого физического свойства сети.

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

Можно осуществить обмен данными между программами, выполняющимися на различных машинах в кластере, который делает DIPC распределенной системой, или выполнять всю гамму процессов разрешающего работу DIPC приложения на единственной машине. Процессы должны вести себя так, будто они используют нормальную IPC System V, поскольку явной ссылки на любой частный компьютер в DIPC нет. Некоторая программа может использовать различные компьютеры в процессе обращения с целью ее успешного завершения, тем самым, освобождая находящуюся в зависимости программу на других машинах с определенными адресами. Отсюда также следует, что программисты могут использовать одиночные машины для разработки своих приложений, а позже выполнять их на многомашинном кластере. Другими словами, пользователь может по своему усмотрению осуществить конечное отображение ресурсов, требующихся программе, на доступных физических ресурсах (cм. в качестве примера программу в каталоге /examples/pi).



2004-06-22

next up previous contents
Next: Ключи DIPC Up: DIPC - Распределенные межпроцессные Previous: Кластеры   Contents

Сбои работы при использовании DIPC

В программах, использующих DIPC, могут возникать два вида ошибок:

  1. Некоторые ошибки совпадают с теми, которые встречаются в обычных IPC, например, ссылка на несуществующую структуру IPC, или попытка получения доступа к структуре без наличия соответствующих прав доступа. Синхронные ошибки обнаруживаются с помощью значений, возвращаемых системными вызовами. При наличии асинхронных ошибок приходится применять асинхронные механизмы - сигналы.
  2. Ошибки, являющиеся специфическими ошибками DIPC. Они могут быть вызваны следующими ситуациями:

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

Процесс попытки вывода системы из ошибочной ситуации (такой, как ошибка сети) сложен, а иногда и невозможен, что добавляет сложности DIPC. Таким образом, когда DIPC обнаруживают ошибку, они не пытаются осуществить повторное выполнение, а делают в точности то, что и IPC: пытаются информировать приложение об ошибке через код возврата, или, в случае, когда процесс пытается получить доступ к разделяемому сегменту памяти через сигнал (SIGSEGV). Остальное возлагается на приложение. Следует помнить, что только ``ответственные'' процессы будут проинформированы об ошибке - для того, чтобы они предприняли что-нибудь, - а не все процессы распределенной программы.

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

Следует отметить, что ошибки могут порождаться невнимательным отношением к системе. Например, структура IPC может быть окончательно удалена на одном компьютере, а другие могут не знать об этом. Они могут считать, что структура продолжает существовать - это может вызвать затруднения в дальнейшем (см. программы в каталоге tools, частично разрешающие эту проблематику).



2004-06-22

next up previous contents
Next: Как работает dipcd Up: DIPC - Распределенные межпроцессные Previous: Сбои работы при использовании   Contents

Ключи DIPC

Процессы в кластере могут использовать один и тот же ключ для получения доступа к структуре DIPC. Эта структура должна быть создана первой. Это делается одним из системных вызовов xxxget (shmget(), semget() или msgget()). После создания и инициализации другие процессы, возможно, на других машинах, могут получить доступ к этой структуре. В данном случае ключ имеет одинаковый смысл для всех компьютеров в пределах кластера. Другими словами, компьютеры в кластере имеют общее ключевое пространство DIPC.

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

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

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

Если не обращать внимания на приведенное выше требование, то структура DIPC может использоваться полностью прозрачно. Даже при написании современных программ, единственная вещь, которую программист должен сделать - это указать флаг
DIPC_IPC.



2004-06-22

next up previous contents
Next: Процессы dipcd Up: DIPC - Распределенные межпроцессные Previous: Ключи DIPC   Contents

Как работает dipcd

В этом разделе описывается, как используется TCP. Для получения дополнительной информации о UDP, пожалуйста, обратитесь к разделу о UDP/IP.

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

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

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

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

Необходимо создавать условия, чтобы dipcd смог получить доступ к требующейся информации в структурах ядра. Новый системный вызов, перекликающийся с прочими вызовами IPC System V, добавлен в Linux только с этой целью; dipcd и другие ``инструменты'' DIPC (такие как dipcker) используют его для передачи данных в ядро и из него. Этот новый системный вызов (известный в программах DIPC как dipc()) предназначен для использования dipcd и другими связанными с ним программами. Пользовательские программы непосредственно применять его не должны.

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



Subsections

2004-06-22

next up previous contents
Next: Как dipcd создает процессы Up: Как работает dipcd Previous: Как работает dipcd   Contents

Процессы dipcd

Демон dipcd ``раздваивает'' нужные процессы для выполнения своей задачи. Вы можете видеть, что все процессы имеют свои собственные исходные тексты. Ниже приводится список процессов
dipcd (их осложняют перекрестные ссылки, поэтому для понимания написанного может понадобится ``многопроходное'' чтение):

-
back_end (в dipcd/back_end.c):

Здесь находится функция main() программы dipcd. Только один back_end может присутствовать на каждом компьютере в кластере. back_end и front_end (см. ниже) - это единственные процессы dipcd, которые используют fork() для создания других процессов: back_end запускает систему - он регистрирует себя в ядре, как процесс, ответственный за обработку запросов DIPC, - читает конфигурационный файл, инициализирует переменные и раздваивает задачу front_end. Если машина, на которой он запущен, является арбитражной, то он также раздваивает и арбитражный процесс.

После этого back_end зацикливается на сборе запросов из ядра и связанных с ними данных - в те моменты, когда это возможно. Эти запросы (наряду с другими) могут быть предназначены для чтения распределенной разделяемой памяти и записи в нее, а также могут запрашивать сведения о некоторых структурах IPC. При этом, back_end будет раздваивать процесс employer (см. ниже) для обработки каждого из этих запросов.

-
front_end (в dipcd/front_end.c):

Процесс ответственный за обработку входящих запросов и передачу данных для компьютера по сети. На каждой машине в кластере DIPC может быть только один процесс front_end. Его предназначение объясняется тем, что в сетях TCP/IP для подсоединения к компьютеру вам нужно знать не только IP-адрес соответствующей машины, но и номер порта TCP; dipcd при работе создает неизвестное заранее число процессов и многие из них требуют получения информации по сети. Таким образом, для избавления от проблемы, возникающей, когда каждый процесс использует собственный номер порта TCP, было решено обрабатывать все входящие запросы единственной общей задачей. В результате, каждый процесс ``знает'' номер порта TCP, который должен использоваться для подключения к другому компьютеру с поддержкой DIPC.

Каждый процесс для взаимодействия с другими машинами может подключаться прямо к front_end любого компьютера. (Это не приводит к взаимодействию с referee. См. ниже.) Если входящее соединение предназначено для выполнения полезной нагрузки, то front_end раздваивает рабочий процесс для обработки. В противном случае он только переправляет входящие данные процессу, для которого они предназначены и который может быть employer, ожидающим результатов, или менеджером разделяемой памяти (см. shm_man).

-
referee (в dipcd/referee.c):

Играет важную роль в налаживании порядка в системе. Во всем кластере может быть только один referee, следовательно, он может выполняться только на одном из компьютеров. Для того, чтобы DIPC работала, каждый компьютер должен знать адрес машины, на которой выполняется referee. Подобно случаю с front_end, referee имеет свой собственный номер порта TCP, и прочие процессы могут взаимодействовать непосредственно с ним; referee хранит всю информацию о действующих структурах IPC в нескольких связанных списках (в документации DIPC на них ссылаются, как на арбитражные таблицы). Наличие одних списков связано со структурами IPC, которые уже созданы, а других - со структурами, которые удаленный процесс пытается создать. Такой подход приводит к возложению на referee функции сервера имен DIPC.

В этом случае referee предоставляет механизм ``черного хода'' (используемый в доменных сокетах UNIX), посредством которого процессы могут посылать свои команды и принимать определенную информацию ( См. документацию на программу dipcref (в каталоге tools) для получения дополнительной информации. См. также раздел ``Арбитраж'', находящийся ниже).

-
shm_man (в dipcd/shm_man.c):

Создается на машинах, которые владеют сегментами разделяемой памяти (машины, которые сначала создали соответствующие структуры) как менеджер разделяемой памяти. Имеется по одному процессу shm_man для каждой распределенной разделяемой памяти в системе. Он исполняется задачей employer, которая успешно обработала shmget(); shm_man определяет, кто может читать разделяемую память или записывать в нее и имеет соответствующий счетчик. Он также управляет передачей содержимого разделяемой памяти на нуждающиеся в нем компьютеры. shm_man будет присоединять разделяемую память к себе. Это предохраняет разделяемую память от разрушения, если она удаляется (shmctl() вследствие команды IPC_RMID), а все процессы в машине-создателе отсоединяют ее от себя. Это может привести к осложнениям, если иные процессы на других компьютерах продолжают нуждаться в разделяемой памяти.

Нет необходимости в процессах, раздваиваемых для обработки набора семафоров и очередей сообщений на компьютере, который создал их (т. е. нет sem_man и msg_man). Только в случае с разделяемой памятью вводится управляющий процесс. Это происходит по следующим причинам:

  1. При наличии разделяемой памяти требуется дополнительная информация (т.е. список компьютеров, имеющих доступ к памяти для чтения или записи), которая используется в течение длительного времени. Сохранить ее при большом числе процессов не просто.
  2. Может возникать более одного запроса - на чтение разделяемой памяти или запись в нее - в одно и то же время. Поэтому необходимо централизованное принятие решений.
  3. Разделяемая память должна присоединяться к адресному пространству процесса - чтобы система не могла окончательно удалить ее (см. выше).
-
employer (в dipcd/employer.c):

Запускается для обработки при удаленном исполнении системного вызова (такого, как shmctl()) или для обработки других видов запросов (таких, как запросы на чтение/запись разделяемой памяти). Он соединяется с соответствующим компьютером (например, с тем, на котором структура IPC создана первой) и затем перенаправляет необходимые данные ответственному процессу (например, front_end) - с ожиданиями подтверждений. Он использует механизм тайм-аута для обнаружения возможных сетевых или машинных проблем.

-
worker (в dipcd/worker.c):

Запускается процессом front_end для исполнения запрашиваемых действий. Запросы могут приходить от employer, referee или shm_man и включать исполнение удаленного системного вызова или пересылку содержимого разделяемой памяти из компьютера в компьютер. Для успешного завершения работы worker может подключаться к оригинальной запрашивающей машине и предоставлять соответствующие результаты; worker может обратиться к прокси и оставаться на связи с ним после реализации такого соответствия (См. раздел о прокси для получения более подробной информации).



2004-06-22

next up previous contents
Next: Работа с сигналами в Up: Сигналы Previous: Понятие о сигналах   Contents

Посылка сигналов с помощью raise() и kill()

Функция raise() имеет следующий вид:

#include <signal.h>

int raise(int sig);

С помощью функции raise() можно посылать программные сигналы типа sig в исполняемую программу. Если программа установила обработчик сигнала для указанного типа sig, эта процедура будет выполнена. Если никакой обработчик не установлен, будет выполнено стандартное действие (SIG_DFL) для данного типа сигнала. В примере ниже приведена программа, которая запрашивает ввод двух чисел для деления. Если делитель равен 0, программе посылается сигнал SIGFPE, чтобы завершить ее:

#include <signal.h>

int main()

{

  int a,b;

  printf("Число : ");

  scanf("%d",&a);

  printf("делится на : ");

  scanf("%d",&b);

  if(b==0)

    raise(SIGFPE);

  else

    printf("Результат = %d\n",a/b);

  return 0;

}

Для передачи сигнала некоторому процессу применяется функция kill(), синтаксис которой приведен ниже.

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int signalnumber);

Запись kill(getpid(), signalnumber) эквивалентна вызову
raise(signalnumber). После вызова kill() в записи таблиц целевого процесса устанавливается соответствующий сигналу бит. Кроме того, должны быть выполнены следующие условия:



2004-06-22

next up previous contents
Next: Как компоненты dipcd ``общаются'' Up: Как работает dipcd Previous: Процессы dipcd   Contents

Как dipcd создает процессы

Потоки позволяют ``разветвить'' задачи для их параллельного выполнения и предоставить совместный доступ к общему адресному пространству. К сожалению, иногда потоки могут не поддерживаться. Это означает, что если в программе желательно выполнять множество действий в одно и то же время, то в ней нужно использовать системный вызов fork(), предполагая, что общего адресного пространства нет, и заботиться о средствах связи между процессами.

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

На рис. 1 показаны взаимоотношения между процессами dipcd и последбвательность их создания.

\includegraphics[scale=0.6]{dipc1}
Рис. 1. Взаимоотношения процессов DIPC

  1. Программа dipcd может исполняться пользователем (или автоматически - из загрузочного скрипта). Процесс back_end получает контроль.
  2. Процесс front_end раздваивается процессом back_end.
  3. back_end раздваивает процесс referee, если данный компьютер также должен содержать referee.
  4. Процесс back_end раздваивает employer, когда он ``открывает'' работу DIPC внутри ядра.
  5. Процесс front_end раздваивает worker в тех случаях, когда входящий запрос должен быть обработан новым процессом.
  6. Процесс employer исполняет код shm_man, когда определяет, что помог успешно создать распределенную структуру IPC для разделяемой памяти.



2004-06-22

next up previous contents
Next: Арбитраж Up: Как работает dipcd Previous: Как dipcd создает процессы   Contents

Как компоненты dipcd ``общаются'' между собой

Различные части dipcd используют структуру, называемую ``сообщением'' (не путайте с сообщениями IPC) для передачи любой информации между собой. В эти сообщения включаются: IP-адреса отправителя и получателя, локальный pid отправителя, функция для реализации, необходимые аргументы и т.д. В некоторых случаях может потребоваться больше данных, чем вмещает структура message - такая информация может ``сопровождать'' сообщение. Содержимое сообщения IPC или структуры msgid_ds, использующееся в msgctl() вследствие команды IPC_SET, - это два примера данных, которые посылаются после сообщения dipcd.

Каждое сообщение имеет поле запроса, которое определяет процесс ``назначения''. Оно может принимать значения REQ_DOWORK - для рабочих, REQ_SHMMAN - для менеджера разделяемой памяти и
REQ_REFEREE - для арбитра. Ответные сообщения содержат в своих полях запроса REQS_DOWORK, RES_SHMMAN либо RES_REFEREE - в зависимости от того, какой из процессов отвечает данным сообщением.

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

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

Процессы, которые устанавливают сокеты TCP/IP - это referee и front_end. Все внутримашинные коммуникации должны вызывать один из них. Процессы, которые устанавливают доменные сокеты UNIX - это employer (для приема каждого результата от front_end на локальной машине), referee (для реализации механизма ``черного хода''), менеджер разделяемой памяти (для приема запросов, связанных с распределенной разделяемой памятью, которой он управляет, а также для реализации механизма ``черного хода'') и worker, когда он переходит к исполнению системного вызова semop(). В случаях с shm_man и employer все данные от других компьютеров первоначально посылаются в TCP/IP сокет front_end, а он копирует их в доменные сокеты UNIX. Доменный сокет UNIX referee доступен локально с помощью средства dipcref. Процесс worker принимает файловый дескриптор, соответствующий межсетевому сокету, когда раздваивается процессом front_end; таким образом, он может непосредственно взаимодействовать с удаленными процессами. Прокси принимают данные от front_end посредством сокетов UNIX.

Имя доменного сокета UNIX для employer назначается примерно так: сначала следует строка DIPC, затем идет тип структуры IPC (SEM - для семафоров, MSG - для очередей сообщений и SHM - для разделяемой памяти) плюс соответствующий структуре IPC ключ и, наконец, идентификатор процесса employer. Это делает имя сокета уникальным, поэтому результаты могут быть отосланы назад ``правильному'' employer. Например, DIPC_SEM.45.89 - дается employer с идентификатором процесса 89, который обрабатывает запрос семафора. Набор семафоров имеет ключевой номер 45. Имя сокета менеджера разделяемой памяти также назначается аналогичным способом, за исключением того, что оно не включает число, соответствующее его идентификатору процесса, так как некоторые процессы могут потребовать связать их без указания таких номеров. Фактически процессы, запрашивающие доступ к разделяемой памяти, не осведомлены о процессе, который управляет этой памятью (См. в разделе о прокси, как эти процессы устанавливают доменные сокеты UNIX).

Некоторые процессы должны использовать сокеты TCP/IP в большинстве случаев информационного обмена (например, между shm_man и front_end, который выполняется на компьютере-читателе разделяемой памяти или писателе), поскольку они обычно находятся на различных компьютерах. Однако бывает, что оба процесса располагаются на одной машине. Для поддержки общих алгоритмов при этом также применяются сокеты TCP/IP, но на сей раз адреса заменяются на так называемые разворачивающие адреса.



2004-06-22

next up previous contents
Next: Как пересылаются данные и Up: DIPC - Распределенные межпроцессные Previous: Как компоненты dipcd ``общаются''   Contents

Арбитраж

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

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

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



2004-06-22

next up previous contents
Next: Сокращение числа копирований Up: DIPC - Распределенные межпроцессные Previous: Арбитраж   Contents

Как пересылаются данные и информация

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

Некоторые вызовы (например, xxxctl() вследствие команды
IPC_STAT) отрабатывают быстро, другие (например, вызов semop()) - могут отнимать очень длительное, если не бесконечное время:

Генерация вызова:

процесс -1-> локальное ядро

Получение результатов: 

процесс <-2- локальное ядро

Сначала параметры копируются в ядро (1), а затем возвращаются результаты (2).

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

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

Удаленный вызов процедур (Remote Procedure Call - RPC) применяется для исполнения системного вызова на удаленном компьютере. Для обеспечения ``прозрачности'' ни один из процессов, использующих DIPC, не должен ``видеть'' какие-либо изменения в сравнении с нормальной (локальной) активностью IPC. К тому же, данные процесса копируются в память ядра. Затем dipcd переносит эти данные в свое адресное пространство и передает их по сети компьютеру, ответственному за обработку запроса. Это должен быть компьютер, на котором структура IPC создана в первую очередь, - в этом случае он называется владельцем данной структуры (см. раздел о владельцах для получения более подробной информации). Удаленный dipcd будет копировать вновь прибывшие данные в пространство ядра машины-владельца. В результате, при такой симуляции системного вызова на целевом компьютере получается три копирования и одно ``задействование'' в сети - для процесса.

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

{Генерация вызова через сеть:
(Запрашивающий компьютер)        |       (Запрашиваемый компьютер)
процесс-1-> локальное ядро       |
|                                |
+-2-> локальный dipcd          --|-3->dipcd владельца-4->удаленное
                                 |                           ядро
                                 |
Получение результатов:           |
              локальный~dipcd~<--|-6-dipcd владельца<-5-удаленное
                               | |                          ядро
процесс <-8-локальное ядро<-7-+|

Процесс приостанавливается до тех пор, пока результаты не возвратятся.

Как видно, пользовательский процесс взаимодействует лишь с локальным ядром и не отмечает изменений при вызовах подпрограмм IPC System V.

Помните, что передача данных по сети требует дополнительного копирования в ядро и из него. Это происходит вследствие ``дизайна'' сетевой поддержки внутри ядра и поэтому неизбежно.

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

НАЧАЛО

 ЕСЛИ присутствует dipcd и вызывающий процесс - не dipcd ТО

   ЕСЛИ операция поддерживается DIPC ТО

    ЕСЛИ операция - в достоверной распределенной структуре и

    это - не компьютер владельца ТО исполнить вызов удаленно

КОНЕЦ



Subsections

2004-06-22

next up previous contents
Next: Почему DIPC не использует Up: Как пересылаются данные и Previous: Как пересылаются данные и   Contents

Сокращение числа копирований

Очевидно, что доступ к сети обеспечить весьма непросто. В некоторых операционных системах используется переотображение адресов (с помощью блока управления памятью (Memory Management Unit - MMU) - для предоставления доступа к части памяти процесса другому процессу), следовательно, пересылки данных между адресными пространствами ядра и пользователя должны быть очень ``дешевыми''. К сожалению, Linux не поддерживает этого метода при реализации механизмов IPC System V.

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

Можно использовать и ``заглушки'' в пользовательском пространстве для перемещения обычного кода IPC System V в ядро. Код "заглушки" должен протестировать данные и цель и решить, нужно ли послать данные по сети, не беспокоясь о локальном ядре. Другой процесс на вызываемой машине должен принять эти данные, выполнить операцию и передать результаты назад, после чего они доставляются оригинальному процессу-заглушке. Для этого необходимо предоставить возможность этому процессу заменить обычные заглушки IPC. Следует также обеспечить наличие объектных файлов, которые должны прикомпоновываться к программе или, что еще лучше, изменить стандартные библиотеки C.

Эти две проблемы можно сформулировать так:

  1. Как найти ожидающий процесс? Заглушка для этого процесса будет ожидать в пользовательском пространстве, и нужно придумать механизм "свидания".
  2. Что делать, если второй процесс еще не требует данных? Посылающий процесс может продолжать исполняться до тех пор, пока передаются данные, но поскольку принимающий процесс еще не запрашивает этих данных, они должны где-то удерживаться.
Заметим, что обе вышеуказанные проблемы решены, - можно сохранить две копии между пользовательской памятью и памятью ядра.



2004-06-22

next up previous contents
Next: Создание структур IPC и Up: Как пересылаются данные и Previous: Сокращение числа копирований   Contents

Почему DIPC не использует указанных методов

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

Вторая причина состоит в том, что необходимость прикомпоновки специального объектного файла к программе может столкнуться с помехами и вызвать ошибки. К тому же часто нет доступа к исходным текстам стандартных библиотек C. В любом случае обновление DIPC может потребовать перекомпиляции использующих его программ. Еще одним сдерживающим фактором является то, что более старые программы, которые не скомпонованы с измененными заглушками, не смогут информировать DIPC о своем ``присутствии'' и не будут предоставлять ему необходимую информацию. DIPC должна "знать" обо всех ключах IPC в кластере, даже если они представляют собой обычные структуры IPC. Программы, скомпилированные без новых заглушек, могут привести DIPC к недееспособности.

Необходимо также иметь в виду, что некоторые пользователи могут пожелать использовать DIPC посредством языков, отличных от C, - при этом могут воникнуть неразрешимые проблемы предоставления альтернативных объектных файлов поддержки всех этих языков.

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



2004-06-22

next up previous contents
Next: Концепция владельца Up: DIPC - Распределенные межпроцессные Previous: Почему DIPC не использует   Contents

Создание структур IPC и доступ к ним

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

DIPC пытаются ``скрыть'' эту ситуацию настолько, насколько возможно. Процессам на разных машинах нужно использовать один и тот же ключ, чтобы обращаться к одной структуре IPC. При DIPC, когда процесс желает организовать или получить доступ к структуре IPC с определенным ключом, локальное ядро в первую очередь отслеживает, используется ли уже данный ключ. При ``нормальной'' IPC эти процессы очень похожи. Если структура найдена, запрос обрабатывается локально, без обращения к арбитру. Но если структура IPC с указанным ключом не найдена, то арбитру нужно осуществить поиск и выяснить, используется ли уже такой ключ в кластере. Арбитр ищет ключ в своих таблицах и сообщает запрашивающему процессу, найден ключ или нет и, если найден, является ли он распределенным ключом или нет. Арбитр может ответить незамедлительно, если ключ присутствует или отсутствует, но ни одна из машин не запрашивает его. После посылки информации в запрашивающий компьютер арбитр ожидает подтверждения, создала ли данная машина локально структуру IPC с этим ключом или нет, чтобы при необходимости смочь обновить свою информацию. Но если ключ не найден, а арбитр уже послал соответствующее сообщение машине, то он не отвечает на все последующие запросы такого ключа, пока машина не ответит, создала ли она структуру IPC. Когда информация поступит, арбитр сможет обслужить прочие поступающие запросы.

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

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

В табл. 6 показано, как процесс решает, может ли он при необходимости создать ранее не существовавшую структуру IPC (xxxget() с флагом IPC_CREAT).


Таблица 6. Ключи создания структур DIPC


Тип запрашиваемого ключа Тип удаленного ключа Действие
локальный не_существующий создание
локальный локальный создание
локальный распределенный ошибка
распределенный не_существующий создание
распределенный локальный ошибка
распределенный распределенный ошибка

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

Таблица 7. Ключи доступа к уже созданным структурам DIPC


Тип запрашиваемого ключа Тип удаленного ключа Действие
локальный не_существующий ошибка
локальный локальный ошибка
локальный распределенный ошибка
распределенный не_существующий ошибка
распределенный локальный ошибка
распределенный распределенный проверка

Проверка подразумевает выполнение действий с целью получения гарантии того, что указанные флаги для xxxget() и права доступа к распределенной структуре IPC позволяют процессу получить доступ к структуре. В успешном случае, ``действие'' становится ``созданием''.

В данном случае, правила проверки флагов близки к правилам для обычной IPC. Например, указание IPC_EXCL и IPC_DIPC при одном системном вызове приводит к ошибке, если структура IPC с указанным распределенным ключом уже присутствует на другой машине. Метод, применяющийся для проверки прав доступа: имя эффективного пользовательского логина, наряду со всеми параметрами xxxget() передается машине, которая имеет структуру с распределенным ключом. Затем dipcd исполняет системный вызов xxxget() на этом компьютере. Если он завершается успешно, то осуществляется попытка выполнения тех же действий на оригинальной запрашивающей машине. Если любое из приведенных действий завершается ненормально, то и оригинальный xxxget() также потерпит неудачу. Для получения более подробной информации о том, как DIPC обрабатывает имена пользователей, обратитесь к разделу о безопасности.

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

Ниже показано, как DIPC обрабатывает запрос о новом создании IPC.

Первая фаза: поиск

                                    Сеть
     (Запрашивающий компьютер)       |    (Арбитрирующий компьютер)
     |-1->back_end >-2-+             |
                        |            |                         
 ядро|<--------6--employer --3-------|-> referee 
                   |                 |        |
                   +-5-< front_end <-|<-----4-+

Предполагается, что структура IPC с указанным ключом на вызывающей машине не заменяется. В этом случае back_end находит запрос о поиске ключа (1) и раздваивает employer для его обработки (2); employer вызывает referee и ``спрашивает'' его (3); referee посылает ответ (нашел или нет ...) процессу front_end запрашивающего компьютера (4) и затем ответ доставляется оригинальному employer (5), который отдает результаты назад в ядро (6). Ядро должно однозначно определить, может быть создана структура IPC или нет. По ходу дела referee запускает таймер - для того, чтобы гарантировать своевременное предоставление информации. После этого может начинаться вторая фаза (непосредственная передача).

Вы можете обратиться к предыдущей схеме для понимания второй фазы. Во время фазы непосредственной передачи referee информируется о результате работы системного вызова xxxget(). Все действия аналогичны действиям во время предыдущей фазы:
back_end находит сведения о результате работы xxxget() (1) и информирует referee (2 и 3), но на сей раз целью действий (4), (5) и (6) является обеспечение гарантии того, что исходный запрашивающий процесс будет продолжен только после того, как информация referee будет обновлена.

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

НАЧАЛО

 ЕСЛИ указанный ключ используется в локальной структуре ТО
   ~КОНЕЦ~алгоритма
 
 ЕСЛИ dipcd присутствует и вызывающий процесс - это не dipcd ТО
    искать с помощью referee ключ (*)

 ЕСЛИ ключ найден ТО
   протестировать совместимость запрашиваемой структуры с
   удаленной структурой. В СЛУЧАЕ если обе структуры 
   являются распределенными
   ВЫПОЛНИТЬ проверку прав доступа (*).

 ЕСЛИ все условия обеспечены ТО попытаться
  создать структуру
 информировать referee о результатах (непосредственная передача)(*)

КОНЕЦ
Проблемы с back_end и employer мгновенно передаются ядру. Прочие ошибки обнаруживаются с помощью механизмов тайм-аута для employer и referee.

Если в системе отсутствует возможность получения необходимой информации от referee, то принимается, что данный ключ не используется, и она ведет себя так, как будто referee сказал ``да''. Это значит, что система будет проявлять себя ``владельцем'' (см. следующий раздел). Хотя такой подход и заставляет процесс продолжаться, он может создать проблемы тогда, когда структуры IPC с тем же самым ключом будут на других машинах. Аналогичные проблемы могут возникнуть, если во время непосредственного обмена не окажется с referee. В таком случае будет наступать тайм-аут, а referee будет предполагать, что запрашивающий процесс при попытке создания структуры завершился ненормально. Таким образом, referee может владеть ошибочной информацией.


next up previous contents
Next: Концепция владельца Up: DIPC - Распределенные межпроцессные Previous: Почему DIPC не использует   Contents
2004-06-22

next up previous contents
Next: Удаление структур IPC Up: DIPC - Распределенные межпроцессные Previous: Создание структур IPC и   Contents

Концепция владельца

Данная концепция применима только к структурам IPC с распределенными ключами. Когда процесс впервые создает структуру IPC в пределах кластера при помощи xxxget(), компьютер, на котором он выполняется, становится ее ``владельцем''. Все операции манипулирования с данной структурой проводятся на машине-владельце. Фактически это означает, что может существовать только один активный экземпляр структуры IPC. Это свойство сильно влияет на простоту и семантику при обеспечении сохранности DIPC.

При DIPC процесс-производитель и процесс-потребитель данных могут состоять в следующих отношениях:

  1. Оба находятся на одной машине, которая является владельцем. Такая ситуация сильно напоминает обычные вызовы IPC.
  2. Оба находятся на одной машине, которая не является владельцем. В такой ситуации производитель будет посылать свои данные компьютеру - владельцу. Потребитель будет обращаться к владельцу данных, который в ответ будет посылать их ему. Данные будут ходить ``по кругу''.
  3. Каждый находится на отдельной машине, но одна из этих машин является владельцем. Один процесс вынужден передавать / принимать данные к/от владельцу(-а), а другой может пользоваться обычными технологиями IPC.
  4. Каждый находится на отдельной машине, но ни одна из этих машин не является владельцем. Данная ситуация подобна ситуации 2, но данные ``используют'' компьютер - владелец как промежуточную остановку при движении от компьютера - производителя к компьютеру - потребителю.
Следующие подходы оказывают влияние на принятие решения о привлечении владельца при любой активности DIPC:

  1. Централизованный подход, упрощающий алгоритмы. Процесс "знает", куда он должен отсылать данные, им производимые, - без необходимости опроса всех машин в кластере. Ситуация может быть наихудшей, если запрашиваемые данные еще не произведены, - потому что в этом случае ``проситель'' не знает, где их ожидать. Сопутствующей проблемой может быть то, как найти все ожидающие процессы в кластере, информировать их о доступности некоторых новых данных и выбрать, кто из них должен получить данные.
  2. Наиболее подходящий подход с обычным поведением IPC.
    Предположим, два процесса на двух различных машинах производят некоторые данные с отсутствием синхронизирующих тактов - им очень тяжело согласовать, какие данные должны быть произведены первыми. Рассмотренным выше методом производство и потребление данных организуется последовательно. В отношении к машине-владельцу это предполагает наличие жесткого порядка во время производства и потребления данных. Это сильно напоминает семантику обычных механизмов IPC.
Владельца структуры IPC после его ``закрепления'' заменить невозможно. Другими словами, миграция IPC отсутствует.



2004-06-22

next up previous contents
Next: Окончательное удаление структур IPC Up: DIPC - Распределенные межпроцессные Previous: Концепция владельца   Contents

Удаление структур IPC

Все запросы об удалении структуры IPC (xxxctl() вследствие команды IPC_RMID) посылаются машине-владельцу. Если структура действительно удалена, то об этом информируется referee, который, в свою очередь, информирует другие машины со структурами с таким же ключом - с целью удаления и этих структур. Удаление делается при наличии прав суперпользователя.

Схема ниже показывает, как обрабатывается ключевое удаление:

                               Сеть
(Компьютер-владелец)            |  (Арбитрирующий компьютер)
                                |
     |-1->back_end >-2-+        |
     |                 |        |
ядро |              employer-3--|-> referee
     |                          |

Здесь структура IPC удаляется на компьютере-владельце;
back_end находит запрос об этом (1) и раздваивает employer для его обработки (2), employer информирует referee об удалении (3).

На следующей схеме показано, что происходит после того, как referee получает эту информацию:

                               Сеть
(Арбитрирующий компьютер)       |         (Компьютер с ключом)
                                |
                                |                           |
               referee~--4------|-> front_end -->worker -6->| ядро
                                |                  |        |
                                |                  +-7--<---|

То, что показано выше, происходит на всех компьютерах, которые имеют созданные структуры IPC с данным ключом: referee посылает запрос об удалении ключа front_end (4), который раздваивает worker для реализации xxxctl() и непосредственного удаления с привилегиями суперпользователя; worker получает результаты этого процесса, но referee не беспокоится (не ожидает) об этом.

Дополнительно referee проверяет, находится ли структура IPC в разделяемой памяти и только ли один компьютер имеет такую структуру. Если это так, то посылается запрос менеджеру разделяемой памяти (shm_man) этой машины об отсоединении от разделяемой памяти. Это можно сделать по следующей причине: известно, что нет такого процесса на любом из компьютеров, имеющих разделяемую память, который будет обращаться к ней, так как при изначальном присоединении (предотвращение разрушения разделяемой памяти, когда все процессы на машине-владельце завершились) не ставилась цель захватывать что-либо еще. На следующей схеме показаны соответствующие действия для случая, когда сказанное выше справедливо:

                             Сеть
(Арбитрирующий компьютер       |  (Владелец разделяемой памяти)
                               |
               referee--8------|-> front_end --9-->shm_man
                               |

В итоге referee информирует shm_man об отсоединении (8 и 9). В данном случае referee также не ожидает подтверждения о том, что shm_man действительно отсоединился.

Далее показаны остальные действия:

                               Сеть
    (Компьютер-владелец)        |       (Арбитрирующий компьютер)
                                |
     |                          |
ядро |<--12--employer           |     referee
     |         |                |        |
     |         +-11-<front_end<-|-----10-+

Теперь referee сообщает оригинальному employer о том, что тот может продолжать работу (10 и 11); employer далее сообщит ядру о том, что и исходный пользовательский процесс может продолжаться (12).

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

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



2004-06-22

next up previous contents
Next: Локальная информация ядра Up: DIPC - Распределенные межпроцессные Previous: Удаление структур IPC   Contents

Окончательное удаление структур IPC

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

                                    Сеть
      (Запрашивающий компьютер)      |    (Арбитрирующий компьютер)
                                     |
     |-1->back_end~>-2-+             |
     |                 |             |
ядро |              employer--3------|-> referee 
                                     |

Когда на компьютере окончательно удаляется структура IPC, об этом сообщается back_end (1). Он раздваивает employer (2) и ``приносит'' эти новости referee (3), который окончательно удаляет свои соответствующие элементы, проверяет наличие такой структуры в разделяемой памяти и является ли действующий компьютер единственным, содержащим эту структуру. Он должен быть владельцем. В этом случае referee действует как показано на схеме:

                            Сеть
(Арбитрирующий компьютер)    |        (Владелец разделяемой памяти)
                             |
            referee --4------|-> front_end --5-->shm_man

В итоге referee сообщает shm_man об отсоединении (4 и 5). Все похоже на случай с простым удалением из разделяемой памяти структуры IPC.

Далее следует все остальное:

                                Сеть
     (Компьютер-владелец)        |        (Арбитрирующий~компьютер)
                                 |
     |                           |
ядро |<----8--employer           |     referee
     |         |                 |        |
     |         +-7-< front_end <-|------6-+

Здесь referee информирует employer о том, что все действия сделаны (6 и 7); employer информирует ядро о том, что исходный пользовательский процесс, который вызвал окончательное удаление, может продолжаться.

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



2004-06-22

next up previous contents
Next: Применение сигналов для IPC Up: Сигналы Previous: Посылка сигналов с помощью   Contents

Работа с сигналами в Linux

В системах Linux концепция сигналов была расширена. Это вызвано следующими недостатками старого подхода ANSI-C:

В новой концепции сигналов вначале вводится переменная примитивного типа данных:
sigset_t signal_set;
Множество сигналов инициализируется функцией sigemptyset():
#include <signal.h>

int sigemptyset(sigset_t *sig_m);

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

#include <signal.h>

int sigaddset(sigset_t *sig_m, int signr);

где signr являются номерами сигналов, которые добавляются во множество. Также можно использовать символическое имя, например:

sigaddset(&signal_set, SIGINT);
С помощью функции

#include <signal.h>

int sigdelset(setsig_t *sig_m, int signr);

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

#include <signal.h>

int sigismember(sigset_t sig_m,int signr);

Если сигнал присутствует во множестве, функция возвращает 1, иначе 0.

Существует функция для сохранения или изменения маски сигналов:

#include <signal.h>

int sigprocmask(int mode, const sigset_t *sig_m,

               sigset_t *alt_sig_m);

Применяются три режима использования этой функции:

  1. sigprocmask (mode, NULL, alt_sig_m). В текущем процессе множество сигналов записывается по адресу alt_sig_m. mode в этом случае не оказывает эффекта;
  2. sigprocmask (mode, sig_m, NULL). Маска сигналов изменяется на новую маску sig_m;
  3. sigprocmask (mode, sig_m, alt_sig_m). Сначала актуальная, используемая в текущем процессе маска сигналов записывается в alt_sig_m, то есть сохраняется. Затем устанавливается множество сигналов sig_m.
Функция позволяет использовать три предопределенные константы:

Если нужно изменить маску сигналов или запретить все сигналы во время выполнения определенной части кода, применяется функция:

#include <signal.h>

int sigsuspend(const sigset_t *sig_m);

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



2004-06-22

next up previous contents
Next: Безопасность Up: DIPC - Распределенные межпроцессные Previous: Окончательное удаление структур IPC   Contents

Локальная информация ядра

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

Некоторые из этих проблем могут быть решены путем использования альтернативных средств идентификации (например, использования логина вместо идентификатора пользователя; см. раздел о безопасности), а другие решения весьма затруднительны в реализации и могут быть вообще неосуществимы без внесения порции модификаций в ядро Linux при его разработке и реализации, а это может привести к потере совместимости. Так, информация о времени и номерах процессов в структурах IPC, например та, которая возвращается системными вызовами xxxctl() вследствие команды IPC_STAT, на прочих машинах не имеет смысла.



2004-06-22

next up previous contents
Next: Системные вызовы Up: DIPC - Распределенные межпроцессные Previous: Локальная информация ядра   Contents

Безопасность

Пользовательские идентификаторы пользователей Linux дифференцированы между ними. Реальные пользователи получают права доступа на какие-либо действия на основе сведений о том, кем они являются (владелец в некоторой группе и др.) в отношении объекта доступа. Значения пользовательских идентификаторов назначаются независимо на каждом компьютере, так что двум различным пользователям на разных машинах могут быть назначены одинаковые идентификаторы. Программы, использующие DIPC, нуждаются в исполнении кода на разных компьютерах - по большому счету, для доступа к структурам ядра. Их вызовы должны обеспечивать некоторый уровень безопасности в системе.

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

Единственным исключением из приведенного правила является пользователь root, который имеет максимальные полномочия на Linux-компьютере. Выполнение роли root на одной машине не налагает обязательств выполнять роль root на другой машине. По этой причине все пользователи root преобразуются в одного пользователя с именем dipcd. Администраторы могут контролировать этого пользователя путем корректировки его пользовательского и группового идентификаторов в отношении к другим группам. Программисты могут выбирать права доступа для структур IPC, чтобы разрешить или запретить доступ к ним.

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

Другой мерой безопасности, предложенной Майклом Шмицем (Michael Schmitz), является проблема избавления от вторжения злоумышленников в кластер DIPC. В файле /etc/dipc.allow перечисляются адреса компьютеров, которые могут быть частью кластера DIPC, т.е. ``надежных'' машин. Арбитр будет просто отбрасывать запросы от компьютеров, чьи адреса не находятся в данном файле. Опция -s (secure) заставит back_end также не выполнять никаких действий при запросах от ненадежных компьютеров (Обратитесь к файлу dipcd.doc для получения более подробной информации).

``Подделка'' IP-адресов при DIPC ``не пройдет''. Причиной этого является то, что dipcd всегда использует новые TCP-соединения для подтверждения запросов, т.е. дает подтверждение, что информация будет направляться достоверному компьютеру, а не ``самозванцу''.



2004-06-22

next up previous contents
Next: Разделяемая память Up: DIPC - Распределенные межпроцессные Previous: Безопасность   Contents

Системные вызовы

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

Во всех других случаях происходит удаленное исполнение системного вызова.

На следующей схеме показано ``прохождение'' пользовательского системного вызова DIPC (например, msgsnd()), обрабатываемого с помощью RPC.

                                       Сеть
       (Вызывающий компьютер)           |  (Компьютер-владелец)
                                        |
      |-1->back_end>--2-+               |                            |
      |                 |               |                            |
 ядро |<----------9--employer --3-------|->front_end--4-->worker -5->| ядро
      |                 |               |                  | |       |
                        +-8-<front_end<-|------------7-----+ +-6----<|

Здесь back_end отыскивает запросы в ядре (1), причем, адрес машины-владельца должен быть однозначно определен на уровне ядра. Затем back_end раздваивает процесс employer для обработки запроса (2); employer подключается к front_end на машине-владельце (3). После чего front_end раздвоит worker (4), который непосредственно исполнит системный вызов (5) и соберет все результаты (6). Результаты посылаются назад front_end на вызывающий компьютер (7), который передает их оригинальному employer (8). Наконец, employer отдаст результаты ядру (9), откуда пользовательский процесс и получит их.

В сети направление посылки пары запросов может отличаться от направления получения их подтверждений. Для обеспечения гарантии того, что ``гибридизации'' не произойдет, операции над структурами IPC упорядочиваются последовательно: пока процесс на удаленной машине что-либо ожидает, другие процессы, "желающие" использовать ту же самую структуру, также приостанавливаются.

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

Далее перечислены поддерживаемые системные вызовы:

  1. msgctl(), msgsnd(), msgrcv(), semctl(), semop() и
    shmctl(). Они исполняются непосредственно на машине - владельце, что осуществляется с помощью RPC. Эти вызовы перехватываются на уровне ядра, а все требующиеся параметры и данные копируются в память ядра.

    Выше было показано, что может выполняться достаточно большое число операций копирования между ядром и пользовательским пространством при одном удаленном системном вызове. Это сильно снижает производительность, особенно, когда копируются большие блоки данных. К счастью, большинство системных вызовов IPC System V передают данные только в одном направлении. Например, msgrcv() только принимает данные, поэтому они копируются из оригинальной задачи в ядро с небольшими затратами. Кроме того, многие другие системные вызовы IPC имеют немного байтов данных, являющихся их параметрами. Одним из примеров может быть xxxctl() с флагом IPC_RMID. Издержки копирования необходимых данных и результатов, даже с учетом задействования сети, незначительны.

    Результатом системных вызовов xxxctl() вследствие команды IPC_RMID на машине - владельце, в успешном случае, будет взаимодействие с referee.

  2. shmget(), semget() и msgget(). Эти вызовы привлекают
    referee, но не ядро машины-владельца (если системный вызов делается на машине-владельце!).

  3. shmat(). Он выполняется локально.

  4. shmdt(). Если реализуется удаление сегмента разделяемой памяти, то в результате он будет взаимодействовать с referee.



2004-06-22

next up previous contents
Next: Планирование разделяемой памяти Up: DIPC - Распределенные межпроцессные Previous: Системные вызовы   Contents

Разделяемая память

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

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

Чтобы защитить страницы разделяемой памяти от записи, нужно подвергнуть изменениям таблицы MMU. Аналогично делается и защита страниц от записи (в версиях DIPC до 2.0 желательно подгрузить страницу свопа для того, чтобы защитить ее от записи). Любой процесс, при попытке прочитать или записать защищенные от чтения страницы, столкнется со страничной ошибкой и будет приостановлен ядром. Для того чтобы страницы снова стали доступными для чтения или записи, новое содержимое передается по сети и замещает старое. С этого момента пользовательские процессы могут обращаться к нему.

DIPC может воспринимать в связке множество страниц виртуальной памяти, одновременно управляя ими всеми и передавая требующиеся. Это значит, что для любого целого n >= 1:

<размер_страницы_DIPC> =

     n * <размер_страницы_виртуальной_памяти>.

Макро dipc_page_size, включенное в файл /etc/dipc.conf, определяет размер распределенной страницы. Все компьютеры в составе кластера должны иметь одинаковый размер страниц DIPC.

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

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

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

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

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

  1. И запрашивающий компьютер, и менеджер разделяемой памяти используют страницы: запрашивающий компьютер посылает запрос о странице и получает ее.
  2. Запрашивающий компьютер использует страницы, в то время как менеджер разделяемой памяти использует сегменты: запрашивающий компьютер посылает запрос менеджеру об одной странице, а менеджер высылает целый сегмент. После этого ``проситель'' может получить доступ ко всей разделяемой памяти.
  3. Запрашивающий компьютер использует сегменты, в то время как менеджер разделяемой памяти использует страницы: запрашивающий компьютер просит весь сегмент, но менеджер интерпретирует это как запрос о странице, к которой происходит действительное обращение, и посылает только ее.
  4. И запрашивающий компьютер, и менеджер разделяемой памяти используют сегменты: запрашивающий компьютер посылает запрос о целом сегменте и забирает его.
В том случае, когда DIPC сконфигурирована в режиме постраничной передачи, любая передача содержимого разделяемой памяти может охватить все сегменты. Это противоречит подходам в ряде других систем с распределенной разделяемой памятью, при которых одновременно передаются только страницы. Именно по этой причине, DIPC может быть ``основанной на сегментах'' системой с распределенной разделяемой памятью. Программы могут замедляться, особенно когда разделяемая память объемная и/или сеть ``медленная'', а пересылки частые. Это также может увеличить вычислительную нагрузку, когда программист помещает ``партию'' информации в разделяемую память и разрешает другим пользоваться целым сегментом. При этом, пиковая нагрузка будет меньше, чем при постраничном доступе.

Следствия разрешения пересылок целыми сегментами при DIPC:

  1. Упрощается работа в гетерогенной среде с различными размерами страниц.
  2. В некоторых сетях время пересылки по сети намного меньше времени подготовки к передаче - поэтому, когда передатчик готов начать действовать, последующие передачи имеют очень малое влияние на общую нагрузку.
  3. Если процессу требуется вся разделяемая память, то ее пересылка одной операцией сокращает ненужные затраты.
  4. Эта идея эксплуатируется недостаточно и заслуживает того, чтобы быть оцененной.
  5. Точное соответствие между размером страницы и программным шаблоном для доступа к разделяемой памяти, в любом случае, является маловероятным (фальшивое разделение), ибо размер страницы в разных системах различен.
При любом раскладе, пользователь может сконфигурировать DIPC на компьютере таким образом, чтобы следовать более традиционным страничным схемам.

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

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

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

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

Возможны следующие четыре варианта запроса машинами доступа к разделяемой памяти:

  1. Имеется читатель и отдельная машина, "желающая" читать. Поскольку владелец тоже читатель, он передает соответствующее содержимое памяти запрашивающей машине и позволяет ей продолжать работу.

    На схеме показано, как это происходит:

                                    Сеть
         (Запрашивающий компьютер)   |      (Компьютер-владелец)
                                     |
         |-1->back_end >-2-+         |      <----------5----+    |
         |                 |         |      |               |    |
     ядро|              employer--3--|->front_end --4-->shm_man  |ядро
         |                           |      |                    |
         |                           |      +--6-->worker        |
    

    Когда процесс "желает" прочесть разделяемую память, уместное содержимое которой отсутствует, возникает исключение, о чем уведомляется часть DIPC в ядре. Внутри ядра подготавливается запрос, который для ``пробуждения'' порождает back_end (1) и раздваивает employer (2); employer подключается к компьютеру-владельцу (3) и доставляет запрос о чтении соответствующего содержимого разделяемой памяти процессу shm_man (4); shm_man посылает сообщение front_end (используя ``разворачивающий'' Internet-сокет) (5); front_end раздваивает worker (6) для действительного выполнения пересылки.

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

                                  Сеть
          (Запрашивающий компьютер)|(Компьютер-владелец)
                                   |
         |                         |                    |
         |                         |                    |
    ядро |       +-8-<front_end<---|-------7---worker   | ядро
         |       |                 |            | |     |
         |<-11-worker<-------------|--10--------+ +-9--<|
    

    Итак, worker на машине-владельце ``отдает'' разделяемую память (с помощью системного вызова shmget()) и подключается к front_end на запрашивающей машине (7), который раздваивает еще один worker (8). Затем worker на машине-владельце читает соответствующее содержимое разделяемой памяти (9) и сразу передает его (без вмешательства front_end) вновь раздвоенному worker (10), который помещает его в разделяемую память запрашивающего компьютера (11). После этого, запрашивающая машина наконец получает информацию, которую хотела.

    Ниже представлены оставшиеся действия:

                                    Сеть
          (Запрашивающий компьютер)  | (Компьютер-владелец)
                                     |
         |      worker--------12-----|------------->front_end
         |                           |                   |
         |                           |                   |
     ядро|            front_end <----|--14--shm_man <-13-+
         |               |           |
         |<-16-employer<-15-+
    

    После того, как worker поместил содержимое в разделяемую память, он посылает подтверждение shm_man (12 и 13). Затем shm_man посылает сигнальное сообщение оригинальному employer, который передает запрос о чтении (14 и 15); employer сообщает ядру об этом (16) и ядро перезапускает процессы, "желающие" читать разделяемую память. На этом вся операция завершается.

  2. Имеются читатель и отдельная машина, "желающая писать". Менеджер посылает защищенное от чтения сообщение всем читателям, а если нужно (новый читатель не совпадает с предыдущим), то предоставляет процессу, ``желающему быть'' писателем, соответствующее содержимое памяти, чтобы позволить ему продолжать выполнение:

                                  Сеть
        (Запрашивающий компьютер)  |  (Компьютер-владелец)
                                   |
        |-1->back_end>-2-+         |                        |
        |                |         |                        |
    ядро|           employer--3----|->front_end--4-->shm_man|ядро
        |                          |                        |
    

    Запрос о записи попадает внутрь ядра, и back_end активируется для его обработки (1). В дальнейшем об этом информируется shm_man на машине-владельце (2, 3 и 4). Заметьте, что все сказанное может происходить на одном компьютере (если владелец хочет записывать в разделяемую память). Иногда все показанные процессы, действительно, выполняются на одной машине:

                                   Сеть
            (Компьютер-читатель)    |   (Компьютер-владелец)
         |                          |
    ядро |<-7-worker<-6-front_end<--|------------5-----shm_man
         |                          |
    

    Менеджер разделяемой памяти посылает защищенное от чтения сообщение всем компьютерам-читателям (среди которых может быть и машина-владелец - в этом случае используется разворачивающий адрес) (5); front_end на каждой машине раздваивает worker (6) и запрос исполняется (7). С этого момента все попытки чтения или записи соответствующего содержимого разделяемой памяти любым из этих компьютеров приведут к остановке ответственных процессов. Указанные выше действия повторяются для каждого читателя.

    Если компьютер, запрашивающий возможность записи, был одним из читателей, ему содержимое не посылается: владелец ответственен за предоставление новому ``просителю'' содержимого разделяемой памяти.

    Далее shm_man ожидает подтверждение, и когда оно прибывает, посылает сигнал employer, который изначально инициировал все процессы.

  3. Известны писатель и некто, желающий читать. Если писатель или ``проситель'' не являются владельцами, владелец возьмет на себя роль ``просителя'' о чтении разделяемой памяти. Это нужно для того, чтобы убедиться в том, что он всегда может стать читателем. После того, как все сделано, предыдущий писатель станет читателем.

    Если shm_man ``включает'' владельца, как ``желающую читать'' машину, происходит следующее:

                                    Сеть
            (Компьютер-писатель)     |    (Компьютер-владелец)
          |                          |
     ядро | worker <-6- front_end <--|------------5-----shm_man
          |                          |
    

    Владелец будет запрашивать у действующего компьютера - писателя о передаче ему соответствующего содержимого разделяемой памяти. Это реализуется посылкой запроса процессу front_end писателя (5), который в свою очередь разветвит worker для выполнения пересылки (6).

    Соответствующее содержимое разделяемой памяти передается компьютеру - владельцу (компьютер - владелец заменяется компьютером - писателем, а запрашивающий компьютер - владельцем).

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

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

    Сначала shm_man уведомляется о запросе. Затем он пошлет сообщение текущему писателю защитить от записи соответствующие части разделяемой памяти и передаст содержимое новому писателю.


next up previous contents
Next: Планирование разделяемой памяти Up: DIPC - Распределенные межпроцессные Previous: Системные вызовы   Contents
2004-06-22

next up previous contents
Next: Прокси Up: DIPC - Распределенные межпроцессные Previous: Разделяемая память   Contents

Планирование разделяемой памяти

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

Новый читатель страницы или целого сегмента будет установлен, если:

Новый писатель будет установлен, если:



2004-06-22

next up previous contents
Next: UDP/IP Up: DIPC - Распределенные межпроцессные Previous: Планирование разделяемой памяти   Contents

Прокси

Прокси - это рабочий процесс, который исполняется системным вызовом semop() с флагом SEM_UNDO. Когда worker исполняет такую функцию, он не прекращает свого выполнения, а записывает сетевой адрес и идентификатор процесса удаленного процесса (процесс, для которого исполняется системный вызов) и продолжает работу. Противоположные действия могут создать эффекты semop(), которые должны исчезать при выходе из worker. Теперь worker становится прокси и устанавливает доменный сокет Linux с именем PROXY_<IP_адрес_удаленного_процесса>_
<идентификатор_удаленного_процесса>
, используя приведенную выше информацию. Примером может быть: PROXY_191.72.2.0_234. Отныне он будет исполнять все операции semop() по защите исходного удаленного процесса независимо от того, указали ему флаг SEM_UNDO или нет.

Процесс front_end будет проверять присутствие подходящего прокси перед тем, как раздваивать процесс worker для обработки функции semop(). Если прокси присутствует, он подключается к нему с помощью доменного сокета Linux и ``инструктирует'' его о выполнении semop().

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

Когда исходный удаленный процесс останавливает исполнение, все его прокси информируются о завершении и ``делаются'' все необходимые ``отмены''.



2004-06-22

next up previous contents
Next: Как определяются адреса Up: DIPC - Распределенные межпроцессные Previous: Прокси   Contents

UDP/IP

По умолчанию DIPC использует TCP/IP, но, начиная с версии 1.0, она также работает с сетевым протоколом UDP/IP. В результате использования UDP значительно повышается скорость пересылки данных по сети, в основном за счет отсутствия перегрузок, связанных с ориентированными на соединение протоколами, например, TCP. Все компьютеры в одном кластере должны использовать одинаковые сетевые протоколы.

UDP не является достоверным - имеется в виду, что вследствие перманентных или временных ошибок некоторые данные могут не доставляться по назначению. UDP не может преодолевать ошибки. Также он не осуществляет проверку контрольных сумм, следовательно, возможно искажение данных. Существуют и иные сервисы, доступные для TCP и отсутствующие для UDP.

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

Сказанное выше означает, что:

  1. Любая проблема может вылиться в ошибку программы распределенного приложения.
  2. Не представляется возможным передавать данные, выходящие за рамки ограничений ядра. Например, при использовании SLIP посредством UDP невозможно за раз передать больше 3200 байтов, что напрямую связано с ограниченными размерами буферов ядра. Все это может заставить программиста удержаться от использования больших размеров сегментов разделяемой памяти и сообщений.
Упомянутую ситуацию со SLIP можно улучшить распределением большего числа буферов (tx_queue_len в linux/drivers/net/slip.c).



Subsections

2004-06-22

next up previous contents
Next: Программирование с помощью DIPC Up: UDP/IP Previous: UDP/IP   Contents

Как определяются адреса

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

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

Подпроцессы, запускающиеся при использовании referee или
front_end хорошо ``знают'' порты. При чтении UDP-сокета также предоставляется адрес отправителя: после первого контакта процессы обмениваются несколькими байтами данных, тем самым получая полные адреса друг друга.



2004-06-22

next up previous contents
Next: Введение Up: DIPC - Распределенные межпроцессные Previous: Как определяются адреса   Contents

Программирование с помощью DIPC



Subsections

2004-06-22

next up previous contents
Next: Удаленное выполнение программ Up: Программирование с помощью DIPC Previous: Программирование с помощью DIPC   Contents

Введение

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

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



2004-06-22

next up previous contents
Next: Трубы (pipes) Up: Сигналы Previous: Работа с сигналами в   Contents

Применение сигналов для IPC

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

#include <unistd.h>

#include <stdio.h>

#include <signal.h>

#include <sys/types.h>

enum { FALSE, TRUE };

sigset_t sig_m1, sig_m2, sig_null;

int signal_flag=FALSE;

void sig_func(int signr) {

  start_signalset();

  signal_flag = TRUE;

}

void start_signalset() {

  if(signal(SIGUSR1, sig_func) == SIG_ERR)

  exit(0);

  if(signal(SIGUSR2, sig_func) == SIG_ERR)

  exit(0);

  sigemptyset(&sig_m1);

  sigemptyset(&sig_null);

  sigaddset(&sig_m1,SIGUSR1);

  sigaddset(&sig_m1,SIGUSR2);

  if(sigprocmask(SIG_BLOCK, &sig_m1, &sig_m2) < 0)

  exit(0);

}

void message_for_parents(pid_t pid) {

  kill(pid,SIGUSR2);

}

void wait_for_parents() {

  while(signal_flag == FALSE)

  sigsuspend(&sig_null);

  signal_flag = FALSE;

  if(sigprocmask(SIG_SETMASK, &sig_m2, NULL) < 0)

  exit(0);

}

void message_for_child(pid_t pid) {

  kill(pid, SIGUSR1);

}

void wait_for_child(void) {

  while(signal_flag == FALSE)

  sigsuspend(&sig_null);

  signal_flag = FALSE;

  if(sigprocmask(SIG_SETMASK, &sig_m2, NULL) < 0)

  exit(0);

}

int main() 

 {

  pid_t pid;

  char x,y;

  start_signalset();

  switch( pid = fork())

  {

    case -1 : fprintf(stderr, "Ошибка fork()\n");

              exit(0);

    case 0 : /*...в потомке...*/

              for(x=2;x<=10;x+=2) {

                wait_for_parents();

                write(STDOUT_FILENO, "ping-",

                      strlen("ping-"));

                message_for_parents(getppid());

              }

              exit(0);

    default : /*...в родителе....*/

              for(y=1;y<=9;y+=2) {

                write(STDOUT_FILENO, "pong-",

                      strlen("pong-"));

                message_for_child(pid);

                wait_for_child();

              }

  }

  printf("\n\n");

  return 0; }



2004-06-22

next up previous contents
Next: Обмен данными Up: Программирование с помощью DIPC Previous: Введение   Contents

Удаленное выполнение программ

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

Для того, чтобы IPC имела смысл, должны выполняться более одного процесса, использующего ее механизмы. При обычной IPC, эти процессы могут быть созданы с помощью системного вызова fork(). За ним должен следовать вызов exec(). В рамках DIPC использовать fork() для создания удаленного процесса нельзя, потому что он создает локальный процесс. Можно ``вручную'' запустить процессы удаленно, т.е. после того, как главная программа подготовила механизмы DIPC (разделяемая память, очереди сообщений или наборы семафоров), она ожидает соответствующее состояние, например, семафоров. После этого другой пользователь может запускать программы с консоли удаленной машины. Теперь эти программы должны использовать предварительные договоренности о семафорах, чтобы информировать главный процесс о своей готовности. Далее пользователь должен уделить внимание тому, чтобы не запускать удаленные программы слишком быстро - из-за возможной неготовности разделяемых структур. Программы-примеры из каталога examples/message работают именно в таком стиле.

Можно также использовать программы, подобные rsh для осуществления задействования удаленных программ. В данном случае главный процесс подготавливает все необходимое и затем раздваивает некий вспомогательный процесс для исполнения скрипта на Shell. Скрипт использует команды rsh для удаленного исполнения программ. Программы-примеры реализации данного метода располагаются в каталоге examples/image. Применение rsh для таких целей имеет определенное достоинство: после того, как вы разработали свою программу, можете просто отредактировать упомянутый скрипт - с целью указания в нем изменений, связанных с компьютерами, на которых вы хотели бы запускать свои программы - без необходимости перекомпиляции вашей программы и других действий. Аналогично перечень удаленных машин могут задать пользователи - без необходимости наличия доступа к исходным текстам вашей программы, - что может оказаться важным преимуществом коммерческих программ.

Еще одно замечание по поводу rsh: если он исполняется очень часто, то inetd может ``выключить'' соответствующий сервис. Вы можете прибегать к редактированию файла /etc/inetd.conf для регулирования подобного поведения.



2004-06-22

next up previous contents
Next: Синхронизация Up: Программирование с помощью DIPC Previous: Удаленное выполнение программ   Contents

Обмен данными

Большинство программ принимают некие данные, обрабатывают их соответствующими способами и выдают результаты. ``Легкость'' обмена данными в распределенных системах очень важна. В рамках DIPC вы можете использовать для этих целей сообщения или сегменты разделяемой памяти. Разделяемая память - это асинхронный механизм. Программа может получить к ней доступ, когда ``пожелает''. При этом может возникнуть длинная задержка между временем предоставления доступа и временем действительного приема данных. Программа ничего этого ``не замечает''. Сообщения являются очень доступным способом передачи данных. Вы можете передать только те данные, которые захотите, и к тому же - только нужному процессу. Такой коммуникационный стиль называется стилем ``один - одному''. Сообщения можно представить как синхронный способ обмена данными. Программа приостанавливается системным вызовом msgrcv() до тех пор, пока другой процесс не прибегнет к вызову msgsnd(). Программист должен сам решать, какой из двух приведенных методов нужно использовать.

Каждая страница разделяемой распределенной памяти может
иметь одновременно несколько читателей на различных компьютерах, но для нее может быть только один компьютер с процессом-писателем. Если DIPC сконфигурирована с поддержкой режима посегментной передачи, то сказанное применимо для сегмента целиком. Это значит, что читатели могут иметь ``разделяемую'' память (страницы) в своей локальной памяти и, следовательно, свободно ей пользоваться; но запись в одну из ``копий'' приведет к недостоверности данных на других машинах. Чтобы можно было продолжить чтение, содержимое памяти должно быть снова передано другим машинам. Если DIPC сконфигурирована с поддержкой режима посегментной передачи, то внутренний контекст разделяемого сегмента данных передается по сети. Если же процессы пользуются различными частями разделяемой памяти, вы должны установить постраничный режим передачи для DIPC, - чтобы программы, соответствующие процессам на различных компьютерах, могли адекватно читать страницы разделяемой памяти и записывать в них.

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

Рассмотрим возможный сценарий использования разделяемой памяти при режиме посегментной передачи. Главный процесс запускается и размещает входные данные в разделяемой памяти. Затем запускаются другие удаленные процессы, которые могут читать данный разделяемый сегмент. Может одновременно быть несколько читателей, причем читать они могут также одновременно. Кроме того, поскольку одно содержимое сегмента пересылается каждой машине, то вероятность возникновения перегрузки при передачах будет невелика. (Это имеет смысл, только если удаленные процессы нуждаются во всех данных из разделяемого сегмента памяти). После обработки входных данных удаленные задачи могут вернуть результаты в тот же или другой сегмент разделяемой памяти, но могут использовать и сообщения. Так как писатель для разделяемой памяти может быть только один, то каждой удаленной машине рекомендуется предоставлять персональный сегмент для возвращения результатов. Таким образом можно организовать параллелизм. Следует напомнить, что DIPC можно использовать, и не придерживаясь рассмотренных выше указаний, но производительность может снизиться.

Постраничный режим передачи DIPC можно применять для того, чтобы позволить процессам параллельно читать и записывать различные части одной и той же разделяемой памяти.

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

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

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

Информирование выполяется двумя прерываниями. Одно из них - DIPC_SIG_WRITER, второе - DIPC_SIG_READER. Если DIPC сконфигурирована с поддержкой их передачи, то первое передается, когда процесс еще только становится писателем (ранее он мог быть просто читателем или вообще не иметь связей с сегментом). При посегментном режиме передачи процесс ``знает'', что имеет эксклюзивный контроль над сегментом. Процесс, например, может записать идентификатор текущей архитектуры в первые байты сегмента и поместить произведенные им данные в разделяемую память. Второе прерывание посылается, когда процесс становится читателем сегмента (ранее он не пользовался его содержимым). Процесс, например, может проверить первые байты сегмента для того, чтобы установить, нужно ли делать какие-либо преобразования. Обратите внимание на то, что прерывание DIPC_SIG_READER генерируется, если данная машина предварительно была писателем, а теперь стала читателем, т.е. ее права доступа стали более ограниченными.

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

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



2004-06-22

next up previous contents
Next: Снижение сетевой нагрузки Up: Программирование с помощью DIPC Previous: Обмен данными   Contents

Синхронизация

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



2004-06-22

next up previous contents
Next: Поддерживаемые системные вызовы Up: Программирование с помощью DIPC Previous: Синхронизация   Contents

Снижение сетевой нагрузки

Известно несколько причин, требующих снизить количество сетевых операций вашего приложения. Одна из них - это, очевидно, производительность, поскольку сетевые операции очень ``дорогостоящие'' в сравнении с локальными. Другая причина связана с некоторыми частными ограничениями кода TCP/IP ядра: за короткое время невозможно сделать слишком много сетевых соединений: через некоторое время ядро выдаст: ``Resource temporarily not available'' и ваше приложение завершится ненормально. Вы столкнетесь с этим, если имеете очень быстрые компьютеры, но не сеть.

Ниже приводятся некоторые рекомендации, которые надо учитывать при программировании DIPC:

  1. Владелец структуры IPC выполняет над ней все операции локально. Поэтому, машину, которая чаще всех использует структуру IPC, лучше сделать ее владельцем. Реализуйте это путем создания процесса на данной машине, который первым создаст структуру посредством xxxget().
  2. Периодические блокировки разделяемой памяти - не лучший выход, особенно если наблюдается ``напряженное'' чередование ``тестирований и установок'' блокировок, что приведет к очень частым пересылкам разделяемой памяти по сети. Однако если периодические проверки в подавляющем числе случаев выражаются как тестирующие, и лишь изредка как установочные, вы можете допустить их применение, а также допустить исключение периодических блокировок с помощью semop().
  3. Работа таких системных вызовов, как неблокирующий semop() (с IPC_NOWAIT) в ``напряженном'' цикле, например, for(;;;) { semop(...); ...}, все равно приведет к большому числу сетевых операций, если она не выполняется на компьютере-владельце. Рекомендуется не применять подобную технику, а если вам все-таки необходимо ее присутствие в приложении, то для улучшения ситуации добавьте некоторую задержку перед каждым соответствующим системным вызовом. Например, sleep(1) приостановит приложение на одну секунду. Для разграничения секундных задержек можно применить системный вызов select() и примерно такой код:
struct timeval tv;

fd_set fd;

for(; ; ;)

{

  /* сначала проверка семафора, затем приостановка */

  check_the_semaphore_and_break_if_needed(...);

  /* замечание: Вы должны выполнить инициализацию

      внутри цикла */

  tv.tv_sec = 0;

  /* ожидание в течение 0.5 секунды = 2 семафора

    за секунду */

  tv.tv_usec = 0.5 * 1000000;

  FD_ZERO(&fd);

  select(FD_SETSIZE, &fd, NULL, NULL, &tv);

}



2004-06-22

next up previous contents
Next: Инсталляция программного обеспечения Up: Программирование с помощью DIPC Previous: Снижение сетевой нагрузки   Contents

Поддерживаемые системные вызовы

К поддерживаемым в DIPC формам системных вызовов IPC относятся:

  1. Разделяемая память:
  2. Сообщения:
  3. Семафоры:

Если dipcd не выполняется, то все описанные системные вызовы исполняются локально, как если бы они были обычными вызовами IPC. Данные системные вызовы могут возвращать код ошибки not found, как и при IPC. Это может означать, например, что произошел сбой сетевой операции или наступил тайм-аут.



2004-06-22

next up previous contents
Next: Программы-примеры Up: Программирование с помощью DIPC Previous: Поддерживаемые системные вызовы   Contents

Инсталляция программного обеспечения

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



2004-06-22

next up previous contents
Next: Интерфейс передачи сообщений MPI Up: Программирование с помощью DIPC Previous: Инсталляция программного обеспечения   Contents

Программы-примеры

Вы можете обратиться к программам-примерам за пояснениями всего рассмотренного выше. Просмотрите и файл Readme из каталога с примерами. Программа в каталоге examples/hello проста для инсталляции и выполнения. Вы можете начать с нее.



2004-06-22

next up previous contents
Next: Первая программа MPI Up: Высокоуровневые средства межпроцессного взаимодействия Previous: Программы-примеры   Contents

Интерфейс передачи сообщений MPI

Интерфейс передачи сообщений MPI представляет собой библиотеку функций и макроопределений, которые могут использоваться в программах на C, Фортране и C++. MPI предназначен для написания программ, которые используют при работе несколько процессоров с помощью обмена сообщениями. Он является одним из первых стандартов для программирования параллельных процессов и первым, основанным на обмене сообщениями. Информация этого раздела предназначена для использования программистами, которые имеют опыт программирования на C, но мало знакомы с механизмами передачи сообщений.



Subsections

2004-06-22

next up previous contents
Next: Структура программы MPI Up: Интерфейс передачи сообщений MPI Previous: Интерфейс передачи сообщений MPI   Contents

Первая программа MPI

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

В MPI процессы, участвующие в выполнении параллельной программы, идентифицируются последовательностью неотрицательных целых чисел. Если в выполнении программы участвуют P процессов, то они будут иметь номера (ранги) 0, 1, ..., P-1. В следующей программе каждый процесс с рангом, не равным 0, посылает сообщение в процесс 0, а процесс 0 выводит все сообщения, которые он получил:

#include <stdio.h>

#include ''mpi.h''

 

main(int argc, char** argv) {

  int my_rank; /* Ранг процесса */

  int p; /* Количество процессов */

  int source; /* Ранг посылающего */

  int dest; /* Ранг принимающего */

  int tag = 50; /* Тэг сообщений */

  char message[100]; /* Память для сообщения */

  MPI_Status status; /* Статус возврата */

 

  MPI_Init(&argc, &argv);

  MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

  MPI_Comm_size(MPI_COMM_WORLD, &p); 

  if (my_rank != 0) {

     sprintf(message, ''Привет из процесса %d!'', my_rank);

     dest = 0;

     /* Используется strlen(message)+1

       чтобы включить '\0' */

     MPI_Send(message, strlen(message)+1,

        MPI_CHAR, dest, tag, MPI_COMM_WORLD);

  } else { /* my_rank == 0 */

     for (source = 1; source < p; source++) {

     MPI_Recv(message, 100, MPI_CHAR, source, tag,

        MPI_COMM_WORLD, &status);

     printf(''%s\n'', message);

     }

  }

  MPI-Finalize();

} /* main */

Если программу откомпилировать и запустить для четырех процессов, она выведет:

Привет из процесса 1!

Привет из процесса 2!

Привет из процесса 3!

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

  1. Пользователь дает команду операционной системе, которая помещает копию выполняемой программы на каждый процессор.
  2. Каждый процессор начинает выполнение его копии программы.
  3. Различные процессы могут в каждый момент времени выполнять различные операторы, осуществляя переходы в пределах программы. Обычно переход зависит от ранга процесса.
Приведенная программа использует парадигму ОПМД (одна программа - множественные данные). При этом различные программы, работающие на различных процессорах, реализуются через ветвление в пределах одной общей программы на основе рангов процесса. Операторы, выполненные процессом 0, отличаются от выполненных другими процессами даже в случае, когда все процессы управляются одной и той же программой. Это наиболее распространенный метод написания программ ОПМД.



2004-06-22

next up previous contents
Next: Определение номера процесса Up: Интерфейс передачи сообщений MPI Previous: Первая программа MPI   Contents

Структура программы MPI

Каждая программа MPI содержит директиву препроцессора:

#include ''mpi.h''
Файл mpi.h содержит определения, макроопределения и прототипы функций, необходимых для компиляции программ MPI. Прежде чем вызывать любые другие функции MPI, нужно однократно вызвать функцию MPI_Init(). Ее аргументы - это указатели на параметры функции main() - argc и argv. Они позволяют системе выполнять любую специальную настройку, чтобы использовать библиотеку MPI. После того, как программа, использующая библиотеку MPI, закончилась, необходимо вызвать MPI_Finalize(). Эта функция завершает все незавершенные действия MPI - например, бесконечное ожидание передач. Типичная программа MPI имеет следующую структуру:

#include ''mpi.h''

. . . 

main(int argc, char** argv) {

. . .

/* Функции MPI нельзя вызывать до этого момента */

MPI_Init(&argc, &argv);

. . . 

MPI_Finalize(); 

/* Функции MPI нельзя вызывать после этого момента */

. . . 

} /* main */



2004-06-22

next up previous contents
Next: Использование труб Up: Локальные и удаленные средства Previous: Применение сигналов для IPC   Contents

Трубы (pipes)



Subsections

2004-06-22

next up previous contents
Next: Структура сообщения Up: Интерфейс передачи сообщений MPI Previous: Структура программы MPI   Contents

Определение номера процесса

MPI предлагает функцию MPI_Comm_rank(), которая возвращает ранг процесса. Ее синтаксис:

int MPI_Comm_rank(MPI_Comm comm, int rank);
Первый аргумент является коммуникатором. По существу коммуникатор - это набор процессов, которые могут посылать друг другу сообщения. Для небольших программ единственным необходимым коммуникатором является MPI_COMM_WORLD. Он предопределен в MPI и содержит все запущенные после начала выполнения программы процессы. Ранг процесса возвращается во второй аргумент rank.

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

int MPI_Comm_size(MPI_Comm comm, int size);
Количество процессов в коммуникаторе comm возвращается в переменную size.



2004-06-22

next up previous contents
Next: Функции передачи сообщений MPI_Send() Up: Интерфейс передачи сообщений MPI Previous: Определение номера процесса   Contents

Структура сообщения

Фактическая передача сообщений в программе выполняется функциями MPI_Send() и MPI_Recv(). Первая функция посылает сообщение определенному процессу. Вторая получает сообщение от некоторого процесса. Эти функции являются самыми основными командами передачи сообщений в MPI. Чтобы сообщение было успешно передано, система должна добавить немного информации к данным, которые "желает" передать прикладная программа. Эта дополнительная информация формирует конверт сообщения. В MPI конверт содержит следующую информацию:

Эти детали могут использоваться приемником, чтобы распознавать поступающие сообщения. Аргумент source применяется, чтобы различать сообщения, полученные от разных процессов. Тэг представляет собой указанное пользователем значение int, которое предназначено, чтобы отличить сообщения, полученные от одного процесса. Пусть процесс А посылает два сообщения процессу B; оба сообщения содержат одно значение типа float. Одно из значений должно использоваться в вычислении, а другое должно быть выведено на экран. Чтобы определить, какое из них первое, А использует различные тэги этих сообщений. Если B употребляет те же самые тэги при приеме, он будет "знать", что с ними делать. MPI предполагает, что в качестве тэгов могут применяться целые числа в диапазоне 0 - 32767. Большинство реализаций позволяет использовать гораздо большие значения.

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



2004-06-22

next up previous contents
Next: Пример приложения с обменом Up: Интерфейс передачи сообщений MPI Previous: Структура сообщения   Contents

Функции передачи сообщений
MPI_Send() и MPI_Recv()

Синтаксис функций приведен ниже:

int MPI_Send(void* message, int count,
    MPI_Datatype datatype, int dest, int tag,
    MPI_Comm comm)

int MPI_Recv(void* message, int count,
    MPI_Datatype datatype, int source, int tag,
    MPI_Comm comm, MPI_Status* status)
Содержимое сообщения хранится в блоке памяти, на который указывает аргумент message. Следующие два аргумента, count и
datatype, позволяют системе определить конец сообщения: оно содержит последовательность count значений, каждое из которых имеет тип данных datatype, который не является встроенным типом C, хотя большинство предопределенных типов соответствует типам C. Предопределенные типы MPI и соответствующие типы C представлены в табл. 8.


Таблица 8. Соответствие между типами MPI и C.


Тип MPI Соответствующий тип С
MPI_CHAR signed char
MPI_SHORT signed short int
MPI_INT signed int
MPI_LONG signed long int
MPI_UNSIGNED_CHAR unsigned char
MPI_UNSIGNED_SHORT unsigned short int
MPI_UNSIGNED unsigned int
MPI_UNSIGNED_LONG unsigned long int
MPI_FLOAT float
MPI_DOUBLE double
MPI_LONG_DOUBLE long double
MPI_BYTE -
MPI_PACKED -

Последние два типа, MPI_BYTE и MPI_PACKED, не соответствуют стандартным типам C. Тип MPI_BYTE используется, если система не должна выполнять преобразование между различными представлениями данных (например, в гетерогенной сети рабочих станций, применяющих различные представления данных). Особенность типов MPI в том, что они позволяют приложениям на гетерогенных архитектурах взаимодействовать единообразно, выполняя необходимые преобразования типов прозрачно для пользователя.

Следует отметить, что количество памяти, выделенной для буфера получения, может точно не соответствовать размеру получаемого сообщении. Например, при работе программы размер сообщения, посылаемый процессом 1 равен strlen (message +1) = 28 символов, а процесс 0 получает сообщение в буфер, который имеет размер 100 символов. В любом случае процесс-получатель не может знать точного размера посланного сообщения. Поэтому MPI позволяет принимать сообщение, пока есть место в выделенной памяти. Если места недостаточно, возникает ошибка выхода за пределы памяти.

Аргументы dest и source соответствуют рангам процессов приема и посылки. MPI позволяет source принимать значение предопределенной константы MPI_ANY_SOURCE, которое может использоваться, если процесс готов получить сообщение от любого иного процесса. Для dest подобной константы нет.

MPI имеет два механизма, предназначенные для разделения пространств сообщении - тэги и коммуникаторы. Аргументы tag и comm соответственно определяют тэг и коммуникатор. Существует групповой тэг MPI_ANY_TAG, определяющий любой тэг для сообщения.

Последний аргумент MPI_Recv(), status, возвращает информацию, относящуюся к фактически полученным данным. Он ссылается на запись с двумя полями: одно - для источника, другое - для тэга. Например, если в качестве источника был указан MPI_ANY_SOURCE, то status будет содержать ранг процесса, который прислал сообщение.

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


Таблица 9. Коммуникационные режимы MPI.


Название режима Условие завершения Функция
Стандартная передача Как для синхронной или буферизованной MPI_SEND
Синхронная передача Завершается, когда завершен прием MPI_SSEND
Буферизованная передача Всегда завершается MPI_BSEND
Передача по готовности Всегда завершается MPI_RSEND
Прием Завершается, когда сообщение принято MPI_RECV


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

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

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

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


next up previous contents
Next: Пример приложения с обменом Up: Интерфейс передачи сообщений MPI Previous: Структура сообщения   Contents
2004-06-22

next up previous contents
Next: Параллелизация правила трапеции Up: Интерфейс передачи сообщений MPI Previous: Функции передачи сообщений MPI_Send()   Contents

Пример приложения с обменом сообщениями

В качестве примера рассматривается программа вычисления определенного интеграла по правилу трапеции. Последовательный вариант вычисляет $\int _{b}^{a}f(x)dx$ разделением интервала $\left[a,b\right]$ на $n$ равных сегментов и суммированием частных оценок интеграла для каждого сегмента по формуле:


\begin{displaymath}
h\left[f(x_{0})/2+f(x_{n})/2+\Sigma _{i=1}^{n-1}f(x_{i})\right]\end{displaymath}

Здесь $h=(b-a/n)$, и $x_{i}=a+ih,$ где i = 0, 1, ..., n. Если поместить вычисление $f(x)$ в отдельную функцию, то последовательный вариант программы выглядит так:

#include <stdio.h>

float f(float x) {

  float return_val;

  /* Вычисляет f(x).

    Запоминает результат в return_val. */

  . . .

  return return_val;

} /* f */

 

main() {

  float integral; /* Результат вычисления */

  float a, b; /* Левая и правая границы */

  int n; /* Количество интервалов */

  float h; /* Ширина интервала */

  float x;

  int i;

  printf(''Введите a, b, и n\n''); 

  scanf(''%f %f %d'', &a, &b, &n);

  h = (b-a)/n;

  integral = (f(a) + f(b))/2.0;

  x = a;

  for (i = 1; i <= n-1; i++) {

    x += h;

    integral += f(x);

  }

  integral *= h;

  printf(''C n = %d трапециями, интеграл \n'', n);

  printf("от %f до %f = %f`\n'', a, b, integral);

} /* main */



Subsections

2004-06-22

next up previous contents
Next: Ввод / вывод для Up: Пример приложения с обменом Previous: Пример приложения с обменом   Contents

Параллелизация правила трапеции

Вариант параллелизации этой программы сводится к тому, чтобы просто разделить интервал $\left[a,b\right]$ между процессами, чтобы каждый процесс оценил интеграл $f(x)$ на своем подинтервале. Для оценки величины полного интеграла локальные значения всех процессов суммируются.

Пусть программа содержит $p$ процессов и $n$ трапеций, где $n$ является кратным $p$. Тогда первый процесс вычисляет область первых $n/p$ трапеций, второй - область следующих $n/p$ и т.д. Тогда процесс $q$ будет оценивать интеграл по интервалу


\begin{displaymath}
\left[a+q\frac{nh}{p},a+\left(q+1\right)\frac{nh}{p}\right]\end{displaymath}

Каждый процесс должен обладать следующей информацией:

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

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

#include <stdio.h>

#include ''mpi.h''

 

float f(float x) {

  float return_val;

 

  /* Вычисляет f(x).

    Возвращает результат в return_val. */

  . . .

  return return_val;

}

 

float Trap(float local_a, float local_b, int local_n,

      float h) {

  float integral; /* Результат вычислений */

  float x;

  int i;

 

  integral = (f(local_a) + f(local_b))/2.0;

  x = local_a;

  for (i = 1; i <= local-n-1; i++) {

    x += h;

    integral += f(x);

  }

  integral *= h;

  return integral;

} /* Trap */

 

main(int argc, char** argv) {

  int my_rank; /* Ранг процесса */

  int p; /* Количество процессов */

  float a = 0.0; /* Левая граница */

  float b = 1.0; /* Правая граница */

  int n = 1024; /* Количество трапеций */

  float h; /* Ширина трапеции */

  float local_a; /* Левая граница для процесса */

  float local_b; /* Правая граница для процесса */

  int local_n; /* Количество вычисляемых трапеций */

  float integral; /* Значение интеграла по интервалу */

  float total; /* Общий интеграл */

  int source; /* Процесс, посылающий интеграл */

  int dest = 0; /* Пункт назначения процесс 0 */

  int tag = 50;

  MPI_Status status;

 

  MPI_Init(&argc, &argv);

  /* Определить ранг процесса */

  MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

  

  /* Определить количество процессов */

  MPI_Comm_size(MPI_COMM_WORLD, &p);

  h = (b-a)/n; /* h одинаково для всех процессов */

  local_n = n/p; /* Получить количество трапеций */

  

  /* Вычислить границы интервала и частный интеграл*/

  local_a = a + my_rank*local_n*h;

  local_b = local_a + local_n*h;

  integral = Trap(local_a, local_b, local_n, h);

 

  /* Суммировать все интегралы */

  if (my_rank == 0) {

    total = integral;

    for (source = 1; source < p; source++) {

       MPI_Recv(&integral, 1, MPI_FLOAT,

           source, tag, MPI_COMM_WORLD, &status);

       total += integral;

    }

  } else {

       MPI_Send(&integral, 1, MPI_FLOAT, dest,

          tag, MPI_COMM_WORLD);

  }

 

  /* Вывод результата */

  if (my_rank == 0) {

    printf(''C n = %d трапециями, оценка\n'', n);

    printf(''интеграла от %f до %f = %f`\n'',

           a, b, total);

  }

 

  /* Завершить приложение MPI */

  MPI_Finalize();

} /* main */


next up previous contents
Next: Ввод / вывод для Up: Пример приложения с обменом Previous: Пример приложения с обменом   Contents
2004-06-22

next up previous contents
Next: Коллективные коммуникации Up: Пример приложения с обменом Previous: Параллелизация правила трапеции   Contents

Ввод / вывод для параллельных процессоров

В предыдущем примере некоторые значения были жестко определены в коде программы. Чаще всего для таких случаев возникает дополнительная задача обеспечения ввода требуемых величин со стороны пользователя.

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

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

void Get_data(int my_rank, int p, float* a_ptr,

     float* b_ptr, int* n_ptr)

{

  int source = 0; /* Локальные переменные */

  int dest; /* используемые MPI_Send и MPI_Recv */

  int tag; 

  MPI_Status status;

 

  if (my_rank == 0) {

     printf(''Введите a, b, и n\n'');

     scanf(''%f %f %d'', a_ptr, b_ptr, n_ptr);

     for (dest = 1; dest < p; dest++) {

        tag = 30;

        MPI_Send(a_ptr, 1, MPI_FLOAT, dest, tag,

            MPI_COMM_WORLD);

        tag = 31;

        MPI_Send(b_ptr, 1, MPI_FLOAT, dest, tag,

            MPI_COMM_WORLD);

        tag = 32;

        MPI_Send(n_ptr, 1, MPI_INT, dest, tag,

            MPI_COMM_WORLD);

     }

  } else {

     tag = 30;

     MPI_Recv(a_ptr, 1, MPI_FLOAT, source, tag,

         MPI_COMM_WORLD, &status);

     tag = 31;

     MPI_Recv(b_ptr, 1, MPI_FLOAT, source, tag,

        MPI_COMM_WORLD, &status);

     tag = 32;

     MPI_Recv(n_ptr, 1, MPI_INT, source, tag,

        MPI_COMM_WORLD, &status);

  }

}/* Get_data */



2004-06-22

next up previous contents
Next: Коммуникации в виде дерева Up: Интерфейс передачи сообщений MPI Previous: Ввод / вывод для   Contents

Коллективные коммуникации

Производительность программы вычисления интеграла можно значительно повысить. Например, пусть программа выполняется на восьми процессорах.

Все процессы начинают выполнять программу практически одновременно. Однако после выполнения основных задач (вызовы
MPI_Init(), MPI_Comm_size() и MPI_Comm_rank()) процессы 1 - 7 будут простаивать, в то время как процесс 0 будет собирать входные данные. После того как процесс 0 примет входные данные, процессы большего ранга должны продолжать ожидать, пока процесс 0 рассылает входные данные всем процессам. Подобная неэффективность наблюдается и в конце программы, когда процесс 0 собирает и суммирует значения локальных интегралов.

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



Subsections

2004-06-22

next up previous contents
Next: Широковещательные посылки Up: Коллективные коммуникации Previous: Коллективные коммуникации   Contents

Коммуникации в виде дерева

Равномерное распределение загрузки по процессорам предполагает определенную организацию передач данных в рамках некоторой структуры. Например, при рассылке входных данных между процессами программы вычисления интеграла хороший результат может дать организация дерева процессов, корнем которого является процесс 0 (рис. 2).

На первой стадии распределения данных процесс 0 посылает данные процессу 4. В течение следующей стадии, процесс 0 посылает данные процессу 2, в то время как процесс 4 посылает те же данные процессу 6. В ходе последней стадии, процесс 0 посылает данные процессу 1, в то время как процесс 2 посылает данные процессу 3, процесс 4 посылает данные процессу 5, и наконец, процесс 6 посылает данные процессу 7. При этом цикл распределения данных уменьшился с 7 этапов до 3. Если существует $p$ процессов, эта процедура позволяет распределять входные данные в $\left\lceil log_{2}(p)\right\rceil $ этапов, вместо $p-1$ этапов, что при достаточно большом $p$ существенно ускоряет процесс.

\includegraphics[scale = 0.7]{fig1.eps}

Рис. 2. Организация процессов в виде дерева

Чтобы модифицировать функцию Get_data() для использования схемы распределения в виде дерева, необходимо ввести цикл из $\left\lceil log_{2}(p)\right\rceil $ итераций. Каждый процесс при этом определяет на каждой стадии:

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

  1. 0 передает в 4;
  2. 0 передает в 2, 4 передает в 6;
  3. 0 передает в 1, 2 передает в 3, 4 передает в 5, 6 передает в 7.
Можно использовать другой вариант, например:

  1. 0 передает в 1;
  2. 0 передает в 2, 1 передает в 3;
  3. 0 передает в 4, 1 передает в 5, 2 передает в 6, 3 передает в 7.



2004-06-22

next up previous contents
Next: Редукция Up: Коллективные коммуникации Previous: Коммуникации в виде дерева   Contents

Широковещательные посылки

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

int MPI_Bcast(void* message, int count,

       MPI_Datatype datatype, int root, MPI_Comm comm);

Эта функция просто посылает копию данных в сообщении от процесса root к каждому процессу в коммуникаторе comm. Ее нужно вызывать всем процессам в коммуникаторе с теми же самыми аргументами для root и comm. Широковещательное сообщение не может быть получено с помощью MPI_Recv(). Параметры count и datatype имеют то же самое значение, которое они имеют в MPI_Send(), и MPI_Recv(): определяют содержание сообщения. Однако в отличие от функций направленной передачи, MPI требует, чтобы при коллективном взаимодействии count и datatype имели одни и те же значения для всех процессов в коммуникаторе.

Функцию получения данных Get_data(), используя MPI_Bcast(), можно переписать следующим образом:

void Get_data2(int my_rank, float* a_ptr, float* b_ptr,

     int* n_ptr) {

  int root = 0; /* Аргументы MPI_Bcast */

  int count = 1;

  if (my_rank == 0) {

    printf(''Введите a, b, и n\n'');

    scanf(''%f %f %d'', a_ptr, b_ptr, n_ptr);

  }

  MPI_Bcast(a_ptr, 1, MPI_FLOAT, root, MPI_COMM_WORLD);

  MPI_Bcast(b_ptr, 1, MPI_FLOAT, root, MPI_COMM_WORLD);

  MPI_Bcast(n_ptr, 1, MPI_INT, root, MPI_COMM_WORLD);

} /* Get-data2 */



2004-06-22

next up previous contents
Next: Другие функции для коллективных Up: Коллективные коммуникации Previous: Широковещательные посылки   Contents

Редукция

В рассматриваемой программе после входной стадии каждый процессор выполняет по существу те же самые команды до заключительной стадии суммирования. Поэтому, если функция $f(x)$ дополнительно не усложнена (т. е. не требует значительной работы для оценки интеграла по некоторым частям отрезка $[a;b]$), то эта часть программы распределяет среди процессоров одинаковую нагрузку. В заключительной стадии суммирования процесс 0 еще раз получает непропорциональное количество работы. Здесь также можно распределить работу вычисления суммы среди процессоров по структуре дерева следующим образом:

  1. Процесс 1 посылает результат процессу 0, процесс 3 посылает результат процессу 2, процесс 5 посылает результат процессу 4, процесс 7 посылает результат процессу 6.
  2. Процесс 0 суммирует результат с процессом 1, процесс 2 суммирует результат с процессом 3, и т.д.
  3. Процесс 2 посылает результат процессу 0, процесс 6 посылает результат процессу 4.
  4. Процесс 0 суммирует результат с процессом 2, процесс 4 суммирует результат с процессом 6.
  5. Процесс 4 посылает результат процессу 0
  6. Процесс 0 суммирует результат с процессом 4
Возможны также варианты организации передач, как и в случае рассылки входных данных. Поэтому следует использовать другие механизмы, более оптимизированные для этой цели.

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

int MPI_Reduce(void* operand, void* result, int count,

    MPI_Datatype datatype, MPI_Op op, int root,

    MPI_Comm comm)

MPI_Reduce() объединяет операнды, сохраненные в *operand, используя оператор op и сохраняет результат в переменной *result корневого процесса root. И операнд, и результат ссылаются на count ячеек памяти с типом datatype. MPI_Reduce() должны вызывать все процессы в коммуникаторе comm. При вызове значения count, datatype и op должны быть одинаковыми в каждом процессе.

Аргумент op может принимать фиксированные значения, указанные в табл. 10:


Таблица 10. Варианты операций редукции.


Название операции Смысл
MPI_MAX Максимум
MPI_MIN Минимум
MPI_SUM Сумма
MPI_PROD Произведение
MPI_LAND Логическое И
MPI_BAND Битовое И
MPI_LOR Логическое ИЛИ
MPI_BOR Битовое ИЛИ
MPI_LXOR Логическое исключающее ИЛИ
MPI_BXOR Битовое исключающее ИЛИ
MPI_MAXLOC Максимум и его расположение
MPI_MINLOC Минимум и его расположение

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

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

/* Суммирование результатов от каждого процесса */

MPI_Reduce(&integral, &total, 1, MPI_FLOAT,

    MPI_SUM, 0, MPI_COMM_WORLD); 

/* Вывод результата */

Следует отметить, что каждый процессор вызывает MPI_Reduce() с одинаковыми аргументами. Например, если total имеет значение только для процесса 0, каждый процесс тем не менее использует этот аргумент.


next up previous contents
Next: Другие функции для коллективных Up: Коллективные коммуникации Previous: Широковещательные посылки   Contents
2004-06-22

next up previous contents
Next: Функция popen() Up: Трубы (pipes) Previous: Трубы (pipes)   Contents

Использование труб

Труба является однонаправленным коммуникационным каналом
между двумя процессами и может использоваться для поддержки коммуникаций и контроля информационного потока между двумя процессами. Труба может принимать только определенный объем данных (обычно 4 Кб). Если труба заполнена, процесс останавливается до тех пор, пока хотя бы один байт из этой трубы не будет прочитан и не появится свободное место, чтобы снова заполнить ее данными. С другой стороны, если труба пуста, то читающий процесс останавливается до тех пор, пока пишущий процесс не внесёт данные в эту трубу.

Труба описывается двумя дескрипторами файлов. Первый дескриптор служит для чтения, второй - для записи в трубу:

#include <unistd.h>

int pipe(int fd[2]);

Здесь fd[0] является дескриптором для чтения, а fd[1] - дескриптором для записи в трубу.

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

В приведенном ниже примере процесс-родитель записывает данные в трубу. В этом случае закрываются дескриптор чтения (fd[0]) родительского процесса и дескриптор записи потомка. Потомок будет только читать данные из трубы:

#include <unistd.h>

#include <sys/wait.h>

#include <stdio.h>

#include <sys/types.h>

#include <fcntl.h>

#define USAGE printf("usage : %s данные\n",

         argv[0]);

#define MAX 4096

int main(int argc, char *argv[]) {

  int fd[2], fd1,i, n;

  pid_t pid;

  char buffer[MAX];

  FILE *dataptr;

  if(argc !=2)

    { USAGE; exit(0); }

  if((fd1=open(argv[1], O_RDONLY)) < 0)

     { perror("open : "); exit(0); }

   

  /*Устанавливаем трубу*/

  if(pipe(fd) < 0)

    { perror("pipe : "); exit(0); }

 

  /*Создаем новый процесс*/

  if((pid=fork()) < 0)

    { perror("pipe : "); exit(0); }

 

  else if(pid > 0) /*Это родитель*/ {

    close(fd[0]); /*Закрываем чтение*/

    n=read(fd1, buffer, MAX);

    if((write(fd[1], buffer, n)) != n)

      { perror(" write : "); exit(0); }

    if((waitpid(pid, NULL, 0)) < 0)

       { perror("waitpid : "); exit(0); }

  }

 

  else /*Это потомок*/ {

    close(fd[1]); /*Закрываем запись*/

    n=read(fd[0], buffer, MAX);

    if((write(STDOUT_FILENO, buffer, n)) != n)

      { perror(" write : "); exit(0); }

  }

  exit(0);

}



2004-06-22

next up previous contents
Next: Группировка данных для пересылки Up: Коллективные коммуникации Previous: Редукция   Contents

Другие функции для коллективных коммуникаций

MPI поддерживает некоторые другие функции для организации коллективных коммуникаций:

int MPI_Barrier(MPI_Comm comm);
MPI_Barrier() предоставляет механизм синхронизации всех процессов в коммуникаторе comm. Каждый процесс коммуникатора приостанавливается, пока все процессы в comm не вызовут MPI_Barrier():

int MPI_Gather(void* send_buf, int send_count,

    MPI_Datatype send_type, void* recv_buf,

    int recv_count, MPI_Datatype recv_type, int root,

    MPI_comm comm);

Каждый процесс в comm посылает содержание send_buf процессу с рангом root. Процесс root соединяет полученные данные в порядке, соответствующем рангам процессов и помещает их в recv_buf. Аргументы recv имеют значение только в процессе с рангом root. Аргумент recv_count указывает число элементов, полученных от каждого процесса, но не общее количество:

int MPI_Scatter(void* send_buf, int send_count,

    MPI_Datatype send_type, void* recv_buf,

    int recv_count, MPI_Datatype recv_type, int root,

    MPI_Comm comm);

Процесс с рангом root распределяет содержимое send_buf среди всех остальных процессов. Содержимое send_buf разделяется на p сегментов, каждый из которых содержит send_count элементов. Первый сегмент передается процессу 0, второй - процессу 1 и т.д. Аргументы send имеют значение только в процессе root:

int MPI_Allgather(void* send_buf, int send_count,

    MPI_Datatype send_type, void* recv_buf,

    int recv_count, MPI_Datatype recv_type,

    MPI_comm comm);

MPI_Allgather() собирает содержимое всех send_buf от каждого из процессов. Эффект вызова сравним с последовательностью p вызовов MPI_Gather(), в каждом из которых процесс root имеет собственный ранг:

int MPI_Allreduce(void* operand, void* result,

    int count, MPI_Datatype datatype, MPI_Op op,

    MPI_Comm comm);

MPI_Allreduce() сохраняет результат операции редукции op в буфере result каждого из процессов.



2004-06-22

next up previous contents
Next: Параметр count Up: Интерфейс передачи сообщений MPI Previous: Другие функции для коллективных   Contents

Группировка данных для пересылки

Для современных компьютеров посылка сообщения является дорогостоящей операцией. Поэтому чем меньше сообщений будет послано, тем выше будет производительность программы. В рассматриваемом примере при распределении входных данных приходится посылать a, b, n в отдельных сообщениях, независимо от того, используется ли пара MPI_Send() и MPI_Recv(), или функция MPI_Bcast(). Можно попытаться улучшить производительность программы, посылая три входных значения в единственном сообщении. MPI обеспечивает три механизма для того, чтобы сгруппировать индивидуальные переменные в единое сообщение:



Subsections

2004-06-22

next up previous contents
Next: Производные типы и MPI_Type_struct Up: Группировка данных для пересылки Previous: Группировка данных для пересылки   Contents

Параметр count

Процедуры MPI_Send(), MPI_Recv(), MPI_Bcast(), и MPI_Reduce() используют аргументы count и datatype. Эти два параметра позволяют пользователю группировать элементы данных, имеющие тот же самый основной тип, в единое сообщение. Чтобы использовать эту возможность, сгруппированные элементы данных должны сохраняться в смежных областях памяти. Поскольку C гарантирует, что элементы массива сохраняются именно таким образом, то посылка элементов массива или подмассива может осуществляться в едином сообщении.

В качестве примера приведем процедуру посылки второй половины вектора, содержащего 100 значений с плавающей точкой от процесса 0 к процессу 1:

float vector[100];

int tag, count, dest, source;

MPI_Status status;

int p;

int my_rank;

. . . 

if (my_rank == 0) {

  /* Инициализация и отсылка вектора */

  . . . 

  tag = 47;

  count = 50;

  dest = 1;

  MPI_Send(vector + 50, count, MPI_FLOAT, dest, tag,

     MPI_COMM_WORLD);

} else {

  /* my_rank == 1 */

  tag = 47;

  count = 50;

  source = 0;

  MPI_Recv(vector+50, count, MPI_FLOAT, source, tag,

     MPI_COMM_WORLD,

     &status);

}

К сожалению, такой подход нельзя использовать при отсылке значений разных типов, не сохраненных в виде массива. Например, если определить переменные:

float a;

float b;

int n;

то C не гарантирует, что они будут сохранены в памяти последовательно. Поэтому для их группировки нужно использовать другие возможности MPI.



2004-06-22

next up previous contents
Next: Другие конструкторы производных типов Up: Группировка данных для пересылки Previous: Параметр count   Contents

Производные типы и MPI_Type_struct

Может показаться, что другим вариантом могло бы стать хранение a, b, n в структуре с тремя членами - два числа с плавающей точкой и целое - и попытка использовать аргумент datatype в функции MPI_Bcast(). Трудность здесь состоит в том, что тип datatype является одним из MPI_Datatype(), которые не являются пользовательскими типами, как структуры в C. Если определить тип:

typedef struct {

  float a;

  float b;

  int n;

} INDATA_TYPE

а затем переменную
INDATA_TYPE indata
то при вызове
MPI_Bcast(&indata, 1, INDATA_TYPE, 0, MPI_COMM_WORLD)
произойдет ошибка. Проблема состоит в том, что MPI является библиотекой готовых функций. При этом функции MPI не ориентированы на применение пользовательских типов данных, определенных в программе.

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

void Build_derived_type(INDATA_TYPE* indata,

     MPI-Datatype* message_type_ptr)

{

  int block_lengths[3];

  MPI_Aint displacements[3];

  MPI_Aint addresses[4];

  MPI_Datatype typelist[3];

 

  /* Создает производный тип данных, содержащий

  * два элемента float и один int */

  /* Сначала нужно определить типы элементов */

  typelist[0] = MPI_FLOAT;

  typelist[1] = MPI_FLOAT;

  typelist[2] = MPI_INT;

 

  /* Определить количество элементов каждого типа */

  block_lengths[0] = block_lengths[1] =

     block_lengths[2] = 1;

  

  /* Вычислить смещения элементов 

  * относительно indata */

  MPI_Address(indata, &addresses[0]);

  MPI_Address(&(indata->a), &addresses[1]);

  MPI_Address(&(indata->b), &addresses[2]);

  MPI_Address(&(indata->n), &addresses[3]);

  displacements[0] = addresses[1] - addresses[0];

  displacements[1] = addresses[2] - addresses[0];

  displacements[2] = addresses[3] - addresses[0];

 

  /* Создать производный тип */

  MPI_Type_struct(3, block_lengths, displacements,

     typelist, Message_type_ptr);

 

  /* Зарегистрировать его для использования */

  MPI_Type_commit(message_type_ptr);

} /* Build_derived_type */

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

Новый тип данных MPI можно использовать в любых коммуникационных функциях MPI. Чтобы использовать его, необходимо применять стартовый адрес переменной типа INDATA_TYPE в качестве первого аргумента, а производный тип данных - в качестве аргумента datatype. При этом функция Get_data() в примере принимает вид.

void Get_data3(INDATA_TYPE* indata, int my_rank) {

  MPI_Datatype message_type; /* Аргументы для */

  int root = 0; /* MPI_Bcast */

  int count = 1;

  if (my_rank == 0) {

    printf(''Введите a, b, and n\n'');

    scanf(''%f %f %d'', &(indata->a),

         &(indata->b), &(indata->n));

  }

  Build_derived_type(indata, &message_type);

  MPI_Bcast(indata, count, message_type, root,

  MPI_COMM_WORLD);

} /* Get_data3 */

Отметим, что тип элементов массива смещений MPI_Aint не является int. Это специальный тип MPI, позволяющий предоставлять адреса, большие по размеру, чем int.

Производные типы данных строятся с помощью функции
MPI_Type_struct(). Синтаксис этой функции таков:

int MPI_Type_Struct(int count,

  int* array_of_block_lengths,

  MPI_Aint* array_of_displacements,

  MPI_Datatype* array_of_types,

  MPI_Datatype* newtype)

Аргумент count определяет число элементов в производном типе. Он также задает размер трех массивов: массива длин блоков
array_of_block_lengths, массива смещений в данном типе
array_of_displacements, и массива типов array_of_types. Массив
array_of_block_lengths содержит число вхождений для каждого элемента типа. Так, если элемент типа - непрерывный массив m элементов, то соответствующий элемент в array_of_block_lengths равен m. Массив array_of_displacements содержит смещение каждого элемента от начала сообщения, а массив array_of_types содержит типы данных MPI_datatype для каждого элемента. Аргумент newtype возвращает указатель на тип данных MPI, созданный вызовом MPI_Type_struct.

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


next up previous contents
Next: Другие конструкторы производных типов Up: Группировка данных для пересылки Previous: Параметр count   Contents
2004-06-22

next up previous contents
Next: Упаковка и распаковка Up: Группировка данных для пересылки Previous: Производные типы и MPI_Type_struct   Contents

Другие конструкторы производных типов

MPI_Type_struct() является самым общим конструктором типов данных в MPI. Поэтому пользователь должен обеспечить полное описание каждого элемента типа. Если данные, которые будут переданы, состоят из подмножества элементов массива, возможно не стоит обеспечивать слишком детальную информацию, так как все элементы имеют тот же самый основной тип. MPI обеспечивает три производных конструктора данных для того, чтобы работать в этой ситуации: MPI_Type_Contiguous(), MPI_Type_vector() и
MPI_Type_indexed(). Первый конструктор строит производный тип, элементами которого являются смежные элементы массива. Второй конструктор строит тип, элементами которого являются равномерно разделенные промежутками элементы массива, а третий строит тип, элементы которого являются произвольными элементами массива. Прежде чем любой производный тип может быть использован в коммуникации, он должен быть объявлен вызовом MPI_Type_commit().

Ниже приведены сведения о синтаксисе дополнительных конструкторов типов MPI:

int MPI_Type_contiguous(int count,

    MPI_Datatype oldtype, MPI_Datatype* newtype);

MPI_Type_contiguous() создает производный тип данных, состоящий из count элементов типа oldtype. Элементы расположены в памяти последовательно:

int MPI_Type_vector(int count, int block_length,

    int stride, MPI_Datatype element_type,

    MPI_Datatype* newtype);

MPI_Type_vector() создает производный тип, состоящий из count элементов. Каждый элемент содержит block_length значений с типом element_type. Stride определяет длину промежутка элементов с типом element_type между действительными элементами new_type (рис. 3):

\includegraphics{fig2.eps}

Рис. 3. Результат вызова MPI_Type_vector() для count=2, stride=3, block_length=2

int MPI_Type_indexed(int count,

    int* array_of_block_lengths,

    int* array_of_displacements,

    MPI_Datatype element_type,

    MPI_Datatype* newtype);

MPI_Type_indexed создает производный тип из count элементов. Элемент i (i = 0, 1, ..., count-1), содержит массив из block_lengths[i] значений с типом element_type, смещенный на
array_of_displacements[i] позиций типа element_type от начала new_type.



2004-06-22

next up previous contents
Next: Выбор используемого метода передачи Up: Группировка данных для пересылки Previous: Другие конструкторы производных типов   Contents

Упаковка и распаковка

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

Для иллюстрации этих функций можно привести еще один вариант текста функции Get_data():

void Get_data4(int my_rank, float* a_ptr, float* b_ptr,

    int* n_ptr) {

  int root = 0; /* Аргумент для MPI_Bcast */

  char buffer[100]; /* Аргументы для MPI_Pack/Unpack */

  int position; /* и MPI_Bcast*/

  if (my_rank == 0) {

    printf(''Введите a, b, и n\n'');

    scanf(''%f %f %d'', a_ptr, b_ptr, n_ptr);

    /* Упаковка данных в буфер */

    position = 0; /* Начать с начала буфера */

    MPI_Pack(a_ptr, 1, MPI_FLOAT, buffer, 100,

      &position, MPI_COMM_WORLD);

    /* Position будет увеличена на */

    /* sizeof(float) байт */

    MPI_Pack(b_ptr, 1, MPI_FLOAT, buffer, 100,

      &position, MPI_COMM_WORLD);

    MPI_Pack(n_ptr, 1, MPI_INT, buffer, 100,

      &position, MPI_COMM_WORLD);

    /* Разослать содержимое буфера */

    MPI_Bcast(buffer, 100, MPI_PACKED, root,

       MPI_COMM_WORLD);

  } else {

    MPI_Bcast(buffer, 100, MPI_PACKED, root,

       MPI_COMM_WORLD);

    /* Распаковать содержимое буфера */

    position = 0;

    MPI_Unpack(buffer, 100, &position, a_ptr, 1,

    MPI_FLOAT, MPI_COMM_WORLD);

    /* Позиция снова увеличивается на */

    /* sizeof(float) байт */ 

    MPI_Unpack(buffer, 100, &position, b_ptr, 1,

       MPI_FLOAT, MPI_COMM_WORLD);

    MPI_Unpack(buffer, 100, &position, n_ptr, 1,

       MPI_INT, MPI_COMM_WORLD);

  }

} /* Get-data4 */

В этой версии Get_data() процесс 0 использует MPI_Pack(), чтобы скопировать в буфер a, а затем добавить к нему b и n. После широковещательной рассылки буфера остальные процессы используют MPI_Unpack(), чтобы извлечь a, b, n из буфера. Тип данных в вызове MPI_Bcast() указан как MPI_PACKED.

Синтаксис вызова MPI_Pack():

int MPI_Pack(void* pack_data, int in_count,

       MPI_Datatype datatype, void* buffer, int size,

       int* position_ptr, MPI_Comm comm)

Параметр pack_data указывает на данные, которые будут буферизованы. Они состоят из in_count элементов, каждый из которых имеет тип datatype. Параметр position_ptr является параметром ввода / вывода. При вводе данные, на которые указывает pack_data копируются в память, начинающуюся в буфере по адресу buffer + *position_ptr. При возвращении *position_ptr ссылается на первую ячейку в буфере после данных, которые были скопированы. Параметр size содержит размер в байтах памяти, на которую указывает буфер, а comm определяет коммуникатор, который будет использовать буфер.

Синтаксис MPI_Unpack():

int MPI_Unpack(void* buffer, int size,

       int* position_ptr, void* unpack_data,

       int count, MPI_Datatype datatype,

       MPI_comm comm);

Параметр buffer ссылается на данные, которые должны быть распакованы. Они содержат size байт. Параметр position_ptr является параметром ввода/вывода. При вызове MPI_Unpack() данные, начинающиеся в буфере по адресу buffer + *position_ptr, копируются в память, на которую указывает unpack_data. При возвращении *position_ptr ссылается на первую ячейку в буфере после данных, которые были только что скопированы. MPI_Unpack() копирует count элементов, имеющих тип datatype в unpack_data. Коммуникатор, связанный с буфером обозначен через comm.



2004-06-22

next up previous contents
Next: Коммуникаторы и топологии Up: Группировка данных для пересылки Previous: Упаковка и распаковка   Contents

Выбор используемого метода передачи данных

Если данные, которые будут посланы, хранятся в последовательных элементах массива, то лучше всего использовать аргументы count и datatype коммуникационных функций. Этот подход не включает никаких дополнительных действий, например, вызовов для создания производных типов данных или MPI_Pack/MPI_Unpack.

Если большое количество элементов не находится в смежных ячейках памяти, то построение производного типа данных будет дешевле и производительнее, чем большое количество вызовов MPI_Pack / MPI_Unpack.

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

Однако в некоторых ситуациях использование MPI_Pack /
MPI_Unpack является предпочтительным. Так, можно избежать коллективного использования системной буферизации при упаковке, поскольку данные явно сохраняются в определенном пользователем буфере. Система может использовать это, отметив, что типом сообщения является MPI_PACKED. Пользователь также может посылать сообщения "переменной длины", упаковывая число элементов в начале буфера. Пусть, например, необходимо посылать строки разреженной матрицы. Если строка хранится в виде пары массивов, где первый содержит индексы колонок, а второй содержит соответствующие элементы матрицы, то можно посылать строку от процесса 0 к процессу 1 следующим образом:

float* entries;

int* column_subscripts;

/* количество ненулевых элементов в строке */

int nonzeroes;

int position;

int row_number;

char* buffer[HUGE]; /* HUGE является константой */

MPI_Status status;

. . .

if (my_rank == 0) {

  /* Получить количество ненулевых элементов строки. */

  /* Выделить память для строки. */

  /* Инициализировать массивы элементов и индексов */

  . . . 

  /* Упаковать и послать данные */ 

  position = 0;

  MPI_Pack(&nonzeroes, 1, MPI_INT, buffer, HUGE,

     &position, MPI_COMM_WORLD);

  MPI_Pack(&row_number, 1, MPI_INT, buffer, HUGE,

     &position, MPI_COMM_WORLD);

  MPI_Pack(entries, nonzeroes, MPI_FLOAT, buffer,

       HUGE, &position, MPI_COMM_WORLD);

  MPI_Pack(column_subscripts, nonzeroes, MPI_INT,

     buffer, HUGE, &position, MPI_COMM_WORLD);

  MPI_Send(buffer, position, MPI_PACKED, 1, 193,

     MPI_COMM_WORLD);

} else { /* my_rank == 1 */

  MPI_Recv(buffer, HUGE, MPI_PACKED, 0, 193,

     MPI_COMM_WORLD, &status);

  position = 0;

  MPI_Unpack(buffer, HUGE, &position, &nonzeroes, 1,

     MPI_INT, MPI_COMM_WORLD);

  MPI_Unpack(buffer, HUGE, &position, &row_number, 1,

     MPI_INT, MPI_COMM_WORLD);

  /* Выделить память для массивов */

  entries = (float *) malloc(nonzeroes*sizeof(float));

  column_subscripts =

     (int *) malloc(nonzeroes*sizeof(int));

  MPI_Unpack(buffer, HUGE, &position, entries,

     nonzeroes, MPI_FLOAT, MPI_COMM_WORLD);

  MPI_Unpack(buffer, HUGE, &position,

     column_subscripts, nonzeroes, MPI_INT,

     MPI_COMM_WORLD);

}



2004-06-22

next up previous contents
Next: Алгоритм Фокса Up: Интерфейс передачи сообщений MPI Previous: Выбор используемого метода передачи   Contents

Коммуникаторы и топологии

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



Subsections

2004-06-22

next up previous contents
Next: Коммуникаторы Up: Коммуникаторы и топологии Previous: Коммуникаторы и топологии   Contents

Алгоритм Фокса

Пусть перемножаемые матрицы $A=(a_{ij})$ и $B=(b_{ij})$ имеют порядок $n$. Количество процессов $p$ является квадратом, квадратный корень которого кратен $n$. В этом случае $p=q^{2}$ и $\bar{n}=n/q$. В алгоритме Фокса матрицы разделяются среди процессов в виде клеток шахматной доски. При этом процессы рассматриваются как виртуальная двухмерная $q\times q$ сетка, и каждому процессу назначена подматрица $\bar{n}\times \bar{n}$ каждого множителя. Реализуется отображение:


\begin{displaymath}
\phi :\left\{ {0,1,...,p-1}\right\} \rightarrow \left\{ {(s,t):}0\leq s,t\leq q-1\right\} \end{displaymath}

Оно определяет сетку процессов: процесс $i$ относится к строке и столбцу, заданному $\phi (i)$. Процесс с рангом $\phi ^{-1}(s;t)$ назначается на подматрицы:

$A_{st}=\left(\begin{array}{ccc}
a_{s*\bar{n},t*\bar{n}} & \cdots & a_{(s+1)*\b...
...+1)*\bar{n}-1} & \ldots & a_{(s+1)*\bar{n}-1,(t+1)*\bar{n}-1}\end{array}\right)$

и

$B_{st}=\left(\begin{array}{ccc}
b_{s*\bar{n},t*\bar{n}} & \cdots & b_{(s+1)*\b...
...+1)*\bar{n}-1} & \ldots & b_{(s+1)*\bar{n}-1,(t+1)*\bar{n}-1}\end{array}\right)$

Например, если $p=9$, $\phi (x)=(x=3;xmod3)$, и $n=6$, то A будет разделена следующим образом (рис. 4).

Процесс 0: $A_{00}=\left(\begin{array}{cc}
a_{00} & a_{01}\\
a_{10} & a_{11}\end{array}\right)$ Процесс 1: $A_{01}=\left(\begin{array}{cc}
a_{02} & a_{03}\\
a_{12} & a_{13}\end{array}\right)$ Процесс 2: $A_{02}=\left(\begin{array}{cc}
a_{04} & a_{05}\\
a_{14} & a_{15}\end{array}\right)$
Процесс 3: $A_{01}=\left(\begin{array}{cc}
a_{20} & a_{21}\\
a_{30} & a_{31}\end{array}\right)$ Процесс 4: $A_{00}=\left(\begin{array}{cc}
a_{22} & a_{23}\\
a_{32} & a_{33}\end{array}\right)$ Процесс 5: $A_{01}=\left(\begin{array}{cc}
a_{24} & a_{25}\\
a_{34} & a_{35}\end{array}\right)$
Процесс 6: $A_{00}=\left(\begin{array}{cc}
a_{40} & a_{41}\\
a_{50} & a_{51}\end{array}\right)$ Процесс 7: $A_{01}=\left(\begin{array}{cc}
a_{42} & a_{43}\\
a_{52} & a_{53}\end{array}\right)$ Процесс 8: $A_{02}=\left(\begin{array}{cc}
a_{44} & a_{45}\\
a_{54} & a_{55}\end{array}\right)$

Рис. 4. Разделение матрицы на подматрицы

В алгоритме Фокса, подматрицы блоков, $A_{rs}$ и $B_{st}$ , где $s=0,1,...,q-1$, перемножаются и собираются в процессе $\phi ^{-1}(r;t)$. Алгоритм состоит в следующем:

for(step = 0; step < q; step++) { 

  Выбрать подматрицу A в каждой строке

      для всех процессов.

  В каждой строке для всех процессов разослать

      сетку подматриц, выбранную в этой строке для

      других процессов в этой строки.

  В каждом процессе, перемножить полученную подматрицу

      A на подматрицу B, находящуюся в процессе.

  В каждом процессе, отослать подматрицу B процессу,

    расположенному выше. (Для процессов первой строки

    отослать подматрицу в последнюю строку.)

}

Подматрицей, выбранной для $r$-ой строки является $A_{r,u}$, где $u=(r+step)modq$



2004-06-22

next up previous contents
Next: Работа с группами, контекстами Up: Коммуникаторы и топологии Previous: Алгоритм Фокса   Contents

Коммуникаторы

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

Механизмом, который обеспечивает рассмотрение подмножества процессов MPI в виде коммуникационной области, является коммуникатор. В MPI существует два типа коммуникаторов: интракоммуникаторы и интеркоммуникаторы. Интракоммуникаторы представляют собой набор процессов, которые могут посылать сообщения друг другу и участвовать в коллективных взаимодействиях. Например, MPI_COMM_WORLD - это интракоммуникатор. Каждая строка и колонка процессов в алгоритме Фокса должны формировать интракоммуникатор. Интеркоммуникаторы, как подразумевается в их названии, используются для того, чтобы послать сообщения между процессами, принадлежащими непересекающимся интракоммуникаторам. Так, интеркоммуникатор полезно использовать в среде, которая позволяет динамически создавать процессы: только что созданный набор процессов, которые сформировали интракоммуникатор, может быть связан интеркоммуникатором с оригинальным набором процессов (например, MPI_COMM_WORLD) .

Минимальный (интра-)коммуникатор состоит из группы и контекста. Группа является упорядоченным набором процессов. Если группа состоит из $p$ процессов, то каждому из них в группе назначен уникальный ранг, который является неотрицательным целым числом в диапазоне $0,1,...,p-1$. Контекст представляет собой определенный системой признак, который присоединен к группе. Два процесса, которые принадлежат одной группе и поэтому используют один и тот же контекст, могут связаться между собой. Соединение группы с контекстом является основной функцией коммуникатора. С коммуникатором могут быть связаны и другие данные. В частности, на процессы в коммуникаторе может быть наложена структура или топология, позволяющая осуществить более естественную схему адресации.



2004-06-22

next up previous contents
Next: FIFO - именованные каналы Up: Трубы (pipes) Previous: Использование труб   Contents

Функция popen()

Чтобы избавить программиста от лишнего труда, для работы с трубами введена удобная функция popen(). Синтаксис этой функции:

FILE *popen(const char* command, const char* mode);
Параметр command соответствует системному вызову команды, которая выполняет некоторую программу. Между процессом пользователя и вызванным процессом устанавливается труба для обмена информацией. Режим работы трубы устанавливается параметром mode, где "r" означает чтение из трубы, а "w" - запись в трубу. В случае ошибки при создании трубы, popen() возвращает NULL, иначе указатель на файл. Таким образом, функция popen() выполняет следующие действия:

Пример использования popen():

#include <unistd.h>

#include <sys/wait.h>

#include <stdio.h>

#include <sys/types.h>

#include <fcntl.h>

#define EXIT(s) {fprintf(stderr, "%s",s); exit(0);}

#define USAGE(s) {fprintf(stderr, "%s Данные для

         чтения\n",s); exit(0);}

#define MAX 8192

enum {ERROR=-1,SUCCESS};

 

int main(int argc, char **argv)

{

  FILE *pipe_writer, *file;

  char buffer[MAX];

  if(argc!=2)

    USAGE(argv[0]);

  if((file=fopen(argv[1], "r")) == NULL)

    EXIT("Ошибка открытия файла.........\n");

  if(( pipe_writer=popen("./filter" ,"w")) == NULL)

    EXIT("Ошибка открытия трубы...........\n");

  while(1)

  {

    if(fgets(buffer, MAX, file) == NULL)

    break;

    if(fputs(buffer, pipe_writer) == EOF)

      EXIT("Ошибка записи........\n");

  }

  pclose(pipe_writer);

}



2004-06-22

next up previous contents
Next: Функция MPI_Comm_split Up: Коммуникаторы и топологии Previous: Коммуникаторы   Contents

Работа с группами, контекстами и коммуникаторами

Для иллюстрации основ работы с коммуникаторами создается коммуникатор, основная группа которого состоит из процессов в первой строке виртуальной сетки. Пусть MPI_COMM_WORLD состоит из $p$ процессов, где $q^{2}=p$. Пусть также $\phi (x)=(x/q;xmodq)$. При этом первая строка процессов состоит из процессов с рангами $0,1,...,q-1$ (Ранги указаны относительно MPI_COMM_WORLD). Чтобы создать группу для нового коммуникатора, следует выполнить следующий код:

MPI_Group MPI_GROUP_WORLD;

MPI_Group first_row_group;

MPI_Comm first_row_comm;

int row_size;

int* process_ranks;

 

/* Создает список процессов нового */

 * коммуникатора */

process_ranks = (int*) malloc(q*sizeof(int));

for (proc = 0; proc < q; proc++) 

   process_ranks[proc] = proc;

 

/* Получить группу, относящуюся к MPI_COMM_WORLD */

MPI_Comm_group(MPI_COMM_WORLD, &MPI_GROUP_WORLD);

 

/* Создать новую группу */

MPI_Group_incl(MPI_GROUP_WORLD, q, process_ranks,

   &first_row_group); 

 

/* Создать новый коммуникатор */

MPI_Comm_create(MPI_COMM_WORLD, first_row_group,

   &first_row_comm);

Этот код строит новый коммуникатор прямым способом. Сначала он создает список процессов, которые будут помещены в новый коммуникатор. Потом он создает группу, состоящую из этих процессов, с помощью двух операций: сначала получается группа, связанная с MPI_COMM_WORLD, поскольку из этой группы будут выбраны процессы новой группы; затем создается новая группа с помощью функции MPI_Group_incl(). Наконец, фактический коммуникатор создается вызовом MPI_Comm_create(). Этот вызов также неявно ассоциирует контекст с новой группой. Результатом является коммуникатор first_row_comm. Теперь процессы в этом коммуникаторе могут выполнять коллективные действия. Например, процесс 0 может передать $A_{00}$ другим процессам в коммуникаторе:

int my_rank_in_first_row;

float* A_00;

 

/* my_rank определяет ранг процесса в MPI_GROUP_WORLD */

if (my_rank < q) {

   MPI_Comm_rank(first_row_comm, &my_rank_in_first_row);

 

   /* Выделить память для A_00, order = n_bar */

   A_00 = (float*) malloc (n_bar*n_bar*sizeof(float));

   if (my_rank_in_first_row == 0) {

      /* Инициализация A_00 */

      . . .

   }

   MPI_Bcast(A_00, n_bar*n_bar, MPI_FLOAT, 0,

      first_row_comm);

}

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

Контексты явно не используются ни в одной из функций MPI. Они неявно связываются с группами при создании коммуникаторов. Операция

int MPI_Comm_group(MPI_Comm comm, MPI_Group* group)
просто возвращает группу, принадлежащую коммуникатору comm. Вторая операция:

int MPI_Group_incl(MPI_Group old_group,

    int new_group_size, int* ranks_in_old_group,

    MPI_Group* new_group)

создает новую группу из списка процессов уже существующей группы old_group. Количество процессов в новой группе равно
new_group_size, а процессы, которые будут включены в группу, перечислены в списке ranks_in_old_group. Процесс 0 в новой группе имеет ранг ranks_in_old_group[0], процесс 1 имеет ранг
ranks_in_old_group [1], и т.д. Последняя операция:

int MPI_Comm_create(MPI_Comm old_comm,

    MPI_Group new_group, MPI_Comm* new_comm)

связывает контекст с группой new_group и создает коммуникатор new_comm. Все процессы в новой группе относятся к группе, принадлежащей old_comm.

Существует важное различие между первыми двумя и третьей функциями. Вызовы MPI_Comm_group() и MPI_Group_incl() являются локальными действиями. Это значит, что нет никакого взаимодействия между процессами, участвующими в их выполнении. Операция MPI_Comm_create() - коллективное действие. Процессы в old_comm должны вызывать MPI_Comm_create() с теми же самыми аргументами.

Если создаются несколько коммуникаторов, они должны создаваться в одном и том же порядке во всех процессах.



2004-06-22

next up previous contents
Next: Топологии Up: Коммуникаторы и топологии Previous: Работа с группами, контекстами   Contents

Функция MPI_Comm_split

В программе умножения матриц нужно создать несколько коммуникаторов - по одному для каждой строки процессов и по одному - для каждой колонки. Это будет чрезвычайно утомительным процессом, если $p$ достаточно большое, и каждый коммуникатор создается с использованием трех функции, обсужденных ранее. Однако MPI содержит функцию MPI_Comm_split(), которая может создать несколько коммуникаторов одновременно. Для иллюстрации ее использования создаются коммуникаторы для каждой строки процессов:

MPI_Comm my_row_comm;

int my_row;

/* my_rank это ранг в MPI_COMM_WORLD.

* q*q = p */

my_row = my_rank/q;

MPI_Comm_split(MPI_COMM_WORLD, my_row, my_rank,

   &my_row_comm);

Единственный вызов MPI_Comm_split() создает $q$ новых коммуникаторов, причем все они имеют то же самое имя my_row_comm. Например, если $p=9$, то группа, принадлежащая my_row_comm будет состоять из процессов 0, 1, и 2 на процессах 0, 1, и 2. На процессах 3, 4, и 5, группа, принадлежащая my_row_comm, будет состоять из процессов 3, 4, и 5, и на процессах 6, 7, и 8 она будет состоять из процессов 6, 7, и 8.

Синтаксис вызова MPI_Comm_split():

int MPI_Comm_split(MPI_Comm old_comm, int split_key,

    int rank_key, MPI_Comm* new_comm)

Функция создает новый коммуникатор для каждого значения
split_key. Процессы с рангами, равными split_key, формируют новую группу. Ранг в новой группе определяется значением rank_key. Если процесс А и процесс B вызывают MPI_Comm_split() с одинаковым значением split_key, и аргумент rank_key, переданный в процесс А, меньше аргумента процесса B, то ранг А в группе, принадлежащей new_comm будет меньше, чем ранг процесса B. Если они вызывают функцию с одинаковым значением rank_key, то система будет произвольно назначать одному из процессов более низкий ранг.

MPI_Comm_split() является коллективной операцией, поэтому ее нужно вызывать всем процессам из old_comm. Функция может использоваться, даже если пользователь не желает назначать каждый процесс на новый коммуникатор. Это можно выполнить, передав предопределенную константу MPI_UNDEFINED в качестве аргумента split_key. Процессы, выполнившие это, получат предопределенное значение MPI_COMM_NULL, возвращенное в new_comm.



2004-06-22

next up previous contents
Next: Функция MPI_Cart_sub Up: Коммуникаторы и топологии Previous: Функция MPI_Comm_split   Contents

Топологии

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

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

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

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

После этого нужно просто выполнить код:

MPI_Comm grid_comm;

int dimensions[2];

int wrap_around[2];

int reorder = 1;

dimensions[0] = dimensions[1] = q;

wrap_around[0] = wrap_around[1] = 1;

MPI_Cart_create(MPI_COMM_WORLD, 2, dimensions,

   wrap_around, reorder, &grid_comm);

После выполнения кода коммуникатор grid_comm будет содержать все процессы MPI_COMM_WORLD (возможно переупорядоченные) и ассоциированную с ними двухмерную систему декартовых координат. Чтобы процесс мог определить свои координаты, он должен вызвать функцию MPI_Cart_coords():

int coordinates[2];

int my_grid_rank;

MPI_Comm_rank(grid_comm, &my_grid_rank);

MPI_Cart_coords(grid_comm, my_grid_rank, 2,

   coordinates);

Следует отметить, что для получения ранга процесса в grid_comm нужно вызвать MPI_Comm_rank(). Это необходимо потому, что в вызове MPI_Cart_create() установлен флаг переупорядочения и, следовательно, первоначальный ранг процесса в MPI_COMM_WORLD, возможно, будет изменен в grid_comm.

Обратной функцией к MPI_Cart_coords() является
MPI_Cart_rank():

int MPI_Cart_rank(grid_comm, coordinates, &grid_rank)
Задавая координаты процесса, с помощью MPI_Cart_rank() можно получить ранг этого процесса в process_rank.

Синтаксис MPI_Cart_create():

int MPI_Cart_create(MPI_Comm old_comm,

    int number_of_dims, int* dim_sizes,

    int* periods, int reorder, MPI_Comm* cart_comm)

MPI_Cart_create() создает новый коммуникатор cart_comm путем кэширования декартовой топологии с old_comm. Информация относительно структуры декартовой топологии содержится в параметрах number_of_dims, dim_sizes, periods. Первый параметр содержит число измерений в декартовой системе координат. Следующие два являются массивами с размером, равным числу измерений. Массив dim_sizes определяет порядок каждого измерения, а periods определяет, является ли измерение циклическим или линейным.

Процессы в cart_comm ранжируются в порядке строк. При этом первый ряд состоит из процессов 0, 1,..., dim_sizes[0]-1, второй ряд - из процессов dim_sizes[0],dim_sizes[0]+1 , ... ,
2*dim_sizes[0]-1
и т.д. Таким образом, иногда выгодно изменить относительное ранжирование процессов в old_comm. Например, пусть физическая топология - это сетка 3x3,а процессы (номера) в old_comm назначены на процессоры (квадраты сетки) следующим образом (рис. 5):

3 4 5
0 1 2
6 7 8

Рис. 5. Распределение процессов по процессорам

Поскольку MPI_Cart_create() создает новый коммуникатор, она является коллективной операцией.

Синтаксис функций, возвращающих адресную информацию:

int MPI_Cart_rank(MPI_Comm comm, int* coordinates,

    int* rank);

int MPI_Cart_coords(MPI_Comm comm, int rank,

    int number_of_dims, int* coordinates)

MPI_Cart_rank() возвращает ранг процесса в декартовом коммуникаторе comm, при этом процесс имеет координаты coordinates. Координаты содержатся в массиве в порядке, эквивалентном порядку размерностей декартовой топологии, ассоциированной с comm. MPI_Cart_coords() возвращает координаты процесса с рангом rank в декартовом коммуникаторе comm. Обе функции являются локальными.



2004-06-22

next up previous contents
Next: Реализация алгоритма Фокса Up: Коммуникаторы и топологии Previous: Топологии   Contents

Функция MPI_Cart_sub

Еще одним способом является деление сетки на участки меньшей размерности. Например, можно создать коммуникатор для каждой строки сетки следующим образом:

int varying_coords[2];

MPI_Comm row_comm;

Varying_coords[0] = 0; varying_coords[1] = 1;

MPI_Cart_sub(grid_comm, varying_coords, &);

Вызов MPI_Cart_sub() создает $q$ новых коммуникаторов. Аргумент varying_coords является массивом логических значений. Они определяют, принадлежит ли определенное измерение новому коммуникатору. Поскольку в примере создаются коммуникаторы для строк сетки, то каждый новый коммуникатор состоит из процессов, получающих фиксированную координату строки, при этом позволяя координате колонки изменяться. Поэтому varying_coords[0] принимает значение 0 - первая координата не изменяется, а
varying_coords[1] принимает значение 1 - вторая координата изменяется. В каждом процессе возвращается новый коммуникатор row_comm. Чтобы создать коммуникаторы для колонок, можно просто изменить оператор присваивания для элементов varying_coords:

MPI_Comm col_comm;

Varying_coords[0] = 1; varying_coords[1] = 0;

MPI_Cart_sub(grid_comm, varying_coord, col_comm);

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



2004-06-22

next up previous contents
Next: Начальные сведения о PETSc Up: Коммуникаторы и топологии Previous: Функция MPI_Cart_sub   Contents

Реализация алгоритма Фокса

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

typedef struct {

  int p; /* Общее число процессов */

  MPI_Comm comm; /* Коммуникатор для сетки */

  MPI_Comm row_comm; /* Коммуникатор строки */

  MPI_Comm col_comm; /* Коммуникатор столбца */

  int q; /* Порядок сетки */

  int my_row; /* Номер строки */

  int my_col; /* Номер столбца */

  int my_rank; /* Ранг процесса в коммуникаторе сетки */

} GRID-INFO-TYPE;

 

/* Пространство для сетки выделено

   в вызывающей процедуре.*/

void Setup_grid(GRID_INFO_TYPE* grid) {

  int old_rank;

  int dimensions[2];

  int periods[2];

  int coordinates[2];

  int varying-coords[2];

 

  /* Настройка глобальной информации о сетке */

  MPI_Comm_size(MPI_COMM_WORLD, &(grid->p));

  MPI_Comm_rank(MPI_COMM_WORLD, &old_rank);

  grid->q = (int) sqrt((double) grid->p);

  dimensions[0] = dimensions[1] = grid->q;

  periods[0] = periods[1] = 1;

  MPI_Cart_create(MPI_COMM_WORLD, 2, dimensions,

      periods, 1, &(grid->comm));

  MPI_Comm_rank(grid->comm, &(grid->my_rank));

  MPI_Cart_coords(grid->comm, grid->my_rank, 2,

     coordinates);

  grid->my_row = coordinates[0];

  grid->my_col = coordinates[1];

 

  /* Настройка коммуникаторов для строк и столбцов */

  varying_coords[0] = 0; varying_coords[1] = 1;

  MPI_Cart_sub(grid->comm, varying_coords,

     &(grid->row_comm));

  varying_coords[0] = 1; varying_coords[1] = 0;

  MPI_Cart_sub(grid->comm, varying_coords,

     &(grid->col_comm));

} /* Setup_grid */

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

Следующая функция выполняет фактическое умножение. Пусть пользователь сам создает определения типа и функции для локальных матриц. При этом определение типа находится в
LOCAL_MATRIX_TYPE, а соответствующий производный тип в
DERIVED_LOCAL_MATRIX. Существуют также три функции:
Local_matrix_multiply, Local_matrix_allocate, Set_to_zero.
Можно также предположить, что память для параметров была выделена в вызывающей функции, и все параметры, кроме локальной матрицы произведения local_C, уже инициализированы:

void Fox(int n, GRID_INFO_TYPE* grid,

     LOCAL-MATRIX-TYPE* local-A,

     LOCAL_MATRIX_TYPE* local_B,

     LOCAL_MATRIX_TYPE* local_C)

{

  LOCAL_MATRIX_TYPE* temp_A;

  int step;

  int bcast_root;

  int n_bar; /* порядок подматрицы = n/q */

  int source;

  int dest;

  int tag = 43;

  MPI_Status status;

 

  n_bar = n/grid->q;

  Set_to_zero(local_C);

 

  /* Вычисление адресов для циклического сдвига B */

  source = (grid->my_row + 1) % grid->q;

  dest = (grid->my_row + grid->q-1) % grid->q;

 

  /* Выделение памяти для рассылки блоков A */

  temp_A = Local_matrix_allocate(n_bar);

  for (step = 0; step < grid->q; step++) {

    bcast_root = (grid->my_row + step) % grid->q;

    if (bcast_root == grid->my_col) {

      MPI_Bcast(local_A, 1, DERIVED_LOCAL_MATRIX,

         bcast_root, grid->row_comm);

      Local_matrix_multiply(local_A, local_B, local_C);

    } else {

      MPI_Bcast(temp_A, 1, DERIVED_LOCAL_MATRIX,

         bcast_root, grid->row_comm);

      Local_matrix_multiply(temp_A, local_B, local_C);

  }

  MPI_Send(local_B, 1, DERIVED_LOCAL_MATRIX, dest, tag,

     grid->col_comm);

  MPI_Recv(local_B, 1, DERIVED_LOCAL_MATRIX, source,

     tag, grid->col_comm, &status);

  } /*for*/

}/*Fox*/


next up previous contents
Next: Начальные сведения о PETSc Up: Коммуникаторы и топологии Previous: Функция MPI_Cart_sub   Contents
2004-06-22

next up previous contents
Next: Запуск программ PETSc. Up: Высокоуровневые средства межпроцессного взаимодействия Previous: Реализация алгоритма Фокса   Contents

Начальные сведения о PETSc

Переносимый расширяемый инструментарий для научных вычислений (PETSc) успешно продемонстрировал, что использование современных парадигм программирования может облегчить разработку крупномасштабных научных приложений на языках Фортран, C, и C++. Возникнув несколько лет назад, продукт эволюционировал в мощный набор средств для численного решения дифференциальных уравнений в частных производных и сходных проблем высокопроизводительных вычислений. PETSc состоит из нескольких библиотек (подобных классам C++). Каждая библиотека оперирует определенным семейством объектов (например, векторами) и ее операции применяются к этим объектам. Объекты и операции в PETSc разработаны с учетом опыта разработки научных приложений. Модули PETSc работают с множествами индексов, включая перестановки, для индексации векторов, перенумерации, и т.д.; c векторами; c матрицами (обычно разреженными); c распределенными массивами (используются для параллелизации задач с регулярной сетевой структурой); c методами подпространств Крылова; c предобработчиками, включая мультисеточные и прямые разреженные решатели; c нелинейными решателями; c пошаговыми решателями для дифференциальных уравнений в частных производных во времени.

Каждый модуль содержит абстрактный интерфейс (простой набор последовательностей вызова) и одну или несколько его реализаций, использующих определенные структуры данных. Таким образом, PETSc предлагает прозрачные и эффективные коды для различных фаз решения дифференциальных уравнений в частных производных с единообразным подходом к любому классу проблем. Такое построение гарантирует простое использование и сравнение различных алгоритмов (например, для экспериментов с различными методами подпространств Крылова, предобработчикамии, или сокращенными методами Ньютона). Кроме того, PETSc предоставляет удобную среду для моделирования научных приложений, а также быстрого построения или прототипирования алгоритмов. Библиотеки позволяют провести легкую настройку и расширение как алгоритмов, так и реализации. Этот подход улучшает гибкость систем и позволяет повторное использование кода, а также отделяет собственно параллелизацию от выбора алгоритма. Инфраструктура PETSc создает основу для построения крупномасштабных приложений. Ее полезно проанализировать для определения взаимосвязи между отдельными частями PETSc. Рис. 6 представляет собой диаграмму ее отдельных частей; на рис. 7 некоторые части изображены более детально. Эти рисунки иллюстрируют иерархическую организацию библиотеки, что позволяет пользователям применить уровень абстракции, наиболее подходящий для определенной задачи.

\includegraphics[scale=0.6]{figure1.eps}

Рис. 6. Организация библиотеки PETSc



Subsections

2004-06-22

next up previous contents
Next: Написание программ PETSc Up: Начальные сведения о PETSc Previous: Начальные сведения о PETSc   Contents

Запуск программ PETSc.

Перед использованием PETSc пользователь должен установить переменную окружения PETSC_DIR, указывающую полный путь к домашнему каталогу PETSc. Например, при работе в UNIX C shell команду вида setenv PETSC_DIR $HOME/petsc нужно поместить в пользовательский файл .cshrc. Дополнительно пользователь должен установить переменную окружения PETSC_ARCH, чтобы указать тип архитектуры (например, rs6000, solaris, IRIX и т.д.), для которой используется PETSc. Для этих целей можно употреблять утилиту ${PETSC_DIR}/bin/petscarch. Например, команда setenv PETSC_ARCH `$PETSC_DIR/bin/petscarch` может быть добавлена в файл .cshrc. Таким образом, даже если несколько машин различных типов разделяют единую файловую систему, при регистрации на любой из них PETSC_ARCH будет устанавливаться корректно .

Все программы PETSc используют стандарт MPI (Message
Passing Interface) для обмена сообщениями. Поэтому, для выполнения программ PETSc, пользователь должен быть знаком с процедурой запуска задач MPI на выбранной системе. Например, при применении MPICH и других реализаций MPI программу, использующую восемь процессоров, выполняет команда:

mpirun -np 8 petsc_program_name petsc_options
PETSc поставляется со скриптом petscmpirun:

$PETSC_DIR/bin/petscmpirun -np 8 

   petsc_program_name petsc_options

Он берет информацию из ${PETSC_DIR}/bmake/${PETSC_ARCH}/
packages
для автоматического использования нужного для вашей конфигурации mpirun. Все программы, совместимые с PETSc, поддерживают использование опций -h или -help, а также опций -v или -version. Определенные опции поддерживаются всеми программами PETSc. Ниже приведен список наиболее используемых опций (полный их список можно получить, запустив любую программу PETSc с опцией -help):



2004-06-22

next up previous contents
Next: Простые примеры PETSc Up: Начальные сведения о PETSc Previous: Запуск программ PETSc.   Contents

Написание программ PETSc

Программы PETSc начинаются с вызова

PetscInitialize (int *argc,char ***argv,char *file,

     char *help);,

который инициализирует PETSc и MPI. Аргументы argc и argv являются аргументами командной строки, передаваемыми всем программам C и C++. Аргумент file опционально указывает альтернативное имя файла опций PETSc, .petscrc, который по умолчанию находится в домашнем каталоге пользователя. База опций PETSc используется для настройки программы во время работы. Последний аргумент help является опциональной строкой символов, которая выводится, если программа запущена с опцией -help. В Фортране команда инициализации имеет вид:

call PetscInitialize (character file,integer ierr)

PetscInitialize() автоматически вызывает MPI_Init(), если MPI не был инициализирован ранее. При определенных условиях, когда MPI нужно инициализировать прямо (или он инициализирован другой библиотекой), пользователь может вначале вызвать MPI_Init() (или позволить другой библиотеке это сделать), а затем вызвать PetscInitialize(). По умолчанию PetscInitialize()
устанавливает `общий'' коммуникатор PETSc, определяемый
PETSC_COMM_WORLD на MPI_COMM_WORLD. Для тех, кто не знаком с MPI, коммуникатор является способом указания набора процессов, которые совместно участвуют в вычислениях или коммуникациях. Коммуникаторы являются переменными типа MPI_Comm. В большинстве случаев пользователи могут пользоваться коммуникатором PETSC_COMM_WORLD, для указания всех запущенных процессов, или PETSC_COMM_SELF, для указания одного процесса. MPI предоставляет процедуры создания новых коммуникаторов, содержащих подмножества процессоров, хотя пользователи очень редко применяют их. Заметьте, что пользователям PETSc не нужно программировать основной обмен сообщениями непосредственно через MPI, но они должны быть знакомы с основными концепциями обмена сообщениями и вычислений в распределенной памяти. Пользователи, которым нужно применить процедуры PETSc на некотором множестве процессоров в большой параллельной задаче, или которые хотят использовать ``главный '' процесс для управления работой ``подчиненных'' процессов PETSc, должны указать альтернативный коммуникатор для PETSC_COMM_WORLD, вызвав
PetscSetCommWorld (MPI Comm comm)
перед вызовом PetscInitialize(), но, конечно, после вызова
MPI_Init().PetscSetCommWorld() может быть вызван как минимум раз в каждом процессе. Большинству пользователей может никогда не понадобиться применять функцию PetscSetCommWorld(). Все процедуры PETSc возвращают целое число, указывающее, возникла ли при вызове ошибка. Код ошибки устанавливается в ненулевое значение, если ошибка возникла; в противном случае возвращается 0. Для интерфейса C/C++ код ошибки является возвращаемым значением, в то время как для версии Фортрана каждая процедура PETSc содержит в качестве последнего аргумента целочисленную переменную кода ошибки. Все программы PETSc должны вызывать PetscFinalize() в качестве последнего оператора, как показано ниже в формате для C/C++ и Фортрана соответственно:

PetscFinalize ();

call PetscFinalize (ierr)

Эта процедура поддерживает опции, вызываемые при завершении программы, а затем вызывает MPI_Finalize(), если вызов
PetscInitialize() предшествовал MPI. Если MPI инициализировался извне PETSc (пользователем или другим приложением), пользователь сам отвечает за вызов MPI_Finalize().



2004-06-22

next up previous contents
Next: Включаемые файлы Up: Начальные сведения о PETSc Previous: Написание программ PETSc   Contents

Простые примеры PETSc

Чтобы помочь пользователю начать освоение PETSc немедленно, следует начать с простого однопроцессорного примера, который решает одномерную задачу Лапласа с конечными разностями. Этот последовательный код, который находится в файле
${PETSC_DIR}/src/sles/examples/tutorials/ex1.c, иллюстрирует решение линейной системы с помощью библиотек SLES, интерфейсов предобработчиков, методов подпространств Крылова и прямых линейных решателей PETSc. По мере движения по коду выделяется несколько наиболее важных частей примера:

/* Program usage: mpirun ex1 [-help] [all PETSc options] */
static char help[] = "Решает тридиагональную линейную систему
    через SLES.\n\n";
/*T
Концепция: SLES^решение системы линейных уравнений
Процессоры: 1
T*/
/*
Включаем "petscsles.h" чтобы использовать решатели SLES .
Отметьте, что этот файл автоматически включает:
petsc.h - основные процедуры PETSc  petscvec.h - векторы
petscsys.h - системные процедуры    petscmat.h - матрицы
petscis.h - индексные множества     petscksp.h - методы
                                         подпространств Крылова
petscviewer.h - просмотрщики        petscpc.h - предобработчики
Замечание:
   Соответствующий параллельный пример находится в файле ex23.c
*/

#include "petscsles.h"
#undef __FUNCT__
#define __FUNCT__ "main"
int main(int argc,char **args)
  Vec x, b, u; /* приближенное решение, RHS, точное решение */
  Mat A;       /* матрица линейной системы */
  SLES sles;   /* контекст линейного решателя */
  PC pc;       /* контекст предобработчика */
  KSP ksp;     /* контекст метода подпространств Крылова */
  PetscReal norm;  /* допустимая ошибка решения */
  int ierr,i,n = 10,col[3],its,size;
  PetscScalar neg_one = -1.0,one = 1.0,value[3];

  PetscInitialize(&argc,&args,(char *)0,help);
  ierr = MPI_Comm_size(PETSC_COMM_WORLD,&size);
         CHKERRQ(ierr);
  if (size != 1) SETERRQ(1,"Это однопроцессорный пример!");
  ierr = PetscOptionsGetInt(PETSC_NULL,"-n",&n,PETSC_NULL);
         CHKERRQ(ierr);
 
  /*
  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  Вычисляет матрицу и правосторонний вектор, который определяет
  линейную систему Ax = b.
  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */
 /*
  Создает векторы. Заметьте, что первый вектор формируется с нуля,
  а затем размножается, если нужно.
 */

 ierr = VecCreate(PETSC_COMM_WORLD,&x);CHKERRQ(ierr);
 ierr = PetscObjectSetName((PetscObject) x, "Решение");CHKERRQ(ierr);
 ierr = VecSetSizes(x,PETSC_DECIDE,n);CHKERRQ(ierr);
 ierr = VecSetFromOptions(x);CHKERRQ(ierr);
 ierr = VecDuplicate(x,&b);CHKERRQ(ierr);
 ierr = VecDuplicate(x,&u);CHKERRQ(ierr);

 /*
  Создает матрицу. При использовании MatCreate() формат матрицы
  можно определять в процессе выполнения.
  Замечание по производительности:
  Для задач существенного объема предварительное распределение
  памяти для матрицы критично для достижения хорошей
  производительности. Поскольку предварительное распределение
  невозможно средствами общей процедуры создания матрицы MatCreate(),
  рекомендуется для конкретных задач использовать
  специализированные процедуры, например:
     MatCreateSeqAIJ() - последовательная AIJ
                        (упакованная разреженная строка),
     MatCreateSeqBAIJ() - блочная AIJ
  См. главу о матрицах руководства пользователя
     для более полной информации.
 */

 ierr = MatCreate(PETSC_COMM_WORLD,PETSC_DECIDE,PETSC_DECIDE,n,n,&A);
        CHKERRQ(ierr);
 ierr = MatSetFromOptions(A);CHKERRQ(ierr);
 /*
   Собирает матрицу
 */

 value[0] = -1.0; value[1] = 2.0; value[2] = -1.0;
 for (i=1; i<n-1; i++)
    col[0] = i-1; col[1] = i; col[2] = i+1;
    ierr = MatSetValues(A,1,&i,3,col,value,INSERT_VALUES);
           CHKERRQ(ierr);

 i = n - 1; col[0] = n - 2; col[1] = n - 1;
 ierr = MatSetValues(A,1,&i,2,col,value,INSERT_VALUES);
        CHKERRQ(ierr);
 i = 0; col[0] = 0; col[1] = 1; value[0] = 2.0; value[1] = -1.0;
 ierr = MatSetValues(A,1,&i,2,col,value,INSERT_VALUES);
        CHKERRQ(ierr);
 ierr = MatAssemblyBegin(A,MAT_FINAL_ASSEMBLY);CHKERRQ(ierr);
 ierr = MatAssemblyEnd(A,MAT_FINAL_ASSEMBLY);CHKERRQ(ierr);
 /*
  Установим точное решение; затем вычислим правосторонний вектор.
 */

 ierr = VecSet(&one,u);CHKERRQ(ierr);
 ierr = MatMult(A,u,b);CHKERRQ(ierr);

 /*
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  Создает линейный решатель и устанавливает различные опции
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

 */

 /*
 Создает контекст линейного решателя
 */

 ierr = SLESCreate(PETSC_COMM_WORLD,&sles);CHKERRQ(ierr);

 /*
  Устанавливаем операторы. Здесь матрица, которая определяет
  линейную систему, служит также в качестве матрицы
  предобработчика.
 */

 ierr = SLESSetOperators(sles,A,A,DIFFERENT_NONZERO_PATTERN);
        CHKERRQ(ierr);
 /*
  Устанавливаем значения по умолчанию для линейного решателя
  (опционально):
   - Выделяя контексты KSP и PC из контекста SLES, можно затем
     непосредственно вызывать любые процедуры KSP и PC
     для установки различных опций.
   - Следующие четыре оператора необязательны; все их параметры
     можно определить во время выполнения через
     SLESSetFromOptions();
 */

 ierr = SLESGetKSP(sles,&ksp);CHKERRQ(ierr);
 ierr = SLESGetPC(sles,&pc);CHKERRQ(ierr);
 ierr = PCSetType(pc,PCJACOBI);CHKERRQ(ierr);
 ierr = KSPSetTolerances(ksp,1.e-7,
        PETSC_DEFAULT,PETSC_DEFAULT,PETSC_DEFAULT);CHKERRQ(ierr);
 /*
   Установить опции времени выполнения, например
   -ksp_type <type> -pc_type <type> -ksp_monitor -ksp_rtol <rtol>
   Эти опции могут переопределить те, которые были указаны выше,
   после того, как SLESSetFromOptions() была вызвана после любой
   другой процедуры настройки.
 */

 ierr = SLESSetFromOptions(sles);CHKERRQ(ierr);
 
 /*
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Решаем линейную систему
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

 ierr = SLESSolve(sles,b,x,&its);CHKERRQ(ierr);

 /*
   Смотреть информацию от решателя; вместо этого можно
   использовать опцию
    -sles_view для вывода этой информации на экран после
       завершения SLESSolve().
 */

 ierr = SLESView(sles,PETSC_VIEWER_STDOUT_WORLD);CHKERRQ(ierr);
 
 /*
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   Проверяем решение и очищаем его
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

 /*
   Проверяем ошибки
 */

 ierr = VecAXPY(&neg_one,u,x);CHKERRQ(ierr);
 ierr = VecNorm(x,NORM_2,&norm);CHKERRQ(ierr);
 ierr = PetscPrintf(PETSC_COMM_WORLD,
        "Погрешность %A, Итераций %d\n",norm,its); CHKERRQ(ierr);
 
 /*
   Освобождаем рабочее пространство. Все объекты PETSc
   должны быть уничтожены, как только они станут ненужными.
 */
 
 ierr = VecDestroy(x);CHKERRQ(ierr);
 ierr = VecDestroy(u);CHKERRQ(ierr);
 ierr = VecDestroy(b);CHKERRQ(ierr);
 ierr = MatDestroy(A);CHKERRQ(ierr);
 ierr = SLESDestroy(sles);CHKERRQ(ierr);

 /* Всегда вызываем PetscFinalize() перед выходом из программы.
    Эта процедура
      - финализирует библиотеки PETSc и MPI
      - предоставляет общую и диагностическую информацию,
        если указаны определенные опции времени выполнения
	(например, -log_summary).
 */

 ierr = PetscFinalize();CHKERRQ(ierr);
 return 0;
}



Subsections

2004-06-22

next up previous contents
Next: База списка опций Up: Простые примеры PETSc Previous: Простые примеры PETSc   Contents

Включаемые файлы

Включаемые файлы C/C++ для PETSc должны использоваться через директивы #include "petscsles.h", где petscsles.h - имя включаемого файла библиотеки SLES. Каждая программа PETSc должна указывать включаемый файл, соответствующий самому высокому уровню объектов PETSc, используемых в программе; все требуемые включаемые файлы нижних уровней автоматически вложены в файлы верхнего уровня. Например, petscsles.h включает petscmat.h (матрицы), petscvec.h (векторы), и petsc.h (основной файл PETSc). Включаемые файлы PETSc находятся в каталоге ${PETSC_DIR}/include.



2004-06-22

next up previous contents
Next: Блокировка файлов Up: Трубы (pipes) Previous: Функция popen()   Contents

FIFO - именованные каналы

С помощью труб могут общаться только родственные друг другу процессы, полученные с помощью fork(). Именованные каналы FIFO позволяют обмениваться данными с абсолютно ``чужим'' процессом.

С точки зрения ядра ОС FIFO является одним из вариантов реализации трубы. Системный вызов mkfifo() предоставляет процессу именованную трубу в виде объекта файловой системы. Как и для любого другого объекта, необходимо предоставлять процессам права доступа в FIFO, чтобы определить, кто может писать, и кто может читать данные. Несколько процессов могут записывать или читать FIFO одновременно. Режим работы с FIFO - полудуплексный, т.е. процессы могут общаться в одном из направлений. Типичное применение FIFO - разработка приложений ``клиент - сервер''.

Синтаксис функции для создания FIFO следующий:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *fifoname, mode_t mode);

При возникновении ошибки функция возвращает -1, в противном случае 0. В качестве первого параметра указывается путь, где будет располагаться FIFO. Второй параметр определяет режим работы с FIFO. Пример использования приведен ниже:

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

 

int main()

{

  int fd_fifo; /*дескриптор FIFO*/

  char buffer[]="Текстовая строка для fifo\n";

  char buf[100];

   

  /*Если файл с таким именем существует, удалим его*/

  unlink("/tmp/fifo0001.1");

  /*Создаем FIFO*/

  if((mkfifo("/tmp/fifo0001.1", O_RDWR)) == -1)

  {

    fprintf(stderr, "Невозможно создать fifo\n");

    exit(0);

  }

  /*Открываем fifo для чтения и записи*/

  if((fd_fifo=open("/tmp/fifo0001.1", O_RDWR)) == - 1)

  {

    fprintf(stderr, "Невозможно открыть fifo\n");

    exit(0);

  }

  write(fd_fifo,buffer,strlen(buffer)) ;

  if(read(fd_fifo, &buf, sizeof(buf)) == -1)

  fprintf(stderr, "Невозможно прочесть из FIFO\n");

  else

  printf("Прочитано из FIFO : %s\n",buf);

  return 0;

}

Если в системе отсутствует функция mkfifo(), можно воспользоваться общей функцией для создания файла:

int mknod(char *pathname, int mode, int dev);

Здесь pathname указывает обычное имя каталога и имя FIFO. Режим обозначается константой S_IFIFO из заголовочного файла <sys/stat.h>. Здесь же определяются права доступа. Параметр dev не нужен. Пример вызова mknod:

if(mknod("/tmp/fifo0001.1", S_IFIFO | S_IRUSR | S_IWUSR,

          0) == - 1)

{ /*Невозможно создать fifo */

Если при открытии FIFO через open() не указать режим O_NONBLOCK, то открытие FIFO блокируется и для записи, и для чтения. При записи канал блокируется до тех пор, пока другой процесс не откроет FIFO для чтения. При чтении канал снова блокируется до тех пор, пока другой процесс не запишет данные.

Флаг O_NONBLOCK может использоваться только при доступе для чтения. При попытке открыть FIFO с O_NONBLOCK для записи возникает ошибка открытия. Если FIFO закрыть для записи через close или fclose, это значит, что для чтения в FIFO помещается EOF.

Если несколько процессов пишут в один и тот же FIFO, необходимо обратить внимание на то, чтобы сразу не записывалось больше, чем PIPE_BUF байтов. Это необходимо, чтобы данные не смешивались друг с другом. Установить пределы записи можно следующей программой:

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

 

int main()

{

  unlink("fifo0001");

  /*Создаем новый FIFO*/

  if((mkfifo("fifo0001", O_RDWR)) == -1)

  {

    fprintf(stderr, "Невозможно создать FIFO\n");

    exit(0);

  }

  printf("Можно записать в FIFO сразу %ld байтов\n",

      pathconf("fifo0001", _PC_PIPE_BUF));

  printf("Одновременно можно открыть %ld FIFO \n",

      sysconf(_SC_OPEN_MAX));

  return 0;

}

При попытке записи в FIFO, который не открыт в данный момент для чтения ни одним процессом, генерируется сигнал SIGPIPE.

В следующем примере организуется обработчик сигнала SIGPIPE, создается FIFO, процесс-потомок записывает данные в этот FIFO, а родитель читает их оттуда. Пример иллюстрирует простое приложение типа ``клиент - сервер'':

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <signal.h>

static volatile sig_atomic_t sflag;

static sigset_t signal_new, signal_old, signal_leer;

 

static void sigfunc(int sig_nr)

{

  fprintf(stderr, "SIGPIPE вызывает завершение

          программы\n");

  exit(0);

}

 

void signal_pipe(void)

{

  if(signal(SIGPIPE, sigfunc) == SIG_ERR)

  {

    fprintf(stderr, "Невозможно получить сигнал

            SIGPIPE\n");

    exit(0);

  }

  /*Удаляем все сигналы из множества сигналов*/

  sigemptyset(&signal_leer);

  sigemptyset(&signal_new);

  sigaddset(&signal_new, SIGPIPE);

  /*Устанавливаем signal_new и сохраняем его*/

  /* теперь маской сигналов будет signal_old*/

  if(sigprocmask(SIG_UNBLOCK, &signal_new,

     &signal_old) < 0)

  exit(0);

}

 

int main()

{

  int r_fifo, w_fifo; /*дескрипторы FIFO*/

  char buffer[]="Текстовая строка для fifo\n";

  char buf[100];

  pid_t pid;

  signal_pipe();

  unlink("/tmp/fifo0001.1");

  /*Создаем FIFO*/

  if((mkfifo("/tmp/fifo0001.1", O_RDWR)) == -1)

  {

    fprintf(stderr, "Невозможно создать fifo\n");

    exit(0);

  }

  pid=fork();

  if(pid == -1)

    { perror("fork"); exit(0);}

  else if(pid > 0) /*Родитель читает из FIFO*/

  {

    if (( r_fifo=open("/tmp/fifo0001.1", O_RDONLY))<0)

      { perror("r_fifo open"); exit(0); }

    /*Ждем окончания потомка*/

    while(wait(NULL)!=pid);

     /*Читаем из FIFO*/

    read(r_fifo, &buf, sizeof(buf));

    printf("%s\n",buf);

    close(r_fifo);

  }

  else /*Потомок записывает в FIFO*/

  {

    if((w_fifo=open("/tmp/fifo0001.1", O_WRONLY))<0)

      { perror("w_fifo open"); exit(0); }

    /*Записываем в FIFO*/

    write(w_fifo, buffer, strlen(buffer));

    close(w_fifo); /*EOF*/

    exit(0);

  }

  return 0;

}


next up previous contents
Next: Блокировка файлов Up: Трубы (pipes) Previous: Функция popen()   Contents
2004-06-22

next up previous contents
Next: Векторы Up: Простые примеры PETSc Previous: Включаемые файлы   Contents

База списка опций

Пользователь может вводить управляющую информацию во время выполнения программы, используя базу опций. В этом примере команда OptionsGetInt(PETSC_NULL,"-n",&n,&flg); проверяет, указал ли пользователь опцию командной строки для установки значения n, т.е. размерности задачи. Если это так, то переменная n устанавливается в нужное значение; в противном случае n остается неизменной.



2004-06-22

next up previous contents
Next: Матрицы Up: Простые примеры PETSc Previous: База списка опций   Contents

Векторы

Можно создать новый параллельный или последовательный вектор x общей размерности M с помощью команд:

VecCreate (MPI_Comm comm,Vec *x);

VecSetSizes (Vec x, int m, int M);

где comm означает коммуникатор MPI, а m является необязательным локальным размером, который может иметь значение PETSC_DECIDE. Тип хранения для вектора может быть указан вызовами VecSetType() или VecSetFromOptions(). Дополнительные векторы того же типа можно сформировать с помощью VecDuplicate(Vec old,Vec *new). Команды:

VecSet (PetscScalar *value,Vec x);

VecSetValues (Vec x,int n,int *indices,

    PetscScalar *values, INSERT_VALUES);

устанавливают все компоненты вектора в определенное скалярное значение и присваивают различные значения каждому компоненту. Отметьте также использование типа переменной PetscScalar в этом примере. Тип PetscScalar определен как double в C/C++ (или тип двойной точности в Фортране) для версий PETSc, которые не компилируются для использования с комплексными числами. Тип данных PetscScalar позволяет использовать один и тот же код, если библиотеки PETSc откомпилированы для использования комплексных чисел.



2004-06-22

next up previous contents
Next: Линейные решатели Up: Простые примеры PETSc Previous: Векторы   Contents

Матрицы

Использование матриц в PETSc похоже на использование векторов. Пользователь может создать новую параллельную или последовательную матрицу A, у которой M строк и N столбцов, с помощью вызова:

MatCreate (MPI_Comm comm,int m,int n,int M,int N, Mat* A);
где формат матрицы может быть определен во время выполнения. Пользователь также может указать каждому процессу локальное число строк и столбцов через параметры m и n. Значения могут быть установлены командой:

MatSetValues (Mat A,int m,int *im,int n,int *in,

   PetscScalar *values, INSERT_VALUES);

После того, как все элементы будут вставлены в матрицу, она может обрабатываться двумя командами:

MatAssemblyBegin (Mat A,MAT_FINAL_ASSEMBLY);

MatAssemblyEnd (Mat A,MAT_FINAL_ASSEMBLY);



2004-06-22

next up previous contents
Next: Нелинейные решатели Up: Простые примеры PETSc Previous: Матрицы   Contents

Линейные решатели

После создания матриц и векторов, определяющих линейную систему Ax = b, пользователь может применить SLES для решения системы следующей последовательностью команд:

SLESCreate (MPI_Comm comm ,SLES *sles);

SLESSetOperators (SLES sles,Mat A,Mat PrecA,

    MatStructure flag);

SLESSetFromOptions (SLES sles);

SLESSolve (SLES sles,Vec b,Vec x,int *its);

SLESDestroy (SLES sles);

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



2004-06-22

next up previous contents
Next: Контроль ошибок Up: Простые примеры PETSc Previous: Линейные решатели   Contents

Нелинейные решатели

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



2004-06-22

next up previous contents
Next: Параллельное программирование Up: Простые примеры PETSc Previous: Нелинейные решатели   Contents

Контроль ошибок

Все процедуры PETSc возвращают целое число, указывающее, возникла ли ошибка при вызове. Макрос CHKERRQ(ierr) в PETSc проверяет значение ierr и вызывает обработчик ошибок PETSc, при их обнаружении. CHKERRQ(ierr) нужно использовать во всех процедурах, чтобы обеспечить полный контроль ошибок. Ниже приведена трасса, созданная контролем ошибок в примере программы PETSc. Ошибка возникла в строке 1673 файла
${PETSC_DIR}/src/mat/impls/aij/seq/aij.c и была вызвана попыткой распределения памяти для слишком большого массива. Процедура была вызвана в программе ex3.c, в строке 71.

eagle:mpirun -np 1 ex3 -m 10000
PETSC ERROR: MatCreateSeqAIJ() line 1673
             in src/mat/impls/aij/seq/aij.c
PETSC ERROR: Out of memory. This could be due to allocating
PETSC ERROR: too large an object or bleeding by not properly
PETSC ERROR: destroying unneeded objects.
PETSC ERROR: Try running with -trdump for more information.
PETSC ERROR: MatCreate () line 99 in src/mat/utils/gcreate.c
PETSC ERROR: main() line 71
             in src/sles/examples/tutorials/ex3.c
MPI Abort by user Aborting program !
Aborting program!
p0 28969: p4 error: : 1

При использовании версий библиотек PETSc для отладки (откомпилированных с опцией BOPT=g) можно также проверять нарушения памяти (запись за границами массива и т.д.). Макрос CHKMEMQ можно вызывать в любом месте кода для проверки текущего состояния памяти и нарушений. Помещая в свой код несколько (или много) таких макросов, вы можете легко отследить, в какой части кода происходит нарушение.



2004-06-22

next up previous contents
Next: Компиляция и запуск программ Up: Простые примеры PETSc Previous: Контроль ошибок   Contents

Параллельное программирование

Поскольку PETSc использует модель передачи сообщений для параллельного программирования и пользуется MPI для межпроцессорного взаимодействия, пользователь может свободно употреблять в своем коде процедуры MPI там, где это необходимо. Однако по умолчанию пользователь огражден от многих деталей обмена сообщениями внутри PETSc, поскольку они скрыты в таких параллельных объектах, как векторы, матрицы и решатели. К тому же, PETSc продоставляет такие инструменты, как обобщенную сборку/рассылку векторов и распределенные массивы, чтобы облегчить управление параллельными данными. Помните, что пользователь должен определить коммуникатор перед созданием любого объекта PETSc (такого как вектор, матрица или решатель), чтобы указать процессоры, на которые будет распределен объект. Например, как упоминалось выше, командами для создания матрицы, вектора и линейного решателя являются:

MatCreate (MPI_Comm comm ,int M,int N,Mat *A);

VecCreate (MPI_Comm comm ,Vec *x);

SLESCreate (MPI_Comm comm ,SLES *sles);

Процедуры создания являются коллективными относительно всех процессоров в коммуникаторе; поэтому все процессоры в коммуникаторе должны вызывать их. Кроме того, если используется последовательность коллективных процедур, они должны вызываться на каждом процессоре в одном порядке. Следующий пример иллюстрирует параллельное решение линейной системы. Этот код, соответствующий примеру в файле ${PETSC_DIR}/src/sles/examples/
tutorials/ex2.c
, обрабатывает двумерный лапласиан, дискретизированный конечными разностями, при этом линейная система снова решается с помощью SLES . Код выполняет ту же задачу, что и последовательная версия, приведенная ранее. Заметьте, что интерфейс пользователя для запуска программы, создания векторов и матриц, а также решения линейной системы совпадает для однопроцессорной и многопроцессорной версий. Основное различие для примеров в том, что в параллельном случае каждый процессор формирует только свою локальную часть матрицы и вектора:
/* Запуск программы: mpirun -np <procs> ex2 [-help]
     [all PETSc options] */
static char help[] =
       "Параллельное решение линейной системы через SLES.\n\
  Входные параметры включают:\n\
    -random_exact_sol : использовать случайный вектор\
                        точного решения\n\
    -view_exact_sol   : вывод вектора точного решения в stdout\n\
    -m <mesh_x>       : количество узлов сетки по x\n\
    -n <mesh_n>       : количество узлов сетки по y\n\n";

 /*T
   Идея: параллельный пример, основанный на SLES;
   Идея: SLES^Laplacian, 2d
   Идея: Laplacian, 2d
   Процессоры: n
 T*/

 /*
   Включаем "petscsles.h", чтобы использовать решатели SLES.
   Этот файл автоматически включает:
   petsc.h    - основные процедуры PETSc  petscvec.h - векторы
   petscsys.h - системные процедуры       petscmat.h - матрицы
   petscis.h  - индексные множества       petscksp.h - методы
                                          подпространств Крылова
   petscviewer.h - просмотрщики        petscpc.h - предобработчики
 */

 #include "petscsles.h"
 #undef __FUNCT__
 #define __FUNCT__ "main"
 int main(int argc,char **args) {
   Vec x,b,u;           /* приближенное решение, RHS,
                           точное решение */
   Mat A;               /* матрица линейной системы */ 
   SLES sles;           /* контекст линейного решателя */
   PetscRandom rctx;    /* контекст генератора случайных чисел */
   PetscReal norm;      /* погрешность решения */
   int i,j,I,J,Istart,Iend,ierr,m = 8,n = 7,its;
   PetscTruth flg;
   PetscScalar v,one = 1.0,neg_one = -1.0;
   KSP ksp;

   PetscInitialize(&argc, &args, (char*)0, help);
   ierr = PetscOptionsGetInt(PETSC_NULL,"-m",&m,PETSC_NULL);
          CHKERRQ(ierr);
   ierr = PetscOptionsGetInt(PETSC_NULL,"-n",&n,PETSC_NULL);
          CHKERRQ(ierr);

 /*
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Вычисляем матрицу и правосторонний вектор, определяющий
     линейную систему Ax = b.
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

 /*
  Создаем параллельную матрицу, определяя только ее общие размеры.
  При использовании MatCreate() формат матрицы может быть задан
  во время выполнения.
  Аналогично параллельное разделение матрицы определяется PETSc
  во время выполнения.

  Для проблем существенной размерности, предварительное 
  распределение памяти для матрицы влияет на 
  производительность.

  Поскольку предварительное распределение невозможно при
  использовании общей процедуры создания матрицы MatCreate(),
  рекомендуется для практических задач вместо этого использовать
  процедуры создания матриц специальных форматов, например:
    MatCreateMPIAIJ() - параллельная AIJ
                        (упакованная разреженная строка)
    MatCreateMPIBAIJ() - параллельная блочная AIJ
  См. главу о матрицах в руководстве пользователя
  для детальной информации.
 */

 ierr = MatCreate(PETSC_COMM_WORLD,PETSC_DECIDE,PETSC_DECIDE,m*n,
        m*n,&A);CHKERRQ(ierr);
 ierr = MatSetFromOptions(A);CHKERRQ(ierr);

 /*
   В настоящее время все параллельные форматы матриц PETSc
   разделяются среди процессоров на непрерывные последовательности
   строк. Определим, какие строки матрицы получены локально
 */

 ierr = MatGetOwnershipRange(A,\&Istart,\&Iend);CHKERRQ(ierr);

 /*
   Параллельно устанавливаем элементы матрицы для 2-D
   пятиточечного шаблона:
     - Каждый процессор должен вставить только элементы, которыми
       он владеет локально (любые нелокальные элементы будут
       пересланы соответствующему процессору во время
       сборки матрицы).
     - Всегда указывайте глобальные номера строк и столбцов
       элементов матрицы.
   Здесь используется не совсем стандартная нумерация,
   которая перебирает сначала все неизвестные для x = h, затем
   для x = 2h и т.д.; поэтому Вы видите J = I +- n вместо
   J = I +- m, как Вы ожидали. Более стандартная нумерация сначала
   проходит все переменные для y = h, затем y = 2h и т.д.
 */

 for (I=Istart; I<Iend; I++) {
   v = -1.0; i = I/n; j = I - i*n;
   if (i>0) {J = I - n; ierr=MatSetValues(A,1,&I,1,&J,&v,
      INSERT_VALUES); CHKERRQ(ierr);}
   if (i<m-1) {J = I+n;ierr=MatSetValues(A,1,&I,1,&J,&v,
      INSERT_VALUES); CHKERRQ(ierr);}
   if (j>0) {J = I-1; ierr = MatSetValues(A,1,&I,1,&J,&v,
      INSERT_VALUES); CHKERRQ(ierr);}
   if (j<n-1) {J = I+1; ierr=MatSetValues(A,1,&I,1,&J,&v,
      INSERT_VALUES); CHKERRQ(ierr);}
   v = 4.0; ierr = MatSetValues(A,1,&I,1,&I,&v,INSERT_VALUES);
      CHKERRQ(ierr);
 }

 /*
   Собираем матрицу, используя двухшаговый процесс:
      MatAssemblyBegin(), MatAssemblyEnd()}
   Можно выполнять вычисления, пока сообщения передаются,
   если поместить код между двумя этими операторами.
 */

 ierr = MatAssemblyBegin(A,MAT_FINAL_ASSEMBLY);CHKERRQ(ierr);
 ierr = MatAssemblyEnd(A,MAT_FINAL_ASSEMBLY);CHKERRQ(ierr);

 /*
   Создаем параллельные векторы.
   - Первый вектор формируется с нуля, а затем делаются дубликаты,
     если нужно.
   - При использовании VecCreate(), VecSetSizes и
     VecSetFromOptions() в этом примере, определяются только
     глобальные размеры вектора; параллельное разделение
     определяется во время выполнения.
   - При решении линейной системы и векторы, и матрицы должны
     соответственно разделяться. PETSc автоматически генерирует
     правильно разделенные матрицы и векторы, если процедуры
     MatCreate() и VecCreate() используются с одним и тем же
     коммуникатором.
   - Пользователь может самостоятельно определить локальные
     размеры векторов и матриц, если требуется более сложное
     разделение (заменив аргумент PETSC_DECIDE в процедуре
     VecSetSizes() ниже).
 */

 ierr = VecCreate(PETSC_COMM_WORLD,&u);CHKERRQ(ierr);
 ierr = VecSetSizes(u,PETSC_DECIDE,m*n);CHKERRQ(ierr);
 ierr = VecSetFromOptions(u);CHKERRQ(ierr);
 ierr = VecDuplicate(u,&b);CHKERRQ(ierr);
 ierr = VecDuplicate(b,&x);CHKERRQ(ierr);

 /*
   Установим точное решение; затем вычислим правосторонний
   вектор. По умолчанию мы используем точное решение вектора
   со всеми элементами 1.0; однако использование
   опции времени выполнения -random_sol формирует вектор
   решения со случайными составляющими.
 */

 ierr = PetscOptionsHasName(PETSC_NULL,"-random_exact_sol",&flg);
        CHKERRQ(ierr);
 if (flg) {
    ierr = PetscRandomCreate(PETSC_COMM_WORLD,RANDOM_DEFAULT,&rctx);
           CHKERRQ(ierr);}
    ierr = VecSetRandom(rctx,u);CHKERRQ(ierr);
    ierr = PetscRandomDestroy(rctx);CHKERRQ(ierr);

 } else {
    ierr = VecSet(&one,u);CHKERRQ(ierr);
 }
 ierr = MatMult(A,u,b);CHKERRQ(ierr);

 /*
   Посмотрим вектор точного решения, если хочется
 */

 ierr = PetscOptionsHasName(PETSC_NULL,"-view_exact_sol",&flg);
        CHKERRQ(ierr);
 if (flg) {ierr = VecView(u,PETSC_VIEWER_STDOUT_WORLD);
        CHKERRQ(ierr);}

 /*
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  Создаем линейный решатель и устанавливаем различные опции}
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

 /*
   Создаем контекст линейного решателя
 */

 ierr = SLESCreate(PETSC_COMM_WORLD,&sles);CHKERRQ(ierr);

 /*
   Установим операторы. Здесь матрица, определяющая линейную
   систему, служит также матрицей предобработчиков.
 */

 ierr = SLESSetOperators(sles,A,A,DIFFERENT_NONZERO_PATTERN);
        CHKERRQ(ierr);

 /*
   Установим значения линейного решателя по умолчанию для
   этой задачи (необязательно):
    - Извлекая контексты KSP и PC из контекста SLES, затем можно
      непосредственно вызывать любые процедуры KSP и PC
      для установки различных опций.
    - Следующие два оператора опциональны; все параметры
      могут быть альтернативно заданы во время выполнения
      через SLESSetFromOptions().
      Все значения по умолчанию можно переопределить
      во время выполнения, как показано ниже.
 */

 ierr = SLESGetKSP(sles,&ksp);CHKERRQ(ierr);
 ierr = KSPSetTolerances(ksp,1.e-2/((m+1)*(n+1)),1.e-50,
        PETSC_DEFAULT,PETSC_DEFAULT); CHKERRQ(ierr);

 /*
   Установим опции времени выполнения, например,
     -ksp_type <type> -pc_type <type> -ksp_monitor -ksp_rtol <rtol>
   Эти опции могут переопределить значения, указаннаые выше
   до тех пор, пока SLESSetFromOptions() будет вызван после любой
   процедуры настройки.
 */

 ierr = SLESSetFromOptions(sles);CHKERRQ(ierr);

 /*
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  Решаем линейную систему
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

 ierr = SLESSolve(sles,b,x,&its);CHKERRQ(ierr);

 /*
  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  Проверяем решение и очищаем его
  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */

 /*
   Проверяем ошибку
 */

 ierr = VecAXPY(&neg_one,u,x);CHKERRQ(ierr);
 ierr = VecNorm(x,NORM_2,&norm);CHKERRQ(ierr);

 /*
   Масштабируем ошибку
 */

 /* norm *= sqrt(1.0/((m+1)*(n+1))); */

 /* 
   Выводим информацию о сходимости. PetscPrintf() создает единый
   оператор вывода из всех процессов, разделяющих коммуникатор.
   Альтернативой является PetscFPrintf(), который выводит в файл.
 */

 ierr = PetscPrintf(PETSC_COMM_WORLD,"Norm of error %A
        iterations %d\n", norm,its); CHKERRQ(ierr);

 /*
   Освобождаем рабочее пространство. Все объекты PETSc должны быть
   удалены, если они больше не нужны.
 */

 ierr = SLESDestroy(sles);CHKERRQ(ierr);
 ierr = VecDestroy(u);CHKERRQ(ierr);
 ierr = VecDestroy(b);CHKERRQ(ierr);
 ierr = VecDestroy(x);CHKERRQ(ierr);
 ierr = MatDestroy(A);CHKERRQ(ierr);

 /*
   Всегда вызывайте PetscFinalize() перед выходом из программы.
   Эта процедура
    - финализирует библиотеки PETSc, а также MPI
    - выдает итоговую и диагностическую информацию, если указаны
      определенные опции времени выполнения
      (например, -log_summary).
 */

 ierr = PetscFinalize();CHKERRQ(ierr);
 return 0;
}



2004-06-22

next up previous contents
Next: Разработка приложений PETSc Up: Простые примеры PETSc Previous: Параллельное программирование   Contents

Компиляция и запуск программ

Ниже приведен пример компиляции и запуска программы PETSc, использующей MPICH:

eagle: make BOPT=g ex2

gcc -pipe -c -I../../../ -I../../..//include

-I/usr/local/mpi/include -I../../..//src -g

-DPETSC_USE_DEBUG -DPETSC_MALLOC -DPETSC_USE_LOG ex1.c

gcc -g -DPETSC_USE_DEBUG -DPETSC_MALLOC -DPETSC_USE_LOG

  -o ex1 ex1.o

/home/bsmith/petsc/lib/libg/sun4/libpetscsles.a

-L/home/bsmith/petsc/lib/libg/sun4 -lpetscstencil -lpetscgrid

-lpetscsles -lpetscmat -lpetscvec -lpetscsys -lpetscdraw

/usr/local/lapack/lib/lapack.a /usr/local/lapack/lib/blas.a

/usr/lang/SC1.0.1/libF77.a -lm /usr/lang/SC1.0.1/libm.a -lX11

/usr/local/mpi/lib/sun4/ch p4/libmpi.a

/usr/lib/debug/malloc.o /usr/lib/debug/mallocmap.o

/usr/lang/SC1.0.1/libF77.a -lm /usr/lang/SC1.0.1/libm.a -lm

rm -f ex1.o

eagle: mpirun -np 1 ex2

Norm of error 3.6618e-05 iterations 7

eagle: mpirun -np 2 ex2

Norm of error 5.34462e-05 iterations 9

Отметьте, что различные рабочие места могут иметь различные библиотеки и имена компиляторов. Пользователи, столкнувшиеся с трудностями при сборке программ PETSc, могут ознакомиться с руководством по исправлению ошибок на веб-странице PETSc по адресу http://www.mcs.anl.gov/petsc или в файле
${PETSC_DIR}/docs/troubleshooting.html.

Опция -log_summary активирует вывод итоговой информации о производительности, включая времена выполнения, скорость операций с плавающей точкой, и активность обмена сообщениями. Следующая глава содержит детальную информацию о профилировании, включая интерпретацию данных. Этот отдельный пример выполняет решение линейной системы на одном процессоре с использованием GMRES и ILU. Малая скорость операций с плавающей точкой в этом примере обусловлена тем, что код решает малую систему. Этот пример предназначен в основном для демонстрации простоты получения информации о производительности:

eagle> mpirun -np 1 ex1 -n 1000 -pc_type ilu -ksp_type gmres
       -ksp_rtol 1.e-7 -log_summary 
------------------------------- PETSc Performance Summary:--------- 
ex1 on a sun4 named merlin.mcs.anl.gov with 1 processor,
    by curfman Wed Aug 7 17:24:27 1996 
                Max          Min     Avg          Total 
Time (sec):     1.150e-01    1.0     1.150e-01  
Objects:        1.900e+01    1.0     1.900e+01 
Flops:          3.998e+04    1.0     3.998e+04    3.998e+04  
Flops/sec:      3.475e+05    1.0                  3.475e+05 
MPI Messages:   0.000e+00    0.0     0.000e+00    0.000e+00 
MPI Messages:   0.000e+00    0.0     0.000e+00    0.000e+00 (lengths) 
MPI Reductions: 0.000e+00   0.0    
------------------------------------------------------------------- 
Phase   Count   Time (sec)    Flops/sec                       Global} 
  Max    Ratio   Max   Ratio  Mess  Avg len  Reduct  %T  %F  %M  %L %R 
------------------------------------------------------------------- 
MatMult          2 2.553e-03 1.0 3.9e+06 1.0 0.0e+00 0.0e+00 0.0e+00  2 25 0 0 0 
MatAssemblyBegin 1 2.193e-05 1.0 0.0e+00 0.0 0.0e+00 0.0e+00 0.0e+00  0  0 0 0 0 
MatAssemblyEnd   1 5.004e-03 1.0 0.0e+00 0.0 0.0e+00 0.0e+00 0.0e+00  4  0 0 0 0 
MatGetReordering 1 3.004e-03 1.0 0.0e+00 0.0 0.0e+00 0.0e+00 0.0e+00  3  0 0 0 0 
MatILUFctrSymbol 1 5.719e-03 1.0 0.0e+00 0.0 0.0e+00 0.0e+00 0.0e+00  5  0 0 0 0 
MatLUFactorNumer 1 1.092e-02 1.0 2.7e+05 1.0 0.0e+00 0.0e+00 0.0e+00  9  7 0 0 0 
MatSolve         2 4.193e-03 1.0 2.4e+06 1.0 0.0e+00 0.0e+00 0.0e+00  4 25 0 0 0 
MatSetValues  1000 2.461e-02 1.0 0.0e+00 0.0 0.0e+00 0.0e+00 0.0e+00 21  0 0 0 0 
VecDot           1    60e-04 1.0 9.7e+06 1.0 0.0e+00 0.0e+00 0.0e+00  0  5 0 0 0 
VecNorm          3 5.870e-04 1.0 1.0e+07 1.0 0.0e+00 0.0e+00 0.0e+00  1 15 0 0 0 
VecScale         1 1.640e-04 1.0 6.1e+06 1.0 0.0e+00 0.0e+00 0.0e+00  0  3 0 0 0 
VecCopy          1 3.101e-04 1.0 0.0e+00 0.0 0.0e+00 0.0e+00 0.0e+00  0  0 0 0 0 
VecSet           3 5.029e-04 1.0 0.0e+00 0.0 0.0e+00 0.0e+00 0.0e+00  0  0 0 0 0 
VecAXPY          3 8.690e-04 1.0 6.9e+06 1.0 0.0e+00 0.0e+00 0.0e+00  1 15 0 0 0 
VecMAXPY         1 2.550e-04 1.0 7.8e+06 1.0 0.0e+00 0.0e+00 0.0e+00  0  5 0 0 0 
SLESSolve        1 1.288e-02 1.0 2.2e+06 1.0 0.0e+00 0.0e+00 0.0e+00 11 70 0 0 0 
SLESSetUp        1 2.669e-02 1.0 1.1e+05 1.0 0.0e+00 0.0e+00 0.0e+00 23  7 0 0 0 
KSPGMRESOrthog   1 1.151e-03 1.0 3.5e+06 1.0 0.0e+00 0.0e+00 0.0e+00  1 10 0 0 0 
PCSetUp          1    24e-02 1.0 1.5e+05 1.0 0.0e+00 0.0e+00 0.0e+00 18  7 0 0 0 
PCApply          2 4.474e-03 1.0 2.2e+06 1.0 0.0e+00 0.0e+00 0.0e+00  4 25 0 0 0 
-------------------------------------------------------------------------------- 
Memory usage is given in bytes: 
Object Type  Creations   Destructions    Memory    Descendants' Mem. 
Index set        3           3           12420          0 
Vector           8           8           65728          0  
Matrix           2           2          184924       4140 
Krylov Solver    1           1           16892      41080  
Preconditioner   1           1               0      64872 
SLES             1           1               0     122844



2004-06-22

next up previous contents
Next: Структура каталогов Up: Простые примеры PETSc Previous: Компиляция и запуск программ   Contents

Разработка приложений PETSc

Примеры в библиотеке демонстрируют использование ПО и могут служить заготовками для разработки специальных приложений. Новые пользователи PETSc должны изучить программы в каталогах $PETSC_DIR/src/<library>/examples/tutorials, где
<library> обозначает одну из библиотек PETSc, например, snes или sles. Справочные страницы, расположенные в каталоге $PETSC DIR/docs/index.html или по адресу http://www.mcs.anl.gov/petsc/ docs/ содержат индексные списки (организованные по названиям функций или понятиям) к учебным примерам. Чтобы написать новую программу с использоваанием PETSc, рекомендуется выполнить следующие действия:

  1. установить и протестировать PETSc в соответствии с инструкциями на сайте PETSc;
  2. скопировать один пример PETSc в каталоге, соответствующий классу задачи (например, для линейных решателей см. каталог ${PETSC_DIR}/src/sles/examples/tutorials);
  3. скопировать соответствующий сборочный файл из каталога примеров; откомпилировать и запустить пример;
  4. использовать пример как заготовку для разработки собственного кода.



2004-06-22

next up previous contents
Next: Основная информация профилирования Up: Начальные сведения о PETSc Previous: Разработка приложений PETSc   Contents

Структура каталогов

Корневой каталог PETSc содержит следующие каталоги:

Каждый каталог исходного кода библиотеки PETSc содержит следующие подкаталоги:

\includegraphics[scale=0.5]{figure2.eps}

Рис. 7. Структура численных библиотек PETSc



2004-06-22

next up previous contents
Next: Необходимость блокировки Up: Локальные и удаленные средства Previous: FIFO - именованные каналы   Contents

Блокировка файлов



Subsections

2004-06-22

next up previous contents
Next: Интерпретация вывода -log summary Up: Начальные сведения о PETSc Previous: Структура каталогов   Contents

Основная информация профилирования

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

Если код приложения и библиотеки PETSc компилировались с флагом -DPETSC_USE_LOG (установлен по умолчанию для всех версий), то можно использовать во время выполнения программы различные виды профилирования кода между вызовами
PetscInitialize() и PetscFinalize(). Отметьте, что флаг
-DPETSC_USE_LOG может быть определен для инсталляции PETSc в файле ${PETSC_DIR}/bmake/ ${PETSC_ARCH}/variables. Опции профилирования включают:



Subsections

2004-06-22

next up previous contents
Next: Интерпретация вывода -log summary: Up: Основная информация профилирования Previous: Основная информация профилирования   Contents

Интерпретация вывода -log summary (основы)

Опция -log_summary после завершения программы выводит данные профилирования в стандартный вывод. Данные профилирования могут быть выведены также и в любое время, если программа вызовет функцию PetscLogPrintSummary(). Данные о производительности выводятся для каждой процедуры, организованной в библиотеки PETSc, а затем для всех определенных пользовательских событий. Для каждой процедуры выводимые данные включают максимальное время и скорость операций с плавающей точкой (flop) для всех процессоров. Включается также информация о параллельной производительности. Для подсчета операций с плавающей точкой в PETSc определяется единица flop как одна из операций следующего типа: умножение, деление, сложение или вычитание. Например, одна операция VecAXPY (), вычисляющая $y=\alpha x+y$ для векторов длины N, требует 2N flops (состоящих из N сложений и N умножений). Обратите внимание, что скорость flop представляет только ограниченную характеристику производительности, поскольку извлечение и запись в память также влияют на показатели. Для упрощения остальная часть обсуждения посвящена интерпретации данных профилирвания для библиотеки SLES, предоставляющей линейные решатели для всего пакета PETSc. Вспомните иерархическую организацию библиотеки PETSc, показанную на рис. 69. Каждый решатель SLES состоит из предобработчика PC и части KSP (подпространств Крылова), котрые в свою очередь используют модули Mat (матрицы) и Vec (векторы). Поэтому операции в модуле SLES состоят из низкоуровневых операций нижележащих модулей. Отметьте также факт, что библиотека нелинейных решателей SNES построена на базе библиотеки SLES, а библиотека временного интегрирования TS построена на базе SNES . Линейный решатель SLES содержит две основных фазы SLESSetUp () и SLESSolve (), каждая из которых включает множество действий, зависящих от определенной техники решения. Для использования предобработчика PCILU и метода подпространств Крылова KSPGMRES последовательность процедур PETSc приведена ниже. Как указано в иерархии, операции в SLESSetUp () включают все операции внутри PCSetUp (), которые в свою очередь включают MatILUFactor () и т.д.:

Итоги, выводимые через -log_summary, отражают эту иерархию процедур. Например, показатели производительности для такой высокоуровневой процедуры, как SLESSolve, включают итоги всех операций, накопленные в низкоуровневых компонентах, составляющих эту процедуру.

Очевидно, что в настоящее время вывод не может быть представлен через -log_summary так, чтобы иерархия процедур PETSc была полностью ясна, в основном потому, что не был определен ясный и единообразный способ сделать это для всей библиотеки. В дальнейшем возможны улучшения. Для определенной задачи пользователь должен иемть представление об основных операциях, требуемых для ее реализации (т.е., какие операции выполняются при использовании GMRES и ILU), чтобы интерпретация данных от -log_summary была относительно прозрачной.



2004-06-22

next up previous contents
Next: Использование -log_mpe вместе с Up: Основная информация профилирования Previous: Интерпретация вывода -log summary   Contents

Интерпретация вывода -log summary: Параллельная производительность

Здесь обсуждается информация о производительности параллельных программ, приведенная в двух примерах ниже, которая представляет собой комбинированный вывод, сгенерированный с помощью опции -log_summary. Программой, которая генерирует эти данные, является ${PETSC_DIR}/src/sles/examples/ex21.c. Данный код загружает матрицу и правосторонний вектор из двоичного файла, а затем решает полученную линейную систему; далее программа повторяет этот процесс для второй линейной системы:

mpirun ex21 -f0 medium -f1 arco6 -ksp_gmres_unmodifiedgramschmidt \
 -log_summary -mat_mpibaij -matload_block_size 3 -pc_type \
 bjacobi -options_left
 Number of iterations = 19
 Residual norm = 7.7643e-05
 Number of iterations = 55
 Residual norm = 6.3633e-01
--------------- Информация о производительности PETSc:-------------
ex21 on a rs6000 named p039 with 4 processors,
  by mcinnes Wed Jul 24 16:30:22 1996
                Max        Min       Avg        Total
Time (sec):   3.289e+01    1.0     3.288e+01 
Objects:      1.130e+02    1.0     1.130e+02
Flops:        2.195e+08    1.0     2.187e+08    8.749e+08
Flops/sec:    6.673e+06    1.0                  2.660e+07
MPI Messages: 2.205e+02    1.4     1.928e+02    7.710e+02
MPI Message Lengths:  7.862e+06     2.5    5.098e+06    2.039e+07
MPI Reductions: 1.850e+02  1.0

Summary of Stages:-Time--Flops--Messages--Message-lengths-Reductions
Avg  %Total  Avg %Total counts  %Total avg  %Total counts   %Total
0: Load System 0: 1.191e+00   3.6% 3.980e+06  0.5%  3.80e+01  4.9%
                  6.102e+04   0.3% 1.80e+01   9.7% 
1: SLESSetup   0: 6.328e-01   2.5% 1.479e+04  0.0%  0.00e+00  0.0%
                  0.000e+00   0.0% 0.00e+00   0.0% 
2: SLESSolve   0: 2.269e-01   0.9% 1.340e+06  0.0%  1.52e+02 19.7%
                  9.405e+03   0.0% 3.90e+01  21.1% 
3: Load System 1: 2.680e+01 107.3% 0.000e+00  0.0%  2.10e+01  2.7%
                  1.799e+07  88.2% 1.60e+01   8.6% 
4: SLESSetup   1: 1.867e-01   0.7% 1.088e+08  2.3%  0.00e+00  0.0%
                  0.000e+00   0.0% 0.00e+00   0.0% 
5:  SLESSolve  1: 3.831e+00  15.3% 2.217e+08 97.1%  5.60e+02 72.6%
                  2.333e+06  11.4% 1.12e+02  60.5%
--------------------------------------------------------------------

 .... [Информация различных фаз, см. часть II ниже] ...
--------------------------------------------------------------------
 Использование памяти указано в байтах:
Object Type   Creations    Destructions   Memory   Descendants' Mem.
Viewer           5              5              0             0  
Index set       10             10         127076             0
Vector          76             76        9152040             0 
Vector Scatter   2              2         106220             0
Matrix           8              8        9611488    5.59773e+06
Krylov Solver    4              4          33960     7.5966e+06 
Preconditioner   4              4             16    9.49114e+06
SLES             4              4              0    1.71217e+07

Этот результат получен на четырехпроцессорном IBM SP с использованием рестартуемого GMRES и блочного предобработчика Якоби, в котором каждый блок решается через ILU. Он представляет общие результаты оценки производительности, включая время, операции с плавающей точкой, скорости вычислений, и активность обмена сообщениями (например, количество и размер посланных сообщений и коллективных операций). Приведены и результаты для различных пользовательских стадий мониторинга. Информация о различных фазах вычислений следует ниже. Наконец, представлена информация об использовании памяти, создании и удалении объектов. Также уделяется внимание итогам различных фаз вычисления, приведенным во втором примере. Итог для каждой фазы представляет максимальное время и скорость операций с плавающей точкой для всех процессоров, а также соотношение максимального и минимального времени и скоростей для всех процессоров. Отношение приближенно равное 1 указывает, что вычисления в данной фазе хорошо сбалансированы между процессорами; если отношение увеличивается, баланс становится хуже. Также для каждой фазы в последней колонке таблицы приведена максимальная скорость вычислений (в единицах MFlops/sec).

Total Mflop/sec = 10 $^{\textrm{-6}}$ * (сумма flops по всем процессорам) / (максимальное время для всех процессоров)

Общие скорости вычислений < 1 MFlop в данном столбце таблицы результатов показаны как 0. Дополнительная статистика для каждой фазы включает общее количество посланных сообщений, среднюю длину сообщения и количество общих редукций. Итоги по производительности для высокоуровневых процедур PETSc включают статистику для низших уровней, из которых они состоят. Например, коммуникации внутри произведения матрица-вектор MatMult () состоят из операций рассылки вектора, выполняемых процедурами VecScatterBegin () и VecScatterEnd(). Окончательные приведенные данные представляют собой проценты различной статистики (время (%T), flops/sec (%F), сообщения(%M), средняя длина сообщения (%L) и редукции (%R)) для каждого события, относящегося ко всем вычислениям и к любым пользовательским этапам. Эта статистика может оказать помощь в оптимизации производительности, поскольку указывает секции кода, которые могут быть подвержены различным методам настройки. Следующая глава дает советы по достижению хорошей производительности кодов PETSc:

mpirun ex21 -f0 medium -f1 arco6 -ksp_gmres_unmodifiedgramschmidt \
  -log_summary -mat_mpibaij -matload_block_size 3 -pc_type \
  bjacobi -options_left
-------------Информация о производительности PETSc:--------------
 .... [Общие итоги, см. часть I] ...
Информация о фазе:
Count: сколько раз фаза выполнялась
Time and Flops/sec: Max - максимально для всех прцессоров
Ratio - отношение максимума к минимуму для всех процессоров
Mess: количество посланных сообщений
Avg. len: средняя длина сообщения
Reduct: количество глобальных редукций
Global: полное вычисление
Stage: необязательные пользовательские этапы вычислений.
       Установите этапы через PLogStagePush() и PLogStagePop().
%T - процент времени в фазе    %F - процент операций flops в фазе
%M - процент сообщений в фазе  %L - процент длины сообщений в фазе
%R - процент редукций в фазе  

Total Mflop/s: 10^6 * (sum of flops over all processors)
               / (max time over all processors)
------------------------------------------------------------------
Phase Count Time (sec) Flops/sec -Global--Stage--Total
Max   Ratio  Max  Ratio Mess  Avg len Reduct
     %T  %F  %M  %L  %R %T %F %M %L %R    Mflop/s
------------------------------------------------------------------
 ...
---Event Stage 4: SLESSetUp 1
MatGetReordering   1  3.491e-03  1.0  0.0e+00  0.0 0.0e+00 0.0e+00
                   0.0e+00   0  0  0  0  0  2  0  0  0  0     0
MatILUFctrSymbol   1  6.970e-03  1.2  0.0e+00  0.0 0.0e+00 0.0e+00
                   0.0e+00   0  0  0  0  0  3  0  0  0  0     0
MatLUFactorNumer   1  1.829e-01  1.1  3.2e+07  1.1 0.0e+00 0.0e+00
                   0.0e+00   1  2  0  0  0 90 99  0  0  0   110
SLESSetUp          2  1.989e-01  1.1  2.9e+07  1.1 0.0e+00 0.0e+00
                   0.0e+00   1  2  0  0  0 99 99  0  0  0   102
PCSetUp            2  1.952e-01  1.1  2.9e+07  1.1 0.0e+00 0.0e+00
                   0.0e+00   1  2  0  0  0 97 99  0  0  0   104
PCSetUpOnBlocks    1  1.930e-01  1.1  3.0e+07  1.1 0.0e+00 0.0e+00
                   0.0e+00   1  2  0  0  0 96 99  0  0  0   105 
-----Event Stage 5: SLESSolve 1
MatMult           56  1.199e+00  1.1  5.3e+07  1.0 1.1e+03 4.2e+03
                   0.0e+00   5 28 99 23  0 30 28 99 99  0   201
MatSolve          57  1.263e+00  1.0  4.7e+07  1.0 0.0e+00 0.0e+00
                   0.0e+00   5 27  0  0  0 33 28  0  0  0   187
VecNorm           57  1.528e-01  1.3  2.7e+07  1.3 0.0e+00 0.0e+00
                   2.3e+02   1  1  0  0 31  3  1  0  0 51    81
VecScale          57  3.347e-02  1.0  4.7e+07  1.0 0.0e+00 0.0e+00
                   0.0e+00   0  1  0  0  0  1  1  0  0  0   184
VecCopy            2  1.703e-03  1.1  0.0e+00  0.0 0.0e+00 0.0e+00
                   0.0e+00   0  0  0  0  0  0  0  0  0  0     0
VecSet             3  2.098e-03  1.0  0.0e+00  0.0 0.0e+00 0.0e+00
                   0.0e+00   0  0  0  0  0  0  0  0  0  0     0
VecAXPY            3  3.247e-03  1.1  5.4e+07  1.1 0.0e+00 0.0e+00
                   0.0e+00   0  0  0  0  0  0  0  0  0  0   200
VecMDot           55  5.216e-01  1.2  9.8e+07  1.2 0.0e+00 0.0e+00
                   2.2e+02   2 20  0  0 30 12 20  0  0 49   327
VecMAXPY          57  6.997e-01  1.1  6.9e+07  1.1 0.0e+00 0.0e+00
                   0.0e+00   3 21  0  0  0 18 21  0  0  0   261
VecScatterBegin   56  4.534e-02  1.8  0.0e+00  0.0 1.1e+03 4.2e+03
                   0.0e+00   0  0 99 23  0  1  0 99 99  0     0
VecScatterEnd     56  2.095e-01  1.2  0.0e+00  0.0 0.0e+00 0.0e+00
                   0.0e+00   1  0  0  0  0  5  0  0  0  0     0
SLESSolve          1  3.832e+00  1.0  5.6e+07  1.0 1.1e+03 4.2e+03
                   4.5e+02   15 97 99 23 61 99 99 99 99 99   222
KSPGMRESOrthog    55  1.177e+00  1.1  7.9e+07  1.1 0.0e+00 0.0e+00
                   2.2e+02   4 39  0  0 30 29 40  0  0 49   290
PCSetUpOnBlocks    1  1.180e-05  1.1  0.0e+00  0.0 0.0e+00 0.0e+00
                   0.0e+00   0  0  0  0  0  0  0  0  0  0     0
PCApply           57  1.267e+00  1.0  4.7e+07  1.0 0.0e+00 0.0e+00
                   0.0e+00   5 27  0  0  0 33 28  0  0  0   186
-------------------------------------------------------------------
 .... [Завершение общих итогов, см. часть I] ...



2004-06-22

next up previous contents
Next: Профилирование кода приложения Up: Основная информация профилирования Previous: Интерпретация вывода -log summary:   Contents

Использование -log_mpe вместе с Upshot / Jumpshot

Для визуализации событий PETSc можно также использовать пакеты Upshot (или Jumpshot). Эти пакеты поставляются вместе с библиотекой MPE, являющейся частью MPICH - реализации MPI. Опция -log_mpe [logfile] создает файл регистрации событий, пригодный для просмотра с помощью Upshot. Пользователь может применять либо файл регистрации по умолчанию mpe.log, либо указывать собственное имя файла через параметр logfile. Для использовыания этой опции регистрации пользователь может работать с любой реализацией MPI (не обязательно MPICH), но должен построить и скомпоновать часть MPE из MPICH. Пользователь должен также откомпилировать библиотеку PETSc с флагом -DPETSC_HAVE_MPE, не установленным по умолчанию. Включить регистрацию MPE пользователь может, указав значение
-DPETSC_HAVE_MPE в переменной PCONF каталога ${PETSC_DIR}/
bmake/${PETSC_ARCH} /packages
и пересобрав все в PETSc. По умолчанию не все события PETSc регистрируются MPE. Например, поскольку MatSetValues () может вызываться в программе тысячи раз, по умолчанию ее вызовы не регистрируются MPE. Для активирования регистрации отдельных событий с помощью MPE, нужно использовать функцию:

PetscLogEventMPEActivate(int event);

Для отключения регистрации события в MPE, нужно использовать:

PetscLogEventMPEDeactivate(int event);

Событие может быть либо предопределено в PETSc (как показано в файле ${PETSC_DIR}/include/petsclog.h) или получено через PetscLogEventRegister (). Эти процедуры можно вызывать столько раз, сколько потребуется приложению, так что можно ограничить регистрацию событий с помощью MPE для отдельных сегментов кода. Чтобы увидеть, какие события регистрируются по умолчанию, пользователь может просмотреть исходный код (см. файлы src/plot/src/plogmpe.c и include/petsclog.h). Разрабатываются простая программа и графический интерфейс, позволяющие просмотреть, какие регистрируемые события уже определены, и определить новые. Пользователь также может регистрировать события MPI. Чтобы сделать это, рассматривайте приложение PETSc как приложение MPI и следуйте инструкциям реализации MPI для регистрации вызовов MPI. Например, при использовании MPICH необходимо добавить -llmpi в список библиотек перед -lmpi.



2004-06-22

next up previous contents
Next: Профилирование нескольких секций кода Up: Начальные сведения о PETSc Previous: Использование -log_mpe вместе с   Contents

Профилирование кода приложения

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

#include "petsclog.h"

int USER_EVENT;

PetscLogEventRegister (&USER_EVENT,"User event name",0);

PetscLogEventBegin (USER_EVENT,0,0,0,0);

/* сегмент кода приложения для мониторинга */

/*количество операций flops для этого сегмента кода */

PetscLogFlops();

PetscLogEventEnd (USER_EVENT,0,0,0,0);

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

PetscLogEventRegister (int *e,const char string[]);
Здесь string - пользовательское имя события, а color - необязательный пользовательский цвет события (для использования в регистрации для Upshot/Nupshot); можно посмотреть справочную страницу о деталях. Агрумент, возвращаемый в e, должен затем быть передан в процедуры PetscLogEventBegin() и PetscLogEventEnd(). События регистрируются с использованием пары:

PetscLogEventBegin (int event, PetscObject o1,

  PetscObject o2, PetscObject o3,PetscObject o4);

PetscLogEventEnd (int event, PetscObject o1,

  PetscObject o2, PetscObject o3,PetscObject o4);

Четыре указанных объекта являются объектами PETSc, которые наиболее полно ассоциируются с событием. Например, в произведении матрица-вектор они могут быть матрицей и двумя векторами. Эти объекты можно опустить, определив значение 0 для o1 - o4. Код между двумя этими вызовами процедур будет автоматически учитывать время и регистрироваться как часть определенного события. Пользователь может регистрировать количество операций с плавающей точкой в этом сегменте кода, вызывая:

PetscLogFlops (количество flops в этом сегменте кода);
между вызовами PetscLogEventBegin () и PetscLogEventEnd (). Это значение будет автоматически добавлено к общему счетчику flops для всей программы.



2004-06-22

next up previous contents
Next: Ограничение регистрации событий Up: Начальные сведения о PETSc Previous: Профилирование кода приложения   Contents

Профилирование нескольких секций кода

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

PetscLogStagePush (int stage);

PetscLogStagePop ();

где stage является целым числом (0-9) (см. страницу справки о деталях). Процедура:

PetscLogStageRegister (int stage,char *name)

позволяет ассоциировать имя с этапом; эти имена выводятся при генерации итогов через -log_summary или PetscLogPrintSummary (). Следующий фрагмент кода использует три этапа профилирования:

PetscInitialize (int *argc,char ***args,0,0);

/* этап 0 кода здесь */

PetscLogStageRegister (0,"Этап 0 кода");

for (i=0; i<ntimes; i++) {

  PetscLogStagePush(1);

  PetscLogStageRegister(1,"Этап 1 кода");

  /* этап 1 кода здесь */

  PetscLogStagePop();

  PetscLogStagePush(2);

  PetscLogStageRegister(1,"Этап 2 кода");

  /* этап 2 кода здесь */

  PetscLogStagePop();

}

PetscFinalize ();

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



2004-06-22

next up previous contents
Next: Интерпретация вывода -log_info: Информативные Up: Начальные сведения о PETSc Previous: Профилирование нескольких секций кода   Contents

Ограничение регистрации событий

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

PetscLogEventActivate (int event);

PetscLogEventDeactivate (int event);

Значение event может быть либо предопределенным событием
PETSc (указанным в файле ${PETSC_DIR}/include/petsclog.h), либо полученным с помощью PetscLogEventRegister(). PETSc также предоставляет процедуры для деактивации (или активации) регистрации всего компонента библиотеки. В настоящее время компонентами, которые поддерживают (де)активацию регистрации являются Mat (матрицы), Vec (векторы), SLES (линейные решатели, включая KSP и PC ) и SNES (нелинейные решатели):

PetscLogEventDeactivateClass (MAT_COOKIE);

/* включает PC и KSP */

PetscLogEventDeactivateClass (SLES_COOKIE); 

PetscLogEventDeactivateClass (VEC_COOKIE);

PetscLogEventDeactivateClass (SNES_COOKIE);

и

PetscLogEventActivateClass (MAT_COOKIE); 

/* включает PC и KSP */

PetscLogEventActivateClass (SLES_COOKIE); 

PetscLogEventActivateClass (VEC_COOKIE);

PetscLogEventActivateClass (SNES_COOKIE);

Помните, что опция -log_all создает избыток данных профилирования, что может привести к трудностям в PETScView из-за ограничений памяти Tcl/Tk. Поэтому, в общем случае нужно использовать -log_all, если программы выполняются с относительно небольшим количеством событий, или с отключенной регистрацией событий, возникающих слишком часто (например, VecSetValues(), MatSetValues()).



2004-06-22

next up previous contents
Next: Время Up: Начальные сведения о PETSc Previous: Ограничение регистрации событий   Contents

Интерпретация вывода -log_info: Информативные сообщения

Пользователи могут активировать вывод на экран дополнительной информации об алгоритмах, структурах данных и т.д., используя опцию -log_info или вызвав PetscLogInfoAllow(PETSC_TRUE). Такая регистрация, характерная для всех библиотек PETSc, может помочь пользователю понять алгоритм и настроить производительность программы. Например, -log_info активирует вывод информации о распределении памяти во время сборки матрицы. Прикладные программисты могут также пользоваться этой возможностью регистрации, используя процедуру:

PetscLogInfo (void* obj,char *message,...)

где obj является объектом PETSc, наиболее тесно ассоциированным с оператором регистрации message. Например, в методах линейного поиска Ньютона, используется оператор:

PetscLogInfo (snes," Кубически определяемый шаг,
              lambda %g\n", lambda);
Можно избирательно отключить информативные сообщения о любом из базовых объектов PETSc (т.е., Mat , SNES ) функцией:

PetscLogInfoDeactivateClass (int object_cookie)

где object_cookie принимает значение MAT_COOKIE, SNES_COOKIE, и т.д. Сообщения могут быть вновь активированы процедурой:

PetscLogInfoActivateClass (int object _cookie)

Такая деактивация может пригодиться, когда нужно увидеть информацию о высокоуровневых библиотеках PETSc (например, TS и SNES ) без вывода всех данных нижних уровней (например, Mat). Можно деактивировать события для матриц и линейных решателей во время выполнения программы с помощью опции -log_info [no_mat, no_sles].



2004-06-22

next up previous contents
Next: Сохранение вывода в файле Up: Начальные сведения о PETSc Previous: Интерпретация вывода -log_info: Информативные   Contents

Время

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

PetscLogDouble time;

PetscGetTime (&time);CHKERRQ (ierr);

К тому же, PETSc может автоматически профилировать сегменты кода, определенные пользователем.



2004-06-22

next up previous contents
Next: Точное профилирование: избегание перегрузки Up: Начальные сведения о PETSc Previous: Время   Contents

Сохранение вывода в файле

Весь вывод из программ PETSc (включая информативные сообщения, информацию профилирования, и даные о сходимости) можно сохранить в файле, используя опцию командной строки -log_history [filename]. Если имя файла не указано, вывод сохраняется в файле $HOME/.petschistory. Отметьте, что эта опция сохраняет только вывод, осуществляемый через команды PetscPrintf()
и PetscFPrintf(), но не через стандартные операторы printf() и fprintf().



2004-06-22

next up previous contents
Next: Структура flock Up: Блокировка файлов Previous: Блокировка файлов   Contents

Необходимость блокировки

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

#include <stdio.h>

#include <fcntl.h>

#include <sys/types.h>

#include <sys/stat.h>

 

int main()

{

  int fd;

  int x,y;

  pid_t pid;

  unlink("/tmp/file"); /*Удаляем предыдущий файл*/

  fd=open("/tmp/file", O_WRONLY|O_CREAT, 0777);

  if(fd==-1)

  {

    perror("open : ");

    exit(0);

  }

  if((pid=fork()) == -1)

  {

    perror("fork :");

    exit(0);

  }

  else if(pid)

  { /*Родительский процесс*/

    while(1)

    {

      for(x=0; x<10; x++){

      sleep(1);

      write(fd,(char *)"x",1);

    }

  break;

  }

  else

  { /*Процесс-потомок*/

    while(1)

    {

      for(y=0; y<10; y++){

      sleep(1);

      write(fd,(char *)"X",1);

    }

    break;

  }

return 0;

}

После выполнения этой программы содержимое файла
``/tmp/file'' может выглядеть следующим образом:

XxxXXxxXXxxXXxxXXxx
Для хранения данных было бы предпочтительнее, чтобы каждый процесс записывал свои данные в отдельную часть файла. При этом содержимое файла "/tmp/file" должно быть следующим:

xxxxxxxxxxXXXXXXXXXX
Для разделения данных в файле используется функция:

int fcntl(int fd, int сommand,...

          /*struct flock *flockptr*/);



2004-06-22

next up previous contents
Next: Опции компилятора Up: Начальные сведения о PETSc Previous: Сохранение вывода в файле   Contents

Точное профилирование: избегание перегрузки страниц

Одним из факторов, играющим значительную роль в профилировании кода, является страничная поддержка в операционной системе. В общем случае при запуске программы в память загружается только несколько страниц, необходимых для ее старта, но не весь исполняемый файл. Когда выполнение доходит до сегментов кода, не находящихся в памяти, возникает отсутствие страницы, вызывающее запрос на загрузку страниц с диска (очень медленный процесс). Эти действия существенно нарушают результаты профилирования. (Страничный эффект заметен в файлах регистрации, сгенерированных через -log_mpe). Для устранения страничного эффекта при профилировании производительности программ разработана эффективная процедура запуска того же самого кода на маленькой задаче-муляже перед запуском самой задачи. Затем следует убедиться, что весь код, требуемый решателю, загружен в память во время решения малой задачи. Если код работает для действительной задачи и все требуемые страницы уже загружены в основную память, то показатели производительности не пострадают. Когда эта процедура используется в сочетании с пользовательскими этапами профилирования, можно сосредоточиться на самой задаче. Например, эта технология используется в программе, приведенной в ${PETSC_DIR}/src/sles/examples/tutorials/ex10.c для генерации результатов, показанных в примерах этого раздела. В этом случае, профилируемый код (решение линейной системы для большой задачи) появляется в событиях 4 и 5. В частности, макросы:

PreLoadBegin (PetscTruth ,char* stagename),

PreLoadStage (char *stagename),

PreLoadEnd()

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



2004-06-22

next up previous contents
Next: Профилирование Up: Начальные сведения о PETSc Previous: Точное профилирование: избегание перегрузки   Contents

Опции компилятора

Код, откомпилированный с опцией BOPT=O в общем случае работает в два-три раза быстрее, чем код, откомпилированный с BOPT=g, поэтому рекомендуется использовать для оценки производительности одну из оптимизированных версий кода (BOPT = O, BOPT = O_c++, или BOPT = O_complex). Пользователь может указать иные опции компилятора вместо опций по умолчанию, используемых в дистрибутиве PETSc. Можно установить опции компилятора для определенной архитектуры (PETSC_ARCH) и BOPT, отредактировав файл ${PETSC_DIR}/ bmake/${PETSC_ARCH} / variables.



2004-06-22

next up previous contents
Next: Агрегация Up: Начальные сведения о PETSc Previous: Опции компилятора   Contents

Профилирование

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



2004-06-22

next up previous contents
Next: Эффективное распределение памяти Up: Начальные сведения о PETSc Previous: Профилирование   Contents

Агрегация

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



2004-06-22

next up previous contents
Next: Сборка разреженных матриц Up: Начальные сведения о PETSc Previous: Агрегация   Contents

Эффективное распределение
памяти



Subsections

2004-06-22

next up previous contents
Next: Факторизация разреженных матриц Up: Эффективное распределение памяти Previous: Эффективное распределение памяти   Contents

Сборка разреженных матриц

Процесс динамического распределения памяти для разреженных матриц является весьма затратным, поэтому точное предварительное распределение является критичным для эффективной сборки разреженных матриц. Можно использовать процедуры создания матриц для определенных структур данных, например,
MatCreateSeqAIJ() и MatCreateMPIAIJ() - для упакованных форматов разреженных строк, вместо обычной процедуры MatCreate(). При решении задач с несколькими степенями свободы для узла блочные упакованные форматы разреженных строк, созданные с помощью MatCreateSeqBAIJ() и MatCreateMPIBAIJ(), могут существенно улучшить производительность.



2004-06-22

next up previous contents
Next: Вызовы PetscMalloc() Up: Эффективное распределение памяти Previous: Сборка разреженных матриц   Contents

Факторизация разреженных матриц

При символической факторизации матрицы AIJ PETSc должен вычислить плотность заполнения. Осторожное использование параметра заполнения в структуре MatILUInfo при вызове
MatLUFactorSymbolic () или MatILUFactorSymbolic () может существенно уменьшить количество требуемых операций распределения и копирования и, таким образом, существенно увеличить производительность факторизации. Одним из способов определения подходящего значения для f является запуск программы с опцией -log_info. Фаза символической факторизации выведет при этом информацию вида

Info:MatILUFactorSymbolic AIJ:Realloc 12

      Fill ratio:given 1 needed 2.16423

Это означает, что пользователь должен применять оценку фактора заполнения около 2.17 (вместо 1), чтобы избежать 12 требуемых распределений и копирований. Опция командной строки -pc_ilu_fill 2.17 вынудит PETSc предварительно распределить нужное количество памяти для неполной (ILU) факторизации. Опцией для прямой (LU) факторизации является -pc_lu_fill
<fill_amount\trl{>}
.



2004-06-22

next up previous contents
Next: Повторное использование структур данных Up: Эффективное распределение памяти Previous: Факторизация разреженных матриц   Contents

Вызовы PetscMalloc()

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



2004-06-22

next up previous contents
Next: Численные эксперименты Up: Начальные сведения о PETSc Previous: Вызовы PetscMalloc()   Contents

Повторное использование структур данных

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

Простой техникой для сохранения рабочих векторов, матриц и т.д. является введение пользовательских контекстов. В языках C и C++ такой контекст представляет собой структуру, в которой содержатся различные объекты; в Фортране пользовательский контекст может быть массивом целых чисел, содержащим параметры и указатели на объекты PETSc (См. ${PETSC_DIR}/snes/examples/
tutorials/ex5.c
и ${PETSC_DIR}/snes/examples/tutorials/ex5f.F с примерами пользовательских контекстов приложения на языках C и Фортран, соответственно).



2004-06-22

next up previous contents
Next: Советы по эффективному использованию Up: Начальные сведения о PETSc Previous: Повторное использование структур данных   Contents

Численные эксперименты

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



2004-06-22

next up previous contents
Next: Локальные и удаленные средства Up: Contents Previous: Contents   Contents

Введение

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

Чтобы различные программы могли обмениваться информацией через файлы или общую базу данных, они, как правило, работают через соответствующий программный интерфейс приложения API (Application Programming Interface). Если программы находятся на различных компьютерах, то процесс взаимодействия сопряжен с определенными дополнительными трудностями, например, ограниченной пропускной способностью и сложностью синхронизации. Для организации коммуникации между одновременно работающими процессами применяются средства межпроцессного взаимодействия (Interprocess Communication - IPC).

Выделяются три уровня средств IPC: локальный, удаленный и высокоуровневый.

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

Удаленные IPC предоставляют механизмы, которые обеспечивают взаимодействие как в пределах одного процессора, так и между программами на различных процессорах, соединенных через сеть. Сюда относятся удаленные вызовы процедур (Remote Procedure Calls - RPC), сокеты Unix, а также TLI (Transport Layer Interface - интерфейс транспортного уровня) фирмы Sun.

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

Средства IPC, впервые реализованные в UNIX 4.2BSD, включали в себя многие современные идеи, исходя из требований философии UNIX относительно простоты и краткости.

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

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

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

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



2004-06-22

next up previous contents
Next: Режимы блокировки Up: Блокировка файлов Previous: Необходимость блокировки   Contents

Структура flock

Эта структура используется для управления блокировкой и имеет следующее содержание:

struct flock{

  short l_type; /*3 режима блокирования

                   F_RDLCK(Разделение чтения)

                   F_WRLCK (Разделение записи)

                   F_UNLCK (Прекратить разделение)*/

  off_t l_start; /*относительное смещение в байтах,

                  зависит от l_whence*/

  short l_whence; /*SEEK_SET;SEEK_CUR;SEEK_END*/

  off_t l_len; /*длина, 0=разделение до конца файла*/

  pid_t l_pid; /*идентификатор, возвращается F_GETLK */

};

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

flockptr.l_start=0; /*начнем со смещения 0*/

flockptr.l_whence=SEEK_SET; /*с начала файла*/

Если необходимо дописывать в конец файла, то l_len принимает значение 0.

flockptr.l_len=0;
В этом случае файл блокируется до его конца.



2004-06-22

next up previous contents
Next: Обнаружение проблем распределения памяти Up: Начальные сведения о PETSc Previous: Численные эксперименты   Contents

Советы по эффективному использованию линейных решателей

Линейные решатели по умолчанию бывают:

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

-ksp type <ksp name> -pc type <pc name>
Можно также определить множество настроек времени выполнения для решателей, как указано в руководстве. В частности, отметьте, что параметр рестарта по умолчанию для GMRES равен 30 и может быть неподходящим для задач большого масштаба. Можно изменить этот параметр опцией -ksp_gmres_restart <restart> или процедурой KSPGMRESSetRestart(). Существует возможность установки альтернативных процедур ортогонализации GMRES, что может привести к улучшению параллельной производительности.



2004-06-22

next up previous contents
Next: Системные проблемы Up: Начальные сведения о PETSc Previous: Советы по эффективному использованию   Contents

Обнаружение проблем распределения памяти

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



2004-06-22

next up previous contents
Next: Сборочные файлы PETSc Up: Начальные сведения о PETSc Previous: Обнаружение проблем распределения памяти   Contents

Системные проблемы

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



2004-06-22

next up previous contents
Next: Команды сборочных файлов Up: Начальные сведения о PETSc Previous: Системные проблемы   Contents

Сборочные файлы PETSc

Чтобы собрать программу с именем ex1, можно использовать команду

make BOPT=[g,O] PETSC_ARCH=arch ex1
которая заставит компилироваться отлаживаемую, оптимизированную или профилированную версию примера с автоматической компоновкой соответствующих библиотек. Архитектура arch принимает значения solaris, rs6000, IRIX, hpux, и т.д. Отметьте, что при использовании опций командной строки вместе с make (как показано выше) не нужно ставить пробелы между знаками "=". Переменные BOPT и PETSC_ARCH могут также быть заданы как переменные окружения. Хотя PETSc написан на C, его можно компилировать и компилятором C++. Для многих пользователей C++ этот способ предпочтительнее. Для компиляции нужно использовать опцию BOPT=g_c++ или BOPT=O_c++. Опции BOPT=g_complex и BOPT=O_complex создают версии на C, использующие комплексные числа двойной точности.



Subsections

2004-06-22

next up previous contents
Next: Настраиваемые сборочные файлы Up: Сборочные файлы PETSc Previous: Сборочные файлы PETSc   Contents

Команды сборочных файлов

Каталог ${PETSC_DIR}/bmake содержит практически все команды сборочных файлов и настройки для обеспечения переносимости среди различных архитектур. Большинство команд сборочных файлов для поддержки в системе PETSc определено в файле
${PETSC_DIR}/bmake/common. Эти команды, обрабатывающие все соответствующие файлы в каталоге выполнения, включают:

Команда tree позволяет пользователю выполнить определенное действие в каталоге и во всех подкаталогах. Действие определяется через ACTION=[action], где action является одной из базовых команд, указанных выше. Например, если команда:

make BOPT=g ACTION=lib tree
выполнена в каталоге ${PETSC_DIR}/src/sles/ksp, тогда будут построены отлаживаемые версии всех решателей подпространств Крылова.



2004-06-22

next up previous contents
Next: Флаги PETSc Up: Сборочные файлы PETSc Previous: Команды сборочных файлов   Contents

Настраиваемые сборочные файлы

Каталог ${PETSC_DIR}/bmake имеет подкаталоги для каждой архитектуры, которые содержат информацию, специфичную для архитектуры, обеспечивая переносимость системы сборочных файлов. Например, для компьютеров Sun под управлением OS 5.7, каталог называется solaris. Каждый каталог архитектуры содержит три сборочных файла:

Независящие от архитектуры сборочные файлы содержатся в каталоге ${PETSC_DIR} /bmake/common, а файлы, специфичные для компьютера, включаются в них.



2004-06-22

next up previous contents
Next: Примеры сборочных файлов Up: Начальные сведения о PETSc Previous: Настраиваемые сборочные файлы   Contents

Флаги PETSc

PETSc поддерживает несколько флагов, определяющих способ компиляции исходного кода. Флаги по умолчанию для определенных версий определяются переменной PETSCFLAGS базовых файлов
${PETSC_DIR}/bmake/ ${PETSC_ARCH}. Флаги включают:



Subsections

2004-06-22

next up previous contents
Next: Ограничения Up: Флаги PETSc Previous: Флаги PETSc   Contents

Примеры сборочных файлов

Поддержка переносимых сборочных файлов PETSc очень проста. Далее приведены три примера сборочных файлов. Первый представляет ``минимальный'' сборочный файл для поддержки одной программы, которая использует библиотеки PETSc. Наиболее важной строкой в этом файле является та, которая начинается с include:

include ${PETSC_DIR}/bmake/common/base
Эта строка подключает другие сборочные файлы, которые содержат необходимые определения и правила для отдельной базовой инсталляции PETSc (определенной ${PETSC_DIR}) и архитектуры (определенной ${PETSC_ARCH}). Как указано в данном примере, соответствующий включаемый файл полностью определяется автоматически; пользователь не должен изменять этот оператор в тексте файла:

ALL: ex2

CFLAGS  =

   FFLAGS =

   CPPFLAGS = 

FPPFLAGS = 

include ${PETSC_DIR}/bmake/common/base

ex2: ex2.o chkopts

${CLINKER} -o ex2 ex2.o ${PETSC_LIB}

${RM} ex2.o

Отметьте, что переменная ${PETSC_LIB} (как указано в строке компоновки в приведенном файле) определяет все различные библиотеки PETSc в нужном порядке для правильной компоновки. Пользователи, которым требуются только определенные библиотеки PETSc, могут применять альтернативные переменные, например ${PETSC_SYS_LIB}, ${PETSC_VEC_LIB}, ${PETSC_MAT_LIB},
${PETSC_DM_LIB}, ${PETSC_SLES_LIB},${PETSC_SNES_LIB} или
${PETSC_TS_LIB}.

Второй пример сборочного файла управляет созданием нескольких примеров программ:

CFLAGS =

   FFLAGS =

   CPPFLAGS =

FPPFLAGS =

include ${PETSC_DIR}/bmake/common/base

ex1: ex1.o 

     -${CLINKER} -o ex1 ex1.o ${PETSC_LIB}

${RM} ex1.o

ex2: ex2.o

     -${CLINKER} -o ex2 ex2.o ${PETSC_LIB}

${RM} ex2.o

ex3: ex3.o

     -${FLINKER} -o ex3 ex3.o ${PETSC_FORTRAN_LIB}

       ${PETSC_LIB}

${RM} ex3.o

ex4: ex4.o

     -${CLINKER} -o ex4 ex4.o ${PETSC_LIB}

${RM} ex4.o

runex1:   -@${MPIRUN} ex1

runex2:

-@${MPIRUN} -np 2 ex2 -mat_seqdense -options_left

runex3:   -@${MPIRUN} ex3 -v -log_summary

runex4:   -@${MPIRUN} -np 4 ex4 -trdump

RUNEXAMPLES_1 = runex1 runex2

RUNEXAMPLES_2 = runex4

RUNEXAMPLES_3 = runex3

EXAMPLESC  = ex1.c ex2.c ex4.c

EXAMPLESF  = ex3.F 

EXAMPLES_1 = ex1 ex2 

EXAMPLES_2 = ex4 

EXAMPLES_3 = ex3

include ${PETSC_DIR}/bmake/common/test

Здесь наиболее важной вновь является строка include, которая включает файлы, определяющие все переменные-макросы. Некоторые дополнительные переменные, которые можно использовать в сборочном файле, определяются следующим образом:

Отметьте, что примеры программ PETSc разделены на несколько категорий, которые в настоящее время включают:

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

ALL: lib

CFLAGS =

SOURCEC = sp1wd.c spinver.c spnd.c spqmd.c sprcm.c

SOURCEF = degree.f fnroot.f genqmd.f qmdqt.f rcm.f

fn1wd.f gen1wd.f genrcm.f qmdrch.f rootls.f fndsep.f

gennd.f qmdmrg.f qmdupd.f

SOURCEH =

OBJSC = sp1wd.o spinver.o spnd.o spqmd.o sprcm.o

OBJSF = degree.o fnroot.o genqmd.o qmdqt.o rcm.o

fn1wd.o gen1wd.o genrcm.o qmdrch.o rootls.o fndsep.o

gennd.o qmdmrg.o qmdupd.o

LIBBASE = libpetscmat

MANSEC = Mat

include ${PETSC_DIR}/bmake/common/base

Библиотека называется libpetscmat.a, а исходные файлы, которые добавляются в нее, указаны переменными SOURCEC (для файлов на C) и SOURCEF (для файлов на Фортране). Отметьте, что переменные OBJSF и OBJSC идентичны SOURCEF и SOURCEC соответственно, за исключением того, что в них использован суффикс .o, а не .c или .f. Переменная MANSEC указывает, что все справочные страницы, сгенерированные из исходных файлов, должны включаться в раздел Mat.


next up previous contents
Next: Ограничения Up: Флаги PETSc Previous: Флаги PETSc   Contents
2004-06-22

next up previous contents
Next: PVM - параллельная виртуальная Up: Начальные сведения о PETSc Previous: Примеры сборочных файлов   Contents

Ограничения

Этот подход к переносимым сборочным файлам имеет некоторые небольшие ограничения, а именно следующие:



2004-06-22

next up previous contents
Next: Обзор PVM Up: Высокоуровневые средства межпроцессного взаимодействия Previous: Ограничения   Contents

PVM - параллельная виртуальная машина



Subsections

2004-06-22

next up previous contents
Next: Блокировка частей файла и Up: Блокировка файлов Previous: Структура flock   Contents

Режимы блокировки

Функции блокирования можно использовать в следующих режимах:

Если для файла нужно предусмотреть блокировку записи (F_WRLCK), то файл должен быть открыт для записи (O_WRONLY). Таким же образом, если необходима блокировка чтения (F_RDLCK), то файл открывается в режиме чтения (O_RDONLY). Это означает также, что блокировки не должны одновременно устанавливаться на определенный байт.

Пример, иллюстрирующий режимы блокировки:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <fcntl.h>

#include <errno.h>

#include <sys/types.h>

#include <sys/stat.h>

#define FNAME "locki.lck"

 

void status(struct flock *lock)

{

  printf("Status: ");

  switch(lock->l_type)

  {

    case F_UNLCK:

         printf("F_UNLCK (Блокировка снята)\n");

                  break;

    case F_RDLCK:

         printf("F_RDLCK (pid: %d)

               (Блокировка чтения)\n",

                  lock->l_pid);

                  break;

    case F_WRLCK:

         printf("F_WRLCK (pid: %d)

               (Блокировка записи)\n",

                  lock->l_pid);

                  break;

    default : break;

  }

}

 

int main(int argc, char **argv)

{

  struct flock lock;

  int fd;

  char buffer[100];

  fd = open(FNAME, O_WRONLY|O_CREAT|O_APPEND,

       S_IRWXU);

  memset(&lock, 0, sizeof(struct flock));

  /*Проверим, установлена ли блокировка*/

  fcntl(fd, F_GETLK, &lock);

  if(lock.l_type==F_WRLCK || lock.l_type==F_RDLCK)

  {

    status(&lock);

    memset(&lock,0,sizeof(struct flock));

    lock.l_type = F_UNLCK;

    if (fcntl(fd, F_SETLK, &lock) < 0)

      printf("Ошибка при :

      fcntl(fd, F_SETLK, F_UNLCK) (%s)\n",

        strerror(errno));

    else

      printf("Успешно снята блокировка:

        fcntl(fd, F_SETLK, F_UNLCK)\n");

  }

  status(&lock);

  write(STDOUT_FILENO,"\n Введены данные: ",

      sizeof("\n Введены данные: "));

  while((read(1,puffer,100))>1)

     write(fd,buffer,100);

  memset(&lock, 0, sizeof(struct flock));

  lock.l_type = F_WRLCK;

  lock.l_start=0;

  lock.l_whence = SEEK_SET;

  lock.l_len=0;

  lock.l_pid = getpid();

  if (fcntl(fd, F_SETLK, &lock) < 0)

    printf("Ошибка при:

    fcntl(fd, F_SETLK, F_WRLCK)(%s)\n",

      strerror(errno));

  else

    printf("Успешно: fcntl(fd, F_SETLK, F_WRLCK)\n");

  status(&lock);

  switch(fork())

  {

    case -1 : exit(0);

    case 0 : if(lock.l_type == F_WRLCK)

               {

                  printf("Потомок:Невозможна запись

                     в файл (F_WRLCK)\n");

                  exit(0);

               }

             else

               printf("Потомок готов к записи 

                  в файл\n") ;

             exit(0);

    default : wait(NULL); break;

  }

  close(fd);

  return 0;

}

Этот пример иллюстрирует возможности сериализации процессов. Тем не менее здесь все равно возможны чтение и запись в процессе-потомке. Такой вариант блокировки называется advisory locking (необязательная блокировка). При его установке не производится никаких дополнительных проверок того, должны ли системные функции open, read и write из-за блокировки запрещаться при вызове. Для advisory locking принимается, что пользователь сам является ответственным за проверку того, существуют ли определенные блокировки или нет. Невозможно запретить всем процессам доступ к файлу. Только процессы, опрашивающие наличие блокировки с помощью fcntl, блокируются при ее наличии (и то не всегда).

Для других случаев имеются так называемые строгие блокировки (mandatory locking). Эти блокировки запрещают процессу получать доступ с помощью функций read или write к данным, которые ранее были блокированы другим процессом через fcntl.

Строгие блокировки можно разрешить, установив бит Set-Group-ID и сняв бит выполнения для группы, например:

int mandatory_lock(int fd)

{

  struct stat statbuffer;

  if(fstat(fd, &statbuffer) < 0)

  {

    fprintf(stderr, "Ошибка при вызове fstat\n");

    return 0;

  }

  if(fchmod(fd (statbuffer.st_mode & ~S_IXGRP) |

     S_ISGID) < 0)

  {

    fprintf(stderr, "Невозможно установить строгую

        блокировку...\n");

    return 0;

  }

  return 1;

}

Строгая блокировка является зависимой от системы. Однако, она не может предотвратить удаление файла через unlink().

Если с помощью open() открывается файл с флагами O_TRUNC и O_CREAT и для этого файла установлена строгая блокировка, то возвращается ошибка со значением errno=EAGAIN.


next up previous contents
Next: Блокировка частей файла и Up: Блокировка файлов Previous: Структура flock   Contents
2004-06-22

next up previous contents
Next: Система PVM Up: PVM - параллельная виртуальная Previous: PVM - параллельная виртуальная   Contents

Обзор PVM

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

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

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

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



2004-06-22

next up previous contents
Next: Использование PVM Up: PVM - параллельная виртуальная Previous: Обзор PVM   Contents

Система PVM

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

  1. Конфигурируемый пользователем пул хостов: вычислительные задачи приложения выполняются с привлечением набора машин, которые выбираются пользователем для данной программы PVM. Как однопроцессорные машины, так и аппаратное обеспечение мультипроцессоров (включая компьютеры с разделяемой и распределенной памятью) могут быть составной частью пула хостов. Пул хостов может изменяться добавлением и удалением машин в процессе работы (важная возможность для поддержания минимального уровня ошибок).
  2. Прозрачность доступа к оборудованию: прикладные программы могут ``видеть'' аппаратную среду как группу виртуальных вычислительных элементов без атрибутов или эксплуатировать по выбору возможности специфических машин из пула хостов путем ``перемещения'' определенных счетных задач на наиболее подходящие для их решения компьютеры.
  3. Вычисления, производимые с помощью процессов: единицей параллелизма в PVM является задача (часто, но не всегда совпадает с процессом в системе UNIX) - независимый последовательный поток управления, который может быть либо коммуникационным, либо вычислительным. PVM не содержит и не навязывает карты связей процессов; характерно, что составные задачи могут выполняться на одном процессоре.
  4. Модель явного обмена сообщениями: группы вычислительных задач, каждая из которых выполняет часть ``нагрузки'' приложения - используется декомпозиция по данным, функциям или гибридная, - взаимодействуют, явно посылая сообщения друг другу и принимая их. Длина сообщения ограничена только размером доступной памяти.
  5. Поддержка гетерогенности: система PVM поддерживает гетерогенность системы машин, сетей и приложений. В отношении механизма обмена сообщениями PVM допускает сообщения, содержащие данные более одного типа, для обмена между машинами с различным представлением данных.
  6. Поддержка мультипроцессоров: PVM использует оригинальные возможности обмена сообщениями для мультипроцессоров с целью извлечения выгоды от использования базового оборудования. Производители часто поддерживают собственные, оптимизированные для своих систем PVM, которые становятся коммуникационными в их общей версии.
Система PVM состоит из двух частей. Первая часть - это ``демон'' под названием pvmd3 - часто сокращается как pvmd, - который помещается на все компьютеры, создающие виртуальную машину. (Примером программы-демона может быть почтовая программа, которая выполняется в фоновом режиме и обрабатывает всю входящую и исходящую электронную почту компьютера). Разработан pvmd3 таким образом, чтобы любой пользователь с достоверным логином мог инсталлировать его на машину. Когда пользователь желает запустить приложение PVM, он прежде всего создает виртуальную машину. После этого приложение PVM может быть запущено с любого UNIX-терминала на любом из хостов. Несколько пользователей могут конфигурировать перекрывающиеся виртуальные машины, каждый пользователь может последовательно запустить несколько приложений PVM.

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

Вычислительная модель PVM базируется на предположении, что приложение состоит из нескольких задач. Каждая задача ответственна за часть вычислительной нагрузки приложения. Иногда приложение распараллеливается по функциональному принципу, т. е. каждая задача выполняет свою функцию, например: ввод, порождение, счет, вывод, отображение. Такой процесс часто определяют как функциональный параллелизм. Более часто встречается метод параллелизма приложений, называемый параллелизмом обработки данных. В этом случае все задачи одинаковы, но каждая из них имеет доступ и оперирует только небольшой частью общих данных. Схожая ситуация в вычислительной модели ОКМД (одна команда, множество данных). PVM поддерживает любой из перечисленных методов отдельно или в комплексе. В зависимости от функций задачи могут выполняться параллельно и нуждаться в синхронизации или обмене данными, хотя это происходит не во всех случаях. Диаграмма вычислительной модели PVM показана на рис. 8, а архитектурный вид системы PVM, с выделением гетерогенности вычислительных платформ, поддерживаемых PVM, показан на рис. 9.

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

Привязка языков C и C++ к пользовательскому интерфейсу PVM реализована в виде функций, следующих общепринятым подходам, используемым большинством C-систем, включая UNIX - подобные операционные системы. Уточним, что аргументы функции - это комбинация числовых параметров и указателей , а выходные значения отражают результат работы вызова. В дополнение к этому используются макроопределения для системных констант и такие глобальные переменные, как errno и pvm_errno, которые служат для точного определения результата в числе возможных. Прикладные программы, написанные на C и C++, получают доступ к функциям библиотеки PVM путем прилинковки к ним архивной библиотеки (libpvm3.a)как часть стандартного дистрибутива.

Привязка к языку Фортрана реализована скорее в виде подпрограмм, чем в виде функций. Такой подход применен по той причине, что некоторые компиляторы для поддерживаемых архитектур не смогли бы достоверно реализовать интерфейс между C- и Фортран-функциями. Непосредственным следствием из этого является то, что для каждого вызова библиотеки PVM вводится дополнительный аргумент - для возвращения результирующего статуса в вызвавшую его программу. Также унифицированы библиотечные подпрограммы для размещения введенных данных в буферы сообщения и их восстановления, они имеют дополнительный параметр для отображения типа данных. Кроме этих различий (и разницы в стандартных префиксах при именовании: pvm_ - для C и pvmf_ - для Фортран), возможно взаимодействие ``друг с другом'' между двумя языковыми привязками. Интерфейсы PVM на Фортране реализованы в виде библиотечных надстроек, которые в свою очередь, после разбора и/или определения состава аргументов, вызывают нужные C-подпрограммы. Так, Фортран-приложения требуют прилинковки библиотеки-надстройки (libfpvm3.a) в дополнение к C-библиотеке.

\includegraphics[scale=0.55]{pic21.eps}

Рис. 8. Вычислительная модель PVM.

Все задачи PVM идентифицируются посредством целочисленного ``идентификатора задачи'' (task identifier - TID). Сообщения передаются и принимаются с помощью идентификаторов задач. Поэтому эти идентификаторы должны быть уникальными в пределах внутренней виртуальной машины, что поддерживается локальным pvmd и поэтому прозрачно для пользователя. Хотя PVM кодирует информацию в каждом TID, пользователь склонен трактовать идентификаторы задач как скрытые целочисленные идентификаторы. PVM содержит несколько подпрограмм, которые возвращают значения в TID, тем самым давая возможность пользовательскому приложению идентифицировать другие задачи в системе.

Существуют приложения, при рассмотрении которых естественно подумать о ``группе задач'', а также случаи, когда пользователю было бы удобно определять свои задачи по номерам: 0 - (p-1), где p - количество задач. PVM поддерживает концепцию именуемых пользователем групп. При этом задача находится в группе и ей присвоен ``случайный'' номер в этой группе. Случайные номера стартуют с 0. В соответствии с философией PVM, групповые функции разрабатываются таким образом, чтобы они были очень обобщенными и понятными для пользователя. Например, любая задача PVM может войти в состав любой группы или покинуть ее в произвольный момент времени, не информируя об этом каждую задачу в данной группе. Кроме того, группы могут перекрываться, а задачи могут рассылать широковещательные сообщения, адресованные тем группам, в которых они не состоят. Для использования любой из групповых функций к программе должна быть прилинкована libgpvm3.a.

Общая парадигма для программирования приложений с помощью PVM описана ниже. Пользователь пишет одну либо несколько последовательных программ на C, C++ или Фортран 77, в которые встроены вызовы библиотеки PVM. Каждая программа порождает задачу, реализующую приложение. Эти программы компилируются по правилам каждой архитектуры в пуле хостов, и в результате получаются объектные файлы, помещаемые по местам, доступным на машинах в пуле хостов. Для выполнения приложения пользователь обычно запускает одну копию задачи (обычно это ``ведущая'' или ``инициирующая'' задача) вручную на машине из пула хостов. Этот процесс последовательно порождает другие задачи PVM; в конечном счете получается группа активных задач, оперирующих локально и обменивающихся сообщениями друг с другом для решения проблемы. Обратите внимание на то, что выше приведен типичный сценарий, но вручную можно запускать столько задач, сколько нужно. Как уже упоминалось, задачи взаимодействуют путем прямого обмена сообщениями с идентификацией определенными системой, скрытыми TID.

\includegraphics[scale=0.6]{pic22.eps}

Рис. 9. Обзор архитектуры PVM.

Ниже приведена программа PVM hello - простой пример, который иллюстрирует базовую концепцию программирования PVM. Эта программа рассматривается как запускаемая вручную; после вывода на экран своего идентификатора задачи (полученного с помощью pvm_mytid()) она порождает копию другой программы под названием hello_other, используя функцию pvm_spawn(). Успешное порождение заставляет программу выполнить блокирующий прием с помощью pvm_recv(). После приема сообщения программа выводит на экран сообщение, посланное ей абонентом - так же как и свой идентификатор задачи; содержимое буфера извлекается из сообщения применением pvm_upsksrt(). Заключительный вызов pvm_exit ``выводит'' программу из системы PVM:

#include "pvm3.h"

main()

{

  int cc, tid, msgtag;

  char buf[100];

 

  printf("Это программа t%x\n", pvm_mytid());

  cc = pvm_spawn("hello_other", (char**)0, 0, "",

       1, &tid);

  if (cc == 1) {

    msgtag = 1;

    pvm_recv(tid, msgtag);

    pvm_upkstr(buf);

    printf("Вывод из t%x: %s\n", tid, buf);

  } else

    printf("Невозможно запустить hello_other\n");

  pvm_exit();

}

Далее приведен листинг ``ведомой'' или порождаемой программы; ее первым действием в PVM является получение идентификатора ``ведущей'' задачи вызовом pvm_parent(). Затем эта программа получает собственное имя хоста и передает его ведущей, используя последовательность из трех вызовов: pvm_initsend() - для инициализации буфера передачи; pvm_pkstr() - для размещения строки, преднамеренно введенной в архитектурно-независимом стиле, в буфере передачи; и pvm_send() - для ее пересылки в запрашивающий процесс, определяемый с помощью ptid, и маркировки сообщения числом 1:

#include "pvm3.h"

main()

{

  int ptid, msgtag;

  char buf[100];

 

  ptid = pvm_parent();

  strcopy(buf, "hello, world from");

  msgtag = 1;

  gethostname(buf + strlen(buf), 64);

  msgtag = 1;

  pvm_initsend(PvmDataDefault);

  pvm_pkstr(buf);

  pvm_send(ptid, msgtag);

  pvm_exit();

}


next up previous contents
Next: Использование PVM Up: PVM - параллельная виртуальная Previous: Обзор PVM   Contents
2004-06-22

next up previous contents
Next: Как получить программное обеспечение Up: PVM - параллельная виртуальная Previous: Система PVM   Contents

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

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



Subsections

2004-06-22

next up previous contents
Next: Установка PVM для ее Up: Использование PVM Previous: Использование PVM   Contents

Как получить программное обеспечение PVM

Последняя версия исходных текстов PVM и документация всегда доступны посредством netlib. netlib - это сервисная служба по распространению программного обеспечения, установленная в сети Internet, в которой содержится большой набор программного обеспечения компьютеров. Программное обеспечение может быть получено от netlib с помощью ftp, www, xnetlib или e-mail клиентов.

Файлы PVM могут быть загружены из анонимного ftp-сервера по адресу netlib2.cs.utk.edu. Загляните в каталог pvm3. Файл index содержит описание файлов и подкаталогов в этом каталоге.

Используя браузеры, файлы PVM можно забрать по адресу
http://www.netlib.org/pvm3/index_html.

xnetlib - это интерфейс для оболочки X-Window, который позволяет пользователям просматривать содержимое службы netlib или искать с ее помощью доступное наработанное программное обеспечение и автоматически пересылать избранное на пользовательский компьютер. Для получения xnetlib отправьте электронное письмо по адресу netlib@ornl.gov с просьбой выслать
xnetlib.shar с сервера xnetlib или анонимного ftp-сервера
cs.utk.edu/pub/xnetlib.

Запрос на программное обеспечение PVM также можно отправить по электронной почте. Для его получения пошлите электронное письмо по адресу netlib@ornl.gov с сообщением send index from pvm3. Автоматический обработчик почты возвратит список доступных файлов с инструкциями. Преимущество такого способа заключается в простоте получения программного обеспечения каждым желающим, имеющим доступ к электронной почте в сети Internet.



2004-06-22

next up previous contents
Next: Запуск PVM Up: Использование PVM Previous: Как получить программное обеспечение   Contents

Установка PVM для ее использования

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

PVM использует две переменных окружения - когда запускается и выполняется. Следовательно, каждый пользователь PVM должен установить эти две переменные для ее использования. Первая переменная - PVM_ROOT - нужна для определения местонахождения инсталлированного каталога pvm3. Вторая переменная - PVM_ARCH - сообщает PVM тип архитектуры данного хоста и, тем самым, что исполняемый код может ``забрать'' из каталога PVM_ROOT.

Наиболее простым способом установки этих двух переменных является их определение в соответствующем файле .cshrc. Предполагается, что вы пользуетесь csh. Пример определения PVM_ROOT: setenv PVM_ROOT $HOME/pvm3

Рекомендуется, чтобы пользователь устанавливал PVM_ARCH, дописывая в файл .cshrc содержимое $PVM_ROOT/lib/cshrc.stub. Для успешного определения PVM_ARCH строка должна помещаться после строки c переменной PATH. Эта строка автоматически задает PVM_ARCH данного хоста и может быть частично использована, когда пользователь разделяет общую файловую систему (например, NFS) в среде с несколькими различными архитектурами.

Исходные тексты PVM для большинства архитектур поставляются совместно с другим содержимым каталогов и файлами сборки. Процесс сборки для каждого из поддерживаемых типов архитектур происходит автоматически - путем логина на хосте, обращения к каталогу PVM_ROOT и ввода make. Файл сборки будет автоматически определять, на какой архитектуре он начал выполняться, создавать соответствующие подкаталоги и ``строить'' pvm, pvmd3, libpvm3, libfpvm3.a, pvmgs, libgpvm3.a. Он поместит все эти файлы в $PVM_ROOT/lib/PVM_ARCH, за исключением pvmgs, который переносится в $PVM_ROOT /bin/PVM_ARCH.

Таким образом, необходимо:

В табл. 11 перечисляются возможные значения PVM_ARCH и соответствующие им типы архитектур, поддерживаемые PVM версии 3.3.


Таблица 11. Возможные значения константы PVM_ARCH


PVM_ARCH Тип компьютера Примечания
AFX8 Alliant FX/8  
ALPHA DEC Alpha DEC OSF-1
BAL Sequent Balance DYNIX
BFLY BBN Butterfly TC2000  
BSD386 80386/486 ПК с системой Unix BSDI, 386BSD, NetBSD
CM2 Thinking Machines CM2 наиболее совершенный Sun
CM5 Thinking Machines CM5 используются оригинальные сообщения
CNVX Convex C-series IEEE формат п.з.
CNVXN Convex C-series оригинальная п.з.
CRAY доступны порты C-90 YMP, T3D UNICOS
CRAY2 Cray2  
CRAYSMP Cray S-MP  
DGAV Data General Aviion  
E88K Encore 88000  
HP300 HP-9000 модели 300 HPUX
HPPA HP-9000 PA-RISC
I860 Intel iPSC/860 используются оригинальные сообщения
IPSC2 Intel iPSC/2 386 хост SysV, используются оригинальные сообщения
KSR1 Kendall Square KSR-1 OSF-1, используется разделяемая память
LINUX 80386/486 ПК с системой Unix LINUX
MASPAR Maspar наиболее совершенный DEC
MIPS MIPS 4680  
NEXT NeXT  
PGON Intel Paragon используются оригинальные сообщения
PMAX DECstation 3100, 5100 Ultrix
RS6K IBM/RS6000 AIX 3.2
RT IBM RT  
SGI Silicon Graphics IRIS IRIX 4.x
SGI5 Silicon Graphics IRIS IRIX 5.x
SGIMP мультипроцессор SGI используется разделяемая память
SUN3 Sun 3 SunOS 4.2
SUN4 Sun 4, SPARCstation SunOS 4.2
SUN4SOL2 Sun 4, SPARCstation Solaris 2.x
SUNMP мультипроцессор SPARC Solaris 2.x, используется разделяемая память
SYMM Sequent Symmetry  
TITN Stardent Titan  
U370 IBM 370 AIX
UVAX DEC MicroVAX  



next up previous contents
Next: Запуск PVM Up: Использование PVM Previous: Как получить программное обеспечение   Contents
2004-06-22

next up previous contents
Next: Типичные ошибки при запуске Up: Использование PVM Previous: Установка PVM для ее   Contents

Запуск PVM

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

После этого должно появиться приглашение консоли PVM, говорящее о том, что PVM теперь запущена на данном хосте. Можно добавить хосты в свою виртуальную машину, введя с консоли pvm> add имя_хоста.

Также можно удалить хосты (исключая тот, за которым вы находитесь) из своей виртуальной машины, введя
pvm> delete имя_хоста.

Для того, чтобы увидеть, что представляет собой в настоящий момент виртуальная машина, введите pvm> conf.

А чтобы увидеть, какие задачи PVM выполняются на виртуальной машине, введите pvm> ps -a.

Если ввести quit с консоли, то консоль прекратит свое существование, но виртуальная машина сохранится, а задачи будут продолжать выполняться. В случае с любым приглашением Unix на любом хосте из виртуальной машины можно ввести % pvm. и получить сообщение ``pvm already running'' на консоль. При завершении работы с виртуальной машиной следует ввести pvm> halt.

Эта команда принудительно завершит работу всех задач PVM, выключит виртуальную машину и произойдет выход из консоли.

Рекомендуемый способ остановки PVM гарантирует нормальное завершение работы виртуальной машины. Если вы не хотите вводить связку из имен хостов каждый раз, то воспользуйтесь опцией hostfile. Вы можете перечислить имена хостов в файле - по одному в строчке - и затем ввести pvm> hostfile.

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

Существуют другие варианты запуска PVM. Функции консоли и монитора производительности объединены в графическом пользовательском интерфейсе, названном XPVM, который в нескомпилированном варианте доступен в библиотеке netlib. Для запуска PVM с графическим интерфейсом X window введите % xpvm При нажатии кнопки под названием hosts ``выпадет'' список хостов, которые можно добавлять. Если Вы ``кликнете'' имя хоста, то он будет добавлен, а иконка машины станет анимированной, соответствующей виртуальной машине. Хост удаляется, если Вы ``кликните'' имя хоста, который уже был включен в виртуальную машину (см. рис. 10). При запуске XPVM происходит чтение файла $HOME/.xpvm_hosts, в котором перечислены хосты для отображения в меню. Все хосты без префикса & при запуске добавляются сразу.

\includegraphics[scale=0.6]{pic31.eps}

Рис. 10. Добавление хостов в системе XPVM.

Назначение кнопок quit и halt аналогично соответствующим командам консоли PVM. Если вы выходите из XPVM и затем перезапускаете его, то XPVM автоматически отображает, что при этом представляет собой виртуальная машина. Попрактикуйтесь в запуске, остановке XPVM и добавлении хостов с его помощью. Возникающие ошибки должны находить отображение в окне, из которого вы запустили XPVM.



2004-06-22

next up previous contents
Next: Выполнение программ PVM Up: PVM - параллельная виртуальная Previous: Запуск PVM   Contents

Типичные ошибки при запуске

Если PVM не может стартовать успешно, она выводит сообщение об ошибке на экран либо в файл протокола /tmp/pvml.<uid>. В этом подразделе описываются наиболее общие трудности, возникающие при запуске и пути их решения.

Если сообщение представляет собой: [t80040000] Can't start
pvmd, то прежде всего проверьте соответствующий файл .rhosts на удаленном хосте - содержится ли в нем имя хоста, на котором вы запускаете PVM. Внешняя проверка на предмет корректности спецификации файла .rhosts проводится вводом % rsh remote_host ls.

Если спецификация файла .rhosts корректна, то вы увидите список своих файлов на удаленном хосте.

Другими причинами ошибок могут оказаться отсутствие проинсталлированной PVM на удаленном хосте или некорректная спецификация PVM_ROOT на данном хосте. Проверить это можно вводом % rsh remote_host $PVM_ROOT/lib/pvmd.

Некоторые оболочки Unix, например ksh, не устанавливают переменные окружения на удаленных хостах при использовании rsh. В PVM версии 3.3 есть два подхода к таким оболочкам. Первый - вы инициализируете переменную окружения PVM_DPATH на ведущем хосте как pvm3/lib/pvmd, тем самым подменяя путь, установленный по умолчанию с помощью dx. Второй подход - нужно явно сообщить PVM, где найти удаленный исполняемый pvmd с помощью опции dx= в файле.

Если работа PVM принудительно завершилась вручную или завершилась ненормально (например, из-за краха системы), то проверьте наличие файла /tmp/pvml.<uid>. Этот файл нужен для аутентификации и должен существовать только в процессе работы PVM. Если этот файл сохранился, он будет препятствовать запуску PVM. Удалите этот файл.

Если сообщение представляет собой [t80040000] Login incorrect, это может означать отсутствие аккаунта на удаленной машине с вашим логином. Если ваш текущий логин отличается от логина на удаленной машине, то вы должны воспользоваться опцией lo= в файле хоста.

Если вы получили любое другое странное сообщение, проверьте файл .cshrc. Важно, чтобы не было никакого ввода/вывода в файл .cshrc, потому что это повлияет на процесс запуска PVM. Если вы хотите выводить на экран информацию (например, who или uptime) при входе в систему, вы должны обеспечить это с помощью собственного скрипта .login - только не во время выполнения командного скрипта csh.



2004-06-22

next up previous contents
Next: Подробное описание консоли PVM Up: PVM - параллельная виртуальная Previous: Типичные ошибки при запуске   Contents

Выполнение программ PVM

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

Первым шагом здесь является копирование программы-образца в свою пользовательскую область на диске:

% cp -r /PVM_ROOT/examples $HOME/pvm3/examples

% cd $HOME/pvm3/examples

Каталог с примерами содержит файлы Makefile.aimk и Readme, в которых описано как построить примеры. PVM поддерживает архитектурно независимую программу построения aimk, которая автоматически определяет PVM_ARCH и компонует любые необходимые для данной операционной системы библиотеки к разрабатываемому приложению; aimk автоматически доопределяет соответствующую $PATH, когда Вы помещаете cshrc.stub в файл .cshrc. Применение aimk позволит Вам сохранить исходные тексты и файл с программой сборки неизменными при компиляции в среде с различными архитектурами.

Программная модель ``ведущий-ведомый'' - наиболее популярная из используемых в распределенных вычислениях. (В области параллельного программирования более популярна модель ОКМД).

Для компиляции примера ``ведущий-ведомый'' на С введите
% aimk master slave. Если вы предпочитаете работать c Фортраном, откомпилируйте версию на Фортране % aimk fmaster fslave.

В зависимости от расположения каталога согласно PVM_ROOT, выражения INCLUDE в заголовках примеров на Фортране могут нуждаться в изменениях. Если PVM_ROOT не совпадает с HOME/pvm3, то замените ее таким образом, чтобы она указывала на $PVM_ROOT
/include/fpvm3.h
. Заметьте, PVM_ROOT для Фортранне ``расширяется'', поэтому вы должны добавлять определение достоверного пути.

Программа сборки пересылает исполняемые файлы в
$HOME/pvm3/bin/PVM_ARCH, где PVM по умолчанию будет искать их на всех хостах. Если задействованные файловые системы на всех хостах PVM не одинаковы, то вам понадобится дополнительно создавать или копировать (в зависимости от архитектуры) эти исполняемые файлы на хосты в составе PVM.

Теперь, задействовав одно окно, запустите PVM и сконфигурируйте несколько хостов. Приводимые примеры разработаны для выполнения на произвольном числе хостов, начиная с одного. В другом окне смените каталог на HOME/pvm3/bin/PVM_ARCH и введите % master. Программа выдаст запрос о количестве задач. В примерах это количество не обязательно должно совпадать с количеством хостов. Проверьте несколько комбинаций.

Первый пример иллюстрирует возможность запустить программу PVM из любого терминала Unix на любом хосте виртуальной машины. Этот процесс подобен способу, котором вы запускали бы последовательную программу a.out на рабочей станции. В следующем примере, который также относится к модели ``ведущий-ведомый'', под названием hitc, вы можете увидеть, как порождается работа с консоли PVM, а также из XPVM.

Здесь hitc иллюстрирует балансирование динамической загрузки с применением парадигмы ``пула задач''. Согласно парадигме пула задач, ведущая программа управляет большой очередью задач, всегда посылая ``простаивающим'' ведомым программам больше заданий для выполнения - до тех пор, пока очередь не опустеет. Такая парадигма эффективна в ситуациях, когда хосты очень рознятся по вычислительной мощности, потому что наименее загруженные или более мощные хосты выполняют больше работы, причем все хосты остаются занятыми до тех пор, пока задание не будет выполнено. Для компиляции hitc введите % aimk hitc hitc_slave.

С этого момента hitc не требует никакого пользовательского ввода, он может быть порожден прямо с консоли PVM. Запустите консоль PVM и добавьте ограниченное число хостов. В ответ на приглашение консоли PVM введите pvm> spavn -> hitc.

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

Далее, hitc может использоваться для иллюстрирования возможностей анимации в реальном масштабе времени для XPVM. Запустите XPVM и постройте виртуальную машину с четырьмя хостами. ``Кликните'' кнопку tasks и выберите в меню spawn. Введите hitc, когда XPVM запросит команду и ``кликните'' start. Вы увидите подсветку иконок хостов, как только виртуальная машина станет задействованной; увидите порождение задач hitc_slave и все сообщения, которые ``бродят'' между задачами на дисплее Space Time. Можно выбрать несколько других вариантов просмотра с помощью XPVM меню views. Способ task output эквивалентен применению опции -> для консоли PVM. Это заставляет стандартный вывод всех задач принудительно перенаправлять в окно, которое при этом ``всплывает''.

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



2004-06-22

next up previous contents
Next: Опции в файле хостов. Up: PVM - параллельная виртуальная Previous: Выполнение программ PVM   Contents

Подробное описание консоли PVM

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

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

pvm [-n<hostfile>] [hostfile]
Опция -n может быть использована для указания альтернативного имени ведущего pvmd (в случае, если имя хоста не соответствует требуемому IP-адресу). Как только PVM запускается, на консоли появляется приглашение >pvm.

Консоль может воспринимать команды со стандартного ввода. Возможные команды:

add,
- сопровождаемая одним или несколькими именами хостов, - добавляет эти хосты к виртуальной машине;
alias
- определяет условные имена команд или выводит их список;
сonf
- выдает конфигурацию виртуальной машины, включая имя хоста; идентификатор задачи pvmd, тип архитектуры и относительную оценку скорости;
delete,
- сопровождаемая одним или несколькими именами хостов, - удаляет эти хосты из виртуальной машины. Процессы PVM, еще выполняющиеся на этих хостах, ``теряются'';
echo
- выводит на экран аргументы;
halt
- прекращает работу всех процессов PVM, включая консоль, и затем выключает PVM. Все ``демоны'' также завершают работу;
help
- может использоваться для получения информации о любой из интерактивных команд. Запрос помощи может сопровождаться именем команды - выдает список возможных опций и флагов этой команды;
id
- выводит на экран идентификатор задачи консоли;
jobs
- выдает список выполняющихся заданий;
kill
- может использоваться для уничтожения любого процесса PVM.;
mstat
- показывает статус указанных хостов;
ps
-a - выдает список всех процессов, протекающих в виртуальной машине, их ``местонахождение'', идентификаторы задач и идентификаторы их предков;
pstat
- показывает статус выбранного процесса PVM;
quit
- вызывает выход из консоли, оставляя ``демоны'' и задания PVM выполняющимися;
reset
- прекращает работу всех процессов PVM, за исключением консолей, и сбрасывает все внутренние таблицы PVM и очереди сообщений. ``Демоны'' остаются в холостом состоянии;
setenv
- отображает или устанавливает переменные окружения;
sig
, - сопровождаемая номером сигнала и TID, - посылает сигнал задаче;
spawn
- запускает приложение PVM. В качестве опций могут выступать:
-count
- количество задач, по умолчанию - 1;
-host
- порождает на указанном хосте, по умолчанию - на любом;
-ARCH
- порождает на хостах типа ARCH;
-?
- разрешает отладку;
->
- перенаправляет выходной поток задачи на консоль;
->file
- перенаправляет выходной поток задачи в файл;
->>file
- перенаправляет выходной поток задачи для дозаписи в файл;
-@
- трассирует задание, отображает выходной поток на консоли;
-@file
- трассирует задание, выходной поток направляется в файл;
trace
- устанавливает или отображает маску событий трассировки;
unalias
- отменяет действие условных имен команд;
version
- выводит на экран версию PVM, имеющуюся в распоряжении.
Консоль считывает $HOME/.pvmrc перед тем, как считывать команды с tty, так что вы можете набрать что-нибудь подобное:

alias ? help

alias h help

alias j jobs

setenv PVM_EXPORT DISPLAY

# print my id

echo new pvm shell

id

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



2004-06-22

next up previous contents
Next: Базовые технологии программирования Up: PVM - параллельная виртуальная Previous: Подробное описание консоли PVM   Contents

Опции в файле хостов.

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

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

Файл хостов в его простейшей форме - это просто список имен хостов - по одному в строке. Пустые строки игнорируются, а строки, которые начинаются с # считаются строками комментариев. Такой подход позволяет Вам документировать файл хостов и дополнительно предоставляет ``ручной'' способ модификации начальной конфигурации путем комментирования различных имен хостов. Простейший файл хостов с конфигурацией виртуальной машины приведен ниже:

# конфигурация для запуска приложений

amox

tf2.evm.bsuir.unibel.by

solaris2

Ряд опций может применяться в каждой строке после имени хоста. Опции разделяются с помощью пробелов:

lo=userid
- позволяет вам указать альтернативный логин для данного хоста; в противном случае, при запуске машины будет использоваться ваш логин;
so=pw
- заставит PVM сделать запрос пароля при обращении к данному хосту. Это может применяться в случаях, когда вы имеете отличные идентификатор пользователя и пароль в удаленной системе. По умолчанию PVM использует rsh для запуска удаленных pvmd, но если указана pw, PVM будет использовать вместо него rexec();
dx=location
of pvmd - позволяет вам указать иное местонахождение для него на данном хосте. Это применимо, если вы хотите использовать свою собственную копию pvmd;
ep=paths
to user executables - позволит вам указать последовательность путей для поиска порождаемых на данном хосте файлов. Множество путей разделяется двоеточиями. Если ep= не указана, то PVM просматривает $HOME/pvm3/bin/ PVM_ARCH в процессе работы задач приложения;
sp=value
- указывает относительную вычислительную скорость хоста в сравнении с другими хостами в данной конфигурации. Диапазон возможных значений: от 1 до 1000000, причем значение 1000 является значением по умолчанию;
bx=location
of debugger - указывает какой отладочный скрипт вызвать на данном хосте, если в порождающей подпрограмме запрашивается отладка. (Может устанавливаться и переменная окружения PVM_DEBUGGER. По умолчанию используется отладчик pvm3/lib/debugger);
wd=working_directory
- указывает рабочий каталог, из которого будут запускаться все порождаемые на данном хосте задачи. По умолчанию это $HOME;
ip=hostname
- указывает альтернативное имя для восстановления IP-адреса хоста;
so=ms
- указывает, что ведомый pvmd на этом хосте будет запускаться вручную. Это применимо, если сетевые сервисы rsh и rexec запрещены, но возможность IP-связи существует. При использовании этой опции Вы увидите на tty, связанном с pvmd3:
[t80040000] ready Fri Aug 27 18:47:47 1993

*** Ручной запуск ***

Загрузитесь в "honk" и введите:

pvm3/lib/pvmd -S -d0 -nhonk 1 80a9ca95:0cb6

    4096 2 80a95c43:0000

Введите ответ:

В сопровождении звукового сигнала, после ввода ответной строки, вы должны увидеть:

ddpro<2312> arch<ALPHA> ip<80a95c43:0a8e> mtu<4096>
Эту строку Вы должны вернуть ведущему pvmd. На этом этапе вы увидите сообщение Thanks, после чего оба pvmd должны получить доступ к коммуникации.

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

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

# Комментарии начинаются с символа #

# (пустые строки игнорируются)

gstws

ipsc dx=/usr/geist/pvm3/lib/I860/pvmd3

ibm1.scri.fsu.edu lo=gst so=pw

 

# Опции по умолчанию устанавливаются символом *

*ep=$sun/problem1:~/nla/mathlib

amox

#tf1.evm.bsuir.unibel.by

solaris2

 

# Замена опций по умолчанию новыми значениями

* lo=gageist so=pw ep=problem1

st1.bsu.edu.by

st2.bsu.edu.by

 

# машины, добавляемые позже, обозначены &

&sun4 ep=problem1

&corsair dx=/usr/local/bin/pvm3

&kill lo=gageist



2004-06-22

next up previous contents
Next: Очереди сообщений Up: Блокировка файлов Previous: Режимы блокировки   Contents

Блокировка частей файла и тупики

Взаимная блокировка процессов может возникнуть из-за блокировки файлов. Пусть, например, процесс 1 пытается установить блокировку в некотором файле dead.txt в позиции 10.

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

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

Пример программы, вызывающей тупик:

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdio.h>

#include <errno.h>

extern int errno;

 

void status(struct flock *lock)

{

  printf("Status: ");

  switch(lock->l_type)

  {

    case F_UNLCK: printf("F_UNLCK\n"); break;

    case F_RDLCK: printf("F_RDLCK (pid: %d)\n",

         lock->l_pid); break;

    case F_WRLCK: printf("F_WRLCK (pid: %d)\n",

         lock->l_pid); break;

    default : break;

  }

}

 

void writelock(char *proсess, int fd, off_t from,

     off_t to) {

  struct flock lock;

  lock.l_type = F_WRLCK;

  lock.l_start=from;

  lock.l_whence = SEEK_SET;

  lock.l_len=to;

  lock.l_pid = getpid();

  if (fcntl(fd, F_SETLKW, &lock) < 0)

  {

    printf("%s : Ошибка 

       fcntl(fd, F_SETLKW, F_WRLCK) (%s)\n",

    proсess,strerror(errno));

    printf("\nВозник DEADLOCK (%s - proсess)!\n\n",

           proсess);

    exit(0);

  }

  else

    printf("%s : fcntl(fd, F_SETLKW, F_WRLCK)

          успешно\n", proсess);

  status(&lock);

}

 

int main()

{

  int fd, i;

  pid_t pid;

  if(( fd=creat("dead.txt", S_IRUSR |

                 S_IWUSR | S_IRGRP | S_IROTH))<0)

  {

    fprintf(stderr, "Ошибка при создании......\n");

    exit(0);

  }

  /*Заполняем dead.txt 50 байтами символа X*/

  for(i=0; i<50; i++)

    write(fd, "X", 1);

  if((pid = fork()) < 0)

  {

    fprintf(stderr, "Ошибка fork()......\n");

    exit(0);

  }

  else if(pid == 0) //Потомок

  {

    writelock("Потомок", fd, 20, 0);

    sleep(3);

    writelock("Потомок" , fd, 0, 20);

  }

  else //Родитель

  {

    writelock("Родитель", fd, 0, 20);

    sleep(1);

    writelock ("Родитель", fd, 20, 0);

  }

  exit(0);

}

Вначале создается файл данных dead.txt, в который записывается 50 символов X. Затем родительский процесс организует блокировку от байта 0 до байта 19, а потомок - блокировку от байта 20 до конца файла (EOF). Потомок "засыпает" на 3 сек., а родитель теперь устанавливает блокировку от байта 20 до байта EOF и приостанавливается, так как байты от 20 до EOF блокированы в данный момент потомком, а родитель использует команду F_SETLKW. Наконец, потомок пытается установить блокировку на запись от байта 0 до байта 19, причем он также приостанавливается, так как в этой области уже установлена блокировка родителя и используется команда F_SETLKW. Здесь возникает тупик, что подтверждается выдачей кода ошибки для errno = EDEADLK (возникновение тупика по ресурсам). Тупик может возникнуть только при использовании команды F_SETLKW. Если применять команду F_SETLK, выдается код ошибки для errno = EAGAIN (ресурс временно недоступен).


next up previous contents
Next: Очереди сообщений Up: Блокировка файлов Previous: Режимы блокировки   Contents
2004-06-22

next up previous contents
Next: Общие парадигмы параллельного программирования Up: PVM - параллельная виртуальная Previous: Опции в файле хостов.   Contents

Базовые технологии программирования

Разработка приложений для системы PVM - по крайней мере в общем смысле - следует традиционной парадигме программирования микропроцессоров с распределенной памятью, таких как мультипроцессоры семейства Intel nCUBE. Базовые технологии как для логических аспектов программирования, так и для разработки алгоритмов совпадают. Наиболее существенные различия, однако, наблюдаются в следующем:



Subsections

2004-06-22

next up previous contents
Next: ``Беспорядочные'' вычисления Up: Базовые технологии программирования Previous: Базовые технологии программирования   Contents

Общие парадигмы параллельного программирования

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

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



Subsections

2004-06-22

next up previous contents
Next: ``Древовидные'' вычисления Up: Общие парадигмы параллельного программирования Previous: Общие парадигмы параллельного программирования   Contents

``Беспорядочные'' вычисления

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

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

\includegraphics[scale=0.6]{pic41.eps}

Рис. 11. Структура приложения для алгоритма Манделброта

{Алгоритм Манделброта для ведущего}

 

{Начальное размещение}

for i := 0 to NumWorkers - 1

  pvm_spawn(<worker name>} {Запуск рабочего i}

  pvm_send(<worker tid>,999) {Передача задачи рабочему i}

endfor

 

{Прием-передача}

while (WorkToDo)

  pvm_recv(888) {Прием результата}

  pvm_send(<available worker tid>,999)

 

  {Передача следующей задачи доступному рабочему}

  display result

endwhile

 

{Сбор оставшихся результатов}

for i := 0 to NumWorkers - 1

  pvm_recv(888) {Прием результата}

  pvm_kill(<worker tid i>) {Завершение рабочего i}

  display result

endfor

 

{Алгоритм Манделброта для рабочего}

while (true)

  pvm_recv(999) {Прием задачи}

{Вычисление результата}

  result := MandelbrotCalculations(task) 

{Передача результата ведущему}

  pvm_send(<master tid>,888) 

endwhile

Пример ``ведущий-ведомый'', описанный выше, не предполагает коммуникаций между ведомыми. Большинство беспорядочных вычислений любой сложности требуют взаимодействия между вычислительными процессами; структура таких приложений проиллюстрирована на примере использования ``только станции'' для матричного умножения по алгоритму Каннона (подробности программирования похожих алгоритмов даются в другом разделе). Пример ``умножения матриц'' графически показан на рис. 12 (локально умножаются подматрицы матриц и используется широковещательная построчная передача строк подматриц A в связке с постолбцовыми сдвигами подматриц матрицы B):

\includegraphics[scale=0.57]{pic42.eps}

Рис. 12. Общие беспорядочные вычисления

{Матричное умножение с использованием алгоритма

    ``сдвинуть-умножить-повернуть''}

{Процессор 0 запускает другие процессы}

if (<my processor number>) = 0) then

  for i := 1 to MeshDimension*MeshDimension

    pvm_spawn(<component name>, ...)

  endfor

endif

 

forall processors Pij, 0 <= i,j < MeshDimension

  for k := 0 to MeshDimension-1

    {Сдвиг}

    if myrow = (mycolumn+k) mod MeshDimension

       {Передача A во все Pxy, x = myrow, y <> mycolumn}

       pvm_mcast((Pxy, x = myrow, y <> mycolumn,999)

    else

       pvm_recv(999) {Прием A}

    endif

 

    {Умножение. Обработка всего, содержащегося в C}

    Multiply(A,B,C)

    {Вращение}

    {Передача B в Pxy, x = myrow-1, y = mycolumn}

    pvm_send((Pxy, x = myrow-1, y = mycolumn),888)

    pvm_recv(888) {Прием B}

  endfor

endfor



2004-06-22

next up previous contents
Next: Распределение рабочей нагрузки Up: Общие парадигмы параллельного программирования Previous: ``Беспорядочные'' вычисления   Contents

``Древовидные'' вычисления

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

\includegraphics[scale=0.54]{pic43.eps}

Рис. 13. Пример древовидного вычисления

{Порождение и разделение списка на базе

   широковещательной передачи шаблона дерева}

for i := 1 to N, причем 2^N = NumProcs

  forall processors P, причем P < 2^i

    pvm_spawn(...) {идентификатор процесса P XOR 2^i}

    if P < 2^(i-1) then

      midpt := PartitionList(list);

      {Send list[0..midpt] to P XOR 2^i}

      pvm_send((P XOR 2^I,999)

      list := list[midpt+1..MAXSIZE]

    else

      pvm_recv(999) {Прием списка}

    endif

  endfor

endfor

 

{Сортировка оставшегося списка}

Quicksort(list[midpt+1..MAXSIZE])

{Сбор/слияние отсортированных подсписков}

for i := N downto 1, причем 2^N = NumProcs

  forall processors P, причем P < 2^i

    if P >2^(i-1) then

      pvm_send((P XOR 2^i),888)

      {Send list to P XOR 2^i}

    else

      pvm_recv(888) {Прием временного списка}

      merge templist into list

    endif

   endfor

endfor



2004-06-22

next up previous contents
Next: Декомпозиция данных Up: PVM - параллельная виртуальная Previous: ``Древовидные'' вычисления   Contents

Распределение рабочей нагрузки

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



Subsections

2004-06-22

next up previous contents
Next: Функциональная декомпозиция Up: Распределение рабочей нагрузки Previous: Распределение рабочей нагрузки   Contents

Декомпозиция данных

В качестве простого примера декомпозиции данных рассмотрим сложение двух векторов $A{[}1..N{]}$ и $B{[}1..N{]}$, в результате чего получится вектор $C{[}1..N{]}$. Если предположить, что над этой задачей работает $P$ процессов, разбиение данных приведет к распределению $N/P$ элементов каждого вектора для каждого процесса, который вычисляет соответствующие $N/P$ элементов результирующего вектора. Такое распределение данных может быть сделано либо ``статически'', когда каждый процесс ``априори'' знает (по крайней мере, в терминах переменных $N$ и $P$) свою долю рабочей нагрузки, либо ``динамически'', когда контролирующий процесс (т.е. ведущий) распределяет подблоки рабочей нагрузки для процессов - как и когда они освободятся. Принципиальная разница между этими двумя подходами - это ``диспетчеризация''. При статической диспетчеризации индивидуальная рабочая нагрузка процесса фиксирована; при динамической, она варьирует в зависимости от состояния вычислительного процесса. В большинстве мультипроцессорных сред статическая диспетчеризация эффективна для таких задач как пример сложения векторов; однако в обобщенной среде PVM статическая диспетчеризация не очень необходима. Смысл заключается в том, что среды PVM, базирующиеся на сетевых кластерах, восприимчивы к внешним воздействиям; поэтому статически диспетчеризированные задачи с разделенными данными могут конфликтовать с одним или более процессами, которые реализуют свою порцию рабочей нагрузки намного быстрее или намного медленнее, чем другие. Эта ситуация может также возникнуть, когда машины в системе PVM гетерогенны, обладают различными скоростями ЦПУ, различной памятью и прочими системными атрибутами.

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

  1. Индивидуальные процессы генерируют свои собственные данные внутренне, например, используя генераторы случайных чисел или статически заданные величины. Это возможно только в очень специфических ситуациях или применяется в целях тестирования.
  2. Индивидуальные процессы независимо вводят подмножества своих данных из внешних устройств. Такой метод является значащим во многих случаях, но возможен только при поддержке услуг параллельного ввода/вывода.
  3. Контролирующий процесс посылает индивидуальные подмножества данных каждому процессу. Это наиболее обобщенный сценарий, особенно полезный при отсутствии услуг параллельного ввода/вывода. Кроме того, этот метод также применим при вводе подмножеств данных, полученных при предыдущих вычислениях, относящихся к данному приложению.
Третий метод распределения индивидуальной рабочей нагрузки также придерживается динамической диспетчеризации для приложений, в которых межпроцессные взаимодействия в ходе вычислений редки или не существуют вообще. Однако нетривиальные алгоритмы обычно требуют непосредственных обменов значениями данных, и поэтому только изначальное назначение порций данных удовлетворяет таким схемам. Например, рассмотрим метод разбиения данных, изображенный на рис. 85. Чтобы умножить две матрицы A и B, в первую очередь порождается группа процессов - согласно парадигме ``ведущий-ведомый'' или ``только станции''. Этот набор процессов предназначен для формирования петли. Каждая подматрица матриц A и B помещается в соответствующий процесс с помощью одной декомпозиции данных и одной из перечисленных выше стратегий распределения рабочей нагрузки. В процессе вычисления подматрицы требуют передачи или обмена между процессами, тем самым преобразуя оригинальную карту распределения, как показано на рисунке. В конце вычисления, однако, подматрицы результирующей матрицы ``разбросаны'' по индивидуальным процессам в соответствии с занимаемой позицией в сети процессов и наполнены данными согласно карте разбиения результирующей матрицы C. Предшествующая дискуссия выявила основы декомпозиции данных. В следующем разделе будут представлены программы-образцы, выдвигающие на передний план подробности этого подхода.



2004-06-22

next up previous contents
Next: Портирование существующих приложений в Up: Распределение рабочей нагрузки Previous: Декомпозиция данных   Contents

Функциональная декомпозиция

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

Хотя концепция функциональной декомпозиции и проиллюстрирована выше тривиальным примером, этот термин, как правило, используется для обозначения разбиения и распределения рабочей нагрузки функцией within, относящийся к вычислительной фазе. В типовом случае вычисления приложения содержат несколько особых подалгоритмов - иногда для одних и тех же данных (МКОД или сценарий: ``много команд и одни данные''), иногда в виде конвейеризированной последовательности преобразований, а иногда - представленных неструктурированными шаблонами обменов. Парадигма обобщенной функциональной декомпозиции основаывается на гипотетическом симулировании ``продвижения'' самолета, состоящего из множества взаимосвязанных и взаимодействующих, функционально декомпозированных подалгоритмов. Диаграмма,
предоставляющая возможность взглянуть на такой пример, показана на рис. 14 (кроме того, она будет использоваться в разделах, где описывается графическое программирование PVM).

\includegraphics[scale=0.48]{pic44.eps}

Рис. 14. Пример функциональной декомпозиции

На рисунке каждое состояние, т.е. круг на ``графе'', представляет функционально декомпозированную часть приложения. Функция ввода распределяет частичные параметры задачи на различные функции 2 - 6, после порождения процессов соответствующих подпрограмм, реализующих каждый из подалгоритмов приложения. Некоторые данные могут быть переданы нескольким функциям (как в случае с двумя функциями wing) или данные могут предназначаться только для одной функции. После выполнения некоторого количества вычислений, эти функции доставляют непосредственно конечный результат в функции 7, 8 и 9, которые могут порождаться в начале вычислительного процесса и поэтому быть доступными. Диаграмма отражает первичную концепцию декомпозиции приложений по функциям с тем же успехом, что и отношения зависимости по контролю и данным. Параллелизм достигается благодаря двум причинам: параллельному и независимому исполнению модулей (функциями 2 - 6) плюс одновременному и конвейеризированному исполнению модулей в цепи зависимости (функциями 1, 6, 8 и 9).



2004-06-22

next up previous contents
Next: Пользовательский интерфейс PVM Up: PVM - параллельная виртуальная Previous: Функциональная декомпозиция   Contents

Портирование существующих приложений в PVM

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

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

Современные параллельные программы могут базироваться либо на парадигме общей памяти, либо на парадигме распределенной памяти. Приспособление написанных программ для общей памяти к PVM похоже на преобразование из последовательного кода, причем версии с общей памятью базируются на векторном или на уровне петель параллелизме. В случае с программами, явно разделяющими память, первичная задача состоит в поиске точек синхронизации и замещении их обменами сообщений. Для преобразования параллельного кода для распределенной памяти в PVM главная задача состоит в преобразовании одного набора параллельных конструкций в другой. Обычно существующие параллельные программы для распределенной памяти написаны либо для аппаратных мультипроцессоров, либо для других сетевых сред, таких как p4 или Express. В обоих случаях, главные изменения требуется провести в отношении подсистемы управлении процессами. В примере с семейством Intel DMMP обычной практикой является запуск процессов с помощью командной строки интерактивных оболочек. Такая парадигма должна замещаться PVM - либо посредством ведущей программы, либо посредством программы для станции, берущей на себя ответственность за порождение процессов. В смысле взаимодействия, при этом, к счастью, много общего между вызовами по обмену сообщениями в различных средах программирования. Основными же различиями PVM и других систем в этом контексте являются:



2004-06-22

next up previous contents
Next: Контроль процессов Up: PVM - параллельная виртуальная Previous: Портирование существующих приложений в   Contents

Пользовательский интерфейс PVM

В этом разделе приведено ознакомительное описание подпрограмм PVM версии 3. Раздел отсортирован по функциям, выполняемым подпрограммами. Например, в подразделе об ``обмене сообщениями'' идет речь о всех подпрограммах передачи и приема данных между двумя задачами PVM и опциях обмена сообщениями PVM.

В PVM версии 3 все задачи идентифицируются с помощью целого числа, предоставляемого pvmd. Далее по тексту такой идентификатор задачи обозначается TID. Это обозначение похоже на обозначение идентификатора процесса (PID), используемое в системе UNIX, предполагающее прозрачность для пользователя; значение TID также не имеет специального значения для пользователя. Фактически, PVM кодирует информацию в TID для своего собственного внутреннего использования.

Все подпрограммы PVM написаны на C. Приложения на C++ могут компоноваться с библиотекой PVM. Приложения на Фортране могут вызывать эти подпрограммы посредством интерфейса Фортран 77, поддерживаемого исходными текстами PVM версии 3. Данный интерфейс переводит аргументы, которые переданы при обращении на Фортранв соответствующие значения - если это нужно - для находящихся в более низком слое C-подпрограмм. Интерфейс также затрагивает формы представлений символьных строк на Фортране и различные соглашения при именовании, которые разные Фортран-компиляторы используют при вызове C-функций.

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

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

Модель PVM гарантирует сохранение порядка сообщений. Если задача 1 посылает сообщение A задаче 2, а затем эта же задача посылает сообщение B той же задаче, то сообщение A поступит задаче 2 раньше, чем сообщение B. Более того, если оба сообщения возникнут до того, как задача 2 выполнит прием, то прием с указанием специальных символов будет всегда возвращать сообщение A.

Буферы сообщений распределяются динамически. Однако максимальная длина сообщения, которое может быть передано или принято, ограничивается объемом доступной памяти на данном хосте. В PVM версии 3.3 встроен только ``ограничительный'' текущий контроль. PVM может выдавать пользователю ошибку ``Can't get memory'' в тех случаях, когда суммарный объем входящих сообщений превышает размер доступной памяти, но при этом PVM не просит другие задачи остановить передачу для данного хоста.



2004-06-22

next up previous contents
Next: Информация Up: PVM - параллельная виртуальная Previous: Пользовательский интерфейс PVM   Contents

Контроль процессов

int tid = pvm_mytid(void)

call pvmfmytid (tid)

Подпрограмма pvm_mytid() возвращает TID текущего процесса и может вызываться неограниченное число раз. Она регистрирует данный процесс в PVM - если это ее первый вызов. Любой системный вызов PVM (не только pvm_mytid) будет регистрировать задачу в PVM, если перед этим вызовом задача не зарегистрирована, но общая практика требует для обеспечения регистрации вызова pvm_mytid.

int info = pvm_exit(void)

call pvmfexit(info)

Подпрограмма pvm_exit сообщает локальному pvmd о том, что процесс ``покинул'' PVM. Эта подпрограмма не завершает процесс принудительно - он может продолжать выполнять задачу наряду с другими процессами UNIX. Пользователи обычно вызывают pvm_exit прямо перед выходом - в своих программах на C - и прямо перед STOP - в своих программах на Фортране.

int numt = pvm_spawn (char *task, char **argv, int flag,

    char *where, int ntask, int *tids)

call pvmfspawn (task, argv, flag, where, ntask, tids,

     numt)

Подпрограмма pwm_spawn запускает до ntask копий исполняемого файла task на виртуальной машине; argv - это указатель на массив аргументов для task, причем конец массива указывается с помощью NULL. Если задача не получает аргументов, то argv равняется NULL. Аргумент flag используется для указания опций, которые обобщены в табл. 12 (см.).


Таблица 12. Опции порождения потомков в PVM


Значение Опция Cмысл
0 PvmTaskDefault Место порождения процессов выбирает сама PVM
1 PvmTaskHost аргумент where - определенный хост для порождения
2 PvmTaskArch аргумент where - PVM_ARCH для порождения
4 PvmTaskDebug запускает задачи под отладчиком
8 PvmTaskTrace генерируются трассировочные данные
16 PvmMppFront задачи запускаются в среде MPP
32 PvmHostCompl where дополняет набор хостов


Эти имена предопределены в pvm3/include/pvm3.h. На Фортране все имена предопределяются в параметрических конструкциях, которые могут быть найдены в разделе include файла
pvm3/include/pvm3.h.

PvmTaskTrace - это новая возможность PVM версии 3.3. Она заставляет порождаемые задачи генерировать трассировочные события. PvmTaskTrace используется XPVM. В ином случае пользователь должен с помощью pvm_setopt() указать, когда генерировать трассировочные события.

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

Вызов pvm_spawn() может также запускать задачи и на мультипроцессорах. В случае с Intel iPSC/860 накладываются следующие ограничения. Каждый порождающий вызов получает подкуб размера ntask и загружает программу task во все соответствующие станции. ОС iPSC/860 имеет распределительное ограничение: 10 подкубов для всех пользователей, так что лучше запустить блок из задач в iPSC/860 одним вызовом, чем несколькими. Два самостоятельных блока задач, порожденные в iPSC/860 раздельно, все равно могут взаимодействовать друг с другом так же, как и с другими задачами PVM, даже с учетом того, что они существуют в разных подкубах. ОС iPSC/860 имеет еще одно ограничение: сообщения, следующие от станций во ``внешний мир'', должны быть меньше 256 КБайтов.

int info = pvm_kill (int tid)

call pvmfkill (tid, info)

Подпрограмма pvm_kill() принудительно завершает некоторую задачу PVM, идентифицированную TID. Эта подпрограмма разработана для принудительного завершения задач, которые должны завершаться вызовами pvm_exit(), следующими за exit().

int info = pvm_catchout (FILE *ff)

call pvmfcatchout (onoff)

По умолчанию PVM записывает stdout и stderr порожденных задач в файл протокола /tmp/pvml.<uid>. Подпрограмма pvm_catchout заставляет вызывающую задачу ``перехватывать'' выходной поток последовательно порожденных задач. Символы, выводимые в stdout или stderr дочерних задач, собираются демонами pvmd и отсылаются в форме сообщений родительской задаче, которая ``помечает'' каждую строку и добавляет ее в указанный файл (для C) или выводит в стандартный поток вывода (для Фортрана). Все выводимое на экран предваряется информацией о том, какая из задач сгенерировала вывод на экран, а завершение каждого вывода отметчается с целью помочь разграничить ``перекрывающиеся'' выходные потоки нескольких задач.

Если pvm_exit вызывается предком тогда, когда активирован сбор выходных потоков, он будет блокироваться до тех пор, пока все задачи, выполняющие вывод, не отработают в том порядке, в котором они осуществляли весь свой вывод. Во избежание такой ситуации можно ``выключить сбор выходных потоков'' вызовом pvm_catchout(0) перед тем, как вызвать pvm_exit.

Новые возможности PVM версии 3.3 включают и возможность регистрировать специальные задачи PVM для обработки заданий по включению новых хостов, размещению задач на хостах и запуску новых задач. С этой целью создается интерфейс для ``продвинутых'' пакетных планировщиков (примерами могут быть Condor, DQS и LSF), которые подключаются к PVM и позволяют выполнять задания PVM в пакетном режиме. Такие регистрирующие подпрограммы также создают интерфейс для разработчиков отладчиков - для стимулирования удовлетворительных разработок сложных отладчиков специально для PVM.

Имена таких подпрограмм: pvm_reg_rm(), pvm_reg_hoster() и
pvm_reg_tasker(). Эти ``продвинутые'' функции не имеют значения для среднестатистического пользователя PVM и потому здесь подробно не рассматриваются.


next up previous contents
Next: Информация Up: PVM - параллельная виртуальная Previous: Пользовательский интерфейс PVM   Contents
2004-06-22

next up previous contents
Next: Общие сведения Up: Локальные и удаленные средства Previous: Блокировка частей файла и   Contents

Очереди сообщений



Subsections

2004-06-22

next up previous contents
Next: Динамическая конфигурация Up: PVM - параллельная виртуальная Previous: Контроль процессов   Contents

Информация

int tid = pvm_parent(void)

call pvmfparent (tid)

Подпрограмма pvm_parent() возвращает TID процесса, который порожден данной задачей, или значение PvmNoParent, если он не создан с помощью pvm_spawn()

int dtid = pvm_tidtohost( int tid)

call pvmftidtohost (tid, dtid)

Подпрограмма pvm_tidhost() возвращает TID - dtid - демона, выполняющегося на том же хосте, что и задача с TID. Эта подпрограмма применима для определения того, на каком хосте выполняется данная задача. Более обобщенная информация о внутренней структуре виртуальной машины, включая текстовые имена сконфигурированных хостов, может быть получена с использованием следующих функций.

int info = pvm_config( int *nhost, int *narch,

    struct pvmhostinfo **hostp)

call pvmfconfig( nhost, narch, dtid, name, arch, speed,

    info)

Подпрограмма pvm_config() возвращает информацию о виртуальной машине, включая количество хостов - nhost - и количество различных форматов данных - narch; hostp - это указатель на декларированный пользователем массив из структур pvmhostinfo. Размер массива по длине должен как минимум соответствовать nhost. При возврате каждая структура pvmhostinfo содержит TID pvmd, имя хоста, имя архитектуры и относительную характеристику скорости процессора для определенного хоста в конфигурации.

Функции на Фортране возвращают информацию об одном хосте за вызов, поэтому для ``опроса'' всех хостов нужен цикл. Если
pvmfconfig вызывается nhost раз, то будет представлена полная внутренняя структура виртуальной машины. Работа с Фортран-интерфейсом подразумевает сохранение копии массива hostp и возврат только одного ``вхождения'' за вызов. На всех хостах должны отработать циклы для того, чтобы они получили обновленный массив hostp. Поэтому, если виртуальная машина в течение этих вызовов изменяется, то изменение проявится в параметрах nhost и narch, но не отразится на информации о хосте. В настоящее время не существует способ ``сбросить'' pvmfconfig() и заставить его перезапустить цикл в процессе его работы.

int info = pvm_tasks ( int which, int *ntask,

    struct pvmtaskinfo **taskp)

call pvmftasks ( which, ntask, tid, ptid, dtid, flag,

     aout, info)

Подпрограмма pvm_tasks() возвращает информацию о задачах PVM, выполняющихся на виртуальной машине. Целое число which указывает, о каких задачах надо вернуть информацию. В настоящее время опциями могут быть: 0 - о всех задачах, pvmd TID (dtid) - о задачах, выполняющихся на указанном хосте, или TID - только об указанной задаче.

Количество задач возвращается в ntask. taskp - это указатель на массив структур pvmtaskinfo - массив размера ntask. Каждая структура pvmtaskinfo содержит TID, pvmd TID, TID предка, флаг статуса и имя файла для порождения. (PVM ``не знает'' имя файла вручную запущенной задачи и поэтому ``не заполняет'' это имя.) Функция на Фортране возвращает информацию об одной задаче за вызов, поэтому для ``опроса'' всех задач нужен цикл. Так что, если нужно ``опрашивать'' все задачи и если pvmftasks вызывается ntask раз, то все задачи будут представлены. Фортран-реализации предполагают, что пул задач не подвержен изменениям, пока имеются циклы ``опроса'' задач. Если же пул изменился, эти изменения не проявятся до тех пор, пока не начнется следующий цикл из ntask вызовов.

Примеры использования pvm_config и pvm_tasks можно найти в исходных текстах консоли PVM, которая сама является задачей PVM. Примеры использования Фортран-версий этих подпрограмм можно найти в исходных текстах pvm3/examples/ testall.f.



2004-06-22

next up previous contents
Next: Посылка сигналов Up: PVM - параллельная виртуальная Previous: Информация   Contents

Динамическая конфигурация

int info = pvm_addhosts( char **hosts, int nhost,

    int *infos)

int info = pvm_delhosts( char **hosts, int nhost,

    int *infos)

call pvmfaddhost( hostinfo)

call pvmfdelhost( hostinfo)

Подпрограммы на C добавляют к виртуальной машине или удаляют из нее hosts узлов. Подпрограммы Фортран добавляют к виртуальной машине или удаляют из нее только один host. При использовании Фортран-подпрограммы info возвращается равное 1 или коду статуса. При использовании C-версии infos возвращается как количество успешно добавленных хостов. Аргумент infos - это массив размера nhost, который содержит код статуса для каждого добавленного или удаленного хоста. Все это позволяет пользователю проверить - возможно, что только один хост из набора привел к ошибке - чтобы не пытаться повторно добавлять или удалять набор хостов целиком.

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



2004-06-22

next up previous contents
Next: Установка и получение опций Up: PVM - параллельная виртуальная Previous: Динамическая конфигурация   Contents

Посылка сигналов

int info = pvm_sendsig( int tid, int signum)

call pvmfsendsig( tid, signum, info)

int info = pvm_notify( int what, int msgtag, int cnt,

    int tids)

call pvmfnotify( what, msgtag, cnt, tids, info)

Подпрограмма pvm_sendsig посылает сигнал signum некоторой задаче PVM, идентифицированной TID. Подпрограмма pvm_notify запрашивает PVM об извещении вызывающей задачи о наступлении определенных событий. В настоящий момент имеются следующие опции:

В ответ на запрос об извещении некоторое количество сообщений возвращается PVM вызывающей задаче. Сообщения ``помечаются'' поддерживаемым пользователями msgtag. Массив tids указывает задачу, которую нужно отслеживать при использовании TaskExit или HostDelete. При применении HostAdd массив пуст. Если требуется, подпрограммы pvm_config и pvm_tasks могут использоваться и для получения идентификаторов задачи и pvmd.

Если имеется хост, на котором задача A потерпела неудачу при выполнении, а задача B запросила извещение о выходе из задачи A, то задача B будет извещена даже в том случае, когда выход был вызван косвенно - сбоем на хосте.



2004-06-22

next up previous contents
Next: Обмен сообщениями Up: PVM - параллельная виртуальная Previous: Посылка сигналов   Contents

Установка и получение опций

int oldval = pvm_setopt( int what, int val)

int val = pvm_getopt( int what)

call pvmfsetopt( what, val, oldval)

call pvmfgetopt( what, val)

Подпрограмма pvm_setopt - это функция общего назначения, которая позволяет пользователю установить или получить опции для системы PVM. В PVM версии 3 pvm_setopt может быть использована для установки ряда опций, задающих автоматический вывод на экран сообщений об ошибках, уровень отладки и метод коммуникационной маршрутизации для всех последующих вызовов PVM; pvm_setopt возвращает предыдущее значение запрашиваемой опции из набора в oldval. В PVM версии 3.3 what может иметь значения, указанные в табл. 13 (см.).


Таблица 13. Значения опций PVM.


Опция Значение Смысл
PvmRoute 1 дисциплина маршрутизации
PvmDebugMask 2 отладочная маска
PvmAutoErr 3 автоматический рапорт об ошибках
PvmOutputTid 4 конечная цель stdout потомков
PvmOutputCode 5 msgtag для выходного потока
PvmTraceTid 6 конечная цель трассировки потомков
PvmTraceCode 7 msgtag для трассировки
PvmFragSize 8 размер фрагментов сообщений
PvmResvTids 9 разрешение доставки сообщений с зарезервированными тегами для задач с идентификаторами задач
PvmSelfOutputTid 10 конечная цель собственного stdout
PvmSelfOutputCode 11 msgtag для собственного выходного потока
PvmSelfTraceTid 12 конечная цель собственной трассировки
PvmSelfTraceCode   msgtag для собственной трассировки


Наиболее популярное применение pvm_setopt - это ``включение'' прямолинейной маршрутизации между задачами PVM. В соответствии с обобщенными требованиями, пропускная сетевая коммуникационная способность PVM удваивается после вызова:

pvm_setopt(PvmRoute, PvmRouteDirect);
Препятствие заключается в том, что такой метод коммуникационного ускорения в UNIX не масштабируется, следовательно, он может не срабатывать, если приложение задействует более 60 задач, которые непредсказуемо взаимодействуют друг с другом. Если метод не срабатывает, то PVM автоматически переключается на использование ``стандартного'' коммуникационного метода. Вызов может выполняться из приложения множество раз - для избирательной установки режима ``прямых'' коммуникационных связей ``задача-задача''; но типичное его применение заключается в одиночном вызове после вызова pvm_mytid().


next up previous contents
Next: Обмен сообщениями Up: PVM - параллельная виртуальная Previous: Посылка сигналов   Contents
2004-06-22

next up previous contents
Next: Буферы сообщений Up: PVM - параллельная виртуальная Previous: Установка и получение опций   Contents

Обмен сообщениями

Посылка сообщения в PVM совершается тремя шагами. Первый: буфер передачи должен быть инициализирован вызовом
pvm_initsend() или pvm_mkbuf(). Второй: сообщение должно быть ``упаковано'' в этот буфер с помощью произвольного количества вызовов подпрограмм pvm_pk*() в любой комбинации. (На Фортране упаковка сообщений делается подпрограммой pvmfpack().) Третий: подготовленное сообщение посылается соответствующему процессу вызовом подпрограммы pvm_send() или широковещательной передачей с помощью подпрограммы pvm_mcast().

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

Если требуется, то с помощью PVM версии 3 прием можно обработать в дополнительном контексте. Подпрограмма pvm_recvf() позволяет пользователям определять свои собственные контексты приема, в которых будут работать все последующие подпрограммы приема PVM.



Subsections

2004-06-22

next up previous contents
Next: Упаковка данных Up: Обмен сообщениями Previous: Обмен сообщениями   Contents

Буферы сообщений

int bufid = pvm_initsend( int encoding)

call pvmfinitsend( encodingб bufid)

Если пользователь применяет только один буфер передачи (а это типовой случай), то возникает потребность только в одной ``буферной'' подпрограмме pvm_initsend(). Она вызывается для упаковки нового сообщения в буфер. Подпрограмма pvm_initsend очищает буфер передачи и пересоздает его для упаковки нового сообщения. Схема кодирования, используемая при упаковке, устанавливается с помощью encoding. Новый идентификатор буфера возвращается в bufid.

Опция encoding может иметь следующие значения:

PvmDataDefault.
По умолчанию используется XDR-кодирование - по той причине, что PVM не может знать, собирается ли пользователь добавить гетерогенную машину перед отправкой некоторого сообщения. Если пользователь знает о том, что очередное сообщение будет послано машине, которая понимает оригинальный формат, то он может воспользоваться кодированием PvmDataRaw и сэкономить вычислительные затраты.
PvmDataRaw.
Не выполняется никакого кодирования. Сообщения посылаются в исходном формате. Если принимающий процесс не сможет прочитать данный формат, он вернет ошибку в процессе распаковки.
PvmDataInPlace.
Данные остаются на своем месте с целью уменьшения затрат на кодирование. Буфер содержит только размеры элементов для передачи и указатели на них. Когда
pvm_send() вызывается, элементы копируются прямо из пользовательской памяти. Данная опция позволяет снизить число копирований сообщения, а значит, и затраты в соответствии с требованием пользователя не модифицировать элементы в интервале между моментом упаковки и моментом передачи. Дополнительным применением опции может быть: вызов одноразовой упаковки-модификации и последующая многократная передача определенных элементов (или массивов) по ходу работы приложения. Примером может быть передача пограничных регионов в дискретной реализации PDE.
Следующие подпрограммы работы с буферами сообщений нужны только в тех ситуациях, когда пользователь желает на уровне приложения управлять несколькими буферами сообщений. Большое количество буферов сообщений для большинства случаев обмена сообщениями между процессами вовсе не требуется . В PVM версии 3 в любой момент времени для каждого процесса существуют только один ``активный'' буфер передачи и только один ``активный'' буфер приема. Разработчик может создать произвольное число буферов сообщений и переключаться между ними с целью упаковки и передачи данных. Подпрограммы упаковки, передачи, приема и распаковки затрагивают только ``активные'' буферы.

int bufid = pvm_mkbuf( int encoding)

call pvmfmkbuf( encodingб bufid)

Подпрограммой pvm_mkbuf создается новый пустой буфер передачи и указывается метод кодирования для упаковки сообщений. Она возвращает идентификатор буфера bufid.

int info = pvm_freebuf( int bufid)

call pvmffreebuf(bufidб info)

Подпрограммой pvm_freebuf() возвращается в свободное пользование буфер с идентификатором bufid. Эти действия должны выполняться после того, как сообщение уже послано и больше не нужно. Если требуется, вызывайте pvm_mkbuf(), чтобы вновь создать буфер для нового сообщения. Ни один из данных запросов не нужен, если применяется pvm_initsend(), которая реализует эти функции за пользователя.

int bufid = pvm_getsbuf( void)

call pvmfgetsbuf( bufid)

int bufid = pvm_getrbuf( void)

call pvmfgetrbuf( bufid)

Подпрограмма pvm_getsbuf() возвращает идентификатор активного буфера передачи; pvm_getrbuf() возвращает идентификатор активного буфера приема.

Этими подпрограммами буфер с bufid устанавливается как активный буфер передачи (или приема); состояние предыдущего активного буфера сохраняется, а его идентификатор возвращается в oldbuf.

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

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

bufid = pvm_recv (src, tag);

oldid = pvm_setsbuf (bufid);

info = pvm_send (dst, tag);

info = pvm_freebuf (oldid);



2004-06-22

next up previous contents
Next: Передача и прием данных Up: Обмен сообщениями Previous: Буферы сообщений   Contents

Упаковка данных

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

Аргументами для каждой из подпрограмм являются: указатель на первый из элементов для упаковки, nitem - суммарное число элементов для упаковки из данного массива и stride - ``шаг'' для использования во время упаковки. Шаг, равный 1, означает последовательную упаковку вектора, равный 2 - упаковку ``через раз'' и т.д. Исключение составляет подпрограмма pvm_pkstr(), которая завершает упаковку строки символов при появлении NULL и поэтому не требует наличия аргументов nitem и stride.

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

int info = pvm_pkbyte( char *cp, int nitem,

    int stride)

int info = pvm_pkcplx( float *xp, 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 *np, int nitem,

    int stride)

int info = pvm_pklong( long *np, int nitem,

    int stride)

int info = pvm_pkshort( short *np, int nitem,

    int stride)

int info = pvm_pkstr( char *cp)

int info = pvm_packfconst( char fmt, ...)

Единственная подпрограмма на Фортране реализует все упаковочные функции перечисленных подпрограмм на C.

call pvmfpack( what, xp, nitem, stride, info)
Аргумент xp - это первый элемент для упаковки из массива. Заметьте, что на Фортране количество символов в упаковываемой строке должно указываться через nitem. Целое число what указывает тип упаковываемых данных. Поддерживаются следующие опции.

STRING 0 REAL4 4
BYTE 1 COMPLEX8 5
INTEGER2 2 REAL8 6
INTEGER4 3 COMPLEX16 7

Эти имена уже предопределены в параметрических конструкциях заголовочного файла /pvm3/include/pvm3.h. Ряд производителей может расширять этот список и включать в него поддержку 64-битных архитектур в своей реализации. INTEGER8, REAL16 и др. уже будут добавлены, как только будет реализована XDR-поддержка этих типов данных.



2004-06-22

next up previous contents
Next: Распаковка данных Up: Обмен сообщениями Previous: Упаковка данных   Contents

Передача и прием данных

int info = pvm_send( int tid, int msgtag)

call pvmfsend( tid, msgtag, info)

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

call pvmfmcast( ntask, tids, msgtag, info)

Подпрограмма pvm_send() помечает сообщение целочисленным идентификатором msgtag и передает его непосредственно процессу TID.

Подпрограмма pvm_mcast() помечает сообщение целочисленным идентификатором msgtag и широковещательно передает это сообщение всем задачам, указанным в целочисленном массиве tids (исключая себя). Массив tids имеет длину ntask.

int info = pvm_psend( int tid, int msgtag, void *vp,

    int cnt, int type)

call pvmfpsend( tid, msgtag, xp, cnt, type, info)

Подпрограмма pvm_psend() упаковывает и посылает массив данных указанного типа задаче, идентифицированной TID. Предопределенные типы данных на Фортране такие же, как и для pvmfpack(). В языке C аргумент type может иметь любое из следующих значений:

PVM_STR PVM_FLOAT
PVM_BYTE PVM_CPLX
PVM_SHORT PVM_DOUBLE
PVM_INT PVM_DCPLX
PVM_LONG PVM_UINT
PVM_USHORT PVM_ULONG

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

int bufid = pvm_recv( int tid, int msgtag)

call pvmfrecv( tid, msgtag, bufid)

Эта подпрограмма блокирующего приема будет ожидать до тех пор, пока от задачи с TID не поступит сообщение с меткой msgtag. Значение -1 в msgtid или TID означает ``все задачи'' (специальный символ). После поступления она помещает сообщение в новый создаваемый активный буфер приема. Предыдущий активный буфер приема очищается, если он не был сохранен вызовом pvm_setrbuf().

int bufid = pvm_nrecv(int tid, int msgtag)

call pvmfnrecv( tid, msgtag, bufid)

Если запрашиваемое сообщение не прибыло, то неблокирующий прием pvm_nrecv() при завершении вернет код, равный 0. Эта подпрограмма может вызываться сколько угодно раз для определенного сообщения - с целью проверки его прибытия - в промежутках выполнения работы программы. Если же возможной в данной ситуации работы не осталось, для того же сообщения можно воспользоваться блокирующим приемом pvm_recv(). Если сообщение с меткой msgtag поступило от задачи с TID, pvm_nrecv() помещает это сообщение в новый активный буфер (который она создает) и возвращает идентификатор данного буфера. Предыдущий активный буфер приема очищается, если он не был сохранен вызовом pvm_setrbuf(). Значение -1 в msgtid или TID означает ``все задачи'' (специальный символ).

int bufid = pvm_probe( int tid, int msgtag)

call pvmfprobe( tid, msgtag, bufid)

Если запрашиваемое сообщение не прибыло, то pvm_probe() возвращает bufid, равный 0. В противном случае она возвращает bufid сообщения, но не ``принимает'' его. Эта подпрограмма может вызываться сколько угодно раз для определенного сообщения - с целью проверки его прибытия - в промежутках выполнения работы. Дополнительно может быть вызвана pvmbufinfo() с возвращенным bufid - для получения информации о сообщении перед его непосредственным приемом.

int bufid = pvm_trecv( int tid, int msgtag,

    struct timeval *tmout)

call pvmftrecv( tid, msgtag, sec, usec, bufid)

PVM также поддерживает версию приема с тайм-аутом. Рассмотрим случай, при котором сообщение не прибывает никогда (из-за ошибки или сбоя): подпрограмма pvm_recv может заблокироваться навечно. Для избежания такой ситуации пользователь может захотеть ``прекратить'' ожидание после истечения фиксированного временного отрезка. Подпрограмма pvm_trecv() предоставляет пользователю возможность указать период тайм-аута. Если этот период очень велик, то pvm_trecv() действует подобно pvm_recv. Если же период тайм-аута установлен в ноль, то pvm_trecv() действует подобно pvm_nrecv. Так, pvm_trecv ``заполняет пробел'' между функциями блокирующего и неблокирующего приема.

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

int info = pvm_bufinfo( int bufid, int *bytes,

    int *msgtag, int *tid)

call pvmfbufinfo( bufid, bytes, msgtag, tid, info)

Подпрограмма pvm_precv() сочетает в себе функции блокирующего приема и распаковки буфера приема. Она не возвращает bufid. Вместо него она возвращает действительные значения TID, msgtag и cnt.

int info = pvm_precv( int tid, int msgtag, void *vp,

    int cnt, int type, int *rtid, int *rtag,

    int *rcnt)

call pvmfprecv( tid, msgtag, xp, cnt, type, rtid, rtag,

    rcnt, info)

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


next up previous contents
Next: Распаковка данных Up: Обмен сообщениями Previous: Упаковка данных   Contents
2004-06-22

next up previous contents
Next: Динамические группы процессов Up: Обмен сообщениями Previous: Передача и прием данных   Contents

Распаковка данных

Следующие подпрограммы на C распаковывают (многократно) данные определенных типов из активного буфера приема. На уровне приложения они должны соответствовать подпрограммам упаковки - по типу, числу элементов и шагу; nitem - число элементов данного типа для распаковки, а stride - шаг.

int info = pvm_upkbyte( char *cp, int nitem,

    int stride)

int info = pvm_upkcplx( float *xp, 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 *np, int nitem,

    int stride)

int info = pvm_upklong( long *np, int nitem,

    int stride)

int info = pvm_upkshort(short *np, int nitem,

    int stride)

int info = pvm_upkstr( char *cp)

int info = pvm_unpackf( const char *fmt, ...)

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

Единственная Фортран-подпрограмма выполняет все перечисленные функции приведенных C-подпрограмм.

call pvmfunpack( what, xp, nitem, stride, info)

Аргумент xp - это массив, куда помещается то, что распаковывается. Целочисленный аргумент what указывает тип данных для распаковки. (Та же опция what, что и для pvmfpack()).



2004-06-22

next up previous contents
Next: Примеры программ Up: PVM - параллельная виртуальная Previous: Распаковка данных   Contents

Динамические группы процессов

Функции динамической группировки процессов составляют основу ключевых подпрограмм PVM. Специализированная библиотека
libgpvm3 должна компоноваться с пользовательскими программами, которые используют любую групповую функцию; pvmd не реализует групповых функций. Эта задача обрабатывается групповым сервером, который запускается автоматически - при первом вызове групповой функции. Здесь приводятся некоторые сведения о том, как могут обрабатываться группы в среде с интерфейсом ``обмена сообщениями''. Выводы касаются эффективности и надежности - в данном случае найден компромисс между статическими и динамическими группами. Некоторые авторы приводят аргументы в пользу того, что только задачи из группы могут пользоваться групповыми функциями.

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

int inum = pvm_joingroup(char *group)

int info = pvm_lvgroup( char *group)

call pvmfjoingroup( group, inum)

call pvmflvgroup( group, info)

Эти подпрограммы позволяют задаче присоединиться к именованной пользователем группе и покинуть ее. Первый вызов
pvm_joingroup() создает группу с именем group и включает в нее вызывающую задачу; pvm_joingroup возвращает номер экземпляра процесса (inum) в некоторой группе. Номера экземпляров находятся в диапазоне от нуля до количества членов в группе - минус один. В PVM версии 3 одна задача может состоять в нескольких группах.

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

Чтобы помочь пользователю в управлении последовательностью назначения номеров, независимо от того, происходит присоединение или отсоединение, функция pvm_lvgroup() не завершается до тех пор, пока задача не будет достоверно извещена об этом факте; pvm_joingroup(), вызываемая после этого, назначит вакантный номер экземпляра новой задаче. Ответственность за последовательность назначения номеров экземпляров накладывается на пользователя, если в алгоритме задачи возникает потребность в этом. Если несколько задач покинет группу и в ней не останется членов, то в номерах экземпляров возникнут ``пробелы''.

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

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

int size = pvm_gsize( char *group)

call pvmfgettid( group, inum, tid)

call pvmfgetinst( group, tid, inum)

call pvmfgsize( group, size)

Подпрограмма pvm_gettid() возвращает TID процесса с именем группы и номером экземпляра; pvm_gettid() позволяет двум не знающим друг друга задачам получить TID друг друга посредством присоединения к общей группе. Подпрограмма pvm_getinst возвращает номер задачи с TID из указанной группы. Подпрограмма pvm_gsize возвращает количество членов в указанной группе.

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

call pvmfbarrier( group, count, info)

Вызовом pvm_barrier() процесс блокируется до тех пор, пока count членов группы не вызовут pvm_barrier. В общем случае count должен совпадать с количеством членов в группе. Счетчик требуется потому, что при динамической группировке процессов PVM не может знать, сколько процессов имеется в группе в данный момент времени. Ошибочным действием для процесса будет вызов pvm_barrier с указанием группы, членом которой он не является. Также ошибочными, при данной барьерной синхронизации, будут вызовы с несовпадающими аргументами count. Например, ошибка возникает, если один член группы вызывает pvm_barrier() со счетчиком, равным 4, а другой член вызывает pvm_barrier со счетчиком, равным 5.

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

call pvmfbcast( group, msgtag, info)

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

int info = pvm_reduce( void (*func()), void *data,

    int nitem, int datatype, int msgtag, char *group,

    int root)

call pvmfreduce( func, data, count, datatype, msgtag,

     group, root, info)

Подпрограмма pvm_reduce() выполняет глобальную арифметическую операцию в группе, например нахождение глобальной суммы или глобального максимума. Результат редуцирующей операции помещается в root. PVM поддерживает четыре предопределенные функции, которые пользователь может передать через func. Это функции PvmMax, PvmMin, PvmSum, PvmProduct

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

В дополнение, пользователи могут определять свои собственные функции для глобальных операций и указывать их в func. Пример дается в исходных текстах PVM. Обратитесь к PVM_ROOT/examples/ gexamples.

Функция pvm_reduce() не блокируется. Если задача вызывает pvm_reduce, а затем покидает группу до того, как результат pvm_reduce появится в root, то может произойти ошибка.



2004-06-22

next up previous contents
Next: Структуры данных очередей сообщений Up: Очереди сообщений Previous: Очереди сообщений   Contents

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

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

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

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

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

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



2004-06-22

next up previous contents
Next: ``Раздваивание - присоединение'' Up: PVM - параллельная виртуальная Previous: Динамические группы процессов   Contents

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

В этом разделе обсуждаются несколько завершенных программ. Первый пример - forkjoin.c - показывает, как порождать процессы и синхронизировать их. Второй пример - PSDOT.F - нужен при обсуждении программы вычисления так называемого точечного произведения на Фортране. Третьим примером - failure.c - демонстрируется, как пользователь может применять вызов
pvm_notify() для создания ``устойчивых'' приложений. Представлен пример матричного умножения. И наконец, показано, как PVM может быть использована для вычислений, связанных c высокотемпературной диффузией.



2004-06-22

next up previous contents
Next: Точечное произведение Up: PVM - параллельная виртуальная Previous: Примеры программ   Contents

``Раздваивание - присоединение''

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

Программа ``раздваивание - присоединение'' (рис. [*]) содержит код как задачи-предка, так и задачи-потомка. Самое первое действие, которое делает программа - вызов функции pvm_mytid(). Функция должна вызываться для того, чтобы впоследствии можно было выполнять остальные вызовы PVM. В результате pvm_mytid() всегда должно возвращаться положительное целое число. Если это не так, то что-то осуществлено неправильно. В примере ``раздваивание - присоединение'' проверяется значение mytid; если оно отражает ошибку, то вызывается pvm_perror() и осуществляется выход из программы. Вызов pvm_perror() выведет на экран сообщение, показывающее, что при последнем вызове PVM имела место ошибка. В данном случае, последним вызовом был pvm_mytid(), так что pvm_perror может вывести на экран сообщение о том, что PVM не смогла запуститься на текущей машине. Аргумент pvm_perror() - это строка, которой при выводе на экран непосредственно будет предшествовать само сообщение об ошибке. В данном случае передается argv[0], который содержит имя программы в том виде, в котором оно было введено через командную строку. Функция pvm_perror() вызывается после функции UNIX perror().

Допустим, что в результате получается достоверное значение mytid. Теперь вызывается pvm_parent(). Функция pvm_parent() вернет TID задачи, которая породила задачу, сделавшую вызов. Начиная с того момента, как была запущена на выполнение инициирующая программа ``раздваивание-присоединение'' из оболочки UNIX, она не имеет предка; эта инициирующая задача не будет порождаться другими задачами, но может снова запускаться пользователями вручную. Для инициирующей задачи ``раздваивание - присоединение'' результатом pvm_parent будет не определенный идентификатор задачи, а код ошибки PvmNoParent. Таким образом - проверкой эквивалентности результата вызова pvm_parent() значению PvmNoParent - можно отличить задачу-предка ``раздваивание - присоединение'' от потомка. Естественно, если задача является предком, то она должна породить потомка. Если же она предком не является, то должна послать предку сообщение.

Количество задач определяется посредством командной строки - аргументом argv[1]. Если указанное количество задач недопустимо, то программа завершается, вызывая pvm_exit(). Вызов pvm_exit() очень важен, потому что он сообщает PVM, что программа не будет больше пользоваться ее услугами. (В подобном случае, задача выходит из PVM, а PVM сделает вывод о том, что задача ``умерла'' и больше не требует обслуживания. Так или иначе - это правильный стиль ``чистого'' завершения.) Приняв, что количество задач допустимо, после этого программа ``раздваивание - присоединение'' будет пытаться породить потомка.

Вызов pvm_spawn() приказывает PVM запустить ntask задач с именами argv[0]. Второй параметр - список аргументов для передачи порождаемым задачам. В данном случае не нужно заботиться о передаче потомкам частных аргументов через командную строку, поэтому он равен NULL. Третий параметр при порождении -
PvmTaskDefault - флаг, приказывающий PVM породить задачи в ``местах'' по умолчанию. Если бы было необходимо размещение потомков на специфических машинах или на машинах с особенной архитектурой, то можно было бы воспользоваться значением флага PvmTaskHost или PvmTaskArch, а четвертым параметром указать хост или архитектуру. Поскольку не важно, где задачи будут исполняться, используется значение PvmTaskDefault для флага и значение NULL для четвертого параметра. Наконец, через ntask сообщается число задач для запуска; целочисленный массив для потомков будет содержать идентификаторы задач вновь порожденных потомков. Возвращаемое pvm_spawn() значение отражает, сколько задач было порождено успешно. Если info не соответствует ntask, то в процессе порождения произошел ряд ошибок. В случае с ошибкой ее код помещается в массив идентификаторов задач вместо действительного идентификатора задачи соответствующего потомка. В программе ``раздваивание - присоединение'' этот массив проверяется с помощью цикла, а идентификаторы задач и возможные коды ошибок выводятся на экран. Если ни одна задача не порождена успешно, то осуществляется выход из программы.

От каждой задачи-потомка предок принимает сообщение и выводит его содержимое на экран. Вызовом pvm_recv() принимается сообщение (с JOINTAG) от любой задачи. Возвращаемое pvm_recv() значение - это целое число, определяющее буфер сообщения. Это целое число может быть использовано как информация для поиска буферов сообщений. Последующий вызов pvm_bufinfo() определяет длину, тег и идентификатор задачи передающего процесса для сообщения, заданного с помощью buf. В программе ``раздваивание-присоединение'' сообщения, посланные потомками, содержат только по одному целочисленному значению - идентификатору задачи соответствующей задачи-потомка. Вызовом pvm_upkint() целое число распаковывается из сообщения в переменную mydata. В программе ``раздваивание - присоединение'' тестируются значения
mydata и идентификатора задачи, возвращенного pvm_bufinfo(). Если эти значения различны, то программа содержит ошибку и сообщение об ошибке выводится на экран. Наконец на экран выводится информация из сообщения, и на этом работа программы-предка завершается.

Последний сегмент кода программы ``раздваивание - присоединение'' будет исполняться как задача-потомок. Чтобы можно было поместить данные в буфер сообщения, он должен быть инициализирован вызовом pvm_initsend(). Параметр PvmDataDefault говорит о том, что PVM должна преобразовать данные обычным способом, чтобы удостовериться, что они прибыли в формате, корректном для целевого процессора. В некоторых случаях нет необходимости в преобразовании данных. Если пользователь уверен, что целевая машина использует тот же формат данных и, действительно, преобразовывать данные нет нужды, то он может применить PvmDataRaw в качестве параметра pvm_initsend(). Вызов pvm_pkint() размещает единственное целое число - metid - в буфере сообщения. Важно быть уверенными, что соответствующий распаковочный вызов точно соответствует данному упаковочному. Если число упаковывается как целое, а распаковывается как число с плавающей запятой, то такой подход не корректен. Аналогично, если пользователь упаковывает два целых числа одним вызовом, то он не сможет их впоследствии распаковать двойным вызовом pvm_upkint() - по одному вызову на каждое целое число. Таким образом, между упаковочным и распаковочным вызовами должно существовать соответствие ``один к одному''. В завершение сообщение с тегом JOINTAG передается задаче-предку.

Пример "раздваивание - присоединение":

/*

Пример ``раздваивание-присоединение''

Демонстрируется, как порождать процессы и

обмениваться сообщениями

*/

/* определения и прототипы библиотеки PVM */

#include <pvm3.h>

/* максимальное число потомков, которые будут

   порождаться этой программой */

#define MAXNCHILD 20

/* тег для использования в сообщениях, связанных

  с присоединением */

#define JOINTAG 11

 

int main(int argc, char* argv[]) {

 /* количество задач для порождения, 3 используются

   по умолчанию */

 int ntask = 3;

 /* код возврата для вызовов PVM */

 int info;

 /* свой идентификатор задачи */

 int mytid;

 /* свой идентификатор задачи-предка*/

 int myparent;

 /* массив идентификаторов задач-потомков*/

 int child[MAXNCHILD];

 int i, mydata, buf, len, tag, tid;

 /* поиск своего идентификатора задачи */

 mytid = pvm_mytid();

 /* проверка на ошибки*/

 if (mytid < 0) {

     /* вывод на экран сообщения об ошибке*/

     pvm_perror(argv[0]);

     /* выход из программы */

     return -1;

 }

 /* нахождение числа-идентификатора задачи-предка */

 myparent = pvm_parent();

 /* выход, если есть ошибки, но не PvmNoParent */

 if ((myparent < 0 ) && (myparent != PvmNoParent() {

     pvm_perror(argv[0]);

     pvm_exit();

     return -1;

 }

 /* если предка не найдено, то это и есть предок */

 if (myparent == PvmNoParent) {

  /* определение числа задач для порождения */

  if (argc == 2) ntask = atoi(argv[1]);

  /* удостоверение, что ntask - допустимо */

  if ((ntask < 1) || (ntask > MAXNCHILD))

    {pvm_exit();

    return 0;}

  /* порождение задач-потомков*/

  info = pvm_spawn(argv[0], (char**)0, PvmTaskDefault,

    (char*)0, ntask, child);

  /* вывод на экран идентификаторов задач */

  for (i = 0; i < ntask; i++)

  if (child[i] < 0) /* вывод на экран десятичного

                           кода ошибки*/

    printf(" %d", child[i]);

  else /* вывод на экран шестнадцатеричного

        идентификатора задачи */

   printf("t%x\t", child[i]);

   putchar('\n');

   /* удостоверение, что порождение произошло успешно */

  if (info == 0) {pvm_exit(); return -1;}

  /* ожидание ответов только от тех потомков, которые

  порождены успешно */

  ntask = info;

  for (i = 0; i < ntask; i++) {

  /* прием сообщения от любого процесса-потомка */

  buf = pvm_recv(-1, JOINTAG);

  if (buf < 0) pvm_perror("calling recv");

  info = pvm_bufinfo(buf, &len, &tag, &tid);

  if (info < 0) pvm_perror("calling pvm_bufinfo");

  info = pvm_upkint(&mydata, 1, 1);

  if (info < 0) pvm_perror("calling pvm_upkint");

  if (mydata != tid)

    printf("Этого не может быть!\n");

  printf("Длина %d, Tag %d, Tid t%x\n",

         len, tag, tid);

 }

 pvm_exit();

 return 0;

}

/* это потомок */

info = pvm_initsend(PvmDataDefault);

if (info < 0) {

   pvm_perror("calling pvm_initsend"); pvm_exit();

   return -1;

}

info = pvm_pkint(&mytid, 1, 1);

if (info < 0) {

   pvm_perror("calling pvm_pkint"); pvm_exit();

   return -1;

}

info = pvm_send(myparent, JOINTAG);

if (info < 0) {

   pvm_perror("calling pvm_initsend"); pvm_exit();

   return -1;

}

pvm_exit(); return 0;

}

Ниже показано, что выводит на экран выполняющаяся программа ``раздваивание - присоединание''.

% forkjoin

t10001c t40149 tc0037

Длина 4, Tag 11, Tid t40149

Длина 4, Tag 11, Tid tc0037

Длина 4, Tag 11, Tid t10001c

% forkjoin 4

t10001e t10001d t4014b tc0038

Длина 4, Tag 11, Tid t4014b

Длина 4, Tag 11, Tid tc0038

Длина 4, Tag 11, Tid t10001d

Длина 4, Tag 11, Tid t10001e

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


next up previous contents
Next: Точечное произведение Up: PVM - параллельная виртуальная Previous: Примеры программ   Contents
2004-06-22

next up previous contents
Next: Пример с ошибкой Up: PVM - параллельная виртуальная Previous: ``Раздваивание - присоединение''   Contents

Точечное произведение

В этом разделе приведена простая программа на Фортране - PSDOT - для вычисления точечного произведения. Программа вычисляет точечное произведение массивов X и Y. В первую очередь в PSDOT вызываются PVMFMYTID() и PVMFPARENT(). Вызов PVMFPARENT вернет PVMNOPARENT, если задача не была ранее порождена другой задачей PVM. Если это случай, когда PSDOT - ведущая и, следовательно, должна породить отдельные рабочие копии PSDOT, то у пользователя запрашивается число рабочих процессов и длина векторов для вычисления. Каждый порождаемый процесс будет принимать $n/nproc$ элементов X и Y, где $n$ - длина векторов, а $nproc$ - количество процессов, используемых при вычислении. Если $n$ не делится на $nproc$ нацело, то ведущий будет вычислять точечное произведение ``дополнительных элементов''. Подпрограммой SGENMAT случайным образом генерируются значения X и Y. После этого PSDOT порождает $nproc-1$ своих копий и передает каждой новой задаче часть массивов X и Y. Каждое сообщение содержит размеры подмассивов и, собственно, сами подмассивы. После того как ведущий породил рабочие процессы и передал подвекторы, он вычисляет точечное произведение своей порции X и Y. Затем ведущий процесс принимает остальные локальные точечные произведения от рабочих процессов. Обратите внимание на то, что при вызове PVMFRECV как параметр-идентификатор задачи используется специальный символ (-1). Это говорит о том, что сообщение от ``любой'' задачи будет устраивать принимающую сторону. Применение специального символа таким образом может привести к ``гибридизации''. Но в данном случае ``гибридизация'' не создаст проблему, поскольку сложение по природе коммутативно. Другими словами, совершенно не важно, в каком порядке складываются частичные суммы, полученные от рабочих. Если кто-то не уверен, что ``гибридизация'' не приведет к нежелательным программным эффектам, то ему желательно избегать ее возникновения.

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

Если программа PSDOT -это рабочий, то она принимает содержащее подмассивы X и Y сообщение от ведущего процесса. Она вычисляет точечное произведение этих подмассивов и передает результат назад ведущему процессу. В целях краткости сюда не включены подпрограммы SGENMAT и SDOT.

Программа - пример PSDOT.F:

PROGRAM PSDOT

*

* PSDOT параллельно реализует внутреннее (или точечное)

* произведение:

* векторы X и Y сначала находятся на ведущей станции,

* которая затем устанавливает виртуальную машину,

* раздает данные и задания и наконец суммирует

* локальные результаты для получения глобального

* внутреннего произведения.

*

* .. Внешние подпрограммы ..

EXTERNAL PVMFMYTID, PVMFPARENT, PVMFSPAWN, PVMFEXIT

EXTERNAL PVMFINITSEND

EXTERNAL PVMFPACK, PVMFSEND, PVMFRECV, PVMFUNPACK

EXTERNAL SGENMAT

*

* .. Внешние функции ..

INTEGER ISAMAX

REAL SDOT

EXTERNAL ISAMAX, SDOT

*

* .. Внутренние функции ..

INTRINSIC MOD

*

* .. Параметры ..

INTEGER MAXN

PARAMETER ( MAXN = 8000 )

INCLUDE 'fpvm3.h'

*

* .. Скаляры ..

INTEGER N_ LN_ MYTID_ NPROCS_ IBUF_ IERR

INTEGER I, J, K

REAL LDOT, GDOT

*

* .. Массивы ..

INTEGER TIDS(0:63)

REAL X(MAXN), Y(MAXN)

*

* Регистрация в PVM и получение идентификаторов задач

* своего и ведущего процессов.

*

CALL PVMFMYTID( MYTID )

CALL PVMFPARENT( TIDS(0) )

*

* Нужно ли порождать другие процессы 

* (Это - ведущий процесс).

*

IF ( TIDS(0) .EQ. PVMNOPARENT ) THEN

*

* Получение исходных данных.

*

WRITE(*.*)

    'Сколько процессов нужно создать (1-64)?'

READ(*.*) NPROCS

WRITE(*,2000) MAXN

READ(*.*) N

TIDS(0) = MYTID

IF ( N .GT. MAXN ) THEN

WRITE(*.*) 'N слишком велико.

  Увеличьте параметр MAXN'//$ 'для этого случая.'

STOP

END IF

*

* LN - количество элементов точечного произведения

* для локальной обработки. Все имеют одинаковое

* количество, а ``лишние'' элементы достаются ведущему.

* ``Общее'' количество элементов хранится в J.

*

J = N / NPROCS

LN = J + MOD(N, NPROCS)

I = LN + 1

*

* Генерирование случайных X и Y.

*

CALL SGENMAT( N, 1, X, N, MYTID, NPROCS, MAXN, J )

CALL SGENMAT( N, 1, Y, N, I, N, LN, NPROCS )

*

* Обход всех рабочих процессов.

*

DO 10 K = 1, NPROCS-1

*

* Порождение процесса и проверка на ошибки.

*

CALL PVMFSPAWN( 'psdot', 0, 'anywhere', 1, TIDS(K),

     IERR )

IF (IERR .NE. 1) THEN

WRITE(*.*) 'ERROR, невозможно создать процесс #',K,

$ '. Dying . . .'

CALL PVMFEXIT( IERR )

STOP

END IF

*

* Рассылка исходных данных.

*

CALL PVMFINITSEND( PVMDEFAULT, IBUF )

CALL PVMFPACK( INTEGER4, J, 1, 1, IERR )

CALL PVMFPACK( REAL4, X(I), J, 1, IERR )

CALL PVMFPACK( REAL4, Y(I), J, 1, IERR )

CALL PVMFSEND( TIDS(K), 0, IERR )

I = I + J

10 CONTINUE

*

* Вычисление ведущим своей части точечного произведения.

*

GDOT = SDOT( LN, X, 1, Y, 1 )

*

* Получение локальных точечных произведений и их

* сложение с целью формирования глобального.

*

DO 20 K = 1, NPROCS-1)

CALL PVMFRECV( -1, 1, IBUF )

CALL PVMFUNPACK( REAL4, LDOT, 1, 1, IERR )

GDOT = GDOT + LDOT

20 CONTINUE

*

* Вывод на экран результатов.

*

WRITE(*,*) ' '

WRITE(*,*) '<x,y> = ',GDOT

*

* ``Последовательное'' вычисление точечного произведения

* и его вычитание из ``распределенного'' точечного

* произведения - с целью проверки

* на допустимость уровня ошибок.

*

LDOT = SDOT( N, X, 1, Y, 1 )

WRITE(*,*) '<x,y> : последовательное произведение.

    <x,y>^ : ' $ 'распределенное произведение.'

WRITE(*,*) '| <x,y> - <x,y>^ | = ',ABS(GDOT = LDOT)

WRITE(*,*) 'Завершено.'

*

* Является ли этот процесс рабочим?

*

ELSE

*

* Прием исходных данных.

*

CALL PVMFRECV( TIDS(0), 0, IBUF )

CALL PVMFUNPACK( INTEGER4, LN, 1, 1, IERR )

CALL PVMFUNPACK( REAL, X, LN, 1, IERR )

CALL PVMFUNPACK( REAL, Y, LN, 1, IERR )

*

* Вычисление локального точечного произведения

* и передача его ведущему.

*

LDOT = SDOT( LN, X, 1, Y, 1 )

CALL PVMFINITSEND( PVMDEFAULT, IBUF )

CALL PVMFPACK( REAL4, LDOT, 1, 1, IERR )

CALL PVMFSEND( TIDS(0), 1, IERR )

END IF

*

CALL PVMFEXIT( 0 )

*

1000 FORMAT(I10, 'Успешно порожден процесс #',I2,',

    TID =',I10)

2000 FORMAT('Введите длину перемножаемых векторов

   (1 -',I7,'):')

STOP

*

* End program PSDOT

*

END


next up previous contents
Next: Пример с ошибкой Up: PVM - параллельная виртуальная Previous: ``Раздваивание - присоединение''   Contents
2004-06-22

next up previous contents
Next: Матричное умножение Up: PVM - параллельная виртуальная Previous: Точечное произведение   Contents

Пример с ошибкой

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

После реализации запроса об уведомлениях задача-предок принудительно завершает работу одного из потомков. Вызовом
pvm_kill() принудительно завершается работа исключительно той задачи, чей идентификатор передан как параметр. После завершения одной из порожденных задач, предок с помощью
pvm_recv(-1, TASKDIED) ожидает сообщения, уведомляющего о
``смерти'' этой задачи. Идентификатор задачи, которая завершилась, передается в уведомительном сообщении в виде одного целого числа. Процесс распаковывает идентификатор ``мертвой'' задачи и выводит его на экран. Хорошим стилем так же считается и вывод заранее известного идентификатора завершаемой задачи. Эти идентификаторы должны быть идентичны. Задачи-потомки просто ждут примерно по минуте и затем спокойно завершаются.

Программа - пример failure.c:

/*

Пример уведомления о сбое

Демонстрируется, как сообщать о фактах завершения задач

*/

/* определения и прототипы библиотеки PVM */

#include <pvm3.h>

/* максимальное число потомков, которые

   будут порождаться этой программой */

#define MAXNCHILD 20

/* тег для использования в сообщениях, связанных

   с отработкой задач */

#define TASKDIED 11

int

main(int argc, char* argv[])

{

/* количество задач для порождения,

     3 используются по умолчанию */

int ntask = 3;

/* код возврата для вызовов PVM */

int info;

/* свой идентификатор задачи */

int mytid;

/* свой идентификатор задачи-предка */

int myparent;

/* массив идентификаторов задач-потомков */

int child[MAXNCHILD];

int i, deadtid;

int tid;

char *argv[5];

/* поиск своего идентификатора задачи */

mytid = pvm_mytid();

/* проверка на ошибки */

if (mytid < 0) {

/* вывод на экран сообщения об ошибке */

pvm_perror(argv[0]);

/* выход из программы */

return -1;

}

/* нахождение числа-идентификатора задачи-предка */

myparent = pvm_parent();

/* выход, если есть ошибки, но не PvmNoParent */

if ((myparent < 0) && (myparent != PvmNoParent)) {

  pvm_perror(argv[0]);

  pvm_exit();

  return -1;

}

/* если предок не найден, то это и есть предок */

if (myparent == PvmNoParent) {

/* определение числа задач для порождения */

if (argc == 2) ntask = atoi(argv[1]);

/* удостоверение, что ntask - допустимо */

if ((ntask < 1) || (ntask > MAXNCHILD)) {pvm_exit();

    return 0; }

/* порождение задач-потомков */

info = pvm_spawn(argv[0], (char**), PvmTaskDebug,

    (char*)0, ntask, child);

/* удостоверение, что порождение произошло успешно */

if (info != ntask) { pvm_exit(); return -1; }

/* вывод на экран идентификаторов задач */

for (i = 0; i < ntask; i++) printf("t%x\t",child[i]);

    putchar('\n');

/* запрос об уведомлении о завершении потомка */

info = pvm_notify(PvmTaskExit, TASKDIED, ntask,

      child);

if (info < 0) { pvm_perror("notify"); pvm_exit();

    return -1; }

/* уничтожение потомка со ``средним''

   идентификатором */

info = pvm_kill(child[ntask/2]);

if (info < 0) { pvm_perror("kill"); pvm_exit();

    return -1; }

/* ожидание уведомления */

info = pvm_recv(-1, TASKDIED);

if (info < 0) { pvm_ perror("recv"); pvm_exit();

    return -1; }

info = pvm_upkint(&deadtid, 1, 1);

if (info < 0) pvm_perror("calling pvm_upkint");

/* должен быть потомок со ``средним'' номером */

printf("Задача t%x завершилась.\n", deadtid);

printf("Задача t%x является средним потомком.\n",

      child[ntask/2]);

pvm_exit();

return 0;

}

/* это потомок */

sleep(63);

pvm_exit();

return 0;

}


next up previous contents
Next: Матричное умножение Up: PVM - параллельная виртуальная Previous: Точечное произведение   Contents
2004-06-22

next up previous contents
Next: Одномерное температурное уравнение Up: PVM - параллельная виртуальная Previous: Пример с ошибкой   Contents

Матричное умножение

В следующем примере программируется алгоритм матричного умножения, предложенный Фоксом (Fox)[8]. Программа
mmult будет вычислять $C=AB$, где $C$, $A$ и $B$ - все квадратные матрицы. С целью упрощения предполагается, что для вычисления результата будут использоваться $m\times m$ задач. Каждая задача будет вычислять подблок результирующей матрицы $C$. Размер блока и значение $m$ передаются программе как аргументы командной строки. Матрицы $A$ и $B$ также устанавливаются в виде блоков, распределенных в среде из $m^{2}$ задач.

Предположим, что существует ``сеть'' из $m\times m$ задач. Каждая задача ($t_{i,j}$, где $0\leq i,j<m$) первоначально содержит блоки $C_{i,j}$, $A_{i,j}$ и $B_{i,j}$. Первым шагом алгоритма задачи, расположенные по диагонали ($t_{i,j}$, где $i=j$), передают свои блоки $A_{i,i}$ всем остальным задачам в строке $i$. После передачи $A_{i,i}$ каждая задача вычисляет $A_{i,i}\times B_{i,j}$ и добавляет результат к $C_{i,j}$. Следующим шагом блоки $B$ поворачиваются по столбцам. Так, задача $t_{i,j}$ передает свой блок $B$ задаче $t_{i-1,j}$ (задача $t_{0,j}$ передает свой блок $B$ задаче $t_{m-1,j}$). Затем задачи возвращаются к первому шагу: $A_{i,i+1}$ широковещательно передается всем другим задачам в строке $i$ - алгоритм продолжается. После $m$ итераций матрица $C$ будет содержать $A\times B$, а матрица $B$ вернется к исходному состоянию.

В PVM не накладывается ограничений на то, как задача может связываться с любой другой задачей. Однако для данной программы хотелось бы представлять задачи в виде двухмерной модели тора. Чтобы учесть задачи, каждая из них включается в группу mmult. Групповые идентификаторы используются для отображения задач на наш тор. При включении в группу первой задачи она получает групповой идентификатор 0. В программе mmult, задача с нулевым групповым идентификатором порождает остальные задачи и передает параметры матричного умножения этим задачам. Такими параметрами являются m и bklsize - квадратный корень числа блоков и соответственно размер блока. После того, как все задачи порождены, а параметры переданы, вызывается pvm_barrier() - чтобы удостовериться, что все задачи присоединились к группе. Если барьер не организован, то последующий вызов pvm_gettid() может завершиться ненормально, так как некая задача к тому моменту может быть еще не в составе группы.

После того как барьер организован, сохраняются идентификаторы задач из строки myrow в массиве. Это достигается путем определения групповых идентификаторов всех задач в строке и запрашивания PVM о соответствующих им идентификаторах задач. Далее, с помощью malloc() выделяется место для блоков матриц. В действительном программном приложении, может случиться так, что матрицы уже распределены. Далее программа определяет строку и столбец блока $C$, который будет вычисляться. Это основывается на значениях групповых идентификаторов - в диапазоне от $0$ до $m-1$ включительно. Так, если допустить построчное отображение групповых идентификаторов задач, целочисленное деление mygid/m даст строку задач, а mygid mod m - даст столбец. С использованием подобного отображения определяется групповой идентификатор задачи, расположенной в торе ``выше'' или ``ниже'', и сохраняется соответственно в up и down.

Далее блоки инициализируются вызовом InitBlock(): $A$ - случайными числами, $B$ - идентичной матрицей, а $C$ - нулями. Это позволит нам верифицировать вычисление в конце программы простой проверкой равенства $A=C$.

Наконец, выполняется главный вычислительный цикл матричного умножения. Сначала диагональные задачи широковещательно передают имеющиеся блоки $A$ другим задачам в своих строках. Отметим, что массив myrow в действительности содержит идентификаторы задач, выполняющих широковещательные передачи. При повторных вызовах pvm_mcast() сообщения будут передаваться всем задачам из массива, исключая делающую вызов задачу. Для задач mmult эта процедура великолепно срабатывает - чтобы напрасно не обрабатывать ``лишнее'' сообщение, поступающее самой широковещательно передающей задаче при ``лишнем'' pvm_recv(). Как широковещательно передающая задача, так и задачи, принимающие блок, вычисляют $A\times B$ с использованием и диагонального блока $B$ и блоков $B$, принадлежащих задачам.

После того как подблоки умножены и результат добавлен к блоку $C$, вертикально сдвигаются блоки $B$. Особенным образом блок $B$ упаковывается в сообщение, передается задаче с идентификатором задачи up и затем принимается новый блок $B$ от задачи с идентификатором задачи down.

Обратите внимание на то, что используются различные теги сообщений при передачах блоков $A$ и блоков $B$ при различных циклических итерациях. Полностью указываются и идентификаторы задач при выполнении pvm_recv(). Возникает соблазн применять специальные символы в полях pvm_recv(), однако, такая практика может быть опасной. К примеру, если некорректно определить значение для up и указать специальный символ при pvm_recv() вместо down, то это может привести к ``неосознанной'' передаче сообщений не тем задачам. В примере сообщения однозначно направлены, тем самым уменьшая вероятность возникновения ошибок при приеме сообщений от не тех задач, т.е. ошибочных фаз алгоритма.

Как только вычисление завершено, проверяется $A=C$ - только для того, чтобы убедиться в правильности матричного умножения, т.е. корректности значений $C$. Такую проверку не возможно осуществить, например, с помощью подпрограмм из библиотеки матричного умножения.

В вызове pvm_lvgroup() нет необходимости, поскольку PVM выгрузит отработавшие задачи и удалит их из группы. Однако хорошим примером является ``явный'' выход из группы перед вызовом pvm_exit(). Команда консоли PVM reset ``сбросит'' все группы PVM. Команда pvm_gstat выведет на экран статус любой из существующих групп.

/*

Матричное умножение

*/

/* определения и прототипы библиотеки PVM */

#include <pvm3.h>

#include <stdio.h>

/* максимальное число потомков, которые

   будут порождаться этой программой */

#define MAXNTIDS 100

#define MAXNROW 10

/* теги сообщений */

#define ATAG 2

#define BTAG 3

#define DIMTAG 5

void

InitBlock(float *a, float *b, float *c, int blk,

  int row, int col) {

 int len, ind;

 int i, j;

 srand_pvm_mytid__

 len = blk*blk;

 for (ind = 0; ind < len; ind++)

 { a[ind] = (float)(rand()%1000)/100.0; c[ind] = 0.0; }

  for (i = 0; i < blk; i++) {

   for (j = 0; i < blk; j++) {

    if (row == col)

      b[j*blk+i] = (i==j)? 1.0 : 0.0;

    else

      b[j*blk+i] = 0.0;

   }

  }

}

void

BlockMult(float* c, float* a, float* b, int blk) {

 int i,j,k;

 for (i = 0; i < blk; i++)

  for (j = 0; i < blk; j++)

   for (k = 0; k < blk; k++)

    for (j = 0; i < blk; j++)

     c[i*blk+j] += (a[i*blk+k] * b[k*blk+j]);

}

 

int

main(int argc, char* argv[])

{

 /* количество задач для порождения, 3 используются

   по умолчанию */

 int ntask = 2;

 /* код возврата для вызовов PVM */

 int info;

 /* свой идентификатор задачи и групповой

   идентификатор */

 int mytid, mygid;

 /* массив идентификаторов задач-потомков*/

 int child[MAXNTIDS-1];

 int i, m, blksize;

 /* массив идентификаторов задач в своей строке*/

 int myrow[MAXROW];

 float *a, *b, *c, *atmp;

 int row, col, up, down;

 /* поиск своего идентификатора задачи */

 mytid = pvm_mytid();

 pvm_setopt(PvmRoute, PvmRouteDirect);

 /* проверка на ошибки*/

 if (mytid < 0) {

  /* вывод на экран сообщения об ошибке*/

  pvm_perror(argv[0]);

  /* выход из программы */

  return -1;

 }

 /* присоединение к группе mmult*/

 mygid = pvm_joingroup("mmult");

 if (mygid < 0) {

  pvm_perror(argv[0]); pvm_exit(); return -1;

 }

 /* если свой групповой идентификатор не равен 0,

   то нужно породить другие задачи */

 if (mygid == 0) {

   /* определение числа задач для порождения */

   if (argc == 3) {

    m = atoi(argv[1]);

    blksize = atoi(argv[2]);

   }

   if (argc < 3) {

    fprintf(stderr, "usage: mmult m blk\n");

    pvm_lvgroup("mmult"); pvm_exit(); return -1;

   }

   /* удостоверение, что ntask - допустимо */

   ntask = m*m;

   if ((ntask < 1) || (ntask > MAXNTIDS)) {

   fprintf(stderr, "ntask = %d not valid.\n",

           ntask);

   pvm_lvgroup("mmult"); pvm_exit(); return -1;

   }

   /* порождать не нужно, если имеется

    только одна задача */

   if (ntask == 1) goto barrier;

   /* порождение задач-потомков*/

   info = pvm_spawn("mmult", (char**)0,

        PvmTaskDefault, (char*)0, ntask-1, child);

   /* удостоверение, что порождение

     произошло успешно */

   if (info != ntask-1) {

   pvm_lvgroup("mmult"); pvm_exit(); return -1;

   }

   /* передача размерности матрицы */

   pvm_initsend(PvmDataDefault);

   pvm_pkint(&m, 1, 1);

   pvm_pkint(&blksize, 1, 1);

   pvm_mcast(child, ntask-1, DIMTAG);

 }

 else {

   /* прием размерности матрицы */

   pvm_recv(pvm_gettid("mmult", 0), DIMTAG);

   pvm_pkint(&m, 1, 1);

   pvm_pkint(&blksize, 1, 1);

   ntask = m*m;

 }

 /* удостоверение, что все задачи

   присоединились к группе */

 barrier:

 info = pvm_barrier("mmult",ntask);

 if (info < 0) pvm_perror(argv[0]);

 /* поиск идентификаторов задач в своей строке */

 for (i = 0; i < m; i++)

 myrow[i] = pvm_gettid("mmult", (mygid/m)*m + i);

 /* распределение памяти для локальных блоков */

 a = (float*)malloc(sizeof(float)*blksize+blksize);

 b = (float*)malloc(sizeof(float)*blksize+blksize);

 c = (float*)malloc(sizeof(float)*blksize+blksize);

 atmp = (float*)malloc(sizeof(float)*blksize+blksize);

 /* проверка достоверности указателей */

 if (!(a && b && c && atmp)) {

   fprintf(stderr, "%s: out of memory!\n", argv[0]);

   free(a); free(b); free(c); free(atmp);

   pvm_lvgroup("mmult"); pvm_exit(); return -1;

 }

 /* поиск строки и столбца своего блока */

 row = mygid/m; col = mygid % m;

 /* определение соседей сверху и снизу */

 up = pvm_gettid("mmult",

    ((row)?(row-1):(m-1))*m+col);

 down = pvm_gettid("mmult",

    ((row) == (m-1))?col:(row+1)*m+col));

 /* инициализация блоков */

 InitBlock(a, b, c, blksize, row, col);

 /* выполнение матричного умножения */

 for (i = 0; i < m; i++) {

 /* широковещательная передача блока матрицы A */

 if (col == (row + i)%m) {

   pvm_initsend(PvmDataDefault);

   pvm_pkfloat(a, blksize*blksize, 1);

   pvm_mcast(myrow, m, (i+1)*ATAG);

   BlockMult(c,a,b,blksize);

 }

 else {

   pvm_recv(pvm_gettid("mmult", row*m + (row +i)%m),

      (i+1)*ATAG);

   pvm_upkfloat(atmp, blksize*blksize);

   BlockMult(c,atmp,b,blksize);

 }

 /* поворот столбца B */

 pvm_initsend(PvmDataDefault);

 pvm_pkfloat(b, blksize*blksize, 1);

 pvm_send(up, (i+1)*BTAG);

 pvm_recv(down, (i+1)*BTAG);

 pvm_upkfloat(b, blksize*blksize, 1);

 }

 /* проверка */

 for (i = 0; i < blksize*blksize; i++)

 if (a[i] != c[i])

 printf("Error a[%d] (%g) != c[%d] (%g) \n", i,

    a[i], i, c[i]);

 printf("Done.\n");

 free(a); free(b); free(c); free(atmp);

 pvm_lvgroup("mmult");

 pvm_exit();

 return 0;

}


next up previous contents
Next: Одномерное температурное уравнение Up: PVM - параллельная виртуальная Previous: Пример с ошибкой   Contents
2004-06-22

next up previous contents
Next: Практическое использование средств разработки Up: PVM - параллельная виртуальная Previous: Матричное умножение   Contents

Одномерное температурное уравнение

Здесь представлена программа PVM, которая вычисляет температурную диффузию в некой среде - в данном случае это проводник. Уравнение, описывающие одномерную температурную диффузию тонкого проводника:


\begin{displaymath}
\frac{\partial A}{\partial t}=\frac{\partial ^{2}A}{\partial x^{2}}\end{displaymath}

Дискретизация:


\begin{displaymath}
\frac{A_{i+1,j}-A_{i,j}}{\Delta x}=\frac{A_{i,j+1}-2A_{i,j}+A_{i,j-1}}{\Delta x^{2}}\end{displaymath}

В результате получаем явную формулу:


\begin{displaymath}
A_{i+1,j}=A_{i,j}+\frac{\Delta t}{\Delta x^{2}}(A_{i,j+1}-2A_{i,j}+A_{i,j-1})\end{displaymath}

Начальные и граничные условия:

$A(t,0)=0,A(t,1)=0$- для всех $t$;

$A(0,x)=sin(\pi x)$- для $0\leq t\leq 1$.

Псевдокодом для таких вычислений будет следующий:

for i = 1:tsteps-1;

  t = t+dt;

  a(i+1,1)=0;

  a(i+1,n+2)=0;

for j = 2:n+1;

  a(i+1,j)=a(i,j) + mu*(a(i,j+1)-2*a(i,j)+a(i,j-1));

end;

  t;

  a(i+1,1:n+2);

  plot(a(i,:))

end

Для данного примера используется модель ``ведущий - ведомый''. Ведущий - heat.c порождает пять копий программы heatslv. Ведомые параллельно вычисляют температурную диффузию ``подсекций'' проводника. На каждом шаге, ведомые обмениваются граничной информацией: в нашем случае температура проводника ``определяет'' границы между процессорами.

В программе heat.c массив solution будет содержать результаты решений уравнения температурной диффузии на каждом шаге. Этот массив - в формате xgraph - будет ``выходным'' при завершении программы (xgraph - программа, предназначенная для вывода данных на графопостроитель). Сначала порождаются задачи heatslv. Далее, подготавливается исходный набор данных. Обратите внимание на то, что для конца проводника значением начальной температуры будет ноль.

Потом, четыре раза исполняется основная часть программы - каждый раз с новым значением $\Delta t$. Для определения продолжительности времени, прошедшего от начала вычислений данной фазы, используется таймер. Начальный набор данных рассылается задачам heatslv. При каждой посылке совместно с начальным набором данных передаются идентификаторы задач-соседей слева и справа. Задачи heatslv будут использовать эту информацию при граничных коммуникациях. (В противном случае нужно использовать групповые вызовы PVM для отображения задач на сегменты проводника. При использовании групповых вызовов можно избежать явных указаний идентификаторов задач ведомых процессов).

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

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

Программа-пример heat.c:

/*

heat.c

Применение PVM для решения дифференциального уравнения,

описывающего простейшую температурную диффузию,

с использованием 1 ведущей программы и 5 ведомых

Ведущая программа формирует данные, посылает их ведомым

и ожидает результаты, возвращаемые ведомыми.

Файлы с полученными результатами

готовы для использования программой xgraph

*/

#include "pvm3.h"

#include <stdio.h>

#include <math.h>

#include <time.h>

#define SLAVENAME "heatslv"

#define NPROC 5

#define TIMESTEP 100

#define PLOTINC 10

#define SIZE 1000

int num_data = SIZE/NPROC;

 

main()

{

  int mytid, task_ids[NPROC], i, j;

  int left, right, k, l;

  int step = TIMESTEP;

  int info;

  double init[SIZE], solution[TIMESTEP][SIZE];

  double result[TIMESTEP*SIZE/NPROC], deltax2;

  FILE *filenum;

  char *filename[4][7];

  double deltat[4];

  time_t t0;

  int etime[4];

  filename[0][0] = "graph1";

  filename[1][0] = "graph2";

  filename[2][0] = "graph3";

  filename[3][0] = "graph4";

  deltat[0] = 5.0e-1;

  deltat[1] = 5.0e-3;

  deltat[2] = 5.0e-6;

  deltat[3] = 5.0e-9;

  /* регистрация в PVM */

  mytid = pvm_mytid();

  /* порождение ведомых задач */

  info = pvm_spawn(SLAVENAME,(char **)0, 

   PvmTaskDefault,"", NPROC,task_ids);

  /* создание исходного набора данных */

  for (i = 0; i < SIZE; i++)

    init[i] = sin(M_PI * ( (double)i /

        (double)(SIZE-1) ));

    init[0] = 0.0;

    init[SIZE-1] = 0.0;

    /* четырехразовое выполнение с разными

         значениями дельты t */

    for (l = 0; l < 4; l++) {

      deltax2 = (deltat[l]/pow(1.0/(double)SIZE,2.0));

      /*засекаем время*/

      time(&t0);

      etime[l] = t0;

      /* передача исходных данных ведомым */

      /* дополнение информацией о соседях -

        для реализации возможности

       обмена граничными данными */

      for (i = 0; i < NPROC; i++) {

        pvm_initsend(PvmDataDefault);

        left = (i == 0) ? 0 : task_ids[i-1];

        pvm_pkint(&left, 1, 1);

        right = (i == (NPROC-1)) ? 0 : task_ids[i+1];

        pvm_pkint(&right, 1, 1);

        pvm_pkint(&step, 1, 1);

        pvm_pkdouble(&deltax2, 1, 1)

        pvm_pkint(&num_data, 1, 1);

        pvm_pkdouble(&init[num_data*i], num_data, 1);

        pvm_send(task_ids[i], 4);

      }

      /* ожидание результатов */

      for (i = 0; i < NPROC; i++) {

        pvm_recv(task_ids[i], 7);

        pvm_upkdouble(&result[0], num_data*TIMESTEP, 1);

       /* ``обновление'' решения */

        for (j = 0; j < TIMESTEP; j++)

          for (k = 0; k < num_data; k++)

            solution[j][num_data*i+k] = result[wh(j,k)];

      }

      /* остановка формирования временных интервалов */

      time(&t0);

      etime[l] = t0 - etime[l];

      /* получение выходных данных */

      filenum =(fopen_filename[l][0], "w");

      fprintf(filenum,

        "TitleText: Wire Heat over Delta Time: %e\n",

         deltat[l]);

      fprintf(filenum,

        "XUnitText: Distance\nYUnitText: Heat\n");

      for (i = 0; i < TIMESTEP; i = i + PLOTINC) {

        fprintf(filenum,"\"Time index: %d\n",i);

        for (j = 0; j < SIZE; j++)

          fprintf(filenum,"%d %e\n",j, solution[i][j]);

          fprintf(filenum,"\n");

      }

      fclose (filenum);

    }

    /* вывод на экран информации о

      временных интервалах */

    printf("Problem size: %d\n",SIZE);

    for (i = 0; i < 4; i++)

      printf("Time for run %d: %d sec\n",i,etime[i]);

    /* принудительное завершение ведомых процессов */

    for (i = 0; i < NPROC; i++) pvm_kill(task_ids[i]);

      pvm_exit();

  }

 

  int wh(x, y)

  int x, y;

  {

    return(x*num_data+y);

  }

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

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

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

Программа-пример heatslv.c:

/*

heatslv.c

Ведомые принимают исходные данные от хоста,

обмениваются граничной информацией с соседями

и вычисляют изменение температуры проводника.

Это делается за несколько итераций -

``под руководством'' ведущего

*/

#include "pvm3.h"

#include <stdio.h>

int num_data;

 

main()

{

  int mytid, left, right, i, j, master;

  int timestep;

  double *init, *A;

  double leftdata, rightdata, delta, leftside,

     rightside;

  /* регистрация в PVM */

  mytid = pvm_mytid();

  master = pvm_parent();

  /* прием своих данных от ведущей программы */

  while(1) {

    pvm_recv(master, 4);

    pvm_upkint(&lef, 1, 1);

    pvm_upkint(&right, 1, 1);

    pvm_upkint(&timestep, 1, 1);

    pvm_upkdouble(&delta, 1, 1);

    pvm_upkint(&num_data, 1, 1);

    init = (double *) malloc(num_data*sizeof(double));

    pvm_upkdouble(init, num_data, 1);

    /* копирование исходных данных

      в свой рабочий массив */

    A = (double *)

      malloc(num_data * timestep * sizeof(double));

    for (i = 0; i < num_data; i++) A[i] = init[i];

     /* реализация вычисления */

     for (i = 0; i < timestep-1; i++) {

      /* обмен граничной информацией

        со своими соседями */

      /* передача налево, прием справа */

      if (left != 0) {

        pvm_initsend(PvmDataDefault);

        pvm_pkdouble(&A[wh(i,0)],1,1);

        pvm_send(left, 5);

      }

      if (right != 0) {

        pvm_recv(right, 5);

        pvm_upkdouble(&rightdata, 1, 1);

        /* передача направо, прием слева */

        pvm_initsend(PvmDataDefault);

        pvm_pkdouble(&A[wh(i,num_data-1)],1,1);

        pvm_send(right, 6);

      }

      if (left != 0) {

        pvm_recv(left, 6);

        pvm_upkdouble(&leftdata, 1, 1);

      }

      /* выполнение вычислений данной итерации */

      for (j = 0; j < num_data; j++) {

        leftside = (j == 0) ? leftdata : A[wh(i,j-1)];

        rightside =

        (j == (num_data-1)) ? rightdata : A[wh(i,j+1)];

        if ((j == 0) && (left == 0))

          A[wh(i+1,j)] = 0.0;

        else if ((j == (num_data-1)) && (right == 0))

          A[wh(i+1,j)] = 0.0;

        else

          A[wh(i+1,j)] =

          A[wh(i,j)]+

          delta*(rightside-2*A[wh(i,j)]+leftside);

      }

    }

    /* передача результатов назад ведущей программе */

    pvm_initsend(PvmDataDefault);

    pvm_pkdouble(&A[0],num_data*timestep,1);

    pvm_send(master,7);

  }

  /* только при удачном исходе */

  pvm_exit();

}

 

int wh(x, y);

int x, y;

{

  return(x*num_data+y);

}


next up previous contents
Next: Практическое использование средств разработки Up: PVM - параллельная виртуальная Previous: Матричное умножение   Contents
2004-06-22

next up previous contents
Next: Базовые средства программирования в Up: mainfile Previous: Одномерное температурное уравнение   Contents

Практическое использование средств разработки приложений в ОС Linux



Subsections

2004-06-22

next up previous contents
Next: Компилятор GCC Up: Практическое использование средств разработки Previous: Практическое использование средств разработки   Contents

Базовые средства программирования в ОС Linux



Subsections

2004-06-22

next up previous contents
Next: Основные группы системных функций Up: Базовые средства программирования в Previous: Базовые средства программирования в   Contents

Компилятор GCC

GCC - это свободно доступный оптимизирующий компилятор для языков C, C++, Ada 95, а также Objective C. Его версии применяются для различных реализаций Unix (а также VMS, OS/2 и других систем PC), и позволяют генерировать код для множества процессоров.

Вы можете использовать gcc для компиляции программ в объектные модули и для компоновки полученных модулей в единую исполняемую программу. Компилятор способен анализировать имена файлов, передаваемые ему в качестве аргументов, и определять, какие действия необходимо выполнить. Файлы с именами типа name.cc (или name.C) рассматриваются, как файлы на языке C++, а файлы вида name.o считаются объектными (т.е. внутримашинным представлением).

Чтобы откомпилировать исходный код C++, находящийся в файле F.cc, и создать объектный файл F.o, выполните команду:

gcc -c <compile-options> F.cc

Здесь строка compile-options указывает возможные дополнительные опции компиляции.

Чтобы скомпоновать один или несколько объектных файлов, полученных из исходного кода C++ - F1.o, F2.o, ... - в единый исполняемый файл F, используйте команду:

gcc -o F <link-options> F1.o F2.o ... -lg++ \

<other-libraries>

Здесь строка link-options означает возможные дополнительные опции компоновки, а строка other-libraries - подключение при компоновке дополнительных разделяемых библиотек.

Вы можете совместить два этапа обработки - компиляцию и компоновку - в один общий этап с помощью команды:

gcc -o F <compile-and-link-options> F1.cc ... -lg++\

<other-libraries>

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

./F <arguments>,

где строка arguments определяет аргументы командной строки вашей программы.

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

Библиотеки обычно определяются через аргументы вида
-llibrary-name. В частности, -lg++ означает библиотеку стандартных функций C++, а -lm определяет библиотеку различных математических функций (sin, cos, arctan, sqrt, и т.д.). Библиотеки должны быть перечислены после исходных или объектных файлов, содержащих вызовы к соответствующим функциям.

Среди множества опций компиляции и компоновки наиболее часто употребляются следующие:

-c

Только компиляция. Из исходных файлов программы создаются объектные файлы в виде name.o. Компоновка не производится.

-Dname=value

Определить имя name в компилируемой программе как значение value. Эффект такой же, как наличие строки

#define name value

в начале программы. Часть `=value' может быть опущена, в этом случае значение по умолчанию равно 1.

-o file-name

Использовать file-name в качестве имени для создаваемого gcc файла (обычно это исполняемый файл).

-llibrary-name

Использовать при компоновке указанную библиотеку.

-g

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

-MM

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

-pg

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

-Wall

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

-O1

Устанавливает оптимизацию уровня 1. Оптимизированная трансляции требует несколько больше времени и несколько больше памяти для больших функций. Без указания опций `-O' цель компилятора состoит в том, чтобы уменьшить стоимость трансляции и выдать ожидаемые результаты при отладке. Операторы независимы: если вы останавливаете программу на контрольной точке между операторами, то можете назначить новое значение любой переменной или поставить счетчик команд на любой другой оператор в функции и получить точно такие результаты, которые вы ожидали от исходного текста. С указанием `-O' компилятор пробует уменьшить размер кода и время исполнения.

-O2

Устанавливает оптимизацию уровня 2. GNU CC выполняет почти все поддерживаемые оптимизации, которые не включают уменьшение времени исполнения за счет увеличения длины кода. Компилятор не выполняет раскрутку циклов или подстановку функций, когда вы указываете `-O2'. По сравнения с `-O' эта опция увеличивает как время компиляции, так и эффективность сгенерированного кода.

-O3

Устанавливает оптимизацию уровня 3. `-O3' включает все оптимизации, определяемые `-O2', а также включает опцию `inline-functions'.

-O0

Без оптимизации. Если вы используете многочисленные `-O' опции с номерами или без номеров уровня, действительной является последняя такая опция.



2004-06-22

next up previous contents
Next: Ассемблеры gas и nasm Up: Базовые средства программирования в Previous: Компилятор GCC   Contents

Основные группы системных функций

Язык C не предоставляет встроенных возможностей для выполнения таких распространенных операций, как ввод/вывод, управление памятью, работа со строками, и т.п. Эти возможности определены в стандартной библиотеке, которая компонуется в вашу программу.

Библиотека GNU C определяет все библиотечные функции, определенные стандартом ISO C и дополнительные возможности, указанные в стандарте POSIX и иных предписаниях для операционных систем Unix, а также расширения, специфичные для систем GNU. Она является наиболее фундаментальной системной библиотекой и обязательно присутствует в любой системе Linux.

В составе библиотеки можно выделить следующие основные группы функций:



2004-06-22

next up previous contents
Next: Создание очередей сообщений Up: Очереди сообщений Previous: Общие сведения   Contents

Структуры данных очередей сообщений

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

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

struct msg {

/* Указатель на следующее сообщение */

struct msg *msg_next; 

long msg_type; /* Тип сообщения */

short msg_ts; /* Размер текста сообщения */

short msg_spot; /* Адрес текста сообщения */

};

Приведенное определение находится во включаемом файле
<sys/msg.h>.

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

struct msqid_ds {

  /*Структура прав на выполнение операций*/

  struct ipc_perm msg_perm; 

  /*Указатель на первое сообщение в очереди*/

  struct msg *msg_first;

  /*Указатель на последнее сообщение в очереди*/

  struct msg *msg_last;

  /* Текущее число байт в очереди */

  ushort msg_cbytes;

  /* Число сообщений в очереди */

  ushort msg_qnum;

  /* Макс. допустимое число байт в очереди */

  ushort msg_qbytes;

  /* Ид-р последнего отправителя */

  ushort msg_lspid;

  /* Ид-р последнего получателя */

  ushort msg_lrpid;

  /* Время последнего отправления */

  time_t msg_stime;

  /* Время последнего получения */

  time_t msg_rtime;

  /* Время последнего изменения */

  time_t msg_ctime;

};

Это определение также находится во включаемом файле <sys/msg.h>. Поле структуры msg_perm использует в качестве шаблона структуру ipc_perm, которая задает права на операции с сообщениями и определяется так:

struct ipc_perm {

  ushort uid; /* Идентификатор пользователя */

  ushort gid; /* Идентификатор группы */

  ushort cuid; /* Идентификатор создателя очереди */

  ushort cgid; /* Ид-р группы создателя очереди */

  ushort mode; /* Права на чтение/запись */

  /* Последовательность номеров используемых слотов */

  ushort seq;

  key_t key; /* Ключ */

};

Последнее определение находится во включаемом файле <sys/ipc.h>, общем для всех средств межпроцессной связи.

Если в аргументе msgflg системного вызова msgget установлен только флаг IPC_CREAT, выполняется одно из двух действий:

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

Кроме того, можно специфицировать ключ key со значением
IPC_PRIVATE. Если указан такой ``личный'' ключ, для него обязательно выделяется новый уникальный идентификатор и создаются ассоциированные с ним очередь сообщений и структура данных (при условии, что это не приведет к превышению системного лимита). При выполнении утилиты ipcs поле KEY для подобного идентификатора msqid из соображений секретности содержит нули.

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

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

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

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

Для управления очередями сообщений используется системный вызов msgctl. Он позволяет выполнять следующие управляющие действия:



2004-06-22

next up previous contents
Next: Отладчик GDB Up: Базовые средства программирования в Previous: Основные группы системных функций   Contents

Ассемблеры gas и nasm

Использование языка ассемблера в Linux происходит гораздо реже, чем, например, в DOS. Драйверы устройств в Linux гораздо чаще пишутся на языке C, а затем обрабатываются оптимизирующим компилятором, например, GCC. Однако, если вам непременно требуется ассемблер, его можно использовать.

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

GAS - это сокращение от GNU Assembler. Поскольку GAS был разработан для поддержки 32-битных компиляторов Unix, он использует стандартный синтаксис ATT, который несколько отличается от обычного ассемблера DOS. Основные отличия синтаксиса GAS от синтаксиса Intel:



2004-06-22

next up previous contents
Next: Интерфейсы gdb и другие Up: Базовые средства программирования в Previous: Ассемблеры gas и nasm   Contents

Отладчик GDB

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

Когда вы запускаете программу, содержащую ошибки, обнаруживаемые лишь на стадии выполнения, возникают несколько вопросов, на которые вам нужно найти ответ:

Эти действия требуют, чтобы пользователь отладчика был в состоянии:

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

Программа GDB предоставляет все перечисленные возможности. Она называется отладчиком на уровне исходного текста, создавая иллюзию, что вы выполняете операторы C++ из вашей программы, а не машинный код, в который они действительно транслируются.

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

Чтобы указать компилятору (gcc), что вы планируете отлаживать вашу программу, и поэтому нуждаетесь в дополнительной информации, добавьте ключ -g в опции компиляции и компоновки. Например, если ваша программа состоит из двух файлов main.C и utils.C, можете откомпилировать ее командами:

   gcc -c -g -Wall main.C
   gcc -c -g -Wall utils.C
   gcc -g -o myprog main.ob utils.o

или одной командой:

gcc -g -Wall -o myprog main.o utils.o

Обе последовательности команд приводят к созданию исполняемого файла myprog.

Чтобы выполнить полученную программу под управлением gdb, введите

gdb myprog

вы увидите командное приглашение GDB:

(gdb)

Это очень простой, но эффективный тексовый интерфейс отладчика. Его вполне достаточно, чтобы ознакомиться с основными командами gdb.

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

Как только возникает командное приглашение, вы можете использовать следующие команды:

help command

выводит краткое описание команды GDB. Просто help выдает список доступных разделов справки;

run command-line-arguments

запускает Вашу программу с определенными аргументами командной строки. GDB запоминает переданные аргументы, и простой перезапуск программы с помощью run приводит к использованию этих аргументов;

where

создает трассу - цепочку вызовов функций, произошедших до попадания программы в текущее место. Синонимом является команда bt;

up

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

down

производит эффект, обратный up;

print E

выводит значение E в текущем окне программы, где E является выражением C++ (обычно просто переменной). Каждый раз при использовании этой команды, GDB нумерует ее упоминание для будущих ссылок. Например:

(gdb) print A[i] $2 = -16

(gdb) print $2 + ML $3 = -9

сообщает нам, что величина A[i] в текущем окне равна -16, и что при добавлении этого значения к переменной ML получится -9;

quit

выход из GDB;

Ctrl-c

если программа запущена через оболочку shell, Ctrl-c немедленно прекращает ее выполнение. В GDB программа приостанавливается, пока ее выполнение не возобновится;

break place

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

(gdb) break MungeData Breakpoint 1 at 0x22a4:

file main.C, line 16.

Команда break main остановит выполнение в начале программы.

вы можете установить точки останова на определенную строку исходного кода:

(gdb) break 19 Breakpoint 2 at 0x2290:

file main.C, line 19.

(gdb) break utils.C:55 Breakpoint 3 at 0x3778:

file utils.C, line 55.

Когда вы запустите программу и она достигнет точки останова, то увидите сообщение об этом и приглашение, например:

Breakpoint 1, MungeData (A=0x6110, N=7) at main.c:16

(gdb);

delete N

удаляет точку останова с номером N. Если опустить N, будут удалены все точки останова;

cont или continue

продолжает обычное выполнение программы;

step

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

next

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

finish

выполняет команды next без остановки, пока не достигнет конца текущей функции.


next up previous contents
Next: Интерфейсы gdb и другие Up: Базовые средства программирования в Previous: Ассемблеры gas и nasm   Contents
2004-06-22

next up previous contents
Next: Средство управления проектом make Up: Базовые средства программирования в Previous: Отладчик GDB   Contents

Интерфейсы gdb и другие отладчики

Хотя gdb можно использовать в shell, никто обычно этого не делает. Программа Emacs предлагает простой интерфейс, который избавит вас от обльшого количества вводимых команд и поможет избежать ошибок. Выполнив команду Emacs M-x gdb, вы получите новое окно с запущенным gdb, воспринимающее все сокращенные команды. Emacs также интерпретирует вывод от gdb, чтобы вам было удобнее. Когда достигается точка останова, Emacs получает от gdb имя файла и номер строки, чтобы показать содержимое этого файла, с отмеченной точкой останова или ошибкой. Когда вы отлаживаете программу по шагам, Emacs следует за вами по файлам исходного кода.

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

KDbg не может делать больше, чем делает gdb. Например, если имеющаяся у вас версия gdb не поддерживает отладку многопоточных программ, то и KDbg не поможет вам в этом (несмотря на то, что он выводит окно потоков).

Графическим интерфейсом для системы X Window является xxgdb. Интерфейсом для графического представления данных является ddd.

Кроме этого, следует упомянуть отладчик DBX, а среди коммерческих приложений - мощное средство TotalView.



2004-06-22

next up previous contents
Next: Назначение make Up: Практическое использование средств разработки Previous: Интерфейсы gdb и другие   Contents

Средство управления проектом make



Subsections

2004-06-22

next up previous contents
Next: Файл описания проекта - Up: Средство управления проектом make Previous: Средство управления проектом make   Contents

Назначение make

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

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

Существует несколько диалектов make, как среди версий UNIX / Linux, так и в других операционных системх для ПК. Далее нами описана версия gmake (GNU make).



2004-06-22

next up previous contents
Next: Алгоритм работы make Up: Средство управления проектом make Previous: Назначение make   Contents

Файл описания проекта - make-файл

Простой make-файл для компиляции простой программы - редактора edit, состоящего из восьми исходных файлов .cc и трех заголовочных файлов (.h):



Makefile for simple editor

edit : edit.o kbd.o commands.o display.o $\backslash$
insert.o search.o files.o utils.o
gcc -g -o edit edit.o kbd.o commands.o display.o $\backslash$
insert.o search.o files.o utils.o -lg++

edit.o : edit.cc defs.h
gcc -g -c -Wall edit.cc
kbd.o : kbd.cc defs.h command.h
gcc -g -c -Wall kbd.cc
commands.o : command.cc defs.h command.h
gcc -g -c -Wall commands.cc
display.o : display.cc defs.h buffer.h
gcc -g -c -Wall display.cc
insert.o : insert.cc defs.h buffer.h
gcc -g -c -Wall insert.cc
search.o : search.cc defs.h buffer.h
gcc -g -c -Wall search.cc
files.o : files.cc defs.h buffer.h command.h
gcc -g -c -Wall files.cc
utils.o : utils.cc defs.h
gcc -g -c -Wall utils.cc

Этот файл содержит последовательность девяти правил. В общем виде каждое правило выглядит так:

<цель_1> <цель_2> ... <цель_n>: <зависимость_1>
                    <зависимость_2> ... <зависимость_n>
   <команда_1>
   <команда_2>
   ...
   <команда_n>

Правило состоит из строки, содержащей два списка имен, разделенных двоеточием, за которыми следуют одна или несколько строк, начинающихся с табуляции. Любая строка может быть продолжена, как показано выше, если закончить ее наклонной чертой, которая означает пробел, соединяя строку с последующей. Символ `#' отмечает начало комментария, который продолжается до конца данной строки.

Цель (target) - это некий желаемый результат, способ достижения которого описан в правиле. Цель может представлять собой имя файла. В этом случае правило описывает, каким образом можно получить новую версию этого файла.

Цель также может быть именем некоторого действия. В таком случае правило описывает, каким образом совершается указанное действие. Подобного рода цели называются псевдоцели (pseudo targets) или абстрактные цели (phony targets).

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

Команды - это действия, которые необходимо выполнить для обновления либо достижения цели. Утилита make отличает строки, содержащие команды, от прочих строк make-файла по наличию символа табуляции (символа с кодом 9) в начале строки. Это команды shell (присутствуют в оболочке Linux), которые выполняются в определенном порядке, чтобы создать или обновить цель правила (обычно говорят обновить - update).



2004-06-22

next up previous contents
Next: Переменные в make-файлах Up: Средство управления проектом make Previous: Файл описания проекта -   Contents

Алгоритм работы make

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

Главная цель может быть прямо указана в командной строке при запуске make. В следующем примере make будет стремиться достичь цели edit (получить новую версию файла edit):

make edit

Если не указывать какой-либо цели в командной строке, то make выбирает в качестве главной первую, встреченную в make-файле цель. Схематично ``верхний уровень'' алгоритма работы make можно представить так:

make()
    {
        главная_цель = Выбрать_Главную_Цель()

        Достичь_Цели( главная_цель )
    }

После того как главная цель выбрана, make запускает ``стандартную'' процедуру достижения цели. Сначала в make-файле ищется правило, которое описывает способ достижения этой цели (функция НайтиПравило). Затем, к найденному правилу применяется обычный алгоритм обработки правил (функция ОбработатьПравило):

Достичь_Цели( Цель )
    {
        правило = Найти_Правило( Цель )

        Обработать_Правило( правило )
    }

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

Обработать_Правило( Правило )
    {
        Обработать_Зависимости( Правило )

        если Нужно_Выполнять_Команды( Правило )
            {
            Выполнить_Команды( Правило )
            }
    }

Функция ОбработатьЗависимости поочередно проверяет все перечисленные в теле правила зависимости. Некоторые из них могут оказаться целями каких-нибудь правил. Для этих зависимостей выполняется обычная процедура достижения цели (функция ДостичьЦели). Те зависимости, которые не являются целями, считаются именами файлов. Для таких файлов проверяется факт их наличия. При их отсутствии make аварийно завершает работу с сообщением об ошибке:

Обработать_Зависимости( Правило )
    {
        цикл от i=1 до Правило.число_зависимостей
            {
            если Есть_Такая_Цель( Правило.зависимость[i])
                {
                Достичь_Цели( Правило.зависимость[i])
                }
            иначе
            {
             Проверить_Наличие_Файла(Правило.зависимость[i])
            }
        }
    }

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

В противном случае (если ни одно из вышеприведенных условий не выполняется) описанные в правиле команды не выполняются. Алгоритм принятия решения о выполнении команд схематично можно представить так:
Нужно_Выполнять_Команды( Правило )
    {
        если Правило.Цель.Является_Абстрактной()
            return  true

        //  цель является именем файла

        если Файл_Не_Существует( Правило.Цель )
            return  true

        цикл от i=1 до Правило.Число_зависимостей
        {
        если Правило.Зависимость[i].Является_Абстрактной()
            return  true
        иначе
            //  зависимость является именем файла
            {
            если Время_Модификации(Правило.Зависимость[i])
              >  Время_Модификации( Правило.Цель )
                return  true
            }
        }

        return  false
    }

В указанном примере целью по умолчанию является edit. Первым шагом по его обновлению будет обновление всех файлов объектов (.o), перечисленных как зависимости. Обновление edit.o в свою очередь требует обновления edit.cc и defs.h. Предполагается, что edit.cc является исходным файлом, из которого создается edit.o, а defs.h является заголовочным файлом, который включается в edit.cc. Правил, указывающих на эти файлы, нет; поэтому такие файлы, как минимум должны просто существовать. Теперь edit.o считается готовым, если он изменен позже, чем edit.cc или defs.h (если он старше их, это значит, что один из этих файлов изменился со времени последней компиляции edit.o). Если edit.o старше своих зависимостей, gmake выполняет действие OP ``gcc -g -c -Wall
edit.cc'', создавая новый edit.o. Когда edit.o и все другие файлы .o будут обновлены, они будут собраны вместе действием
``gcc -g -o edit ...'', чтобы создать программу edit, если либо edit еще не существует, либо любой из файлов .o новее, чем существующий файл edit.

Чтобы вызвать gmake для этого примера, используйте команду:

gmake -f <makefile-name> <target-names>,

где <target-names> - это имена целей, которые вы хотите обновить, а <makefile-name>, заданное после ключа -f, является именем make-файла. По умолчанию целью является первое правило в файле. Вы можете (обычно так и делают) опустить -f makefile-name, и в этом случае по умолчанию будет выбрано имя makefile или Makefile, если любой из этих файлов существует.

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

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


edit.o : edit.cc

gcc -g -c -Wall edit.cc
kbd.o : kbd.cc
gcc -g -c -Wall kbd.cc
commands.o : command.cc
gcc -g -c -Wall commands.cc
display.o : display.cc
gcc -g -c -Wall display.cc
insert.o : insert.cc
gcc -g -c -Wall insert.cc
search.o : search.cc
gcc -g -c -Wall search.cc
files.o : files.cc
gcc -g -c -Wall files.cc
utils.o : utils.cc
gcc -g -c -Wall utils.cc

edit.o kbd.o commands.o display.o $\backslash$
insert.o search.o files.o utils.o: defs.h
kbd.o commands.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

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



2004-06-22

next up previous contents
Next: Шаблонные правила Up: Средство управления проектом make Previous: Алгоритм работы make   Contents

Переменные в make-файлах

GNU Make поддерживает два способа задания переменных, которые несколько различаются по смыслу. Первый способ - традиционный, с помощью оператора '=':

compile_flags = -O3 -funroll-loops

-fomit-frame-pointer|

Такой способ поддерживают все варианты утилиты make. Его можно сравнить, например, с заданием макроса в языке Си:

#define compile_flags "-O3 -funroll-loops

-fomit-frame-pointer"

Значение переменной, заданной с помощью оператора '=', будет вычислено в момент ее использования. Например, при обработке make-файла

var1 = one
var2 = $(var1) two
var1 = three

all:
    @echo $(var2)

на экран будет выдана строка ``three two''. Значение переменной var2 будет вычислено непосредственно в момент выполнения команды echo, и будет представлять собой текущее значение переменной var1, к которому добавлена строка ``two''. Как следствие - одна и та же переменная не может одновременно фигурировать в левой и правой части выражения, так как это может привести к бесконечной рекурсии. GNU Make распознает подобные ситуации и прерывает обработку make-файла. Следующий пример вызовет ошибку:

compile_flags = -pipe $(compile_flags)

GNU Make поддерживает также и второй, новый способ задания переменной с помощью оператора ':=':

compile_flags := -O3 -funroll-loops

-fomit-frame-pointer

В этом случае переменная работает подобно ``обычным'' текстовым переменным в каком-либо языке программирования. Вот приблизительный аналог этого выражения на языке C++:

string compile_flags = "-O3 -funroll-loops

-fomit-frame-pointer";

Значение переменной вычисляется в момент обработки оператора присваивания. Если, например, записать

var1 := one
var2 := $(var1) two
var1 := three

all:
     @echo $(var2),

то при обработке такого make-файла на экран будет выдана строка ``one two''.

Переменная может ``менять'' свое поведение в зависимости от того, какой из операторов присваивания был применен к ней последним. Одна и та же переменная на протяжении ее жизни может вести себя и как ``макрос'', и как ``текстовая переменная''.

Зависимости цели edit в приведенных выше примерах являлись аргументами для команд компоновки. Можно избежать их повторения, определив переменную, которая будет содержать имена всех объектных файлов:


 Makefile for simple editor

OBJS = edit.o kbd.o commands.o display.o $\backslash$
insert.o search.o files.o utils.o
edit : $(OBJS)
gcc -g -o edit $(OBJS)

Строка, начинающаяся с ``OBJS ='' определяет переменную OBJS, на которую можно в дальнейшем сослаться через ``$(OBJS)'' или ``OBJS''. Эти более поздние ссылки приводят к тому, что обозначение OBJ будет дословно заменяться, прежде чем правило будет обработано. Иногда неудобно, что и gmake и shell используют `$' как префикс для ссылок на переменные; gmake определяет `$$', такое же, как `$', позволяя вам передавать `$' в shell, если это необходимо.

Иногда вам может понадобиться величина в виде обычной переменной, но с некоторыми уточнениями. Например, задавая переменную со списком имен всех исходных файлов, вы можете попытаться задать имена всем получающимся объектным файлам. Можно переопределить OBJS следующим образом:


SRCS = edit.cc kbd.cc commands.cc display.cc $\backslash$

insert.cc search.cc files.cc utils.cc
OBJS = $(SRCS:.cc=.o)
Суффикс `:.cc=.o' определяет необходимые уточнения. Теперь у нас есть переменные для имен всех исходных и объектных файлов, при этом не пришлось повторять в определениях множество имен.

Переменные могут упоминаться и в командной строке при вызове gmake. Например, если make-файл содержит


edit.o: edit.cc

gcc $(DEBUG) -c -Wall edit.cc ,
то команда типа gmake DEBUG=-g ... приведет к использованию ключа -g (добавить символическую информацию отладчика) при компиляции, а отсутствие DEBUG=-g исключит использование этого ключа. Определения переменных в командной строке имеют преимущество перед определениями внутри make-файла, что позволяет задать в make-файле значения по умолчанию.

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

setenv DEBUG -g gmake ...

для последнего примера также приведет к использованию ключа -g во время компиляции.



2004-06-22

next up previous contents
Next: Специальные действия Up: Средство управления проектом make Previous: Переменные в make-файлах   Contents

Шаблонные правила

Шаблонные правила (implicit rules или pattern rules) могут быть применены к целой группе файлов. В этом их отличие от обычных правил - описывающих отношения между конкретными файлами.

Традиционные реализации make поддерживают так называемую ``суффиксную'' форму записи шаблонных правил:

.<расширение_файлов_зависимостей>.<расширение_файлов_целей>:
    <команда_1>
    <команда_2>
    ...
    <команда_n>

Например, следующее правило гласит, что все файлы с расширением "o" зависят от соответствующих файлов с расширением "cpp":

.cpp.o:
        gcc -c $^

Обратите внимание на использование автоматической переменной $^ для передачи компилятору имени файла-зависимости. Поскольку шаблонное правило может применяться к разным файлам, использование автоматических переменных - единственный способ узнать, для каких файлов сейчас задействуется правило.

Шаблонные правила позволяют упростить make-файл и сделать его более универсальным.

В самом первом примере все правила компиляции для получения файлов типа .o имеют одинаковую форму. Очень неудобно повторять их снова и снова; это может привести к ошибке при написании make-файла. Поэтому gmake можно указать - для известных стандартных случаев - действия и имена файлов по умолчанию для создания различных типов файлов. Для наших целей наиболее важным является то, что gmake известно, как создать файл F.o из исходного файла F.cc. В данном случае gmake автоматически использует правило


F.o : F.cc

$(CXX) -c -Wall $(CXXFLAGS) F.cc,
которое вызывается, чтобы создать F.o, если существует файл C++ с именем F.cc, но не определяет точные действия для создания F.o. Использование префикса ``CXX'' является следствием соглашения об именах для переменных, связанных с C++. Также создается команда

F : F.o

$(CC) $(LDFLAGS) F.o $(LOADLIBES) -o F,
чтобы сообщить, как создать исполняемый файл F из F.o.

В результате предыдущий пример можно переписать следующим образом:


 Makefile for simple editor

SRCS = edit.cc kbd.cc commands.cc display.cc $\backslash$
insert.cc search.cc files.cc utils.cc

OBJS = $(SRCS:.cc=.o)
CC = gcc
CXX = gcc
CXXFLAGS = -g
LOADLIBES = -lg++

edit : $(OBJS)
edit.o : defs.h
kbd.o : defs.h command.h
commands.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h



2004-06-22

next up previous contents
Next: Создание make-файлов Up: Средство управления проектом make Previous: Шаблонные правила   Contents

Специальные действия

Часто удобно иметь цели, которым не ставится в соответствие никакая генерация файлов. Если действия цели не создают файла с соответствующим именем, то исходя из принципов работы gmake действия для цели будут выполняться всякий раз, когда gmake обратится к ней. Обычно это используют для помещения в make-файл стандартной операции ``очистки'', определяющей способ удаления неиспользуемых файлов. Например, вы часто можете встретить в make-файле такое правило:

clean:

rm -f *.o

Каждый раз, при выполнении команды gmake clean, это действие будет удалять все файлы .o.

Программа gmake обычно выводит информацию о выполнении каждого действия, но иногда это не желательно. Поэтому символ `@' в начале действия может запретить вывод по умолчанию. Вот пример его обычного использования:


edit : $(OBJS)|

@echo Linking edit ...
@gcc -g -o edit $(OBJS)
@echo Done
Результатом этих действий будет то, что в начале компиляции вы увидите строку ``Linking edit...'', а в конце компиляции - строку ``Done''.

Когда gmake встречает действие, возвращающее ненулевой код выхода, т.е., сообщение об ошибке по соглашениям UNIX, его стандартной реакцией является прекращение обработки. Коды ошибок от строк действий, начинающихся с символа `--' (возможно с предшествующим `@') игнорируются. Ключ -k для gmake приводит в случае ошибки к прекращению обработки только текущего правила (и всех, зависящих от него целей), позволяя продолжить обработку всех последующих.



2004-06-22

next up previous contents
Next: Управление очередями сообщений Up: Очереди сообщений Previous: Структуры данных очередей сообщений   Contents

Создание очередей сообщений

Cинтаксис системного вызова msgget выглядит так:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgget ( key_t key,int msgflg);

Тип key_t описан во включаемом файле <sys/types.h> при помощи typedef как целый тип.

Целочисленное значение, возвращаемое в случае успешного завершения системного вызова, есть идентификатор очереди сообщений (msqid). В случае неудачи результат равен -1.

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

Целое число, передаваемое в качестве аргумента msgflg, удобно рассматривать как восьмеричное. Оно задает права на выполнение операций и флаги. Права на выполнение операций - это права на чтение из очереди и запись в нее (т. е. на прием или посылку сообщений) для владельца, членов группы и прочих пользователей. В табл. 2 сведены возможные элементарные права и соответствующие им восьмеричные значения:


Таблица 2. Права доступа к очередям.


Права на операции Восьмеричное значение
Чтение для владельца 0400
Запись для владельца 0200
Чтение для группы 0040
Запись для группы 0020
Чтение для остальных 0004
Запись для остальных 0002

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

Флаги определены во включаемом файле <sys/ipc.h>. В табл. 3 сведены мнемонические имена флагов и соответствующие им восьмеричные значения:


Таблица 3. Флаги работы с очередями.


Флаг Восьмеричное значение
IPC_CREAT 0001000
IPC_EXCL 0002000

Значение аргумента msgflg в целом является, следовательно, результатом операции побитного ИЛИ (операция | в языке C) для прав на выполнение операций и флагов, например:

msqid = msgget (key, (IPC_CREAT | 0644));

msqid = msgget (key, (IPC_CREAT | IPC_EXCL | 0400));

Как уже указывалось, системный вызов вида
msqid = msgget (IPC_PRIVATE, msgflg); приведет к попытке выделения нового идентификатора очереди сообщений и ассоциированной информации, независимо от значения аргумента msgflg. Попытка может быть неудачной только из-за превышения системного лимита на общее число очередей сообщений, задаваемого настраиваемым параметром MSGMNI.

При использовании флага IPC_EXCL в сочетании с IPC_CREAT системный вызов msgget завершается неудачей в том и только в том случае, когда с указанным ключом key уже ассоциирован идентификатор. Флаг IPC_EXCL необходим, чтобы предотвратить ситуацию процесса, когда надежда получить новый (уникальный) идентификатор очереди сообщений не сбывается. Иными словами, когда используются и IPC_CREAT и IPC_EXCL, при успешном завершении системного вызова обязательно возвращается новый идентификатор msqid.

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

Программа-пример для msgget, приведенная ниже, управляется посредством меню. Она позволяет поупражняться со всевозможными комбинациями системного вызова msgget, проследить, как передаются аргументы и получаются результаты. Имена переменных выбраны максимально близкими к именам, используемым в спецификации синтаксиса системного вызова, что облегчает чтение программы.

Выполнение программы начинается с приглашения ввести шестнадцатеричный ключ key, восьмеричный код прав на операции и, наконец, выбираемую при помощи меню комбинацию флагов. В меню предлагаются все возможные комбинации, даже бессмысленные, что позволяет при желании проследить за реакцией на ошибку. Затем выбранные флаги комбинируются с правами на операции, после чего выполняется системный вызов, результат которого заносится в переменную msqid. Если значение msqid равно -1, выдается сообщение об ошибке и выводится значение внешней переменной errno. Если ошибки не произошло, выводится значение полученного идентификатора очереди сообщений:

/* Программа иллюстрирует

возможности системного вызова msgget()

(получение идентификатора очереди сообщений) */

 

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#include <errno.h>

 

main ()

{

  key_t key; /* Тип описан как целое */

  int opperm, flags; /* Права на операции и флаги */

  int msgflg, msqid;

 

  /* Ввести требуемый ключ */

  printf ("\nВведите шестнадцатеричный ключ: ");

  scanf ("%x", &key);

 

  /* Ввести права на операции */

  printf ("\nВведите права на операции ");

  printf ("в восьмеричной записи: ");

  scanf ("%o", &opperm);

 

  /* Установить требуемые флаги */

  printf ("\nВведите код, соответствущий ");

  printf ("нужной комбинации флагов:\n");

  printf (" Нет флагов = 0\n");

  printf (" IPC_CREAT = 1\n");

  printf (" IPC_EXCL = 2\n");

  printf (" IPC_CREAT и IPC_EXCL = 3\n");

  printf (" Выбор = ");

 

  /* Получить флаги, которые нужно установить */

  scanf ("%d", &flags);

 

  /* Проверить значения */

  printf ("\nключ = 0x%x, права = 0%o, флаги = %d\n",

          key, opperm, flags);

 

  /* Объединить флаги с правами на операции */

  switch (flags) {

  case 0: /* Флаги не устанавливать */

          msgflg = (opperm | 0);

          break;

  case 1: /* Установить флаг IPC_CREAT */

          msgflg = (opperm | IPC_CREAT);

          break;

  case 2: /* Установить флаг IPC_EXCL */

          msgflg = (opperm | IPC_EXCL);

          break;

  case 3: /* Установить оба флага */

          msgflg = (opperm | IPC_CREAT | IPC_EXCL);

  }

 

  /* Выполнить системный вызов msgget */

  msqid = msgget (key, msgflg);

  if (msqid == -1) {

  /* Сообщить о неудачном завершении */

    printf ("\nmsgget завершился неудачей!\n"

  printf ("Код ошибки = %d\n", errno);

  }

  else

  /* При успешном завершении сообщить msqid */

  printf ("\nИдентификатор msqid = %d\n", msqid);

  exit (0);

}


next up previous contents
Next: Управление очередями сообщений Up: Очереди сообщений Previous: Структуры данных очередей сообщений   Contents
2004-06-22

next up previous contents
Next: LEX - лексический анализатор Up: Средство управления проектом make Previous: Специальные действия   Contents

Создание make-файлов

Наилучший способ создания make-файлов - наличие заготовки, которую можно адаптирповать под нужную программу. Пример заготовки приведен ниже:


PROGRAM = <REPLACE WITH PROGRAM NAME>

LOADLIBES = <EXTRA LOAD LIBRARIES> -lg++
CXX.SRCS = <C++ SOURCE FILE NAMES>
CC = gcc
LDFLAGS = -g
CXX = gcc
CXXFLAGS = -g -Wall -fno-builtins
OBJS = $(CXX.SRCS:.cc=.o)
$(PROGRAM) : $(OBJS)
$(CC) $(LDFLAGS) $(OBJS) $(LOADLIBES) -o $(PROGRAM)
clean:
/bin/rm -f *.o $(PROGRAM) *
depend:
$(CXX) -MM $(CXX.SRCS)
<DEPENDENCIES ON .h FILES GO HERE>

Скопировав подобную заготовку и заменив разделы, ограниченные символами <> нужным текстом, Вы получите работающий make-файл:


PROGRAM = edit

LOADLIBES = -lg++
CXX.SRCS = edit.cc kbd.cc commands.cc display.cc $\backslash$
insert.cc search.cc files.cc utils.cc
CC = gcc
LDFLAGS = -g
CXX = gcc
CXXFLAGS = -g -Wall -fno-builtins
OBJS = $(CXX.SRCS:.cc=.o)
$(PROGRAM) : $(OBJS)
$(CC) $(LDFLAGS) $(OBJS) $(LOADLIBES) -o $(PROGRAM)
clean:
/bin/rm -f *.o $(PROGRAM) *
depend:
$(CXX) -MM $(CXX.SRCS)

edit.o : defs.h
kbd.o : defs.h command.h
commands.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h



2004-06-22

next up previous contents
Next: Введение Up: Практическое использование средств разработки Previous: Создание make-файлов   Contents

LEX - лексический анализатор



Subsections

2004-06-22

next up previous contents
Next: Команда lex Up: LEX - лексический анализатор Previous: LEX - лексический анализатор   Contents

Введение

Пусть заданы некоторое конечное множество слов (лексем) в некотором языке и некоторое входное слово. Лексический анализ - это распознавание лексем в определенном входном потоке. Необходимо установить, какой элемент множества (если он существует) совпадает с данным входным словом. Обычно лексический анализ выполняется так называемым лексическим анализатором (ЛА). ЛА - это программа, LEX - генератор программ лексического анализа. Лексический анализ применяется во многих случаях, например, для построения редакторов или в качестве распознавателя директив в диалоговой программе и т.д. Однако наиболее важное применение ЛА - использование его в различного рода компиляторах, где он выполняет функцию программы ввода данных. ЛА выполняет первую стадию компиляции: читает строки компилируемой программы, выделяет лексемы и передает их на дальнейшие стадии компиляции (синтаксический анализ и генерацию кода). ЛА распознает тип каждой лексемы и соответствующим образом помечает ее. Например, при компиляции программы на языке Си могут быть выделены следующие типы лексем: число, идентификатор, оператор и др. Необходимо не только выделить лексему, но и выполнить некоторые преобразования. Например, если лексема - число, то его необходимо перевести во внутреннюю (двоичную) форму записи как число с плавающей или фиксированной точкой. Если лексема - идентификатор, то его необходимо разместить в таблице для того, чтобы в дальнейшем обращаться к нему не по имени, а по адресу в таблице. Хотя лексический анализ по идее достаточно прост, эта фаза работы компилятора часто занимает больше времени, чем любая другая. Частично это происходит из-за необходимости просматривать и анализировать исходный текст символ за символом. Происходит это потому, что часто бывает трудно определить, где проходят границы лексемы. Пусть имеются две лексемы:make и makefile. Пусть из входного потока поступает набор литеральных символов:...makefile....

При анализе входного потока может быть выделена лексема make, хотя правильно было бы выделить лексему makefile. Единственный способ преодолеть это затруднение - просмотр полученной цепочки вперед и назад. В примере при выделении лексемы make нужно просмотреть следующий поступающий литерал, и, если он будет символом f, то вполне возможно, что поступает лексема makefile. Процесс просмотра входного потока можно рассматривать как движение рамки влево и вправо над цепочкой символов. При этом анализируется только тот символ, который охвачен рамкой:

           ...
           . .
source make.f.ile file compiler
           . .
           ...
Анализ заключается в определении соответствия рассматриваемой последовательности некоторому, так называемому регулярному выражению. Например, регулярное выражение (\+?[0-9])+|(-?[0-9])+ позволяет выделить во входном потоке все лексемы типа целое, перед которыми либо стоит знак (+ или -), либо не стоит.

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

Для любого компилятора можно выделить три языка:

  1. входной - язык, который анализируется компилятором.
  2. рабочий - язык, на котором написан сам компилятор.
  3. выходной - язык, который появляется в результате работы компилятора.
Например, на Си может быть написан компилятор, преобразующий программы на языке Си в машинные коды.

LEX частично или полностью автоматизирует процесс написания программы лексического анализа. Генерируемые с помощью LEX ЛА описываются на языке LEX. Последовательность использования LEX схематически показана на рис. 15:

\includegraphics[ scale=0.45]{lex_ris.eps}

Рис. 15. Схема использования LEX

Таким образом в этой последовательности можно выделить три этапа:

  1. С помощью команды lex из файла (например, program.lex), описывающего ЛА некоторого языка и написанного на языке LEX (входной язык для lex), получается файл (например, lex.yy.c) с текстом ЛА на языке Си (выходной язык для lex).
  2. С помощью команды cc из файла (lex.yy.c) на языке Си (входной язык для cc) получается исполняемый файл (например, a.out) с машинными кодами (выходной язык для cc), непосредственно реализующими ЛА.
  3. После запуска исполняемого файла (a.out) осуществляется интерактивный лексический анализ вводимого с клавиатуры текста или лексический анализ файла, написанного на некотором языке (входной язык ЛА), и, возможно, вывод на экран или в файл информации, связанной с разбором (выходной язык ЛА). Гибкие возможности системы UNIX по управлению потоками позволяют использовать входной и выходной файлы и на этом этапе.
В качестве рабочего языка на первом и третьем этапах выступает язык Си. Для систем UNIX язык Си выступает в роли так называемого хост-языка (главного языка). Команда lex, в конечном счете генерирующая ЛА, впрочем, как и сама система UNIX, написаны на Си. Исполняемый файл команды lex, как и других команд получается при компиляции во время установки системы с применением все того же компилятора Си, привязанного к определенной аппаратной платформе. Таким образом, компилятор Си cc (написанный разработчиком, например, на Си и ассемблере для выбранной платформы и поставляемый вместе с системой) является базой.



2004-06-22

next up previous contents
Next: Регулярные выражения Up: LEX - лексический анализатор Previous: Введение   Contents

Команда lex

В целом подсистема LEX для систем UNIX включает следующие файлы:

/usr/ccs/bin/lex;
lex.yy.c;
/usr/ccs/lib/lex/ncform;
/usr/lib/libl.a;
/usr/lib/libl.so.
В каталоге /usr/ccs/lib/lex имеется файл-заготовка ncform, который LEX используется для построения ЛА. Этот файл является уже готовой программой лексического анализа. Но в нем не определены действия, которые необходимо выполнять при распознавании лексем, отсутствуют и сами лексемы, не сформированы рабочие массивы и т.д. С помощью команды lex файл ncform достраивается. В результате мы получаем файл со стандартным именем lex.yy.c. Если LEX-программа размещена в файле program.l, то для получения ЛА с именем program необходимо выполнить следующий набор команд:

lex program.l
cc lex.yy.c -ll -o program
Если имя входного файла для команды lex не указано, то будет использоваться файл стандартного ввода. Флаг -ll требуется для подключения /usr/ccs/lib/libl.a - библиотеки LEX. Если необходимо получить самостоятельную программу, как в данном случае, подключение библиотеки обязательно, поскольку из нее подключается главная функция main. В противном случае, если имеется необходимость включить ЛА в качестве функции в другую программу (например, в программу синтаксического анализа), эту библиотеку необходимо вызвать уже при сборке. Тогда, если main определен в вызывающей ЛА программе, редактор связей не будет подключать раздел main из библиотеки LEX.

Общий формат вызова команды lex:

lex [-ctvn -V -Q[y|n]] [file]
Флаги:

-c - включает фазу генерации Си-файла (устанавливается по умолчанию);

-t - поместить результат в стандартный файл вывода, а не в файл lex.yy.c;

-v - вывести размеры внутренних таблиц;

-n - не выводить размеры таблиц (устанавливается по умолчанию);

-V - вывести информацию о версии LEX в стандартный файл ошибок;

-Q - вывести (Qy) либо не выводить (Qn, устанавливается по умолчанию) информацию о версии в файл lex.yy.c.



2004-06-22

next up previous contents
Next: Структура LEX-программы Up: LEX - лексический анализатор Previous: Команда lex   Contents

Регулярные выражения

В LEX-программе регулярные выражения используются для определения лексем. (Кроме того, регулярные выражения широко используются при описании синтаксиса команд в разделах помощи UNIX-систем). Ниже приводится полное описание регулярных выражений применительно к LEX.

Регулярное выражение может содержать буквы латинского и русского алфавитов в верхнем и нижнем регистрах, цифры, знаки препинания и т.д. (литеральные символы), а также символы-операторы (метасимволы). Кроме того, в регулярных выражениях можно использовать управляющие символы потока stdout языка Си, например:

\восьмеричный код - указание символа его восьмеричным кодом;

\xшестнадцатеричный код - указание символа его шестнадцатеричным кодом;

\n - символ новой строки;

\t - символ табуляции;

\b - возврат курсора на один шаг назад.

В данном разделе понятие ``символ'' используется в смысле языка программирования Си, т.е. подразумевается прежде всего литеральный символ. Операторы позволяют расширить возможности для описания цепочек символов. Регулярные выражения допускают использование операций конкатенации, т.е., ``стыковки'' друг с другом. Если в выражении используется символ пробела и он не находится внутри квадратных скобок, то его необходимо экранировать по правилам, описанным ниже, так как пробел и табуляция используются в качестве разделителей внутри LEX-программы.

Операторы обозначаются символами-операторами, к которым относятся:

\ "" . [] - * + / | ? $ ^ {} <> ()
Каждый из этих символов или пар скобок в регулярном выражении играет свою роль:

  1. \ и "" - операторы экранирования. Используются для отмены специального значения символа, обозначающего оператор, либо последовательности таких символов.
    \c - отменяет специальное значение символа c. В общем случае c может быть любым символом (в том числе и ), кроме цифры и скобки (т.е. специальное значение скобок таким способом отменить нельзя).
    "сс" - отменяет специальное значение последовательности символов c. В общем случае в двойные кавычки можно заключать любую последовательность символов, но специальное значение таким способом отменить нельзя.
    Примеры:
    abc+ ,где + является символом-оператором, а в выражении
    abc\+ специальное значение + отменяется.
  2. . - оператор выделения произвольного символа.
    . - означает вхождение любого символа, кроме символа новой строки n.
  3. [] - оператор выделения символа из определенного класса. Используется для определения вхождения одного любого символа из числа заключенных в квадратные скобки. Заключение символа в квадратные скобки также выполняет роль экранирования. Квадратные скобки внутри квадратных скобок также теряют свой специальный смысл.
    [xy] - означает вхождение либо символа x, либо символа y.
    [x-z] - означает вхождение любого символа из лексикографически упорядоченной последовательности от x до z.
    Примеры:
    [A-z]
    выделяет любой латинский символ, а
    [+-0-9]
    означает все цифры и знаки + и -.
  4. * и + - операторы-повторители. Используются, когда необходимо указать повторяемость вхождения символа или класса символов в регулярном выражении.
    x* - означает любое (в том числе и нулевое) число вхождений символа x.
    x+ - означает одно и более вхождений символа x.
    Примеры:
    [A-z]*
    выделяет любое количество любых латинских букв, а
    [A-ZА-Яa-zа-я0-9]*
    выделяет любое количество русских и латинских букв, знаков подчеркивания и цифр.
    [A-z]+
    выделяет одну любую латинскую букву или более.
  5. /, |, ?, $, ^ - операторы выбора. Управляют процессом выбора символов.
    x/y - означает, что вхождение x учитывается только тогда, когда за ним следует y.
    x|y - означает или x, или y.
    x? - означает необязательность вхождения символа x.
    x$ - означает учет вхождения символа x, если он является последним в строке (cтоит перед символом n).
    ^x - означает учет вхождения символа x, если он является первым символом строки.
    [^x] - означает вхождение любого символа, кроме x. Внутри квадратных скобок ^ должен обязательно стоять первым.
    Примеры:
    _?[A-Za-z]*
    выделяет цепочку из любого количества латинских букв, перед которой может быть необязательный символ подчеркивания.
    -?[0-9]+
    выделяет любое целое число с необязательным минусом впереди.
    ^[A-Z]
    выделяет цепочку из любых символов, кроме прописных латинских букв.
  6. {} - оператор имеет два различных применения:
    x{n,m} - означает от n до m вхождений x (здесь n и m натуральные, m > n).
    {имя} - означает, что вместо имени в данное место выражения будет подставлено определение имени из области определений LEX-программы.
    Пример:
    a{2,7}
    выделяет от 2 до 7 вхождений a.
  7. <> - оператор используется для учета состояния ЛА при определении вхождений.
    <состояние>x - означает учет вхождения x, если ЛА находится в положении "состояние".
  8. () - оператор изменения группировки регулярного выражения. Все перечисленные выше операторы имеют соответствующим образом определенные приоритеты. Заключение в круглые скобки позволяет необходимым способом изменить приоритеты для правильного описания входной информации.
Регулярные выражения, которые описываются с помощью операторов +, /, |, ?, , <> и (), принято называть расширенными регулярными выражениями.



2004-06-22

next up previous contents
Next: Секция определений LEX-программы Up: LEX - лексический анализатор Previous: Регулярные выражения   Contents

Структура LEX-программы

LEX-программа, написанная на языке LEX, имеет следующий формат:

определения
%%
правила
%%
пользовательские_подпрограммы
Таким образом, LEX-программа включает секции опредeлений
(definitions), правил (rules) и пользовательских подпрограмм (user subroutines). Любая из этих секций может быть пустой. Простейшая LEX - программа имеет вид:

%%
Здесь нет никаких определений и никаких правил. Все строки, в которых занята первая позиция, относятся к LEX-программе. Любая строка, не являющаяся частью правила или действия, которая начинается с пробела или табуляции, копируется в генерируемый файл lex.yy.c.



Subsections

2004-06-22

next up previous contents
Next: Секция правил LEX-программы Up: Структура LEX-программы Previous: Структура LEX-программы   Contents

Секция определений LEX-программы

Определения, предназначенные для LEX, помещаются перед первым %%. Любая строка этой секции, не содержащаяся между


, и начинающаяся в первой колонке, является определением LEX. Секция определений LEX-программы может включать:

Начальные условия задаются в форме:

%START имя1 имя2 ...
Если начальные условия определены, то эта строка должна быть первой в LEX-программе.

Непосредственно определения задаются в форме:

имя трансляция
В качестве разделителя используется один или более пробелов или табуляций. Имя (name) - это, как обычно, любая последовательность букв и цифр, начинающаяся с буквы. Трансляция (translation) - это регулярное выражение (или его часть), которое будет подставлено всюду, где указано имя. Пример:

БУКВА [A-ZА-Яa-zа-я_]
ЦИФРА [0-9]
ИДЕНТИФИКАТОР {БУКВА}({БУКВА}|{ЦИФРА})*
%%
{ИДЕНТИФИКАТОР} printf("\n%s",yytext);
. . .
LEX построит ЛА, который будет находить и выводить все идентификаторы Си-программы из входного файла. В этом примере
ИДЕНТИФИКАТОР заменит БУКВА(БУКВА|ЦИФРА), затем на
A-ZА-Яa-zа-я(A-ZА-Яa-zа-я|0-9). yytext - это внешний массив символов программы lex.yy.c, которую строит LEX; yytext формируется в процессе чтения входного файла и содержит текст, для которого установлено соответствие какому-либо выражению. Этот массив доступен пользовательским разделам LEX-программы. Функция printf выводит каждый идентификатор на новой строке.

Фрагменты пользовательских подпрограмм вводятся двумя способами:

отступ фрагмент;
или

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

Таблица наборов символов задается в виде:

%T
целое_число строка_символов
. . .
целое_число строка_символов
%T
Сгенерированная программа lex.yy.c осуществляет ввод/вывод символов посредством библиотечных функций LEX с именами input,
output, unput. Таким образом, LEX помещает в массив yytext символы в представлении, используемом в этих библиотечных функциях. Для внутреннего использования символ представляется целым числом, значение которого образовано набором битов, представляющих символ в конкретной ЭВМ. Пользователю представляется возможность менять представление символов (целых констант) с помощью таблицы наборов символов. Если таблица символов присутствует в разделе определений, то любой символ, появляющийся либо во входном потоке, либо в правилах, должен быть определен в таблице символов. Символам нельзя назначать число 0 и число, большее числа, выделенного для внутреннего представления символов конкретной ЭВМ. Пример:

%T
1 Aa
2 Bb
3 Cc
.
.
.
26 Zz
27 \n
28 +
29 -
30 0
31 1
.
.
.
39 9
%T
Здесь символы верхнего и нижнего регистров переводятся в числа 1 - 26, символ новой строки - в число 27, + и - переводятся в числа 28 и 29, а цифры - в числа 30 - 39.

Указатель хост-языка имеет вид:

%C
для языка Си. Для других языков программирования, например Фортрана, указатель выглядит соответственно. Если указатель хост-языка отсутствует, то по умолчанию принимается Си.

Изменения размеров внутренних массивов задаются в форме:

%x число
Число - новый размер массива, x - одна из букв: p - позиции (positions), n - состояния (states), e - узлы дерева разбора (parse tree nodes), a - упакованные переходы (transitions), k - упакованные классы символов (packet character classes), o - массив выходных элементов (output array). LEX имеет внутренние таблицы, размеры которых ограничены. При построении программы лексического анализа может произойти переполнение любой из этих таблиц, о чем LEX сообщит при построении ЛА. Представляется возможность изменить размеры этих таблиц (сокращая размеры одних и увеличивая размеры других) таким образом, чтобы они не переполнялись. Естественно, эти изменения возможны лишь в пределах той памяти, которая выделяется под процесс. Ниже перечислены размеры таблиц, которые устанавливаются по умолчанию: p - 1500 позиций, n - 300 состояний, e - 600 узлов, a - 1500 упакованных переходов, k - 1000 упакованных классов символов, o - 1500 выходных элементов. Для того чтобы определить, каковы размеры таблиц и насколько они заняты, можно использовать флаг -v при вызове команды lex. Число перед / показывает сколько элементов массива занято, а число за / показывает установленный размер массива.

Комментарии в разделе определений задаются в форме хост-языка и должны начинаться не с первой колонки строки.



2004-06-22

next up previous contents
Next: Секция пользовательских подпрограмм LEX-программы Up: Структура LEX-программы Previous: Секция определений LEX-программы   Contents

Секция правил LEX-программы

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

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

Активное правило имеет вид:

регулярное_выражение действие
Активные правила выполняются всегда. По регулярным выражениям (regular expressions), содержащимся в левой части правил, LEX строит детерминированный конечный автомат. Этот автомат осуществляет интерпретацию, а не компиляцию, и переходит из одного состояния в другое. Количество правил и их сложность не влияют на скорость лексического анализа, если только правила не требуют слишком большого объема повторных просмотров входной последовательности символов. Однако с ростом числа правил и их сложности растет размер конечного автомата, интерпретирующего их, и, следовательно, растет размер Си-программы, реализующей этот конечный автомат.

LEX-программа, приведенная ниже, переводит на русский язык названия месяцев и дней недели.

%%
[jJ][aA][nN][uU][aA][rR][yY] {printf("Январь");}
[fF][eE][bB][rR][uU][aA][rR][yY] {printf("Февраль");}
[mM][aA][rR][cC][hH] {printf("Март");}
[aA][pP][rR][iI][lL] {printf("Апрель");}
[mM][aA][yY] {printf("Май");}
[jJ][uU][nN][eE] {printf("Июнь");}
[jJ][uU][lL][yY] {printf("Июль");}
[aA][uU][gG][uU][sS][tT] {printf("Август");}
[sS][eE][pP][tT][eE][mM][bB][eE][rR] {printf("Сентябрь");}
[oO][cC][tT][oO][bB][eE][rR] {printf("Октябрь");}
[nN][oO][vV][eE][mM][bB][eE][rR] {printf("Ноябрь");}
[dD][eE][cC][eE][mM][bB][eE][rR] {printf("Декабрь");}
[mM][oO][nN][dD][aA][yY] {printf("Понедельник");}
[tT][uU][eE][sS][dD][aA][yY] {printf("Вторник");}
[wW][eE][dD][nN][eE][sS][dD][aA][yY] {printf("Среда");}
[tT][hH][uU][rR][sS][dD][aA][yY] {printf("Четверг");}
[fF][rR][iI][dD][aA][yY] {printf("Пятница");}
[sS][aA][tT][uU][rR][dD][aA][yY] {printf("Суббота");}
[sS][uU][nN][dD][aA][yY] {printf("Воскресенье");}

Каждое правило здесь определяет действие (оно взято в фигурные скобки). Открывающая фигурная скобка стоит в той же строке, что и правило, - это требование LEX. Действие в каждом правиле данной LEX-программы - это вывод русского значения найденного английского слова. В качестве оператора, выполняющего действие, используется библиотечная функция языка Си. Пара фигурных скобок определяет блок (в смысле языка Си), который может содержать любое количество строк. Если действие содержит всего одну строку Си, то можно ее записать без фигурных скобок, как обычно. Единственное условие - она должна начинаться в той же строке, где и регулярное выражение. В программе содержится только раздел правил, их всего 19. Регулярное выражение каждого правила определяет английское слово, написанное строчными или прописными латинскими символами. Например, ``may'' (май) определен как mMaAyY. По этому регулярному выражению будет выделена во входном потоке лексема ``may'', а по действию этого правила будет выведено ``Май''. Наличие строчной и прописной буквы в квадратных скобках обеспечивает распознавание слова ``may'', написанного любым способом. После запуска на выполнение ЛА получим:

May
Май
MONDAY
Понедельник
. . .
Ctrl-c
Действие (action) можно представлять либо как специальное действие LEX, либо как оператор Си. Если имеется необходимость выполнить достаточно большой набор преобразований, то действие
оформляют как блок Си-программы (он начинается открывающей фигурной скобкой и завершается закрывающей), содержащий необходимые фрагменты. Действие в правиле записывается не менее чем через один пробел или табуляцию после регулярного выражения (начинается обязательно в той же строке), а его продолжение может быть в следующих строках только в том случае, если действие оформлено как блок Си-программы. Область действия переменных, объявленных внутри блока, распространяется только на этот блок. Внешними переменными для всех действий будут только те переменные, которые объявлены в секции определений. Действия в правилах LEX-программы выполняются, если правило активно, т.е., если ЛА распознает цепочку символов из входного потока как соответствующую регулярному выражению данного правила. Однако если входная цепочка символов не распознана, то она просто копируется из входного потока в выходной. Комбинация символов, не учтенная в правилах и появившаяся на входе, будет напечатана на выходе. Можно сказать, что копирование осуществляется ``по умолчанию'', а действие - это то, что делается вместо копирования в случае успешного распознавания лексемы. Таким образом, ЛА, построенный на основе только простейшей LEX-программы и соответственно файла-заготовки ncform, будет просто копировать входной поток в выходной. Часто бывает необходимо избавиться от подобного копирования. Для этой цели используется пустой оператор Си. Например, правило

[ \t\n] ;
запрещает вывод пробелов, табуляций и символов ``новая строка''. Запрет выражается в том, что для указанных символов во входном потоке осуществляется действие ; - пустой оператор Си, и эти символы не копируются в выводной поток символов.

Как отмечалось выше, в качестве действий могут выступать специальные действия языка LEX:

BEGIN | ECHO REJECT
Для нескольких регулярных выражений можно указывать одно действие. Для этого используется специальное действие |, которое свидетельствует о том, что действие для данного регулярного выражения совпадает с действием для следующего. Например:

" " |
\t |
\n ;
Специальное действие BEGIN имеет следующий формат:

BEGIN метка;
Если LEX-программа содержит активные и неактивные правила, то активные правила работают всегда. Специальное действие BEGIN просто расширяет список активных правил, активизируя помеченные меткой неактивные правила. BEGIN 0; удаляет из ``списка активных'' все помеченные правила, которые до этого были активизированы, и тем самым возвращает ЛА в самое начальное состояние. Кроме того, если из помеченного и активного в данный момент времени правила осуществляется специальное действие BEGIN, то из помеченных правил активными останутся либо станут только те, которые помечены меткой, указанной в этом специальном действии. Например, в качестве первого правила секции правил может быть:

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

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

[A-Z]+ printf("%s",yytext);
распознается и выводится на экран слово, содержащее прописные латинские буквы. Операция вывода распознанного выражения используется очень часто, поэтому имеется сокращенная форма записи этого действия, выражающаяся специальным действием ECHO:

[A-Z]+ ECHO;
Результат действия этого правила аналогичен результату предыдущего примера. В файле lex.yy.c ECHO определяется как макроподстановка:

#define ECHO fprintf(yyout,"%s",yytext);
Когда необходимо знать длину обнаруженной последовательности, используется счетчик найденных символов yyleng, который также доступен в действиях. Например, действием

[A-Z]+ printf("%c",yytext[yyleng-1]);
будет выводится последний символ слова, соответствующего регулярному выражению A-Z+. Еще один пример:

[A-Z]+ {число_слов++; число_букв += yyleng;}
Здесь ведется подсчет числа распознанных слов и количества символов во всех словах.

Обычно LEX разделяет входной поток, не осуществляя поиска всех возможных соответствий каждому выражению. Это означает, что каждый символ рассматривается один и только один раз. Предположим, что мы хотим подсчитать все вхождения she и he во входном тексте. Для этого мы могли бы записать следующие правила:

she s++;
he h++;
. |
\n ;
Поскольку she включает в себя he, ЛА не распознает те вхождения he, которые включены в she, так как прочитав один раз she, эти символы он не вернет во входной поток. Иногда нужно изменить подобную ситуацию. Специальное действие REJECT означает - ``выбрать следующую альтернативу''. Это приводит к тому, что, каким бы ни было правило, после него необходимо выполнить второй выбор. Соответственно изменится и положение указателя во входном потоке:

she {s++; REJECT;}
he {h++; REJECT;}
. |
\n ;
Здесь после выполнения одного правила символы возвращаются во входной поток и выполняется другое правило. Слово the содержит как th, так и he. Если имеется двухмерный массив digram, тогда:

%%
[a-z][a-z] {
digram[yytext[0]][yytext[1]]++;
REJECT;
}
\n ;
Здесь REJECT используется для выделения буквенных пар, начинающихся на каждой букве, а не на каждой следующей. Как видно, REJECT полезно применять для определения всех вхождений, причем вхождения могут перекрываться или включать друг друга.

Неактивное (помеченное) правило имеет вид:

<метка> регулярное_выражение действие
или

<метка1,метка2,...> регулярное_выражение действие
Неактивные правила выполняются только в тех случаях, когда выполняется некоторое начальное условие. Количество помеченных
правил не ограничивается. Кроме того, разрешается одно правило помечать несколькими метками. Начальные условия LEX-программы помещаются в секцию определений. С помощью START вводится список начальных условий как список меток соответствующих состояний автомата (states). Метка формируется так же, как и метка в Си. Неактивные правила помечаются начальными условиями, а специальное действие BEGIN позволяет активизировать правила, помеченные начальными условиями, т.е. автомат переходит в соответствующее состояние. Пример:

%Start КОММ
КОММ_НАЧАЛО "/*"
КОММ_КОНЕЦ "*/"
%%
{КОММ_НАЧАЛО} {
ECHO;
BEGIN КОММ;
};
[\t\n]* ;
<КОММ>[\^*]* ECHO;
<КОММ>\*/[^/] ECHO;
<КОММ>{КОММ_КОНЕЦ} {
ECHO;
printf("\n");
BEGIN 0;
};
LEX строит ЛА, который выделяет комментарии в Си-программе и записывает их в стандартный файл вывода. Программа начинается с ключевого слова START (можно записывать и как Start), за которым поставлена метка начального условия КОММ. В данном случае оператор <КОММ>x означает учет x, если ЛА находится в начальном условии КОММ. Специальное действие BEGIN КОММ; переводит ЛА в состояние, соответствующее начальному условию КОММ. После этого ЛА уже находится в новом состоянии КОММ, и теперь разбор входного потока будет осуществляется и теми правилами, которые начинаются с <КОММ>. Например, правилом

<КОММ>[\^*]* ECHO;
теперь будет записываться в стандартный файл вывода любое число (включая и ноль) символов, отличных от символа *. Оператор BEGIN 0; переводит ЛА в исходное состояние.

LEX-программа может содержать несколько помеченных начальных условий. Например, если LEX-программа начинается строкой

%Start AA BB CC DD
, то это означает, что она управляет четырьмя начальными состояниями ЛА. В каждое из этих начальных состояний ЛА можно перевести, используя BEGIN. Пример с несколькими начальными условиями:

%START AA BB CC
БУКВА [A-ZА-Яa-zа-я_]
ЦИФРА [0-9]
ИДЕНТИФИКАТОР {БУКВА}({БУКВА}|{ЦИФРА})*
%%
^# BEGIN AA;
^[ \t]*main BEGIN BB;
^[ \t]*{ИДЕНТИФИКАТОР} BEGIN CC;
\t ;
\n BEGIN 0;
<AA>define printf("Определение.\n");
<AA>include printf("Включение.\n");
<AA>ifdef printf("Условная компиляция.\n");
<BB>[^\,]*","[^\,]*")" printf("main с аргументами.\n");
<BB>[^\,]*")" printf("main без аргументов.\n");
<CC>":"/[ \t] printf("Метка.\n");
Программа содержит активные и неактивные правила. Все неактивные правила помечены. LEX-программа управляет тремя начальными условиями, в соответствии с которыми активизируются помеченные правила. Здесь представлен ЛА, который будет распознавать в Си-программе строки макропроцессора Си-компилятора, выделять функцию main, распознавая с аргументами она или без них, распознавать метки. ЛА не выводит ничего, кроме сообщений о выделенных лексемах.

Таким образом, в процессе работы ЛА список активных правил может ``видоизменяться'' за счет действий оператора BEGIN. В процессе распознавания лексем во входном потоке может оказаться так, что одна цепочка символов удовлетворяет нескольким правилам и, следовательно, возникает проблема: действие какого правила должно выполняться? Для разрешения этого противоречия можно использовать ``разбиение'' регулярных выражений этих правил LEX-программы на такие новые регулярные выражения, которые дадут однозначное распознавание лексем. Однако, если это не сделано, LEX использует определенный детерминированный механизм разрешения такого противоречия:

Пример:

[Мм][Аа][Йй] ECHO;
[А-Яа-я]+ ECHO;
Слово ``Май'' распознают оба правила, однако выполнится первое из них, так как и первое и второе правила распознали лексему одинакового размера (3 символа). Если во входном потоке будет слово ``майский'', то первые 3 символа удовлетворяют первому правилу, а все 7 символов удовлетворяют второму правилу и, следовательно, выполнится второе правило, так как ему соответствует более длинная последовательность.

Фрагменты пользовательских подпрограмм, содержащиеся в секции правил, становятся частью функции yylex файла lex.yy.c. Они вводятся следующим образом:

%{
фрагмент
%}
Пример:

%%
%{
#include file.h
%}
Здесь строка include file.h станет строкой функции yylex.



2004-06-22

next up previous contents
Next: Структура файла lex.yy.c Up: Структура LEX-программы Previous: Секция правил LEX-программы   Contents

Секция пользовательских подпрограмм LEX-программы

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

Комментарии можно вводить во всех разделах LEX-программы. Формат комментариев должен соответствовать формату комментариев хост-языка, т.е. языка Си. Однако в каждой секции LEX - программы комментарии вводятся по-разному:

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

%Start КОММ
/*
* Программа записывает в стандартный файл вывода
* комментарии Си-программы. Обратите внимание на то,
* что здесь строки комментариев начинаются не с первой
* позиции строки!
*/
КОММ_НАЧАЛО "/*"
КОММ_КОНЕЦ "*/"
%%
{КОММ_НАЧАЛО} {ECHO;
BEGIN КОММ;}
[\t\n]* ;
<КОММ>[^*]* ECHO;
<КОММ>\*/[^/] ECHO;
<КОММ>{КОММ_КОНЕЦ} {ECHO;
printf("\n");
/*
* Здесь приведен пример использования комментариев в
* разделе правил LEX-программы. Обратите внимание на то,
* что комментарий находится внутри блока, определяющего
* действие правила.
*/
BEGIN 0;}
%%
/*
* Здесь приведен пример комментариев в разделе
* пользовательских подпрограмм.
*/



2004-06-22

next up previous contents
Next: yylex(). Up: LEX - лексический анализатор Previous: Секция пользовательских подпрограмм LEX-программы   Contents

Структура файла lex.yy.c

LEX строит программу ЛА на языке Си, которая размещается в файле со стандартным именем lex.yy.c. Так как файл lex.yy.c - это Си-программа, то в него можно внести любые необходимые изменения и добавления. Эта программа содержит ряд основных и вспомогательных функций:



Subsections

2004-06-22

next up previous contents
Next: Операции над очередями сообщений Up: Очереди сообщений Previous: Создание очередей сообщений   Contents

Управление очередями сообщений

Синтаксис системного вызова msgctl:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgctl ( int msqid, int cmd,

        struct msqid_ds *buf);

При успешном завершении результат равен нулю; в случае неудачи возвращается -1.

В качестве аргумента msqid должен выступать идентификатор очереди сообщений, предварительно полученный системным вызовом msgget.

Управляющее действие определяется значением аргумента cmd. Допустимых значений три:

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

Программа-пример в этом разделе иллюстрирует управление очередью. В программе использованы следующие переменные:

Программа ведет себя следующим образом. Прежде всего предлагается ввести допустимый идентификатор очереди сообщений, который заносится в переменную msqid. Это значение требуется в каждом системном вызове msgctl. Затем нужно ввести код выбранного управляющего действия, который заносится в переменную command.

Если выбрано действие IPC_STAT (код 1), выполняется системный вызов и распечатывается информация о состоянии очереди; в программе распечатываются только те поля структуры, которые могут быть переустановлены. Если системный вызов завершается неудачей, распечатывается информация о состоянии очереди на момент последнего успешного выполнения системного вызова. Кроме того, выводится сообщение об ошибке и распечатывается значение переменной errno. Если системный вызов завершается успешно, выводится сообщение, уведомляющее об этом, и значение использованного идентификатора очереди сообщений.

Если выбрано действие IPC_SET (код 2), программа прежде всего получает информацию о текущем состоянии очереди сообщений с заданным идентификатором. Это необходимо, поскольку в примере обеспечивается изменение только одного поля за один раз, в то время как системный вызов изменяет всю структуру целиком. Кроме того, если в одно из полей структуры, находящейся в области памяти пользователя, будет занесено некорректное значение, это может вызвать неудачи в выполнении управляющих действий, повторяющиеся до тех пор, пока значение поля не будет исправлено. Затем программа предлагает ввести код, соответствующий полю структуры, которое должно быть изменено. Этот код заносится в переменную choice. Далее, в зависимости от указанного поля, программа предлагает ввести то или иное новое значение. Значение заносится в соответствующее поле структуры данных, расположенной в области памяти пользователя, и выполняется системный вызов.

Если выбрано действие IPC_RMID (код 3), выполняется системный вызов, удаляющий из системы идентификатор msqid, очередь сообщений и ассоциированную с ней структуру данных. Отметим, что для выполнения этого управляющего действия аргумент buf не требуется, поэтому его значение может быть заменено нулем (NULL):

/* Программа иллюстрирует

возможности системного вызова msgctl()

(управление очередями сообщений) */

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

main ()

{

  extern int errno;

  int msqid, command, choice, rtrn;

  struct msqid_ds msqid_ds, *buf;

  buf = &msqid_ds;

  

  /* Ввести идентификатор и действие */

  printf ("Введите идентификатор msqid: ");

  scanf ("%d", &msqid);

  printf ("Введите номер требуемого действия:\n");

  printf (" IPC_STAT = 1\n");

  printf (" IPC_SET = 2\n");

  printf (" IPC_RMID = 3\n");

  printf (" Выбор = ");

  scanf ("%d", &command);

  

  /* Проверить значения */

  printf ("идентификатор = %d, действие = %d\n",

          msqid, command);

  switch (command) {

  case 1: /* Скопировать информацию

          о состоянии очереди сообщений

          в пользовательскую структуру

          и вывести ее */

          rtrn = msgctl (msqid, IPC_STAT, buf);

          printf ("\n Идентификатор пользователя =

                   %d\n",

                  buf->msg_perm.uid);

          printf ("\n Идентификатор группы = %d\n",

                  buf->msg_perm.gid);

          printf ("\n Права на операции = 0%o\n",

                  buf->msg_perm.mode);

          printf ("\n Размер очереди в байтах =

                  %d\n",

                  buf->msg_qbytes);

          break;

  case 2: /* Выбрать и изменить поле (поля)

          ассоциированной структуры данных */

          /* Сначала получить исходное значение

          структуры данных */

          rtrn = msgctl (msqid, IPC_STAT, buf);

          printf ("\nВведите номер поля, ");

          printf ("которое нужно изменить:\n");

          printf (" msg_perm.uid = 1\n");

          printf (" msg_perm.gid = 2\n");

          printf (" msg_perm.mode = 3\n");

          printf (" msg_qbytes = 4\n");

          printf (" Выбор = ");

          scanf ("%d", &choice);

          switch (choice) {

             case 1:

                     printf ("\nВведите ид-р

                             пользователя: ");

                     scanf ("%d", &buf->msg_perm.uid);

                     printf ("\nИд-р пользователя =

                             %d\n",

                             buf->msg_perm.uid);

                     break;

             case 2:

                     printf ("\nВведите ид-р

                             группы: ");

                     scanf ("%d", &buf->msg_perm.gid);

                     printf ("\nИд-р группы = %d\n",

                             buf->msg_perm.uid);

                     break;

             case 3:

                     printf ("\nВведите восьмеричный

                             код прав: ");

                     scanf ("%o", &buf->msg_perm.mode);

                     printf ("\nПрава на операции =

                             0%o\n",

                             buf->msg_perm.mode);

                     break;

             case 4:

                     printf ("\nВведите размер

                             очереди = ");

                     scanf ("%d", &buf->msg_qbytes);

                     printf ("\nЧисло байт в очереди =

                             %d\n", buf->msg_qbytes);

                      break;

          }

 

          /* Внести изменения */

          rtrn = msgctl (msqid, IPC_SET, buf);

          break;

  case 3: /* Удалить идентификатор и

          ассоциированные с ним очередь

          сообщений и структуру данных */

          rtrn = msgctl (msqid, IPC_RMID, NULL);

}

  if (rtrn == -1) {

  /* Сообщить о неудачном завершении */

  printf ("\nmsgctl завершился неудачей!\n");

  printf ("\nКод ошибки = %d\n", errno);

  }

  else {

  /* При успешном завершении сообщить msqid */

  printf ("\nmsgctl завершился успешно,\n");

  printf ("идентификатор = %d\n", msqid);

  }

  exit (0);

}


next up previous contents
Next: Операции над очередями сообщений Up: Очереди сообщений Previous: Создание очередей сообщений   Contents
2004-06-22

next up previous contents
Next: yylook(). Up: Структура файла lex.yy.c Previous: Структура файла lex.yy.c   Contents

yylex().

Функция содержит разделы действий всех правил, которые определены пользователем. Файл lex.yy.c генерируется из файла - заготовки /usr/lib/lex/ncform, в котором отсутствует функция yylex(). Эта функция включается в lex.yy.c в процессе генерации. С полным текстом файла ncform можно ознакомиться в соответствующем каталоге.



2004-06-22

next up previous contents
Next: yywrap(). Up: Структура файла lex.yy.c Previous: yylex().   Contents

yylook().

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



2004-06-22

next up previous contents
Next: yymore(). Up: Структура файла lex.yy.c Previous: yylook().   Contents

yywrap().

Функция используется для определения конца файла, из которого ЛА читает поток символов. Если yywrap возвращает 1, ЛА прекращает работу. Однако иногда появляется необходимость начать ввод данных из другого источника и продолжить работу. В этом случае нужно написать свою подпрограмму yywrap, которая организует новый входной поток и возвращает 0, что служит сигналом к продолжению работы ЛА. По умолчанию yywrap всегда возвращает 1 при завершении входного потока символов. В LEX-программе невозможно записать правило, которое будет обнаруживать конец файла. Единственный способ это сделать - использовать функцию yywrap. Эта функция также удобна, когда необходимо выполнить какие-либо действия по завершению входного потока символов. Пример:

%START AA BB CC
/*
* Строится ЛА, который распознает наличие включений
* файлов в Си-программе, условных компиляций,
* макроопределений, меток и головной функции main.
* Анализатор ничего не выводит
* пока осуществляется чтение всего входного потока.
* По завершении выводится статистика.
*/
БУКВА [A-ZА-Яa-zа-я_]
ЦИФРА [0-9]
ИДЕНТИФИКАТОР {БУКВА}({БУКВА}|{ЦИФРА})*
int a1,a2,a3,b1,b2,c;
%%
{a1=a2=a3=b1=b2=c=0;}
^# BEGIN AA;
^[ \t]*main BEGIN BB;
^[ \t]*{ИДЕНТИФИКАТОР} BEGIN CC;
\t ;
\n BEGIN 0;
<AA>define {a1++;}
<AA>include {a2++;}
<AA>ifdef {a3++;}
<BB>[^\,]*","[^\,]*")" {b1++;}
<BB>[^\,]*")" {b2++;}
<CC>":"/[ \t] {c++;}
%%
yywrap()
{
if(b1==0&&b2==0)
printf("В программе отсутствует функция main.\n");
if(b1>=1&&b2>=1)
printf("Многократное определение функции main.\n");
else
{
if(b1==1) printf("Функция main с аргументами.\n");
if(b2==1) printf("Функция main без аргументов.\n");
}
printf("Включений файлов: %d.\n",a2);
printf("Условных компиляций: %d.\n",a3);
printf("Определений: %d.\n",a1);
printf("Меток: %d.\n",c);
return(1);
}
Оператор return(1) в функции yywrap указывает, что ЛА должен завершить работу. Если необходимо продолжить работу ЛА для чтения данных из нового файла, нужно ипользовать return(0), предварительно осуществив операции закрытия и открытия файлов. Однако если yywrap не возвращает 1, то это приводит к бесконечному циклу.



2004-06-22

next up previous contents
Next: yyless(n). Up: Структура файла lex.yy.c Previous: yywrap().   Contents

yymore().

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

.
.
.
\"[^"]* {
if(yytext[yyleng-1]=='\\') yymore();
else
{/*
* Здесь должна быть часть программы,
* обрабатывающая закрывающую кавычку.
*/}
}
.
.
.
В этом примере распознаются строки симвoлов, взятые в двойные кавычки, причем символ двойных кавычек внутри этой строки может изображаться с предшествующей косой чертой. ЛА должен распознавать кавычку, ограничивающую строку, и кавычку, являющуюся частью строки, когда она изображена как \". Если на вход поступает строка абв\"где", то сначала будет распознана цепочка "абв и так как последним символом в этой цепочке будет символ \, выполнится вызов yymore. В результате к цепочке "абв\ будет добавлено "где, и в yytext мы получим "абв\"где", что и требовалось.



2004-06-22

next up previous contents
Next: input(). Up: Структура файла lex.yy.c Previous: yymore().   Contents

yyless(n).

В некоторых случаях возникает необходимость использовать не все символы распознанной цепочки в yytext, а только необходимое число. Для этой цели используется функция yyless(n), где n указывает, что в данный момент необходимо только n символов строки из yytext. Остальные найденные символы будут возвращены во входной поток. Пример использования функции yyless:

.
.
.
=-[A-ZА-Яa-zа-я] {
printf("Oператор (=-) двусмысленный.\n");
yyless(yyleng-2);
/*
* Здесь необходимо записать действия для
* случая "=-"
*/
}
.
.
.
В этом примере разрешается двусмысленность выражения =- буква в языке Си. Это выражение можно рассматривать как =- буква или как = -буква. Предположим, что эту ситуацию нужно рассматривать как = -буква и выводить предупреждение. В примере правило распознает эту ситуацию, выводит предупреждение и затем, после вызова yyless(yyleng - 2); два символа -буква будут возвращены во входной поток, а знак = останется в yytext для обработки, как и в нормальной ситуации. Таким образом, при продолжении чтения входного потока уже будет обрабатываться цепочка -буква, что и требовалось.



2004-06-22

next up previous contents
Next: unput(c). Up: Структура файла lex.yy.c Previous: yyless(n).   Contents

input().

Функция читает символ из входного потока символов. Если читается файл и достигнут его конец, то функция возвращает NULL.



2004-06-22

next up previous contents
Next: output(c). Up: Структура файла lex.yy.c Previous: input().   Contents

unput(c).

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



2004-06-22

next up previous contents
Next: YACC - еще один Up: Структура файла lex.yy.c Previous: unput(c).   Contents

output(c).

Функция выводит в выходной поток символ c.

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

При сборке программы лексического анализа редактор связи ld по флагу -ll подключает головную функцию main, если она не определена. Ниже приведен полный исходный текст такой библиотеки libl.a:

#include "stdio.h"
main()
{
yylex();
exit(0);
}



2004-06-22

next up previous contents
Next: Введение Up: Практическое использование средств разработки Previous: output(c).   Contents

YACC - еще один компилятор компиляторов



Subsections

2004-06-22

next up previous contents
Next: Входные спецификации Up: YACC - еще один Previous: YACC - еще один   Contents

Введение

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

date : month_name day ',' year ;
Здесь date, monthname, day, и year представляют собой структуры языка (нетерминалы) описанные где-то еще, а запятая, заключенная в кавычки означает, что после нетерминала day ожидается символ ',' как литера. Двоеточие и точка с запятой являются знаками пунктуации Yacc. Таким образом, строка January 1, 2000 попадает под вышеприведенное правило.

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

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

month_name : 'J' 'a' 'n' ;
month_name : 'F' 'e' 'b' ;
. . .
month_name : 'D' 'e' 'c' ;
В этом случае лексический анализатор должен только распознавать отдельные литеры, а monthname является нетерминальным символом. Но на работу с такими низкоуровневыми правилами тратится много времени и памяти, неимоверно распухают входные спецификации Yacca, поэтому обычно лексический анализатор (как более простой прием) распознает названия месяцев и выдает только признак того, что встретилось такое название. В этом случае monthname является токеном (терминальным символом). Специальные символы, такие, как в нашем примере, должны также распознавальтся лексическим анализатором.

Описание грамматики в виде правил является очень гибким, поэтому очень легко добавить к примеру правило:

date : month '/' day '/' year ;
позволяющее вводить 1 / 1 / 2000, как синоним для
January 1, 2000. В большинстве случаев новое правило может быть введено в работающую систему легко и с минимальным риском испортить уже работающие правила.

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

В некоторых случаях компилятором Yacc не удастся построить анализатор по входным спецификациям, так как либо заданная грамматика не является LALR(1), либо у Yacca не хватает мощности. Обычно эта проблема решается пересмотром системы правил, либо созданием более мощного лексического анализатора. Следует также заметить, что конструкции неподвластные Yacc, часто также непосильны и человеческому разуму...



2004-06-22

next up previous contents
Next: Семафоры Up: Очереди сообщений Previous: Управление очередями сообщений   Contents

Операции над очередями сообщений

Синтаксис системных вызовов msgop для операций над очередями выглядит так:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgsnd (int msqid, struct msgbuf * msgp,

            int msgsz, int msgflg)

int msgrcv (int msqid, struct msgbuf * msgp,

            int msgsz, long msgtyp, int msgflg)

При успешном завершении системного вызова msgsnd() результат равен нулю; в случае неудачи возвращается -1. В качестве аргумента msqid должен выступать идентификатор очереди сообщений, предварительно полученный при помощи системного вызова msgget. Аргумент msgp является указателем на структуру в области памяти пользователя, содержащую тип посылаемого сообщения и его текст. Аргумент msgsz специфицирует длину массива символов в структуре данных, указываемой аргументом msgp, т. е. длину сообщения. Максимально допустимый размер данного массива определяется системным параметром MSGMAX.

Значение поля msg_qbytes у ассоциированной структуры данных может быть уменьшено с предполагаемой по умолчанию величины MSGMNB при помощи управляющего действия IPC_SET системного вызова msgctl, однако впоследствии увеличить его может только суперпользователь. Аргумент msgflg позволяет специфицировать выполнение ``операции с блокировкой''; для этого флаг IPC_NOWAIT должен быть сброшен (msgflg &
IPC_NOWAIT = 0
). Блокировка имеет место, если либо текущее число байт в очереди уже равно максимально допустимому значению для указанной очереди (т. е. значению поля msg_qbytes или MSGMNB), либо общее число сообщений во всех очередях равно максимально допустимому системой (системный параметр MSGTQL). Если в такой ситуации флаг IPC_NOWAIT установлен, то системный вызов msgsnd() завершается неудачей и возвращает -1.

При успешном завершении системного вызова msgrcv() результат равен числу принятых байт; в случае неудачи возвращается -1. В качестве аргумента msqid должен выступать идентификатор очереди сообщений, предварительно полученный системным вызовом msgget. Аргумент msgp является указателем на структуру в области памяти пользователя, содержащую тип принимаемого сообщения и его текст. Аргумент msgsz специфицирует длину принимаемого сообщения. В случае, если значение данного аргумента меньше, чем длина сообщения в массиве, возникает ошибка (см. описание аргумента msgflg).

Аргумент msgtyp используется для выбора из очереди первого сообщения определенного типа. Если значение аргумента равно нулю, запрашивается первое сообщение в очереди, если больше нуля - первое сообщение типа msgtyp, а если меньше нуля - первое сообщение наименьшего типа, превосходящего абсолютной величины аргумента msgtyp.

Аргумент msgflg позволяет специфицировать выполнение ``операции с блокировкой''; для этого должен быть сброшен флаг
IPC_NOWAIT (msgflg & IPC_NOWAIT = 0). Блокировка имеет место, если в очереди нет сообщения с запрашиваемым типом (msgtyp). Если флаг IPC_NOWAIT установлен и в очереди нет сообщения требуемого типа, системный вызов немедленно завершается неудачей. Аргумент msgflg может также указывать, что системный вызов закончится неудачей, если размер сообщения в очереди больше значения msgsz; для этого в данном аргументе должен быть сброшен флаг MSG_NOERROR (msgflg & MSG_NOERROR = 0). Если флаг MSG_NOERROR установлен, сообщение обрезается до длины, указанной аргументом msgsz.

В примере этого раздела используются следующие переменные:

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

Если выбрана операция посылки сообщения, указатель msgp
инициализируется адресом структуры данных sndbuf. После этого запрашивается идентификатор очереди сообщений, в которую должно быть послано сообщение; идентификатор заносится в переменную msqid. Затем должен быть введен тип сообщения; он заносится в поле mtype структуры данных, обозначаемой msgp.

После этого программа принимает текст посылаемого сообщения и выполняет цикл, в котором символы читаются и заносятся в массив mtext структуры данных. Ввод продолжается до тех пор, пока не будет обнаружен признак конца файла; для функции getchar() таким признаком является символ CTRL+D, непосредственно следующий за символом возврата каретки. После того как признак конца обнаружен, определяется размер сообщения: он на единицу больше значения счетчика i, поскольку элементы массива, в который заносится сообщение, нумеруются с нуля. Следует помнить, что сообщение будет содержать заключительные символы и, следовательно, будет казаться, что сообщение на три символа короче, чем указывает аргумент msgsz.

Чтобы обеспечить пользователю обратную связь, текст сообщения, содержащийся в массиве mtext структуры sndbuf, немедленно выводится на экран.

Следующее, и последнее, действие заключается в определении, должен ли быть установлен флаг IPC_NOWAIT. Чтобы выяснить это, программа предлагает ввести 1, если флаг нужно установить, или любое другое число, если он не нужен. Введенное значение заносится в переменную flag. Если введена единица, аргумент msgflg полагается равным IPC_NOWAIT, в противном случае msgflg устанавливается равным нулю.

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

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

После каждой успешной операции посылки сообщения значения этих полей выводятся на экран.

Если указано, что требуется принять сообщение, начальное значение указателя msgp устанавливается равным адресу структуры данных rcvbuf. Запрашивается код требуемой комбинации флагов, который заносится в переменную flags. Переменная msgflg устанавливается в сответствии с выбранной комбинацией. В заключение запрашивается, сколько байт нужно принять; указанное значение заносится в переменную msgsz. После этого выполняется системный вызов msgrcv().

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

msg_qnum - определяет общее число сообщений в очереди; в результате выполнения операции уменьшается на единицу;

msg_lrpid - содержит идентификатор процесса, который последним получил сообщение; полю присваивается соответствующий идентификатор;

msg_rtime - содержит время последнего получения сообщения, время измеряется в секундах, начиная с 00:00:00 1 января 1970 г. (по Гринвичу).

/* Программа иллюстрирует

возможности системных вызовов msgsnd() и msgrcv()

(операции над очередями сообщений) */

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

#define MAXTEXTSIZE 8192

 

struct msgbufl {

  long mtype;

  char mtext [MAXTEXTSIZE];

  } sndbuf, rcvbuf, *msgp;

 

main () {

  extern int errno;

  int flag, flags, choice, rtrn, i, c;

  int rtrn, msqid, msgsz, msgflg;

  long msgtyp;

  struct msqid_ds msqid_ds, *buf;

  buf = &msqid_ds;

 

  /* Выбрать требуемую операцию */

  printf ("\nВведите код, соответствующий ");

  printf ("посылке или приему сообщения:\n");

  printf ("  Послать = 1\n");

  printf ("  Принять = 2\n");

  printf ("  Выбор   = ");

  scanf ("%d", &choice);

  if (choice == 1) {

 

     /* Послать сообщение */

     msgp = &sndbuf; /* Указатель на структуру */

     printf ("\nВведите идентификатор ");

     printf ("очереди сообщений,\n");

     printf ("в которую посылается сообщение: ");

     scanf ("%d", &msqid);

  

     /* Установить тип сообщения */

     printf ("\nВведите положительное число - ");

     printf ("тип сообщения: ");

     scanf ("%d", &msgp->mtype);

   

     /* Ввести посылаемое сообщение */

     printf ("\nВведите сообщение: \n");

     /* Управляющая последовательность CTRL+D

     завершает ввод сообщения */

 

     /* Прочитать символы сообщения

     и поместить их в массив mtext */

     for (i = 0; ((c = getchar ()) != EOF); i++)

        sndbuf.mtext [i] = c;

     /* Определить размер сообщения */

     msgsz = i + 1;

     /* Выдать текст посылаемого сообщения */

     for (i = 0; i < msgsz; i++)

        putchar (sndbuf.mtext [i]);

   

     /* Установить флаг IPC_NOWAIT, если это нужно */

     printf ("\nВведите 1, если хотите установить ");

     printf ("флаг IPC_NOWAIT: ");

     scanf ("%d", &flag);

     if (flag == 1) msgflg = IPC_NOWAIT;

     else msgflg = 0;

 

     /* Проверить флаг */

     printf ("\nФлаг = 0%o\n", msgflg);

 

     /* Послать сообщение */

     rtrn = msgsnd (msqid, msgp, msgsz, msgflg);

     if (rtrn == -1) {

        printf ("\nmsgsnd завершился неудачей!\n");

        printf ("Код ошибки = %d\n", errno); }

     else {

        /* Вывести результат; при успешном

        завершении он должен равняться нулю */

        printf ("\nРезультат = %d\n", rtrn);

   

        /* Вывести размер сообщения */

        printf ("\nРазмер сообщения = %d\n", msgsz);

        /* Опрос измененной структуры данных */

        msgctl (msqid, IPC_STAT, buf);

        /* Вывести изменившиеся поля */

        printf ("Число сообщений в очереди = %d\n",

                buf->msg_qnum);

        printf ("Ид-р последнего отправителя = %d\n",

                buf->msg_lspid);

        printf ("Время последнего отправления = %d\n",

                buf->msg_stime); }

     }

     if (choice == 2) {

       /* Принять сообщение */

       msgp = &rcvbuf;

       /* Определить нужную очередь сообщений */

       printf ("\nВведите ид-р очереди сообщений: ");

       scanf ("%d", &msqid);

       /* Определить тип сообщения */

       printf ("\nВведите тип сообщения: ");

       scanf ("%d", &msgtyp);

       /* Сформировать управляющие флаги

         для требуемых действий */

       printf ("\nВведите код, соответствущий ");

       printf ("нужной комбинации флагов:\n");

       printf ("  Нет флагов               = 0\n");

       printf ("  MSG_NOERROR              = 1\n");

       printf ("  IPC_NOWAIT               = 2\n");

       printf ("  MSG_NOERROR и IPC_NOWAIT = 3\n");

       printf ("  Выбор                    = ");

       scanf ("%d", &flags);

       switch (flags) {

          /* Установить msgflg как побитное ИЛИ

           соответствующих констант */

           case 0:  msgflg = 0;

                    break;

           case 1:  msgflg = MSG_NOERROR;

                    break;

           case 2:  msgflg = IPC_NOWAIT;

                    break;

           case 3: 

                  msgflg = MSG_NOERROR | IPC_NOWAIT;

                  break;

          }

      /* Определить, какое число байт принять */

      printf ("\nВведите число байт, которое ");

      printf ("нужно принять (msgsz): ");

      scanf ("%d", &msgsz);

      /* Проверить значение аргументов */

      printf ("\nИдентификатор msqid = %d\n", msqid);

      printf ("Тип сообщения = %d\n", msgtyp);

      printf ("Число байт = %d\n", msgsz);

      printf ("Флаги = %o\n", msgflg);

      /* Вызвать msgrcv для приема сообщения */

      rtrn = msgrcv (msqid, msgp, msgsz,

                     msgtyp, msgflg);

      if (rtrn == -1) {

          printf ("\nmsgrcv завершился неудачей!\n");

          printf ("Код oшибки = %d\n", errno); }

      else {

          printf ("\nmsgrcv завершился успешно,\n");

          printf ("Идентификатор очереди =

                  %d\n", msqid);

          /* Напечатать число принятых байт,

          оно равно возвращаемому значению */

          printf ("Принято байт: %d\n", rtrn);

          /* Распечатать принятое сообщение */

          for (i = 0; i < rtrn; i++)

              putchar (rcvbuf.mtext [i]);

         }

       /* Опрос ассоциированной структуры данных */

       msgctl (msqid, IPC_STAT, buf);

       printf ("\nЧисло сообщений в очереди = %d\n",

               buf->msg_qnum);

       printf ("Ид-р последнего получателя = %d\n",

               buf->msg_lrpid);

       printf ("Время последнего получения = %d\n",

               buf->msg_rtime); }

       exit (0);

 }


next up previous contents
Next: Семафоры Up: Очереди сообщений Previous: Управление очередями сообщений   Contents
2004-06-22

next up previous contents
Next: Действия Up: YACC - еще один Previous: Введение   Contents

Входные спецификации

Имена относятся либо к токенам, либо к нетерминалам. Компилятор Yacc требует, чтобы все токены были специально описаны, часто желательно, чтобы лексический анализатор был включен в файл входных спецификаций. Впрочем и другие подпрограммы туда тоже можно включать. Таким образом, файл входных спецификаций состоит из трех секций: объявлений (declarations); грамматики (правил/rules); программ (подпрограмм).

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

declarations (объявления)
%%
rules (правила)
%%
programs (программы)
Секция объявлений может быть пустой, кроме того, если пуста секция программ, можно опустить и второй разделитель (%%). Таким образом, самая короткая спецификация может выглядеть так:

%%
rules
Пробелы, табуляции и возвраты каретки игнорируются, но не должны появляться в именах и зарезервированых символах. Везде, где может стоять имя, может быть и комментарий (как в Си):

/* . . . */
Секция правил состоит из одного или более грамматических правил в форме:

A : BODY ;
здесь A - имя нетерминала, а BODY представляет собой последовательность из нуля или более имен и литералов. Двоеточие и точка с запятой - символы пунктуации Yacca. Имена могут быть произвольной длины и состоять из букв, точек, подчеркиваний и цифр, но не должны начинаться с цифры. Прописные и строчные буквы различаются. Имена, использованые в теле правила, могут быть именами токенов или нетерминалов. Литерал - это символ, заключенный в одиночные кавычки. Как и в Си бакслэш (\) является спецсимволом, а все Си-шные спецпоследовательности типа n и t воспринимаются.

По многим причинам технического свойства символ с кодом 0 (NULL) не должен никогда использоваться в правилах.

Если есть несколько правил с одинакой левой частью, можно вместо дублирования левой части использовать символ |. Кроме того точка с запятой может быть поставлена и перед символом |. Таким образом, правила:

A : B C D ;
A : E F ;
A : G ;
могут быть записаны так:

A : B C D
| E F
| G
;
или даже так:

A : B C D ;
| E F ;
| G
;
Не обязательно, чтобы правила с одинаковой левой частью были написаны вместе (или рядом), но такая запись делает их более читаемыми и легкими для изменения. Если нетерминалу соответствует пустая строка, он может быть записан так:

empty : ;
Имена токенов должны быть объявлены, что проще всего сделать, написав: %token name1 name2 . . . в секции объявлений.

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

Из всех нетерминалов, один, называемый начальным нетерминалом, имеет особое значение. Анализатор строится, чтобы разобрать начальный нетерминал, который представляет собой наибольшую и наиобщую конструкцию, описанную правилами. По умолчанию, начальным считается нетерминал, стоящий в левой части первого правила, но желательно, чтобы он был объявлен явно при помощи %start nonterminal в секции объявлений.

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



2004-06-22

next up previous contents
Next: Лексический анализ Up: YACC - еще один Previous: Входные спецификации   Contents

Действия

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

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

A : '(' B ')'
{ hello( 1, "abc" ); }
и

XXX : YYY ZZZ
{ printf("a message\n"); flag = 25; }
являются грамматическими правилами с действиями.

Чтобы обеспечить связь действий с анализатором, используется спецсимвол "доллар" ($). Чтобы вернуть значение, действие обычно присваивает его псевдопеременной $$. Например, действие, которое не делает ничего, но возвращает единицу:

{ $$ = 1; }
Чтобы получить значения, возвращенные предыдущими действиями и лексическим анализатором, действие может использовать псевдопеременные $1, $2 и т. д., которые соответствуют значениям, возвращенным компонентами правой части правила, считая слева направо. Таким образом, если правило имеет вид:

A : B C D ;
то $2 соответствует значению, возвращенному нетерминалом C, a $3 - нетерминалом D. Более конкретный пример:

expr : '(' expr ')' ;
Значением, возвращаемым этим правилом, обычно является значение выражения в скобках, что может быть записано так:

expr : '(' expr ')'
{ $$ = $2 ; }
По умолчанию, значением правила считается значение, возвращенное первым элементом ($1). Таким образом, если правило не имеет действия, Yacc автоматически добаляет его в виде $$=$1; , благодаря чему для правила вида

A : B ;
обычно не требуется самому писать действие.

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

A : B
{ $$ = 1; }
C
{ x = $2; y = $3; }
;
В результате разбора иксу (x) присвоится значение 1, а игреку (y) - значение, возвращенное нетерминалом C. Действие, стоящее в середине правила, считается за его компоненту, поэтому x=$2 присваивает X-у значение, возвращенное действием $$=1;

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

NEW_ACT : /* empty */ /* НОВОЕ ПРАВИЛО */
{ $$ = 1; }
;
A : B NEW_ACT C
{ x = $2; y = $3; }
;
В большинстве приложений действия не выполняют ввода/вывода, а конструируют и обрабатывают в памяти структуры данных, например дерево разбора. Такие действия проще всего выполнять вызывая подпрограммы для создания и модификации структур данных. Предположим, что существует функция node, написанная так, что вызов node( L, n1, n2) создает вершину с меткой L, ветвями n1 и n2 и возвращает индекс свежесозданной вершины. Тогда дерево разбора может строиться так:

expr : expr '+' expr
{ $$ = node( '+', $1, $3 ); }
Программист может также определить собственные переменные, доступные действиям. Их объявление и определение может быть сделано в секции объявлений, заключенное в символы %{ и %}. Такие объявления имеют глобальную область видимости, благодаря чему доступны как действиям, так и лексическому анализатору. Например:

%{
int variable = 0;
%}
Такие строчки, помещенные в раздел объявлений, объявляют переменную variable типа int и делают ее доступной для всех действий. Все имена внутренних переменных Yacca начинаются c двух букв y, поэтому не следует давать своим переменным имена типа yymy.



2004-06-22

next up previous contents
Next: Управление версиями с помощью Up: YACC - еще один Previous: Действия   Contents

Лексический анализ

Программист, использующий Yacc должен написать сам (или создать при помощи программы типа Lex) внешний лексический анализатор, который будет читать символы из входного потока (какого - это внутреннее дело лексического анализатора), обнаруживать терминальные символы (токены) и передавать их синтаксическому анализатору, построенному Yaccом (возможно вместе с неким значением) для дальнейшего разбора. Лексический анализатор должен быть оформлен как функция с именем yylex, возвращающая значение типа int, которое представляет собой тип (номер) обнаруженного во входном потоке токена. Если есть желание вернуть еще некое значение (например номер в группе), оно может быть присвоено глобальной, внешней (по отношению к yylex) переменной по имени yylval.

Лексический анализатор и Yacc должны использовать одинаковые номера токенов, чтобы понимать друг друга. Эти номера обычно выбираются Yaccом, но могут выбираться и человеком. В любом случае механизм define языка Си позволяет лексическому анализатору возвращать эти значения в символическом виде. Предположем, что токен по имени DIGIT был определен в секции объявлений спецификации Yaccа, тогда соответствующий текст лексического анализатора может выглядеть так:

yylex()
{
extern int yylval;
int c;
...
c = getchar();
...
switch (c) {
. . .
case '0':
case '1':
...
case '9':
yylval = c - '0';
return DIGIT;
. . .
}
. . .
Вышеприведенный фрагмент возвращает номер токена DIGIT и значение, равное цифре. Если при этом сам текст лексического анализатора был помещен в секцию программ спецификации Yacca - есть гарантия, что идентификатор DIGIT был определен номером токена DIGIT, причем тем самым, который ожидает Yacc.

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

Как уже было сказано, номера токенов выбираются либо Yaccом, либо человеком, но чаще Yaccом, при этом для отдельных символов (например для ( или ;) выбирается номер, равный ASCII коду этого символа. Для других токенов номера выбираются начиная с 257.

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

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

Очень неплохим средством для создания лексических анализаторов является программа Lex. Лексические анализаторы, построенные с ее помощью прекрасно гармонируют с синтаксическими анализаторами, построенными Yaccом. Lex можно легко использовать для построения полного лексического анализатора из файла спецификаций, основанного на системе регулярных выражений (в отличие от системы грамматических правил для Yacca), но, правда, существуют языки (например Фортран) не попадающие ни под какую теоретическю схему, но для них приходится писать лексический анализатор вручную.



2004-06-22

next up previous contents
Next: Введение Up: Практическое использование средств разработки Previous: Лексический анализ   Contents

Управление версиями с помощью CVS



Subsections

2004-06-22

next up previous contents
Next: Что такое CVS Up: Управление версиями с помощью Previous: Управление версиями с помощью   Contents

Введение

В этом разделе приводится минимальное описание системы CVS: что она может и чего не может делать. Для более полной информации по использованию и настройке CVS вы можете использовать документацию с сайта разработки CVS:
http://www.cvshome.org/docs/manual/index.html

2004-06-22

next up previous contents
Next: Чем CVS не является Up: Управление версиями с помощью Previous: Введение   Contents

Что такое CVS

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

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

CVS также можно использовать, если вы работаете над одним проектом совместно с кем-либо еще. Слишком легко переписать чужие изменения, если вы не очень аккуратны. Некоторые редакторы, такие как GNU Emacs, стараются проследить, чтобы один и тот же файл не изменяли одновременно два человека. К сожалению, если кто-то использует другой редактор, эта предосторожность не будет работать. CVS решает эту проблему, изолируя разработчиков друг от друга. Каждый разработчик работает в своем каталоге, а CVS объединяет полученные результаты.


2004-06-22

next up previous contents
Next: CVS - это не Up: Управление версиями с помощью Previous: Что такое CVS   Contents

Чем CVS не является

Система CVS может сделать много полезного, но она не может быть всем сразу для всех.



Subsections

2004-06-22

next up previous contents
Next: CVS не заменит руководство. Up: Чем CVS не является Previous: Чем CVS не является   Contents

CVS - это не система сборки.

Хотя структура вашего репозитория и файла модулей будут взаимодействовать с системой сборки (т.е. `Makefile'), они независимы друг от друга.

CVS не указывает, как собирать что-то. Она просто хранит файлы для получения нужной вам структуры дерева.

CVS не указывает, как использовать дисковое пространство в полученных рабочих каталогах. Если вы напишете `Makefile' или скрипты в каждом каталоге так, что они должны знать относительное положение чего-то еще, то, возможно, что придется обновлять весь репозиторий.

Если вы постараетесь задать модульную структуру для проекта и создадите систему сборки, которая будет совместно использовать файлы ( посредством ссылок, монтирования, VPATH в `Makefile' и т. д.), то сможете использовать дисковое пространство, как захотите. Но вам надо помнить, что любая подобная система требует серьезной работы по ее созданию и поддержанию. CVS не предназначена для решения возникающих при этом проблем.

Разумеется, вам нужно поместить в CVS средства, созданные для поддержки подобной системы сборки (скрипты, `Makefile' и т. д.).

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



2004-06-22

next up previous contents
Next: CVS не ведет контроля Up: Чем CVS не является Previous: CVS - это не   Contents

CVS не заменит руководство.

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

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

CVS не поможет для вычисления нетекстовых или распределенных конфликтов в логике программы.

Например, вы изменили аргументы функции X, определенные в файле `A'. В то же самое время кто-то еще редактирует файл `B', добавляя новый вызов функции X, используя старые аргументы. Это уже не в компетенции CVS. Возьмите себе за привычку читать спецификации и беседовать с коллегами.



2004-06-22

next up previous contents
Next: CVS не является системой Up: Чем CVS не является Previous: CVS не заменит руководство.   Contents

CVS не ведет контроля изменений.

Под контролем изменений имеется в виду несколько вещей. Прежде всего, это может означать отслеживание ошибок, т.е. хранение базы данных обнаруженных ошибок и их статуса (исправлена ли она? в какой версии? согласился ли обнаруживший ее, что она исправлена?). Для взаимодействии CVS с внешней системой отслеживания ошибок почитайте, как это делается в файлах `rcsinfo' и `verifymsg'.

Другим аспектом контроля изменений является отслеживание такого факта, что изменения в нескольких файлах в действительности являются одним и тем же согласованным изменением. Если вы меняете несколько файлов одной командой cvs commit, то CVS забывает, что эти файлы были изменены одновременно, и единственная возможность, их объединения - это одинаковые журнальные записи. Ведение файла `ChangeLog' в стиле GNU может решить некоторые проблемы.

Еще одним аспектом контроля изменений в некоторых системах является возможность отслеживать статус каждого изменения. Некоторые изменения были написаны разработчиком, другие написаны другим разработчиком, и т.д. Обычно при работе с CVS в этом случае создается diff-файл (используя cvs diff или diff), который посылается по электронной почте кому-нибудь, кто потом применит этот diff-файл, используя программу patch. Это очень гибкий прием, но зависит от внешних по отношению к CVS механизмов, чтобы быть уверенным, что ничего не упущено.



2004-06-22

next up previous contents
Next: Основные сведения Up: Локальные и удаленные средства Previous: Операции над очередями сообщений   Contents

Семафоры



Subsections

2004-06-22

next up previous contents
Next: CVS не имеет встроенной Up: Чем CVS не является Previous: CVS не ведет контроля   Contents

CVS не является системой автоматического тестирования.

Хотя и существует возможность принудительного выполнения серии тестов, используя файл `commitinfo'.



2004-06-22

next up previous contents
Next: Пример работы с CVS Up: Чем CVS не является Previous: CVS не является системой   Contents

CVS не имеет встроенной модели процесса.

Некоторые системы обеспечивают способы убедиться, что изменения и релизы проходят через определенные ступени, с различными подтверждениями на каждой. Этого можно добиться и с помощью CVS, но потребуется немного больше работы. В некоторых случаях, вам придется использовать файлы `commitinfo', `loginfo', `rcsinfo' или `verifymsg', чтобы убедиться, что предприняты определенные шаги, прежде чем CVS позволит зафиксировать изменение. Подумайте также, должны ли использоваться такие возможности, как ветви разработки и метки, чтобы, скажем, поработать над новой веткой разработки, а затем объединять определенные изменения со стабильной веткой, когда эти изменения одобрены.


2004-06-22

next up previous contents
Next: Получение исходного кода. Up: Управление версиями с помощью Previous: CVS не имеет встроенной   Contents

Пример работы с CVS

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

Предположим, что вы работаете над простым компилятором и репозиторий уже настроен. Исходный текст состоит из нескольких C-файлов и `Makefile'. Компилятор называется `tc' (Trivial Compiler), а репозиторий настроен так, что имеется модуль `tc'.


Subsections

2004-06-22

next up previous contents
Next: Фиксирование изменений. Up: Пример работы с CVS Previous: Пример работы с CVS   Contents

Получение исходного кода.

Сначала вам надо получить рабочую копию исходного кода для `tc'. Используйте команду
\$ cvs checkout tc
при этом будет создан каталог `tc', в который будут помещены все файлы с исходными текстами.
\$ cd tc
\$ ls
CVS Makefile backend.c driver.c frontend.c parser.c
Каталог `CVS' используется для внутренних нужд CVS. Обычно вам не следует редактировать или удалять файлы, находящиеся в этом каталоге. Вы запускаете свой любимый редактор, работаете над `backend.c' и через пару часов вы добавили фазу оптимизации в компилятор. Замечание для пользователей RCS и RCCS: не требуется блокировать файлы, которые вы желаете отредактировать.

2004-06-22

next up previous contents
Next: Очистка. Up: Пример работы с CVS Previous: Получение исходного кода.   Contents

Фиксирование изменений.

После проверки, что компилятор все еще компилируется, вы решили создать новую версию `backend.c'. При этом в репозитории появится ваш новый `backend.c', который станет доступным всем, использующим этот репозиторий.
\$ cvs commit backend.c
CVS запускает редактор, чтобы позволить вам ввести журнальную запись. Вы набираете: "Добавлена фаза оптимизации", сохраняете временный файл и выходите из редактора. Переменная окружения
$CVSEDITOR определяет, какой именно редактор будет вызван. Если $CVSEDITOR не установлена, то используется $EDITOR, если она, в свою очередь, установлена. Если обе переменные не установлены, используется редактор по умолчанию для вашей операционной системы, например, vi под Linux или notepad для Windows 95/NT.Когда CVS запускает редактор, в шаблоне для ввода журнальной записи перечислены измененные файлы. Для клиента CVS этот список создается путем сравнения времени изменения файла с его временем изменения, когда он был получен или обновлен. Таким образом, если время изменения файла изменилось, а его содержимое осталось прежним, он будет считаться измененным. Проще всего в данном случае не обращать на это внимания - в процессе фиксирования изменений CVS определит, что содержимое файла не изменилось и поведет себя должным образом. Следующая команда - update - сообщит CVS, что файл не был изменен и его время изменения будет возвращено в прежнее значение, так что этот файл не будет помехой при дальнейших фиксированиях.Если вы хотите избежать запуска редактора, укажите журнальную запись в командной строке, используя флаг `-m', например:
$ cvs commit -m "Добавлена фаза оптимизации" backend.c


2004-06-22

next up previous contents
Next: Просмотр изменений. Up: Пример работы с CVS Previous: Фиксирование изменений.   Contents

Очистка.

Перед тем, как перейти к другим занятиям, вы решаете удалить рабочую копию tc. Конечно же, это можно сделать так:
$ cd ..
$ rm -r tc
но лучшим способом будет использование команды release:
$ cd ..
$ cvs release -d tc
M driver.c
? tc
You have [1] altered files in this repository.
Are you sure you want to release (and delete)
   directory `tc': n
** `release' aborted by user choice.
Команда release проверяет, что все ваши изменения были зафиксированы. Если включено журналирование истории, то в файле истории появляется соответствующая пометка. Если вы используете команду release с флагом `-d', то она удаляет вашу рабочую копию. В вышеприведенном примере команда release выдала несколько строк `? tc', означающих, что файл `tc' неизвестен CVS. Беспокоиться не о чем, `tc' - это исполняемый файл компилятора, и его не следует хранить в репозитории. `M driver.c' - более серьезное сообщение. Оно означает, что файл `driver.c' был изменен с момента последнего получения из репозитория. Команда release всегда сообщает, сколько измененных файлов находится в вашей рабочей копии исходных кодов, а затем спрашивает подтверждения перед удалением файлов или внесения пометки в файл истории. Вы решаете перестраховаться и отвечаете n RET, когда release просит подтверждения.

2004-06-22

next up previous contents
Next: Утилита Autoconf Up: Пример работы с CVS Previous: Очистка.   Contents

Просмотр изменений.

Вы не помните, что изменяли файл
`driver.c', поэтому хотите посмотреть, что именно случилось с ним:
$ cd tc
$ cvs diff driver.c
Эта команда сравнивает версию файла `driver.c', находящейся в репозитории, с вашей рабочей копией. Когда вы рассматриваете изменения, вы вспоминаете, что добавили аргумент командной строки, разрешающий фазу оптимизации. Вы фиксируете это изменение и высвобождаете модуль:
$ cvs commit -m "Добавлена фаза оптимизации" driver.c
Checking in driver.c;
/usr/local/cvsroot/tc/driver.c,v <- driver.c
new revision: 1.2; previous revision: 1.1
done
$ cd ..
$ cvs release -d tc
? tc
You have [0] altered files in this repository.
Are you sure you want to release (and delete)
   directory `tc': y
.



2004-06-22

next up previous contents
Next: Введение Up: Практическое использование средств разработки Previous: Просмотр изменений.   Contents

Утилита Autoconf



Subsections

2004-06-22

next up previous contents
Next: Создание скриптов configure. Up: Утилита Autoconf Previous: Утилита Autoconf   Contents

Введение

Autoconf -- это утилита для создания shell-скриптов, которые автоматически конфигурируют пакеты с исходным кодом для адаптирования ко многим UNIX-подобным системам. Конфигурационные скрипты созданные Autoconf, не зависят от него при выполнении, так что пользователям не нужно устанавливать Autoconf у себя в системе.

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

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



2004-06-22

next up previous contents
Next: Написание `configure.in'. Up: Утилита Autoconf Previous: Введение   Contents

Создание скриптов configure.

Для создания скриптов Autoconf требует наличия программы GNU m4. Скрипты конфигурации, создаваемые Autoconf, по принятым соглашениям, называются configure.

Для того чтобы с помощью Autoconf создать скрипт configure, вам необходимо написать входной файл с именем `configure.in' и выполнить команду autoconf. Если вы напишите собственный код тестирования возможностей системы в дополнение к поставляемым с Autoconf, то вам придется записать его в файлы с именами
`aclocal.m4' и `acsite.m4'. Если вы используете заголовочный файл, который содержит директивы define, то вы должны создать файл `acconfig.h', также вы сможете распространять с пакетом файл `config.h.in', созданный с помощью Autoconf файл `config.h.in'.



Subsections

2004-06-22

next up previous contents
Next: Процессы Up: mainfile Previous: Введение   Contents

Локальные и удаленные средства межпроцессного взаимодействия



Subsections

2004-06-22

next up previous contents
Next: Структуры данных семафоров Up: Семафоры Previous: Семафоры   Contents

Основные сведения

Семафоры являются одним из классических примитивов синхронизации. Значение семафора - это целое число в диапазоне от 0 до 32767. Поскольку во многих приложениях требуется использование более одного семафора, ОС Linux предоставляет возможность создавать множества семафоров. Их максимальный размер ограничен системным параметром SEMMSL. Множества семафоров создаются при помощи системного вызова semget.

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

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

Для выполнения первых двух операций у процесса должно быть право на изменение, для выполнения третьей достаточно права на чтение. Чтобы увеличить значение семафора, системному вызову semop следует передать требуемое число. Чтобы уменьшить значение семафора, нужно передать требуемое число, взятое с обратным знаком; если результат получается отрицательным, операция не может быть успешно выполнена. Для третьей операции нужно передать 0; если текущее значение семафора отлично от нуля, операция не может быть успешно выполнена.

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

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

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

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



2004-06-22

next up previous contents
Next: Использование программы autoscan для Up: Создание скриптов configure. Previous: Создание скриптов configure.   Contents

Написание `configure.in'.

Файл с именем `configure.in' содержит вызовы макросов Autoconf, проверяющие системные возможности, которые он может использовать. Для многих таких возможностей макросы Autoconf уже написаны.

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

Каждый файл `configure.in' должен в самом начале содержать вызов макроса ACINIT, а в самом конце вызов макроса ACOUTPUT. Также некоторые макросы полагаются на то, что другие макросы были вызваны первыми, поскольку перед принятием решения, они проверяют уже установленные значения переменных.

Для того чтобы ваши файлы были последовательны и единообразны, приведем желательный порядок вызова макросов Autoconf:

ACINIT(file)
Проверка программ
Проверка библиотек
Проверка заголовочных файлов
Проверка определений типов
Проверка структур
Проверка характеристик компилятора
Проверка библиотечных функций
Проверка системных сервисов
ACOUTPUT(file...)

При вызове макросов с аргументами, между открывающей скобкой и названием макроса не должно быть пробелов. Аргументы могут занимать несколько строк, если они заключены в кавычки языка m4 - `[' и `]'. Если аргументом является длинная строка, например список имен файлов, то можно использовать символ обратного слэша в конце строки для указания, что список продолжается на следующей строке (эта возможность реализуется командным процессором, без привлечения возможностей Autoconf).

Некоторые макросы отрабатывают два случая: когда заданное условие выполняется и когда это условие не выполняется. В некоторых местах вы можете захотеть сделать что-либо, если условие выполняется, и ничего не делать в противном случае, и наоборот. Для того, чтобы пропустить действие при выполнении условия, передайте пустое значение аргументу action-if-found данного макроса. Для пропуска действия при невыполнении условия уберите аргумент action-if-not-found данного макроса, включив предшествующую ему запятую.

В файл `configure.in' можно включать комментарии, начиная их со встроенного макроса m4 --dnl, который отбрасывает текст вплоть до начала новой строки. Эти комментарии не появятся в созданных скриптах configure.



2004-06-22

next up previous contents
Next: Использование программы Autoconf для Up: Создание скриптов configure. Previous: Написание `configure.in'.   Contents

Использование программы autoscan для создания `configure.in'.

Программа autoscan может помочь вам в создании файла
`configure.in' для программного пакета. Эта программа выполняет анализ дерева исходных текстов, корень которого указан в командной строке или совпадает с текущим каталогом. Программа ищет в исходных текстах следы обычных проблем с переносимостью и создает файл `configure.scan', который является заготовкой для `configure.in' данного пакета.

Вы должны сами просмотреть файл `configure.scan' перед тем, как переименовать его в `configure.in': скорее всего, он будет нуждаться в некоторых исправлениях. Иногда autoscan выдает макросы в неправильном порядке, и поэтому Autoconf будет выдавать предупреждения; вам необходимо вручную передвинуть эти макросы. Если же вы хотите, чтобы пакет использовал заголовочный файл настроек, то сами добавьте вызов макроса ACCONFIGHEADER. Вам также необходимо добавить или изменить некоторые директивы препроцессора if в вашей программе, чтобы заставить ее работать с Autoconf.

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

Чтобы установить программу autoscan вам необходима программа Perl. Программа autoscan распознает следующие ключи командной строки:

--help - выдает список ключей командной строки и прекращает работу;

--macrodir=dir - заставляет программу искать файлы данных в каталоге dir, а не в каталоге, куда производилась установка. Вы также можете установить значение переменной окружения
ACMACRODIR равным пути к этому каталогу; данный ключ командной строки переопределяет значение переменной окружения;

--verbose - выдает имена исследуемых файлов и потенциально интересные символы, обнаруженные в этих файлах. Количество выданной информации может быть довольно объемной;

--version - выдает номер версии Autoconf и прекращает работу.



2004-06-22

next up previous contents
Next: Файлы инициализации и выходные Up: Создание скриптов configure. Previous: Использование программы autoscan для   Contents

Использование программы Autoconf для создания скрипта configure

Чтобы создать скрипт configure из файла `configure.in', просто запустите программу autoconf без аргументов; autoconf обработает файл `configure.in' с помощью макропроцессора m4, используя макросы autoconf. Если вы зададите в качестве аргумента имя файла, то вместо `configure.in' будет использоваться заданный файл, а вывод будет производиться на стандартный вывод, а не в файл configure.

Если в качестве аргумента Autoconf задать `-', то она будет читать со стандартного ввода, а не из файла `configure.in', а результаты будут выдаваться на стандартный вывод.

Макросы Autoconf определены в нескольких файлах. Некоторые из них распространяются вместе с Autoconf; Autoconf читает их в первую очередь. Затем ищется необязательный файл `acsite.m4' в каталоге, который содержит распространяемые с Autoconf файлы макросов, и необязательный файл `aclocal.m4' в текущем каталоге. Эти файлы могут содержать макросы, специфические для вашей машины или макросы для конкретных пакетов программного обеспечения.

Программа autoconf распознает следующие ключи командной строки:

--help, -h - выдает список ключей командной строки и прекращает работу;

--localdir=dir, -l dir - ищет файл `aclocal.m4' для данного пакета в каталоге dir, а не в текущем каталоге;

--macrodir=dir - заставляет программу искать файлы данных в каталоге dir, а не в каталоге, куда производилась установка. Вы также можете установить значение переменной окружения
ACMACRODIR, присвоив ей значение пути к этому каталогу; ключ командной строки переопределяет значение переменной окружения;

--version - выдает номер версии autoconf и прекращает работу.



2004-06-22

next up previous contents
Next: Нахождение ввода configure Up: Утилита Autoconf Previous: Использование программы Autoconf для   Contents

Файлы инициализации и выходные файлы

Скриптам, созданным Autoconf, нужна некоторая инициализационная информация. Например: где найти исходные тексты пакета; какие выходные файлы создавать. Далее следует описание инициализации и создания выходных файлов



Subsections

2004-06-22

next up previous contents
Next: Создание выходных файлов Up: Файлы инициализации и выходные Previous: Файлы инициализации и выходные   Contents

Нахождение ввода configure

Каждый скрипт configure должен в первую очередь вызвать макрос ACINIT.

AC_INIT (unique-file-in-source-dir)
- обрабатывает аргументы командной строки и ищет каталог с исходными текстами. unique-file-in-source-dir -- это некоторый файл в каталоге с исходными текстами пакета, который проверяется на существование, чтобы убедиться, что это именно тот каталог с исходными текстами, который нужен. Когда указывается неверный каталог, используйте ключ командной строки `--srcdir'; эта проверка позволит правильно идентифицировать каталог с исходными текстами.



2004-06-22

next up previous contents
Next: Настройки и проверки Up: Файлы инициализации и выходные Previous: Нахождение ввода configure   Contents

Создание выходных файлов

Каждый скрипт configure, созданный Autoconf, должен заканчиваться вызовом макроса ACOUTPUT. Этот макрос создает файлы `Makefile' и дополнительные файлы, которые являются результатом конфигурации.

AC_OUTPUT ([file... [, extra-cmds [, init-cmds]]])
- создает выходные файлы. Этот макрос вызывается один раз в конце файла `configure.in'. Аргумент file... является списком выходных файлов через пробел; этот список может быть пустым. Макрос создает каждый из файлов `file', копируя входной файл (по умолчанию `file.in') и подставляя туда значения выходных переменных.

Если вызывались макросы ACCONFIGHEADER, ACLINKFILES или
ACCONFIGSUBDIRS, то этот макрос также создаст файлы, указанные в аргументах этих макросов.

Типичный вызов ACOUTPUT выглядит примерно так:

ACOUTPUT(Makefile src/Makefile man/Makefile X/Imakefile).

В параметре extra-cmds можно указать команды, которые будут вставлены в файл `config.status' и сработают после того, как было сделано все остальное.

В параметре init-cmds можно указать команды, которые будут вставлены непосредственно перед extra-cmds, причем configure выполнит в них подстановку переменных, команд и обратных слэшей. Аргумент init-cmds можно использовать для передачи переменных из configure в extra-cmds. Если был вызван макрос
ACOUTPUTCOMMANDS, то команды, переданные ему в качестве аргумента, выполняются прямо перед командами, переданными макросу ACOUTPUT.



2004-06-22

next up previous contents
Next: Создание макросов Up: Файлы инициализации и выходные Previous: Создание выходных файлов   Contents

Настройки и проверки

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



2004-06-22

next up previous contents
Next: Определение макросов. Up: Файлы инициализации и выходные Previous: Настройки и проверки   Contents

Создание макросов

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



Subsections

2004-06-22

next up previous contents
Next: Имена макросов. Up: Создание макросов Previous: Создание макросов   Contents

Определение макросов.

Макросы Autoconf определяются с помощью макроса ACDEFUN, который подобен встроенному макросу define программы m4. Кроме определения макроса, ACDEFUN добавляет к нему некоторый код, который используется для ограничения порядка вызова макросов.

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

ACDEFUN(macro-name, macro-body)

Квадратные скобки не указывают на необязательный параметр: они должны присутствовать в определении макроса для избежания проблем расширения макроса. Вы можете ссылаться на передаваемые макросу параметры с помощью переменных `$1', `$2' и т.д.



2004-06-22

next up previous contents
Next: Зависимости между макросами. Up: Создание макросов Previous: Определение макросов.   Contents

Имена макросов.

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

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

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

C - встроенные возможности языка C.

DECL - объявления переменных C в заголовочных файлах.

FUNC - функции в библиотеках.

GROUP - группа UNIX владеющая файлами.

HEADER - заголовочные файлы.

LIB - библиотеки C.

PATH - полные путевые имена файлов, включая программы.

PROG - базовые имена программ.

STRUCT - определения структур C в заголовочных файлах.

SYS - свойства операционной системы.

TYPE - встроенные или объявленные типы C.

VAR - переменные C в библиотеках.

После категории следует имя тестируемого свойства. Любые дополнительные слова в имени макроса указывают на специфические аспекты тестируемого свойства. Например, ACFUNCUTIMENULL проверяет поведение функции utime при вызове ее с указателем, равным NULL.

Макрос, который является внутренней подпрограммой другого макроса, должен иметь имя, начинается с его имени, за которым следует одно или несколько слов, описывающих, что делает этот макрос. Например, макрос ACPATHX имеет внутренние макросы ACPATHXXMKMF и ACPATHXDIRECT.



2004-06-22

next up previous contents
Next: Создание множеств семафоров Up: Семафоры Previous: Основные сведения   Contents

Структуры данных семафоров

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

struct sem {

  ushort semval; /* Значение семафора */

  /* Идентификатор процесса, выполнявшего

   последнюю операцию */

  short sempid;

  /* Число процессов, ожидающих увеличения

    значения семафора */

  ushort semncnt;

  /* Число процессов, ожидающих обнуления

   значения семафора */

  ushort semzcnt;

};

Определение находится во включаемом файле <sys/sem.h>.

С каждым идентификатором множества семафоров ассоциирована структура данных, содержащая следующую информацию:

struct semid_ds {

 /*Структура прав на выполнение операций*/

  struct ipc_perm sem_perm;

 /*Указатель на первый семафор в множестве*/

  struct sem *sem_base;

 /* Количество семафоров в множестве */

  ushort sem_nsems;

 /* Время последней операции */

  time_t sem_otime;

 /* Время последнего изменения */

  time_t sem_ctime;

};

Это определение также находится во включаемом файле <sys/sem.h>.

Поле sem_perm данной структуры использует в качестве шаблона структуру типа ipc_perm, общую для всех средств межпроцессной связи. Системный вызов semget аналогичен вызову msgget (разумеется, с заменой слов ``очередь сообщений'' на ``множество семафоров''). Он также предназначен для получения нового или опроса существующего идентификатора, а нужное действие определяется значением аргумента key. В подобных ситуациях semget терпит неудачу. Единственное отличие состоит в том, что при создании требуется посредством аргумента nsems указывать число семафоров в множестве.

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



2004-06-22

next up previous contents
Next: Запуск скриптов configure Up: Создание макросов Previous: Имена макросов.   Contents

Зависимости между макросами.

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



2004-06-22

next up previous contents
Next: Утилита Automake Up: Утилита Autoconf Previous: Зависимости между макросами.   Contents

Запуск скриптов configure

Скрипт configure пытается определить правильные значения для различных, зависящих от системы переменных, которые используются в процессе установки. Он использует эти переменные для создания файлов `Makefile' в каждом из каталогов пакета. Дополнительно он может создавать один или несколько файлов `.h', содержащих зависящие от системы определения. В заключение, он создает скрипт командного процессора с именем `config.status', который вы можете в дальнейшем запускать для воссоздания текущей настройки; также создаются файл `config.cache', который сохраняет результаты тестов, для ускорения перенастройки, и файл `config.log', содержащий вывод компилятора (этот файл полезен для отладки configure).

Файл `configure.in' используется для создания скрипта
`configure' программой Autoconf. Вам необходимо иметь только `configure.in', если вы хотите изменить его или заново создать скрипт `configure' с помощью более новой версии Autoconf.

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

  1. Перейдите в каталог, содержащий исходный код пакета, и наберите `./configure' в командной строке, чтобы настроить пакет для вашей системы. Работа configure займет некоторое время. Во время выполнения скрипт выдает сообщения о том, какие свойства он проверяет.

  2. Наберите `make' для компиляции пакета.

  3. Вы можете набрать `make check' для запуска любых собственных тестов, которые поставляются вместе с пакетом.

  4. Наберите `make install' для установки программ и файлов данных и документации.

  5. Вы можете удалить исполнимые файлы программ и объектные файлы из каталога с исходными текстами пакета, набрав `make clean'. Для удаления файлов, созданных configure, наберите `make distclean'. Имеющаяся цель `make
    maintainer-clean' в основном предназначена для разработчиков программного обеспечения.



2004-06-22

next up previous contents
Next: Введение Up: Практическое использование средств разработки Previous: Запуск скриптов configure   Contents

Утилита Automake



Subsections

2004-06-22

next up previous contents
Next: Управление семафорами через semctl Up: Семафоры Previous: Структуры данных семафоров   Contents

Создание множеств семафоров

Для создания множества семафоров служит системный вызов semget. Синтаксис данного системного вызова:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semget (key_t key, int nsems, int semflg);

Целочисленное значение, возвращаемое в случае успешного завершения, есть идентификатор множества семафоров (semid). В случае неудачи результат равен -1.

Смысл аргументов key и semflg тот же, что и у соответствующих аргументов системного вызова msgget. Аргумент nsems задает число семафоров в множестве. Если запрашивается идентификатор существующего множества, значение nsems не должно превосходить числа семафоров в множестве.

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



2004-06-22

next up previous contents
Next: Основные идеи Up: Утилита Automake Previous: Утилита Automake   Contents

Введение

Automake -- это утилита для автоматического создания файлов
`Makefile.in' из файлов `Makefile.am'. Каждый файл
`Makefile.am' фактически является набором макросов для программы make (иногда с несколькими правилами). Полученные таким образом файлы `Makefile.in' соответствуют стандартам
GNU Makefile.

Стандарт GNU Makefile - это длинный, запутанный документ, и его содержание может в будущем измениться. Automake разработан для того, чтобы освободить от бремени сопровождения Makefile человека, ведущего проект GNU.

Типичный входной файл Automake является просто набором макроопределений. Каждый такой файл обрабатывается, и из него создается файл `Makefile.in'. В каталоге проекта должен быть только один файл `Makefile.am'. Automake накладывает на проект некоторые ограничения; например, он предполагает, что проект использует программу Autoconf, а также налагает некоторые ограничения на содержимое файла `configure.in'. Automake требует наличия программы perl для генерации файлов `Makefile.in'. Однако дистрибутив, созданный Automake, является полностью соответствующим стандартам GNU и не требует наличия perl для компиляции.



2004-06-22

next up previous contents
Next: Примеры Up: Утилита Automake Previous: Введение   Contents

Основные идеи

Automake читает файл `Makefile.am' и создает на его основе файл `Makefile.in'. Специальные макросы и цели, определенные в
`Makefile.am', заставляют Automake генерировать более специализированный код; например, макроопределение `binPROGRAMS' заставит создать цели для компиляции и компоновки программ.

Макроопределения и цели из файла `Makefile.am' копируются в файл `Makefile.in' без изменений. Это позволяет вам добавлять в генерируемый файл `Makefile.in' произвольный код. Например, дистрибутив Automake включает в себя нестандартную цель cvs-dist, которую использует человек, сопровождающий Automake, для создания дистрибутивов из системы контроля исходного кода.

Заметьте, что расширения GNU make не распознаются программой Automake. Использование таких расширений в файле
`Makefile.am' приведет к ошибкам или странному поведению программы. Automake пытается сгруппировать комментарии к расположенным по соседству целям и макроопределениям.

Цель, определенная в `Makefile.am', обычно переопределяет любую цель с таким же именем, которая была бы автоматически создана Automake. Хотя этот прием и работает, старайтесь избегать его использования, поскольку иногда автоматически созданные цели являются очень важными. Аналогичным образом макрос, определенный в `Makefile.am', будет переопределять любой макрос, который создает Automake. Это часто более полезно, чем возможность переопределения цели. Но будьте осторожны, поскольку многие из макросов, создаваемых программой Automake, считаются макросами только для внутреннего использования, и их имена могут изменяться в будущих версиях.

При обработке макроопределения Automake рекурсивно обрабатывает макросы, на которые есть ссылка в данном макроопределении. Например, если Automake исследует содержимое fooSOURCES в следующем определении:

xs = a.c b.c
foo_SOURCES = c.c $(xs) ,
то он будет использовать файлы `a.c', `b.c' и `c.c' как содержимое fooSOURCES.

Automake также вводит форму комментария, который не копируется в выходной файл; все строки, начинающиеся с `', полностью игнорируются Automake. Очень часто первая строка файла `Makefile.am' выглядит следующим образом:

## Process this file with Automake to produce Makefile.in
## Для получения Makefile.in обработайте этот файл
## программой Automake .



2004-06-22

next up previous contents
Next: Простой пример (от начала Up: Утилита Automake Previous: Основные идеи   Contents

Примеры

Для полного ознакомления с утилитой Automake рекомендуется прочитать ``Руководство по использованию Automake''.

 http://www.gnu.org/software/Automake/manual/Automake|.html)
Для подробного описания макросов загляните в справочные страницы Automake.Здесь же ограничимся подробными примерами, в которых показано, как использовать Automake для простых целей.



Subsections

2004-06-22

next up previous contents
Next: Классический ``Hello, world'' Up: Примеры Previous: Примеры   Contents

Простой пример (от начала до конца)

Предположим, что мы только что закончили писать программу zardoz. Вы использовали Autoconf для обеспечения переносимости, но ваш файл `Makefile.in' написан бессистемно. Вы же хотите сделать его "пуленепробиваемым", и поэтому решаете использовать Automake.

Сначала вам необходимо обновить ваш файл `configure.in', чтобы вставить в него команды, которые необходимы для работы Automake. Проще всего для этого добавить строку AM_INIT_Automake сразу после ACINIT:

AM_INIT_Automake(zardoz, 1.0)
AM_INIT_Automake
...
Поскольку ваша программа не имеет никаких осложняющих факторов (например, она не использует gettext и не будет создавать разделяемые библиотеки), то первая стадия на этом и заканчивается. Это легко!

Теперь вы должны заново создать файл `configure'. Но для этого нужно указать autoconf, где найти новые макросы, которые вы использовали.

Для создания файла `aclocal.m4' удобнее всего будет использовать программу aclocal. Но будьте осторожны - у вас уже есть `aclocal.m4', поскольку вы уже написали несколько собственных макросов для вашей программы. Программа aclocal позволяет вам поместить ваши собственные макросы в файл `acinclude.m4', так что для сохранения вашей работы просто переименуйте свой файл с макросами, а уж затем запускайте программу aclocal:

mv aclocal.m4 acinclude.m4
aclocal
autoconf
Теперь пришло время написать свой собственный файл `Makefile.am' для программы zardoz. Поскольку zardoz является пользовательской программой, то вам хочется установить ее туда, где располагаются другие пользовательские программы. К тому же, zardoz содержит в комплекте документацию в формате Texinfo. Ваш скрипт `configure.in' использует ACREPLACEFUNCS, поэтому вам необходимо скомпоновать программу с `@LIBOBJS@'. Вот что вам необходимо написать в `Makefile.am'.

bin_PROGRAMS = zardoz
zardoz_SOURCES = main.c head.c float.c vortex9.c gun.c
zardoz_LDADD = @LIBOBJS@
info_TEXINFOS = zardoz.texi
Теперь можно запустить Automake --add-missing, чтобы создать файл `Makefile.in', используя дополнительные файлы.



2004-06-22

next up previous contents
Next: Создание файла `Makefile.in' Up: Примеры Previous: Простой пример (от начала   Contents

Классический ``Hello, world''

GNU hello <ftp://ftp.gnu.org/pub/gnu/hello/hello-1.3.tar.gz> известен своей классической простотой и многогранностью. В этом разделе показывается, как Automake может быть использован с пакетом GNU Hello. Примеры, приведенные ниже, взяты из последней бета-версии GNU Hello, но убран код, предназначенный только для разработчика пакета, а также сообщения об авторских правах.

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

Вот файл `configure.in' из пакета GNU Hello:

## Обработайте этот файл autoconf для получения
## скрипта configure.
AC_INIT(src/hello.c)
AM_INIT_Automake(hello, 1.3.11)
AM_CONFIG_HEADER(config.h)

## Установка доступных языков.
ALL_LINGUAS="de fr es ko nl no pl pt sl sv"

## Проверка наличия программ.
AC_PROG_CC
AC_ISC_POSIX

## Проверка библиотек.
## Проверка заголовочных файлов.
AC_STDC_HEADERS
AC_HAVE_HEADERS(string.h fcntl.h sys/file.h sys/param.h)

## Проверка библиотечных функций.
AC_FUNC_ALLOCA

## Проверка st_blksize в структуре stat
AC_ST_BLKSIZE

## макрос интернационализации
AM_GNU_GETTEXT
AC_OUTPUT([Makefile doc/Makefile intl/Makefile po/Makefile.in \
src/Makefile tests/Makefile tests/hello],
[chmod +x tests/hello])
Макросы `AM' предоставляются Automake (или библиотекой Gettext); остальные макросы является макросами Autoconf.

Файл `Makefile.am' в корневом каталоге выглядит следующим образом:

EXTRA_DIST = BUGS ChangeLog.O
SUBDIRS = doc intl po src tests
Как видите, вся работа выполняется в подкаталогах. Каталоги `po' и `intl' автоматически создаются программой gettextize.

В файле `doc/Makefile.am' мы видим строки:

info_TEXINFOS = hello.texi
hello_TEXINFOS = gpl.texi
Этого достаточно для сборки, установки и распространения руководства GNU Hello.

Вот содержимое файла `tests/Makefile.am':

TESTS = hello
EXTRA_DIST = hello.in testdata
Скрипт `hello' создается configure, и это единственная возможность для тестирования. При выполнении make check этот тест будет запущен.

В заключение приведем содержимое `src/Makefile.am', где и выполняется вся настоящая работа:

bin_PROGRAMS = hello
hello_SOURCES = hello.c version.c getopt.c getopt1.c \
    getopt.h system.h 
hello_LDADD = @INTLLIBS@ @ALLOCA@
localedir = $(datadir)/locale
INCLUDES = -I../intl -DLOCALEDIR="$(localedir)"



2004-06-22

next up previous contents
Next: . Up: Утилита Automake Previous: Классический ``Hello, world''   Contents

Создание файла `Makefile.in'

Для создания всех файлов `Makefile.in' пакета запустите программу Automake в каталоге верхнего уровня без аргументов. Тогда automake автоматически найдет каждый файл `Makefile.am' и сгенерирует соответствующий файл `Makefile.in'. Заметьте, что Automake имеет более простое видение структуры пакета; он предполагает, что пакет имеет только один файл `configure.in', расположенный в каталоге верхнего уровня. Если в вашем пакете имеется несколько файлов `configure.in', то вам необходимо запустить Automake в каждом каталоге, где есть файл `configure.in'.

Вы можете также задать аргумент для Automake: суффикс `.am' добавляется к аргументу, а результат используется как имя входного файла. В основном эта возможность применяется для автоматической перегенерации устаревших файлов `Makefile.in'. Заметьте, что Automake всегда должен запускаться из каталога верхнего уровня проекта, даже если необходимо перегенерировать
`Makefile.in' в каком-либо подкаталоге. Это необходимо, потому что Automake должен просканировать файл `configure.in', а также потому, что Automake в некоторых случаях изменяет свое поведение при обработке `Makefile.in' в подкаталогах.

Программа Automake принимает следующие ключи командной строки:

`-a',`add-missing'.

В некоторых ситуациях Automake требует наличия некоторых общих файлов; например, если в `configure.in' выполняется макрос ACCANONICALHOST, то требуется наличие файла `config.guess'. Automake распространяется с несколькими такими файлами; этот ключ заставит программу автоматически добавить к пакету отсутствующие файлы, если это возможно. В общем, если Automake сообщает вам, что какой-то файл отсутствует, то используйте этот ключ. По умолчанию Automake пытается создать символьную ссылку на собственную копию отсутствующего файла; это поведение может быть изменено с помощью ключа --copy.

`amdir=dir'.

Этот ключ заставляет Automake искать файлы данных в каталоге dir, а не в каталоге установки; этот ключ обычно используется при отладке

`build-dir=dir'.

Сообщает Automake, где располагается каталог для сборки. Этот ключ используется при введении зависимостей в файл `Makefile.in', созданный командой make dist; он не должен использоваться в других случаях.

`-c',`copy'.

При использовании с ключом --add-missing, заставляет копировать недостающие файлы. По умолчанию создаются символьные ссылки.

`cygnus'.

Заставляет сгенерированные файлы `Makefile.in' следовать правилам Cygnus, вместо правил GNU или Gnits.

`foreign'.

Устанавливает глобальную строгость в значение `foreign'.

`gnits'.

Устанавливает глобальную строгость в значение `gnits'.

`gnu'.

Устанавливает глобальную строгость в значение `gnu'. По умолчанию используется именно такая строгость.

`help'.

Печатает список ключей командной строки и завершается.

`-i','-include-deps'.

Включить всю автоматически генерируемую информацию о зависимостях в генерируемый файл `Makefile.in'. Это делается в основном при создании дистрибутива.

`generate-deps'.

Создать файл, объединяющий всю автоматически генерируемую информацию о зависимостях , этот файл будет называться `.depsegment'. В основном этот ключ используется при создании дистрибутива; он полезен при сопровождении `Makefile' или файлов `Makefile' для других платформ (`Makefile.DOS', и т. п.), а также может использоваться с ключами `--include-deps',
`--srcdir-name' и `--build-dir'. Заметьте, что если задан этот ключ, то никакой другой обработки не выполняется.

`no-force'.

Обычно Automake создает все файлы `Makefile.in', указанные в `configure.in'. Этот ключ заставляет обновлять только те файлы `Makefile.in', с учетом зависимостей друг от друга, которые устарели.

`-odir',`output-dir=dir'.

Поместить сгенерированный файл `Makefile.in' в каталог dir. Обычно каждый файл `Makefile.in' создается в том же каталоге, что и соответствующий файл `Makefile.am'. Этот ключ используется при создании дистрибутивов.

`srcdir-name=dir'.

Сообщает Automake имя каталога с исходными текстами текущего дистрибутива. Этот ключ используется при включении зависимостей в файл `Makefile.in', сгенерированный командой make dist; он не должен использоваться в других случаях.

`-v',`verbose'.

Заставляет Automake выдавать информацию о том, какие файлы читаются или создаются.

`version'.

Выдает номер версии Automake и завершается.



2004-06-22

next up previous contents
Next: About this document ... Up: Практическое использование средств разработки Previous: Создание файла `Makefile.in'   Contents

.

Садыхов Рауф Хосровович

Поденок Леонид Петрович

Отвагин Алексей Владимирович

Глецевич Иван Иванович

Пынькин Денис Александрович




Средства параллельного программирования в ОС Linux




Учебное пособие







Редактор

Корректор

Компьютерная верстка Отвагина А.В.




Подписано в печать 21.06.2004. Формат 60x90$^1/_{16}$. Бумага офсетная. Гарнитура "Таймс".


2004-06-22

next up previous contents
Up: mainfile Previous: .   Contents

About this document ...

This document was generated using the LaTeX2HTML translator Version 2002-2-1 (1.70)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html mainfile.tex

The translation was initiated by on 2004-06-22


2004-06-22

next up previous contents
Next: Операции над множествами семафоров Up: Семафоры Previous: Создание множеств семафоров   Contents

Управление семафорами через semctl

Синтаксис системного вызова semctl:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semctl (int semid, int semnum, int cmd, arg);

union semun {

  int val;

  struct semid_ds *buf;

  ushort *array;

  } arg;

Результат системного вызова semctl в случае успешного завершения зависит от управляющего действия. Как правило он равен 0, но четыре действия (GETVAL, GETPID, GETNCNT и GETZCNT) являются исключениями. При возникновении ошибки всегда возвращается -1.

Аргументы semid и semnum определяют множество или отдельный семафор, над которым выполняется управляющее действие. В качестве аргумента semid должен выступать идентификатор множества семафоров, предварительно полученный при помощи системного вызова semget. Аргумент semnum задает номер семафора во множестве. Семафоры нумеруются с нуля.

Назначение аргумента arg зависит от управляющего действия, которое определяется значением аргумента cmd. Допустимы следующие действия:

Чтобы выполнить управляющее действие IPC_SET или IPC_RMID, процесс должен иметь действующий идентификатор пользователя, равный либо идентификаторам создателя или владельца очереди, либо идентификатору суперпользователя. Для выполнения управляющих действий SETVAL и SETALL требуется право на изменение, а для выполнения остальных действий - право на чтение.

Пример работы с семафорами:

/* Программа иллюстрирует

возможности системного вызова semctl()

(управление семафорами) */

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#define MAXSETSIZE 25

 

main ()

{

  extern int errno;

  struct semid_ds semid_ds;

  int length, rtrn, i, c;

  int semid, semnum, cmd, choice;

  union semun {

    int val;

    struct semid_ds *buf;

    ushort array [MAXSETSIZE];

   } arg;

  

  /* Инициализация указателя на структуру данных */

  arg.buf = &semid_ds;

  /* Ввести идентификатор множества семафоров */

  printf ("Введите ид-р множества семафоров: ");

  scanf ("%d", &semid);

 

  /* Выбрать требуемое управляющее действие */

  printf ("\nВведите номер требуемого действия:\n");

  printf (" GETVAL = 1\n");

  printf (" SETVAL = 2\n");

  printf (" GETPID = 3\n");

  printf (" GETNCNT = 4\n");

  printf (" GETZCNT = 5\n");

  printf (" GETALL = 6\n");

  printf (" SETALL = 7\n");

  printf (" IPC_STAT = 8\n");

  printf (" IPC_SET = 9\n");

  printf (" IPC_RMID = 10\n");

  printf (" Выбор = ");

  scanf ("%d", &cmd);

 

  /* Проверить значения */

  printf ("идентификатор = %d, команда = %d\n",

          semid, cmd);

  /* Сформировать аргументы и выполнить вызов */

  switch (cmd) {

      case 1: /* Получить значение */

              printf ("\nВведите номер семафора: ");

              scanf ("%d", &semnum);

              /* Выполнить системный вызов */

              rtrn = semctl (semid, semnum, GETVAL, 0);

              printf ("\nЗначение семафора =

                      %d\n", rtrn);

              break;

      case 2: /* Установить значение */

              printf ("\nВведите номер семафора: ");

              scanf ("%d", &semnum);

              printf ("\nВведите значение: ");

              scanf ("%d", &arg.val);

              /* Выполнить системный вызов */

              rtrn = semctl (semid, semnum, SETVAL,

                     arg.val);

              break;

      case 3: /* Получить ид-р процесса */

              rtrn = semctl (semid, 0, GETPID, 0);

              printf ("\Последнюю операцию выполнил:

                      %d\n",rtrn);

              break;

      case 4: /* Получить число процессов, ожидающих

              увеличения значения семафора */

              printf ("\nВведите номер семафора: ");

              scanf ("%d", &semnum);

          /* Выполнить системный вызов */

          rtrn = semctl (semid, semnum, GETNCNT, 0);

          printf ("\nЧисло процессов = %d\n", rtrn);

          break;

    case 5: /* Получить число процессов, ожидающих

          обнуления значения семафора */

          printf ("Введите номер семафора: ");

          scanf ("%d", &semnum);

          /* Выполнить системный вызов */

          rtrn = semctl (semid, semnum, GETZCNT, 0

          printf ("\nЧисло процессов = %d\n", rtrn);

          break;

    case 6: /* Опросить все семафоры */

          /* Определить число семафоров в множестве */

          rtrn = semctl (semid, 0, IPC_STAT, arg.buf);

          length = arg.buf->sem_nsems;

          if (rtrn == -1) goto ERROR;

          /* Получить и вывести значения всех

          семафоров в указанном множестве */

          rtrn = semctl (semid, 0, GETALL, arg.array);

          for (i = 0; i < length; i++)

             printf (" %d", arg.array [i]);

          break;

    case 7: /* Установить все семафоры */

          /* Определить число семафоров в множестве */

          rtrn = semctl (semid, 0, IPC_STAT, arg.buf);

          length = arg.buf->sem_nsems;

          if (rtrn == -1) goto ERROR;

          printf ("\nЧисло семафоров = %d\n", length);

          /* Установить значения семафоров множества */

          printf ("\nВведите значения:\n");

          for (i = 0; i < length; i++)

          scanf ("%d", &arg.array [i]);

          /* Выполнить системный вызов */

          rtrn = semctl (semid, 0, SETALL, arg.array);

          break;

    case 8: /* Опросить состояние множества */

          rtrn = semctl (semid, 0, IPC_STAT, arg.buf);

          printf ("\nИдентификатор пользователя = %d\n",

                  arg.buf->sem_perm.uid);

          printf ("Идентификатор группы = %d\n",

                  arg.buf->sem_perm.gid);

          printf ("Права на операции = 0%o\n",

                  arg.buf->sem_perm.mode);

          printf ("Число семафоров в множестве = %d\n",

                  arg.buf->sem_nsems);

          printf ("Время последней операции = %d\n",

                  arg.buf->sem_otime);

          printf ("Время последнего изменения = %d\n",

                  arg.buf->sem_ctime);

          break;

    case 9: /* Выбрать и изменить поле

          ассоциированной структуры данных */

          /* Опросить текущее состояние */

          rtrn = semctl (semid, 0, IPC_STAT, arg.buf);

          if (rtrn == -1) goto ERROR;

          printf ("\nВведите номер поля, ");

          printf ("которое нужно изменить: \n");

          printf (" sem_perm.uid = 1\n");

          printf (" sem_perm.gid = 2\n");

          printf (" sem_perm.mode = 3\n");

          printf (" Выбор = ");

          scanf ("%d", &choice);

          switch (choice) {

               case 1: /* Изменить ид-р владельца */

                   printf ("\nВведите ид-р владельца: ");

                   scanf ("%d", &arg.buf->sem_perm.uid);

                   printf ("\nИд-р владельца = %d\n",

                           arg.buf->sem_perm.uid);

                   break;

               case 2: /* Изменить ид-р группы */

                   printf ("\nВведите ид-р группы = ");

                   scanf ("%d", &arg.buf->sem_perm.gid);

                   printf ("\nИд-р группы = %d\n",

                           arg.buf->sem_perm.uid);

                   break;

               case 3: /* Изменить права на операции */

                   printf ("\nВведите восьмеричный код

                           прав доступа: ");

                   scanf ("%o", &arg.buf->sem_perm.mode);

                   printf ("\nПрава = 0%o\n",

                           arg.buf->sem_perm.mode);

                   break;

               }

         /* Внести изменения */

         rtrn = semctl (semid, 0, IPC_SET, arg.buf);

         break;

     case 10: /* Удалить ид-р множества семафоров и

         ассоциированную структуру данных */

         rtrn = semctl (semid, 0, IPC_RMID, 0);

   }

  if (rtrn == -1) {

  /* Сообщить о неудачном завершении */

  ERROR:

        printf ("\nsemctl завершился неудачей!\n");

        printf ("\nКод ошибки = %d\n", errno);

  }

  else {

        printf ("\nmsgctl завершился успешно,\n");

        printf ("идентификатор semid = %d\n", semid);

  }

  exit (0);

}


next up previous contents
Next: Операции над множествами семафоров Up: Семафоры Previous: Создание множеств семафоров   Contents
2004-06-22

next up previous contents
Next: Разделяемые сегменты памяти Up: Семафоры Previous: Управление семафорами через semctl   Contents

Операции над множествами семафоров

Cинтаксис системного вызова semop описан так:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semop (int semid, struct sembuf *sops,

           unsigned int nsops)

При успешном завершении результат системного вызова равен нулю; в случае неудачи возвращается -1.

В качестве аргумента semid должен выступать идентификатор множества семафоров, предварительно полученный при помощи системного вызова semget.

Аргумент sops (массив структур) определяет, над какими семафорами и какие именно операции будут выполняться. Структура, описывающая операцию над одним семафором, определяется следующим образом:

struct sembuf {

  short sem_num; /* Номер семафора */

  short sem_op; /* Операция над семафором */

  short sem_flg; /* Флаги операции */

};

(см. включаемый файл <sys/sem.h>).

Номер семафора задает конкретный семафор в множестве, над которым должна быть выполнена операция.

Выполняемая операция определяется следующим образом:

Допустимые значения флагов операций (поле sem_flg):

IPC_NOWAIT
- если какая-либо операция, для которой задан флаг IPC_NOWAIT, не может быть успешно выполнена, системный вызов завершается неудачей, причем ни у одного из семафоров не будет изменено значение;
SEM_UNDO
- данный флаг задает проверочный режим выполнения операции; он предписывает аннулировать ее результат даже в случае успешного завершения системного вызова semop. Иными словами, блокировка всех операций (в том числе и тех, для которых задан флаг SEM_UNDO) выполняется обычным образом, но когда наконец все операции могут быть успешно выполнены, операции с флагом SEM_UNDO игнорируются.
Аргумент nsops специфицирует число структур в массиве. Максимально допустимый размер массива определяется системным параметром SEMOPM, т. е. в каждом системном вызове semop можно выполнить не более SEMOPM операций.

Пример работы с семафорами приведен ниже:

/* Программа иллюстрирует

возможности системного вызова semop()

(операции над множеством семафоров) */

 

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#define MAXOPSIZE 10

 

main ()

{

  extern int errno;

  struct sembuf sops [MAXOPSIZE];

  int semid, flags, i, rtrn;

  unsigned nsops;

 

  /* Ввести идентификатор множества семафоров */

  printf ("\nВведите идентификатор

          множества семафоров,");

  printf ("\nнад которым будут

          выполняться операции: ");

  scanf ("%d", &semid);

  printf ("\nИд-р множества семафоров = %d", semid);

 

  /* Ввести число операций */

  printf ("\nВведите число операций ");

  printf ("над семафорами из этого множества: \n");

  scanf ("%d", &nsops);

  printf ("\nЧисло операций = %d", nsops);

 

  /* Инициализировать массив операций */

  for (i = 0; i < nsops; i++) {

    /* Выбрать семафор из множества */

    printf ("\nВведите номер семафора: ");

    scanf ("%d", &sops [i].sem_num);

    printf ("\nНомер = %d", sops [i].sem_num);

    /* Ввести число, задающее операцию */

    printf ("\nЗадайте операцию над семафором: ");

    scanf ("%d", &sops [i].sem_op);

    printf ("\nОперация = %d", sops [i].sem_op);

    /* Указать требуемые флаги */

    printf ("\nВведите код, ");

    printf ("соответствующий требуемым флагам:\n");

    printf (" Нет флагов = 0\n");

    printf (" IPC_NOWAIT = 1\n");

    printf (" SEM_UNDO = 2\n");

    printf (" IPC_NOWAIT и SEM_UNDO = 3\n");

    printf (" Выбор = ");

    scanf ("%d", &flags);

    switch (flags) {

         case 0:

                 sops [i].sem_flg = 0;

                 break;

         case 1:

                 sops [i].sem_flg = IPC_NOWAIT;

                 break;

         case 2:

                 sops [i].sem_flg = SEM_UNDO;

                 break;

         case 3:

            sops [i].sem_flg = IPC_NOWAIT | SEM_UNDO;

            break;

        }

    printf ("\nФлаги = 0%o", sops [i].sem_flg);

  }

 

  /* Распечатать все структуры массива */

  printf ("\nМассив операций:\n");

  for (i = 0; i < nsops; i++) {

     printf (" Номер семафора = %d\n",

             sops [i].sem_num);

     printf (" Операция = %d\n", sops [i].sem_op);

     printf (" Флаги = 0%o\n", sops [i].sem_flg);

  }

 

  /* Выполнить системный вызов */

  rtrn = semop (semid, sops, nsops);

  if (rtrn == -1) {

     printf ("\nsemop завершился неудачей!\n");

     printf ("Код ошибки = %d\n", errno);

  }

  else {

     printf ("\nsemop завершился успешно.\n");

     printf ("Идентификатор semid = %d\n", semid);

     printf ("Возвращенное значение = %d\n", rtrn);

  }

  exit (0);

}


next up previous contents
Next: Разделяемые сегменты памяти Up: Семафоры Previous: Управление семафорами через semctl   Contents
2004-06-22

next up previous contents
Next: Общие сведения Up: Локальные и удаленные средства Previous: Операции над множествами семафоров   Contents

Разделяемые сегменты памяти



Subsections

2004-06-22

next up previous contents
Next: Структуры для разделяемых сегментов Up: Разделяемые сегменты памяти Previous: Разделяемые сегменты памяти   Contents

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

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

Разделение памяти обеспечивает наиболее быстрый обмен данными между процессами.

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

Когда разделяемый сегмент становится ненужным, его следует отсоединить, воспользовавшись системным вызовом shmdt().

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



2004-06-22

next up previous contents
Next: Создание разделяемых сегментов памяти Up: Разделяемые сегменты памяти Previous: Общие сведения   Contents

Структуры для разделяемых сегментов памяти

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

struct shmid_ds {

  /*Структура прав на выполнение операций*/

  struct ipc_perm shm_perm;

  /* Размер сегмента */

  int shm_segsz;

  /*Указатель на структуру области памяти*/

  struct region *shm_reg;

  /* Информация для подкачки */

  char pad [4];

  /* Ид-р процесса, вып. последнюю операцию */

  ushort shm_lpid;

  /* Ид-р процесса, создавшего сегмент */

  ushort shm_cpid;

  /* Число присоединивших сегмент */

  ushort shm_nattch;

  /* Число удерживающих сегмент в памяти */

  ushort shm_cnattch;

  /* Время последнего присоединения */

  time_t shm_atime;

  /* Время последнего отсоединения */

  time_t shm_dtime;

  /* Время последнего изменения */

  time_t shm_ctime;

};

(см. включаемый файл <sys/shm.h>).

Информация о возможных состояниях разделяемых сегментов памяти содержится в табл. 4:

Таблица 4. Состояния разделяемых сегментов памяти.


Бит удержания Бит подкачки Бит размещения Состояние
0 0 0 Неразмещенный сегмент
0 0 1 В памяти
0 1 0 Не используется
0 1 1 На диске
1 0 0 Не используется
1 0 1 Удержан в памяти
1 1 0 Не используется
1 1 1 Не используется


Состояния, упомянутые в таблице, таковы:

неразмещенный
сегмент - разделяемый сегмент, ассоциированный с данным идентификатором, не размещен для использования;
в
памяти - сегмент размещен для использования. Это означает, что сегмент существует и в данный момент находится в оперативной памяти;
на
диске - сегмент в данный момент вытолкнут на устройство подкачки;
удержан
в памяти - сегмент удержан в оперативной памяти и не будет рассматриваться в качестве кандидата на выталкивание, пока не будет снято удержание. Удерживать и освобождать разделяемые сегменты может только суперпользователь;
не
используется - состояние в настоящий момент не используется и при работе обычного пользователя с разделяемыми сегментами памяти возникнуть не может.
Системный вызов shmget аналогичен вызову semget (разумеется, с заменой слов ``множество семафоров'' на ``разделяемый сегмент памяти''). Он также предназначен для получения нового или опроса существующего идентификатора, а нужное действие определяется значением аргумента key. В аналогичных ситуациях shmget терпит неудачу. Единственное его отличие состоит в том, что задается не число семафоров в множестве, а размер сегмента в байтах.

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


next up previous contents
Next: Создание разделяемых сегментов памяти Up: Разделяемые сегменты памяти Previous: Общие сведения   Contents
2004-06-22

next up previous contents
Next: Управление разделяемыми сегментами памяти Up: Разделяемые сегменты памяти Previous: Структуры для разделяемых сегментов   Contents

Создание разделяемых сегментов памяти

Для создания разделяемого сегмента памяти служит системный вызов shmget. Синтаксис данного системного вызова описан так:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget (key_t key, int size, int shmflg);

Целочисленное значение, возвращаемое в случае успешного завершения, является идентификатором разделяемого сегмента (shmid). В случае неудачи результат равен -1.

Смысл аргументов key и shmflg тот же, что и у соответствующих аргументов системного вызова semget. Аргумент size задает размер разделяемого сегмента в байтах.

Системный параметр SHMMNI определяет максимально допустимое число уникальных идентификаторов разделяемых сегментов памяти (shmid) в системе. Попытка его превышения ведет к неудачному завершению системного вызова.

Системный вызов завершится неудачей и тогда, когда значение аргумента size меньше, чем SHMMIN, либо больше, чем SHMMAX. Данные системные параметры определяют соответственно минимальный и максимальный размеры разделяемого сегмента памяти.



2004-06-22

next up previous contents
Next: Операции над разделяемыми сегментами Up: Разделяемые сегменты памяти Previous: Создание разделяемых сегментов памяти   Contents

Управление разделяемыми сегментами памяти

В справочной статье shmctl синтаксис данного системного вызова описан так:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmctl (int shmid, int cmd, struct shmid_ds *buf);

При успешном завершении результат равен нулю; в случае неудачи возвращается -1.

В качестве аргумента shmid должен выступать идентификатор разделяемого сегмента памяти, предварительно полученный системным вызовом shmget.

Управляющее действие определяется значением аргумента cmd. Допустимы следующие значения:

IPC_STAT
- поместить информацию о состоянии разделяемого сегмента, содержащуюся в структуре данных, ассоциированной с идентификатором shmid, в пользовательскую структуру, на которую указывает аргумент buf;
IPC_SET
- в структуре данных, ассоциированной с идентификатором shmid, переустановить значения действующих идентификаторов пользователя и группы, а также прав на операции. Нужные значения извлекаются из структуры данных, на которую указывает аргумент buf;
IPC_RMID
- удалить из системы идентификатор shmid, ликвидировать разделяемый сегмент памяти и ассоциированную с ним структуру данных;
SHM_LOCK
- удерживать в памяти разделяемый сегмент, заданный идентификатором shmid;
SHM_UNLOCK
- освободить (перестать удерживать в памяти) разделяемый сегмент, заданный идентификатором shmid.
Чтобы выполнить управляющее действие IPC_SET или IPC_RMID, процесс должен иметь действующий идентификатор пользователя, равный либо идентификаторам создателя или владельца очереди, либо идентификатору суперпользователя.

Управляющие действия SHM_LOCK и SHM_UNLOCK может выполнить только суперпользователь. Для выполнения управляющего действия IPC_STAT процессу требуется право на чтение:

/* Программа иллюстрирует

возможности системного вызова shmctl()

(операции управления разделяемыми сегментами) */

 

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

 

main ()

{

  extern int errno;

  int rtrn, shmid, command, choice;

  struct shmid_ds shmid_ds, *buf;

  buf = &shmid_ds;

 

  /* Ввести идентификатор сегмента и действие */

  printf ("Введите идентификатор shmid: ");

  scanf ("%d", &shmid);

  printf ("Введите номер требуемого действия:\n");

  printf (" IPC_STAT = 1\n");

  printf (" IPC_SET = 2\n");

  printf (" IPC_RMID = 3\n");

  printf (" SHM_LOCK = 4\n");

  printf (" SHM_UNLOCK = 5\n");

  printf (" Выбор = ");

  scanf ("%d", &command);

 

  /* Проверить значения */

  printf ("\nидентификатор = %d, действие = %d\n",

          shmid, command);

  switch (command) {

          case 1: /* Скопировать информацию

              о состоянии разделяемого сегмента

              в пользовательскую структуру

              и вывести ее */

              rtrn = shmctl (shmid, IPC_STAT, buf);

              printf ("\nИд-р пользователя = %d\n",

                      buf->shm_perm.uid);

              printf ("Ид-р группы пользователя = %d\n",

                      buf->shm_perm.gid);

              printf ("Ид-р создателя = %d\n",

                      buf->shm_perm.cuid);

              printf ("Ид-р группы создателя = %d\n",

                      buf->shm_perm.cgid);

              printf ("Права на операции = 0%o\n",

                      buf->shm_perm.mode);

              printf ("Последовательность номеров ");

                      buf->shm_perm.cgid);

              printf ("используемых слотов = 0%x\n",

                      buf->shm_perm.seq);

              printf ("Ключ = 0%x\n",

                      buf->shm_perm.key);

              printf ("Размер сегмента = %d\n",

                      buf->shm_segsz);

              printf ("Выполнил последнюю операцию =

                      %d\n", buf->shm_lpid);

              printf ("Создал сегмент = %d\n",

                      buf->shm_cpid);

              printf ("Число присоединивших сегмент =

                      %d\n", buf->shm_nattch);

              printf ("Число удерживаюших в памяти =

                      %d\n", buf->shm_cnattch);

    printf ("Последнее присоединение = %d\n",

            buf->shm_atime);

    printf ("Последнее отсоединение = %d\n",

            buf->shm_dtime);

    printf ("Последнее изменение = %d\n",

            buf->shm_ctime);

          break;

    case 2: /* Выбрать и изменить поле (поля)

           ассоциированной структуры данных */

           /* Получить исходные значения

            структуры данных */

           rtrn = shmctl (shmid, IPC_STAT, buf);

           printf ("Введите номер

                   изменяемого поля:\n");

           printf (" shm_perm.uid = 1\n");

           printf (" shm_perm.gid = 2\n");

           printf (" shm_perm.mode = 3\n");

           printf (" Выбор = ");

           scanf ("%d", &choice);

           switch (choice) {

              case 1:

                    printf ("\nВведите ид-р

                            пользователя:"),

                    scanf ("%d",

                           &buf->shm_perm.uid);

                    printf ("\nИд-р пользователя =

                            %d\n",

                    buf->shm_perm.uid);

                    break;

              case 2:

                    printf ("\nВведите ид-р группы: "),

                    scanf ("%d", &buf->shm_perm.gid);

                    printf ("\nИд-р группы = %d\n",

                    buf->shm_perm.uid);

                    break;

              case 3:

                    printf ("\nВведите восьмеричный

                            код прав: ");

                    scanf ("%o", &buf->shm_perm.mode);

                    printf ("\nПрава на операции

                            = 0%o\n",

                            buf->shm_perm.mode);

                    break;

                   }

                  /* Внести изменения */

                  rtrn = shmctl (shmid, IPC_SET, buf);

                  break;

   case 3: /* Удалить идентификатор и

             ассоциированную структуру данных */

             rtrn = shmctl (shmid, IPC_RMID, NULL);

             break;

    case 4: /* Удерживать разделяемый сегмент

             в памяти */

             rtrn = shmctl (shmid, SHM_LOCK, NULL);

             break;

    case 5: /* Перестать удерживать сегмент в памяти */

           rtrn = shmctl (shmid, SHM_UNLOCK, NULL);

  }

  if (rtrn == -1) {

        /* Сообщить о неудачном завершении */

        printf ("\nshmctl завершился неудачей!\n");

        printf ("\nКод ошибки = %d\n", errno);

  }

  else {

       /* При успешном завершении сообщить ид-р shmid */

       printf ("\nshmctl завершился успешно, ");

       printf ("идентификатор shmid = %d\n", shmid);

  }

  exit (0);

}


next up previous contents
Next: Операции над разделяемыми сегментами Up: Разделяемые сегменты памяти Previous: Создание разделяемых сегментов памяти   Contents
2004-06-22

next up previous contents
Next: Основные сведения о процессах Up: Локальные и удаленные средства Previous: Локальные и удаленные средства   Contents

Процессы



Subsections

2004-06-22

next up previous contents
Next: Потоки (threads) Up: Разделяемые сегменты памяти Previous: Управление разделяемыми сегментами памяти   Contents

Операции над разделяемыми сегментами памяти

Cинтаксис системных вызовов shmat и shmdt описан так:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmat (int shmid, char *shmaddr, int shmflg);

int shmdt (char *shmaddr);

При успешном завершении системного вызова shmat() результат равен адресу, который получил присоединенный сегмент; в случае неудачи возвращается -1.

Разумеется, чтобы использовать результат shmat() как указатель, его нужно преобразовать к требуемому типу.

В качестве аргумента shmid должен выступать идентификатор разделяемого сегмента, предварительно полученный системным вызовом shmget. Аргумент shmaddr задает адрес, по которому сегмент должен быть присоединен, т. е. тот адрес в виртуальном пространстве пользователя, который получит начало сегмента. Однако не всякий адрес является приемлемым. Можно порекомендовать адреса вида:

0x80000000

0x80040000

0x80080000

. . .

Если значение shmaddr равно нулю, система выбирает адрес присоединения по своему усмотрению. Аргумент shmflg используется для передачи системному вызову shmat() флагов SHM_RND и
SHM_RDONLY. Наличие первого из них означает, что адрес shmaddr следует округлить до некоторой системнозависимой величины. Второй флаг предписывает присоединить сегмент только для чтения; если он не установлен, присоединенный сегмент будет доступен как для чтения, так и для записи (если процесс обладает соответствующими правами).

При успешном завершении системного вызова shmdt() результат равен нулю; в случае неудачи возвращается -1.

Аргумент shmaddr задает начальный адрес отсоединяемого сегмента. После того, как последний процесс отсоединил разделяемый сегмент памяти, этот сегмент вместе с идентификатором и ассоциированной структурой данных следует удалить системным вызовом shmctl.

Пример использования вызовов shmat() и shmdt():

/* Программа иллюстрирует

возможности системных вызовов shmat() и shmdt()

(операции над разделяемыми сегментами памяти) */

 

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

 

main ()

{

  extern int errno;

  int shmid, shmaddr, shmflg;

  int flags, attach, detach, rtrn, i;

   

  /* Цикл присоединений для данного процесса */

  printf ("\nВведите число присоединений ");

  printf ("для процесса (1-4): ");

  scanf ("%d", &attach);

  printf ("\nЧисло присоединений = %d\n", attach);

  for (i = 0; i < attach; i++) {

    /* Ввести идентификатор разделяемого сегмента */

    printf ("\nВведите ид-р разделяемого сегмента,\n");

    printf ("над которым нужно выполнить операции: ");

    scanf ("%d", &shmid);

    printf ("\nИд-р сегмента = %d\n", shmid);

    /* Ввести адрес присоединения */

    printf ("\nВведите адрес присоединения ");

    printf ("в шестнадцатеричной записи: ");

    scanf ("%x", &shmaddr);

    printf ("\nАдрес присоединения = 0x%x\n", shmaddr);

    /* Выбрать требуемые флаги */

    printf ("\nВведите номер нужной

            комбинации флагов:\n");

    printf (" SHM_RND = 1\n");

    printf (" SHM_RDONLY = 2\n");

    printf (" SHM_RND и SHM_RDONLY = 3\n");

    printf (" Выбор = ");

    scanf ("%d", &flags);

       switch (flags) {

               case 1:

                       shmflg = SHM_RND;

                       break;

               case 2:

                       shmflg = SHM_RDONLY;

                       break;

               case 3:

                       shmflg = SHM_RND | SHM_RDONLY;

                       break;

         }

       printf ("\nФлаги = 0%o", shmflg);

 

       /* Выполнить системный вызов shmat */

       rtrn = shmat (shmid, shmaddr, shmflg);

       if (rtrn == -1) {

            printf ("\nshmat завершился неудачей!\n");

            printf ("\Код ошибки = %d\n", errno);

    }

    else {

        printf ("\nshmat завершился успешно.\n");

        printf ("Идентификатор shmid = %d\n", shmid);

        printf ("Адрес = 0x%x\n", rtrn);

    }

  }

 

  /* Цикл отсоединений для данного процесса */

  printf ("\nВведите число отсоединений ");

  printf ("для процесса (1-4): ");

  scanf ("%d", &detach);

  printf ("\nЧисло отсоединений = %d\n", detach);

  for (i = 0; i < detach; i++) {

    /* Ввести адрес отсоединения */

    printf ("\nВведите адрес отсоединяемого сегмента ");

    printf ("в шестнадцатеричной записи: ");

    scanf ("%x", &shmaddr);

    printf ("\nАдрес отсоединения = 0x%x\n", shmaddr);

  

    /* Выполнить системный вызов shmdt */

    rtrn = shmdt (shmaddr);

    if (rtrn == -1) {

       printf ("\nshmdt завершился неудачей!\n");

       printf ("\Код ошибки = %d\n", errno);

    }

    else {

       printf ("\nshmdt завершился успешно,\n");

       printf ("идентификатор shmid = %d\n", shmid);

    }

  }

  exit (0);

}


next up previous contents
Next: Потоки (threads) Up: Разделяемые сегменты памяти Previous: Управление разделяемыми сегментами памяти   Contents
2004-06-22

next up previous contents
Next: Различие между процессами и Up: Локальные и удаленные средства Previous: Операции над разделяемыми сегментами   Contents

Потоки (threads)



Subsections

2004-06-22

next up previous contents
Next: Преимущества многопоточности Up: Потоки (threads) Previous: Потоки (threads)   Contents

Различие между процессами и потоками

С помощью процессов можно организовать параллельное выполнение программ. Для этого процессы клонируются вызовами fork() или exec(), а затем между ними организуется взаимодействие средствами IPC. Это довольно дорогостоящий в отношении ресурсов процесс.

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

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

  1. Процесс располагает определенными ресурсами. Он размещен в некотором виртуальном адресном пространстве, содержащем образ этого процесса. Кроме того, процесс управляет другими ресурсами (файлы, устройства ввода / вывода и т.д.).
  2. Процесс подвержен диспетчеризации. Он определяет порядок выполнения одной или нескольких программ, при этом выполнение может перекрываться другими процессами. Каждый процесс имеет состояние выполнения и приоритет диспетчеризации.
Если рассматривать эти характеристики независимо друг от друга (как это принято в современной теории ОС), то:

Все потоки процесса разделяют общие ресурсы. Изменения, вызванные одним потоком, становятся немедленно доступны другим.

При корректной реализации потоки имеют определенные преимущества перед процессами. Им требуется:



2004-06-22

next up previous contents
Next: Уровни потоков Up: Потоки (threads) Previous: Различие между процессами и   Contents

Преимущества многопоточности

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



2004-06-22

next up previous contents
Next: Пользовательские потоки Up: Потоки (threads) Previous: Преимущества многопоточности   Contents

Уровни потоков

Существует две основных категории потоков с точки зрения реализации:

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



Subsections

2004-06-22

next up previous contents
Next: Потоки уровня ядра Up: Уровни потоков Previous: Уровни потоков   Contents

Пользовательские потоки

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

Преимущества пользовательских потоков в следующем:

Недостатки пользовательских потоков:



2004-06-22

next up previous contents
Next: Создание потока Up: Уровни потоков Previous: Пользовательские потоки   Contents

Потоки уровня ядра

На этом уровне все управление потоком выполняется ядром. Используется программный интерфейс приложения (системные вызовы) для работы с потоками уровня ядра. Ядро поддерживает информацию о контексте процесса и потоков; переключение потоков требует выполнения дисциплины планирования ядра на уровне этих потоков.

Преимущества потоков уровня ядра:

Недостатки:

Основной библиотекой для реализации пользовательских потоков является библиотека потоков POSIX, которая называется pthreads.



2004-06-22

next up previous contents
Next: Ожидание завершения потока Up: Потоки (threads) Previous: Потоки уровня ядра   Contents

Создание потока

Функция pthread_create() позволяет добавить новый поток управления к текущему процессу. Прототип функции:

int pthread_create(pthread_t *tid,

       const pthread_attr_t *tattr,

       void*(*start_routine)(void *), void *arg);

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

#include <pthread.h>

pthread_attr_t tattr;

pthread_t tid;

extern void *start_routine(void *arg);

void *arg;

int ret;

/* поведение по умолчанию*/

ret = pthread_create(&tid, NULL, start_routine, arg);

/* инициализация с атрибутами по умолчанию */

ret = pthread_attr_init(&tattr);

/* определение поведения по умолчанию*/

ret = pthread_create(&tid, &tattr, start_routine, arg);

Функция pthread_create() вызывается с атрибутом attr, определяющим необходимое поведение; start_routine - это функция, с которой новый поток начинает свое выполнение. Когда
start_routine завершается, поток завершается со статусом выхода, установленным в значение, возвращенное start_routine.

Если вызов pthread_create() успешно завершен, идентификатор созданного потока сохраняется по адресу tid.

Создание потока с использованием аргумента атрибутов NULL оказывает тот же эффект, что и использование атрибута по умолчанию: оба создают одинаковый поток. При инициализации tattr он обретает поведение по умолчанию; pthread_create() возвращает 0 при успешном завершении. Любое другое значение указывает, что произошла ошибка.



2004-06-22

next up previous contents
Next: Отделение потока Up: Потоки (threads) Previous: Создание потока   Contents

Ожидание завершения потока

Функция pthread_join() используется для ожидания завершения потока:

int pthread_join(thread_t tid, void **status);
Пример использования функции:

#include <pthread.h>

pthread_t tid;

int ret;

int status;

/* ожидание завершения потока "tid" со статусом status */

ret = pthread_join(tid, &status);

/* ожидание завершения потока "tid" без статуса */

ret = pthread_join(tid, NULL);

Функция pthread_join() блокирует вызывающий поток, пока указанный поток не завершится. Указанный поток должен принадлежать текущему процессу и не должен быть отделен. Если status не равен NULL, он указывает на переменную, которая принимает значение статуса выхода завершенного потока при успешном завершении pthread_join(). Несколько потоков не могут ждать завершения одного и того же потока. Если они пытаются выполнить это, один поток завершается успешно, а все остальные - с ошибкой ESRCH. После завершения pthread_join(), любое пространство стека, связанное с потоком, может быть использовано приложением.

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

Исходный код для thread.c:

void mainline (...)

{

  struct phonebookentry *pbe;

  pthread_attr_t tattr;

  pthread_t helper;

  int status;

  pthread_create(&helper, NULL, fetch, &pbe);

  /* выполняет собственную задачу */

  pthread_join(helper, &status);

  /* теперь можно использовать результат */

}

 

void fetch(struct phonebookentry *arg)

{

  struct phonebookentry *npbe;

  /* ищем значение в базе данных */

  npbe = search (prog_name)

  if (npbe != NULL)

     *arg = *npbe;

  pthread_exit(0);

}

 

struct phonebookentry {

  char name[64];

  char phonenumber[32];

  char flags[16];

}



2004-06-22

next up previous contents
Next: Работа с ключами потока Up: Потоки (threads) Previous: Ожидание завершения потока   Contents

Отделение потока

Функция pthread_detach() применяется как альтернатива
pthread_join(), чтобы утилизировать область памяти для потока, который был создан с атрибутом detachstate, установленным в значение PTHREAD_CREATE_JOINABLE. Прототип функции:

int pthread_detach(thread_t tid);
Пример вызова функции:

#include <pthread.h>

pthread_t tid;

int ret;

/* отделить поток tid */

ret = pthread_detach(tid);

Функция pthread_detach() используется, чтобы указать библиотеке потоков, что выделенная память для потока tid может быть утилизирована, когда поток завершится. Если tid не закончился, pthread_detach() не ускоряет его завершения и возвращает 0 при успешном завершении. Любое другое значение указывает, что произошла ошибка.



2004-06-22

next up previous contents
Next: Таблица процессов Up: Процессы Previous: Процессы   Contents

Основные сведения о процессах

В ОС Linux управление процессами является ключевой технологией при разработке многих программ. Процесс - это находящаяся в состоянии выполнения программа вместе со средой ее выполнения.

Так как Linux - это настоящая многозадачная система, в ней одновременно могут выполняться несколько программ (процессов, задач). Термин ``одновременно'' не всегда соответствует действительности. Обычно центральный процессор (ЦП) может работать в данный момент только с одним процессом. Если необходимо выполнить одновременно несколько программ параллельно, нужно использовать либо несколько компьютеров, либо несколько процессоров. Однако для большинства пользователей этот вариант может быть непривлекательным из-за расходов на приобретение дополнительной техники.

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

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

В Linux реализована вытесняющая многозадачность. Это значит, что система сама решает, как долго конкретный процесс может использовать ЦП, и когда наступит очередь следующего процесса. Если пользователь желает вмешаться в процесс планирования, он может сделать это как root с помощью команды nice.

С помощью команды ps можно узнать, какие процессы выполняются в настоящий момент: ps -x

При этом выводится список выполняющихся в данный момент процессов. Значение колонок следующее:

PID     TTY     STAT     TIME    COMMAND
1234    pts/0    R       0:00    ps -x
PID - идентификатор процесса. Каждый процесс получает собственный однозначный идентификатор. Пользуясь этим идентификатором, можно получать доступ к конкретному процессу. Например, можно получить сведения о процессе с ID 1501 с помощью команды:

ps 1501

Если идентификатор процесса не известен, но известна команда, запустившая этот процесс, то идентификатор можно узнать с помощью команды: pidof /bin/bash

Отметьте, что pidof можно выполнять только как root.

TTY показывает, в каком терминале выполняется процесс. Если в колонке не указано никакого значения, речь идет, как правило, о процессе-демоне.

STAT показывает текущее состояние процесса. В приведенном примере символ R означает выполнение (running). Для процессов применяются следующие обозначения:

TIME указывает время работы процесса.

COMMAND - имя команды, с помощью которой запущен процесс.

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

Если нужно узнать, сколько времени ЦП необходимо для каждого процесса, можно использовать команду top. Она показывает в колонке '%CPU', какое время вычислений занимает определенная программа в процессоре.



2004-06-22

next up previous contents
Next: Остановка потока Up: Потоки (threads) Previous: Отделение потока   Contents

Работа с ключами потока

Однопоточные программы на C содержат два основных класса данных: локальные и глобальные. Для многопоточных программ на C добавляется третий класс: данные потока. Они похожи на глобальные данные, за исключением того, что они являются собственными для потока.

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

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

int pthread_key_create(pthread_key_t *key,

      void(*destructor)(void *));

Пример использования:

#include <pthread.h>

pthread_key_t key;

int ret;

/* создание ключа без деструктора */

ret = pthread_key_create(&key, NULL);

/* создание ключа с деструктором */

ret = pthread_key_create(&key, destructor);

Если pthread_keycreate() завершается успешно, то выделенный
ключ будет сохранен в переменной key. Вызывающий процесс должен гарантировать, что хранение и доступ к этому ключу синхронизированы. Чтобы освободить ранее выделенную память, может использоваться дополнительная функция удаления - деструктор. Если ключ имеет непустой указатель на функцию деструктора, и поток имеет непустое значение ключа, функция деструктора вызывается для значения, связанного с потоком, после его завершения. Порядок, в котором вызываются функции деструктора, может быть произвольным; pthread_keycreate() возвращает 0 при успешном завершении, или любое другое значение при возникновении ошибки.

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

Прототип pthread_keydelete():

int pthread_key_delete(pthread_key_t key);
Пример использования функции:

#include <pthread.h>

pthread_key_t key;

int ret;

/* key был создан ранее */

ret = pthread_key_delete(key);

Как только ключ удален, любая ссылка на него через
pthread_setspecific() или pthread_getspecific() приводит к ошибке EINVAL.

Программист должен сам освобождать любые выделенные потоку ресурсы перед вызовом функции удаления. Эта функция не вызывает деструктора; pthread_keydelete() возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки.

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

int pthread_setspecific(pthread_key_t key,

     const void *value);

Пример вызова:

#include <pthread.h>

pthread_key_t key;

void *value;

int ret;

/* key был создан ранее */

ret = pthread_setspecific(key, value);

Функция pthread_setspecific() возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки; она не освобождает память для хранения ключа. Если установлена новая привязка значения ключа, предыдущая привязка должна быть освобождена; иначе может произойти утечка памяти.

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

int pthread_getspecific(pthread_key_t key);
Пример:

#include <pthread.h>

pthread_key_t key;

void *value;

/* key был создан ранее */

value = pthread_getspecific(key);

Рассмотрим следующий код:

body() {

  ...

  while (write(fd, buffer, size) == -1) {

    if (errno != EINTR) {

       fprintf(mywindow, "%s\n", strerror(errno));

       exit(1);

    }

  }

  ...

}

Этот код может быть выполнен любым числом потоков, но он содержит ссылки на две глобальных переменных, errno и mywindow, которые должны быть ссылками на объекты, являющиеся частными для каждого потока.

Ссылки на errno должны получить код системной ошибки из процедуры, вызванной именно этим конкретным потоком, и никаким другим. Поэтому ссылки на errno в одном потоке относятся к иной области памяти, чем ссылки на errno в других потоках. Переменная mywindow предназначена для обращения к потоку stdio, связанному с окном, которое является частным объектом потока. Так же, как и errno, ссылки на mywindow в одном потоке должны обращаться к отдельной конкретной области памяти (и в конечном счете - к различным окнам). Единственное различие между этими переменными состоит в том, что библиотека потоков реализует раздельный доступ для errno, а программист должен сам реализовать это для mywindow. Следующий пример показывает, как работают ссылки на mywindow. Препроцессор преобразует ссылки на mywindow в вызовы процедур mywindow. Эта процедура в свою очередь вызывает pthread_getspecific(), передавая ему глобальную переменную mywindow_key (это, действительно, глобальная переменная) и выходной параметр win, который принимает идентификатор окна для этого потока.

Следующий фрагмент кода:

thread_key_t mywin_key;

FILE *_mywindow(void) {

  FILE *win;

  pthread_getspecific(mywin_key, &win);

  return(win);

}

 

#define mywindow _mywindow()

 

void routine_uses_win( FILE *win) {

  ...

  }

 

void thread_start(...) {

  ...

  make_mywin();

  ...

  routine_uses_win( mywindow )

  ...

}

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

Теперь можно устанавливать собственные данные потока:

void make_mywindow(void) {

  FILE **win;

  static pthread_once_t mykeycreated =

        PTHREAD_ONCE_INIT;

  pthread_once(&mykeycreated, mykeycreate);

  win = malloc(sizeof(*win));

  create_window(win, ...);

  pthread_setspecific(mywindow_key, win);

}

 

void mykeycreate(void) {

  pthread_keycreate(&mywindow_key, free_key);

}

 

void free_key(void *win) {

  free(win);

}

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

Следующий шаг состоит в выделении памяти для элемента данных вызывающего потока. После выделения памяти выполняется вызов процедуры create_window, устанавливающей окно для потока и выделяющей память для переменной win, которая ссылается на окно. Наконец выполняется вызов pthread_setspecific(), который связывает значение win с ключом. После этого, как только поток вызывает pthread_getspecific(), передав глобальный ключ, он получает некоторое значение. Это значение было связано с этим ключом в вызывающем потоке, когда он вызвал функцию
pthread_setspecific(). Когда поток заканчивается, выполняются вызовы функций деструкторов, которые были настроены при вызове pthread_key_create(). Функция деструктора вызывается, если завершившийся поток установил значение для ключа вызовом pthread_setspecific().

Функция pthread_self() вызывается для получения ID вызывающего ее потока:

#include <pthread.h>

pthread_t tid;

tid = pthread_self();

Функция pthread_equal() вызывается для сравнения идентификаторов двух потоков:

#include <pthread.h>

pthread_t tid1, tid2;

int ret;

ret = pthread_equal(tid1, tid2);

Как и другие функции сравнения, pthread_equal() возвращает значение, отличное от нуля, когда tid1 и tid2 равны; иначе возвращается 0. Если tid1 или tid2 - недействительный идентификатор потока, результат функции будет неопределенным.

Функция pthread_once() используется для вызова процедуры инициализации потока только один раз. Последующие вызовы не оказывают никакого эффекта. Пример вызова функции:

int pthread_once(pthread_once_t *once_control,

    void (*init_routine)(void));

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

#include <sched.h>

int ret;

ret = sched_yield();

После успешного завершения sched_yield() возвращает 0. Если возвращается -1, то системная переменная errno устанавливается на код ошибки.

Функция pthread_setschedparam() используется, чтобы изменить приоритет существующего потока. Эта функция никоим образом не влияет на дисциплину диспетчеризации:

int pthread_setschedparam(pthread_t tid, int policy,

       const struct sched_param *param);

Использование функции:

#include <pthread.h>

pthread_t tid;

int ret;

struct sched_param param;

int priority;

/* sched_priority указывает приоритет потока */

sched_param.sched_priority = priority;

/* единственный поддерживаемый алгоритм диспетчера*/

policy = SCHED_OTHER;

/* параметры диспетчеризации требуемого потока */

ret = pthread_setschedparam(tid, policy, &param);

pthread_setschedparam() возвращает 0 в случае успешного завершения, или другое значение в случае ошибки.

Функция:

int pthread_getschedparam(pthread_t tid, int policy,

      struct schedparam *param)

позволяет получить приоритет любого существующего потока.

Пример вызова функции:

#include <pthread.h>

pthread_t tid;

sched_param param;

int priority;

int policy;

int ret;

/* параметры диспетчеризации нужного потока */

ret = pthread_getschedparam (tid, &policy, &param);

/* sched_priority содержит приоритет потока */

priority = param.sched_priority;

pthread_getschedparam() возвращает 0 - в случае успешного завершения - или другое значение - в случае ошибки.

Поток, как и процесс, может принимать различные сигналы:

#include <pthread.h>

#include <signal.h>

int sig;

pthread_t tid;

int ret;

ret = pthread_kill(tid, sig);

pthread_kill() посылает сигнал sig потоку, обозначенному tid, который должен быть потоком в пределах того же самого процесса, что и вызывающий поток. Аргумент sig должен быть действительным сигналом некоторого типа, определенного для функции signal() в файле < signal.h>.

Если sig имеет значение 0, выполняется проверка ошибок, но сигнал реально не посылается. Таким образом можно проверить правильность tid. Функция возвращает 0 - в случае успешного завершения - или другое значение - в случае ошибки.

Функция pthread_sigmask() может использоваться для изменения или получения маски сигналов вызывающего потока:

int pthread_sigmask(int how, const sigset_t *new,

        sigset_t *old);

Пример вызова функции:

#include <pthread.h>

#include <signal.h>

int ret;

sigset_t old, new;

 /* установка новой маски */

ret = pthread_sigmask(SIG_SETMASK, &new, &old);

 /* блокирование маски */

ret = pthread_sigmask(SIG_BLOCK, &new, &old);

 /* снятие блокировки */

ret = pthread_sigmask(SIG_UNBLOCK, &new, &old);

how определяет режим смены маски. Он принимает значения следующих констант:

SIG_SETMASK
- заменяет текущую маску сигналов новой, при этом new указывает новую маску сигналов;
SIG_BLOCK
- добавляет новую маску сигналов к текущей, при этом new указывает множество блокируемых сигналов;
SIG_UNBLOCK
- удаляет new из текущей маски сигналов, при этом new указывает множество сигналов для снятия блокировки.
Если значение new равно NULL, то значение how не играет роли, и маска сигналов потока не изменяется. Чтобы узнать о блокированных в данный момент сигналах, аргумент new устанавливают в NULL. Переменная old указывает, где хранится прежняя маска сигналов, если ее значение не равно NULL.

Функция pthread_sigmask() возвращает 0 - в случае успешного завершения - или другое значение - в случае ошибки.


next up previous contents
Next: Остановка потока Up: Потоки (threads) Previous: Отделение потока   Contents
2004-06-22

next up previous contents
Next: Компиляция многопоточного приложения Up: Потоки (threads) Previous: Работа с ключами потока   Contents

Остановка потока

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

Функция void pthread_exit(void *status) прерывает выполнение потока точно так же, как функция exit() прерывает процесс:

#include <pthread.h>

int status;

 /* выход возвращает статус status */

pthread_exit(&status);

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

Функция pthread_cancel() предназначена для прерывания потока:

#include <pthread.h>

pthread_t thread;

int ret;

ret = pthread_cancel(thread);

Способ обработки запроса на прерывание потока зависит от состояния указанного потока. Две функции, pthread_setcancelstate() и pthread_setcanceltype(), определяют это состояние; функция pthread_cancel() возвращает 0 в случае успешного завершения, или другое значение в случае ошибки.



2004-06-22

next up previous contents
Next: Отладка многопоточного приложения Up: Потоки (threads) Previous: Остановка потока   Contents

Компиляция многопоточного приложения

Для компиляции и сборки многопоточной программы необходимо иметь:

Файл заголовка <pthread.h>, используемый с библиотекой -lpthread, компилирует код, который является совместимым с интерфейсами многопоточности, определенными стандартом POSIX 1003.1c. Для полной совместимости POSIX флаг _POSIX_C_SOURCE должен быть установлен следующим образом:

cc [flags] file... -D_POSIX_C_SOURCE=N (где N = 199506L)



2004-06-22

next up previous contents
Next: Атрибуты потоков Up: Потоки (threads) Previous: Компиляция многопоточного приложения   Contents

Отладка многопоточного приложения

К наиболее частым оплошностям и ошибкам многопоточных программах относятся:



2004-06-22

next up previous contents
Next: Состояние отделенного потока Up: Потоки (threads) Previous: Отладка многопоточного приложения   Contents

Атрибуты потоков

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

Обычно вызываются три функции:

Пример кода, выполняющего эти действия:

#include <pthread.h> 

pthread_attr_t tattr;

pthread_t tid;

void *start_routine;

void arg

int ret;

/* инициализация атрибутами по умолчанию */

ret = pthread_attr_init(&tattr);

/* вызов соответствующих функций для изменения

     значений */

ret = pthread_attr_*(&tattr,

      SOME_ATRIBUTE_VALUE_PARAMETER);

/* создание потока */

ret = pthread_create(&tid, &tattr, start_routine, arg);

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

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

Функция pthread_attr_init() используется, чтобы инициализировать объект атрибутов значениями по умолчанию. Память распределяется системой потоков во время выполнения.

Пример вызова функции:

#include <pthread.h>

pthread_attr_t tattr;

int ret;

ret = pthread_attr_init(&tattr);

Значения по умолчанию для атрибутов (tattr) приведены в табл. 5.

Таблица 5. Атрибуты потока по умолчанию


Атрибут Значение Смысл
scope PTHREAD_SCOPE_PROCESS Новый поток не ограничен - не присоединен ни к одному процессу
detachstate PTHREAD_CREATE_JOINABLE Статус выхода и поток сохраняются после завершения потока
stackaddr NULL Новый поток получает адрес стека, выделенного системой
stacksize 1 Мбайт Новый поток имеет размер стека, определенный системой
inheritsched PTHREAD_INHERIT_SCHED Поток наследует приоритет диспетчеризации родительского потока
schedpolicy SCHED_OTHER Новый поток использует диспетчеризацию с фиксированными приоритетами. Поток работает, пока не будет прерван потоком с высшим приоритетом или не приостановится


Функция возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Код ошибки устанавливается в переменной errno.

Функция pthread_attr_destroy() используется, чтобы удалить память для атрибутов, выделенную во время инициализации. Объект атрибутов становится недействительным.

Пример вызова функции:

#include <pthread.h>

pthread_attr_t tattr;

int ret;

ret = pthread_attr_destroy(&tattr);

pthread_attr_destroy() возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки.



Subsections
next up previous contents
Next: Состояние отделенного потока Up: Потоки (threads) Previous: Отладка многопоточного приложения   Contents
2004-06-22

next up previous contents
Next: Ограничения потока Up: Атрибуты потоков Previous: Атрибуты потоков   Contents

Состояние отделенного потока

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

Если поток создается неотделенным (PTHREAD_CREATE_JOINABLE), предполагается, что создающий поток будет ожидать его завершения и выполнять в созданном потоке pthread_join(). Независимо от типа потока процесс не закончится, пока не завершатся все потоки; pthread_attr_setdetachstate() возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки.

Пример вызова для отсоединения потока:

#include <pthread.h>

pthread_attr_t tattr;

int ret;

/* устанавливаем состояние потока */

ret = pthread_attr_setdetachstate(&tattr,

      PTHREAD_CREATE_DETACHED);

Если не предусмотрено никакой явной синхронизации, то недавно созданный, отделенный поток может завершиться и переназначить свой ID на другой новый поток, прежде чем его создатель завершит вызов pthread_create(). Для неотделенного (PTHREAD_CREATE_ JOINABLE) потока очень важно, чтобы после того, как он завершится, к нему присоединился другой поток, - иначе ресурсы этого потока не будут освобождены для использования новыми потоками. Это обычно приводит к утечке памяти. Если не требуется создавать поток, который будет присоединен, нужно создавать его отделенным.

Функция pthread_attr_getdetachstate() позволяет определить состояние при создании потока, т.е. был ли он отделенным или присоединяемым. Она возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки. Пример вызова:

#include <pthread.h>

pthread_attr_t tattr;

int detachstate;

int ret;

ret = pthread_attr_getdetachstate (&tattr, &detachstate);

Следующий пример иллюстрирует, как можно создать отделенный поток:

#include <pthread.h> 

pthread_attr_t tattr;

pthread_t tid;

void *start_routine;

void arg

int ret;

ret = pthread_attr_init(&tattr);

ret = pthread_attr_setdetachstate(&tattr,

      PTHREAD_CREATE_DETACHED);

ret = pthread_create(&tid, &tattr, start_routine, arg);



2004-06-22

next up previous contents
Next: Дисциплина планирования потока Up: Атрибуты потоков Previous: Состояние отделенного потока   Contents

Ограничения потока

Поток может быть ограничен (иметь тип PTHREAD_SCOPE_SYSTEM) или неограничен (иметь тип PTHREAD_SCOPE_PROCESS). Оба этих типа доступны только в пределах данного процесса. Функция
pthread_attr_setscope() позволяет создать потоки указанных типов; pthread_attr_setscope() возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки. Пример вызова функции:

#include <pthread.h>

pthread_attr_t attr;

pthread_t tid;

void start_routine;

void arg;

int ret;

/* инициализация атрибутов по умолчанию */

ret = pthread_attr_init (&tattr);

/* ограниченное поведение */

ret = pthread_attr_setscope(&tattr,

      PTHREAD_SCOPE_SYSTEM);

ret = pthread_create (&tid, &tattr, start_routine,

      arg);

Функция pthread_attr_getscope() используется для определения ограниченности потока. Пример вызова:

#include <pthread.h>

pthread_attr_t tattr;

int scope;

int ret;

ret = pthread_attr_getscope(&tattr, &scope);

pthread_att_getscope() возвращает 0 - после успешного завершения - или любое другое значение - в случае ошибки.



2004-06-22

next up previous contents
Next: Размер стека в потоке Up: Атрибуты потоков Previous: Ограничения потока   Contents

Дисциплина планирования потока

Стандарт POSIX определяет несколько значений атрибута планирования: SCHED_FIFO, SCHED_RR (Round Robin) или SCHED_OTHER (метод приложения). Дисциплины SCHED_FIFO и SCHED_RR являются необязательными и поддерживаются только для потоков в режиме реального времени.

Библиотека pthreads поддерживает только значение SCHED_OTHER. Попытка установить другое значение приведет к возникновению ошибки ENOSUP.

Для установки дисциплины диспетчеризации используется следующая функция:

#include <pthread.h>

pthread_attr_t tattr;

int ret;

ret = pthread_attr_setschedpolicy(&tattr, SCHED_OTHER);

Парной к ней является функция pthread_attr_getschedpolicy(), которая возвращает константу, определяющую дисциплину диспетчеризации.

Функция pthread_attr_setinheritsched() используется для наследования дисциплины диспетчеризации из родительского потока. Значение переменной inherit, равное PTHREAD_INHERIT_SCHED (по умолчанию) проявляется в том, что будет использована дисциплина планирования, определенная в создающем потоке, а любые атрибуты планирования, определенные в вызове pthread_create(), будут проигнорированы. Если используется константа
PTHREAD_EXPLICIT_SCHED, то используются и атрибуты, переданные в вызове pthread_create().

Функция возвращает 0 - при успешном завершении - и любое другое значение - в случае ошибки. Пример вызова этой функции:

#include <pthread.h>

pthread_attr_t tattr;

int ret;

ret = pthread_attr_setinheritsched(&tattr,

      PTHREAD_EXPLICIT_SCHED);

Функцию pthread_attr_getinheritsched(pthread_attr_t *tattr, int *inherit) можно использовать для получения информации о дисциплине планирования текущего потока.

Параметры диспетчеризации определены в структуре
sched_param; в настоящее время поддерживается только приоритет
sched_param.sched_priority. Этот приоритет задается целым числом, при этом чем выше значение, тем выше приоритет потока при планировании. Создаваемые потоки получают этот приоритет.

Функция pthread_attr_setschedparam() используется, чтобы установить значения в этой структуре. При успешном завершении она возвращает 0. Пример использования:

#include <pthread.h>

pthread_attr_t tattr;

int newprio;

sched_param param;

/* устанавливает приоритет */

newprio = 30;

param.sched_priority = newprio;

/* устанавливает параметры диспетчеризации */

ret = pthread_attr_setschedparam (&tattr, &param);

Функция pthread_attr_getschedparam (pthread_attr_t *tattr,

const struct sched_param *param) используется для получения приоритета текущего потока.



2004-06-22

next up previous contents
Next: Создание собственного стека потока Up: Атрибуты потоков Previous: Дисциплина планирования потока   Contents

Размер стека в потоке

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

Если определяется стек, то поток должен создаваться с типом
PTHREAD_CREATE_JOINABLE. Этот стек не может быть освобожден, пока не произойдет выход из pthread_join() этого потока, потому что стек потока не может быть освобожден, пока поток не закончится. Единственный надежный способ закончить такой поток - вызов pthread_join().

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

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



2004-06-22

next up previous contents
Next: Синхронизация потоков Up: Атрибуты потоков Previous: Размер стека в потоке   Contents

Создание собственного стека потока

Обычно создание собственного стека предполагает, что он будет немного отличаться от стека по умолчанию. Как правило, задача состоит в выделении более чем одного мегабайта для стека. Иногда стек по умолчанию, наоборот, является слишком большим. Можно создать тысячи потоков, и тогда виртуальной памяти будет недостаточно, чтобы работать с гигабайтами пространств стека при использовании размера по умолчанию.

Абсолютный минимальный предел размера стека можно определить, вызывая макрос PTHREAD_STACK_MIN (определенный в
<pthread.h>), который возвращает количество памяти стека, требуемого для потока, выполняющего пустую процедуру (NULL). Реальные потоки нуждаются в большем стеке, поэтому нужно очень осторожно сокращать его размер.

Функция pthread_attr_setstacksize() используется для установки размера стека текущего потока.

Атрибут stacksize определяет размер стека в байтах. Этот стек выделяется системой и его размер не должен быть меньше минимального. При успешном завершении функция возвращает 0. Пример вызова:

#include <pthread.h>

pthread_attr_t tattr;

int stacksize;

int ret;

/* установка нового размера */

stacksize = (PTHREAD_STACK_MIN + 0x4000);

ret = pthread_attr_setstacksize(&tattr, stacksize);

Если размер стека равен 0, используется размер по умолчанию.

Функция pthread_attr_getstacksize(pthread_attr_t *tattr, size_t *size) используется для получения размера стека текущего потока:

#include <pthread.h>

pthread_attr_t tattr;

int stacksize;

int ret;

/* получение размера стека */

ret = pthread_attr_getstacksize(&tattr, &stacksize);

Размер стека возвращается в переменную stacksize.

Иногда возникает потребность установить базовый адрес стека. Для этого используется функция pthread_attr_setstackaddr():

int pthread_attr_setstackaddr(pthread_attr_t *tattr,

    void *stackaddr);

Параметр stackaddr определяет базовый адрес стека потока. Если это значение не пусто (не равно NULL), то система инициализирует стек по указанному адресу.

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

#include <pthread.h>

pthread_attr_t tattr;

pthread_t tid;

int ret;

void *stackbase;

int size = PTHREAD_STACK_MIN + 0x4000;

stackbase = (void *) malloc(size);

/* инициализация значениями по умолчанию */

ret = pthread_attr_init(&tattr);

/* установка размера стека */

ret = pthread_attr_setstacksize(&tattr, size);

/* установка базового адреса стека */

ret = pthread_attr_setstackaddr(&tattr, stackbase);

ret = pthread_create(&tid, &tattr, func, arg);

Функция pthread_attr_getstackaddr(pthread_attr_t *tattr,
void* *stackaddr)
используется для получения базового адреса стека текущего потока.



2004-06-22

next up previous contents
Next: Создание процессов с помощью Up: Процессы Previous: Основные сведения о процессах   Contents

Таблица процессов

Системный планировщик использует таблицу процессов, описанную в заголовочном файле /usr/include/linux/sched.h

Внутри структуры struct task_struct находятся все сведения о состоянии процесса. Они достаточно хорошо прокомментированы. Основными являются следующие сведения:



2004-06-22

next up previous contents
Next: Блоки взаимного исключения (мьютексы Up: Потоки (threads) Previous: Создание собственного стека потока   Contents

Синхронизация потоков

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

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

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

Основные ситуации, которые требуют использования синхронизации:



Subsections

2004-06-22

next up previous contents
Next: Инициализация и удаление объекта Up: Синхронизация потоков Previous: Синхронизация потоков   Contents

Блоки взаимного исключения (мьютексы - mutex)

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

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

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



2004-06-22

next up previous contents
Next: Область видимости мьютекса Up: Синхронизация потоков Previous: Блоки взаимного исключения (мьютексы   Contents

Инициализация и удаление объекта атрибутов мьютекса

Функция pthread_mutexattr_init() используется, чтобы инициализировать атрибуты, связанные с объектом, значениями по умолчанию. Память для каждого объекта атрибутов выделяется системой поддержки потоков во время выполнения; mattr - закрытый тип, который содержит системный объект атрибутов. Возможные значения типа mattr - PTHREAD_PROCESS_PRIVATE (по умолчанию) и
PTHREAD_PROCESS_SHARED. При вызове этой функции значение по умолчанию атрибута pshared равно PTHREAD_PROCESS_PRIVATE, что позволяет использовать инициализированный мьютекс в пределах процесса.

Прежде, чем повторно инициализировать объект атрибутов мьютекса, его нужно сначала удалить функцией pthread_mutexattr_ destroy(). Вызов функции pthread_mutexattr_init() возвращает указатель на закрытый объект. Если объект не удалить, может произойти утечка памяти; pthread_mutexattr_init() возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка.

Пример вызова функции:

#include <pthread.h>

pthread_mutexattr_t mattr; 

int ret; 

/* инициализация атрибутов значениями по умолчанию */ 

ret = pthread_mutexattr_init(&mattr);

Функция pthread_mutexattr_destroy() удаляет объект атрибутов, созданный с помощью функцииpthread_mutexattr_init(). Она возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка. Пример вызова:

#include <pthread.h>

pthread_mutexattr_t mattr; 

int ret; 

/* удаление атрибутов */ 

ret = pthread_mutexattr_destroy(&mattr);



2004-06-22

next up previous contents
Next: Инициализация мьютекса Up: Синхронизация потоков Previous: Инициализация и удаление объекта   Contents

Область видимости мьютекса

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

Если мьютекс был создан с атрибутом pshared, установленным в состояние PTHREAD_PROCESS_SHARED, и он находится в разделяемой памяти, то он может быть разделен среди потоков нескольких процессов. Если атрибут pshared у мьютекса установлен в
PTHREAD_PROCESS_PRIVATE, то оперировать этим мьютексом могут только потоки, созданные тем же самым процессом. Функция
pthread_mutexattr_setpshared() возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка. Пример вызова:

#include <pthread.h> 

pthread_mutexattr_t mattr; 

int ret; 

ret = pthread_mutexattr_init(&mattr); 

/* переустановка на значение по умолчанию: private */

ret = pthread_mutexattr_setpshared(&mattr,

      PTHREAD_PROCESS_PRIVATE);

Функция

pthread_mutexattr_getpshared(pthread_mutexattr_t *mattr,

int *pshared)

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

#include <pthread.h>

pthread_mutexattr_t mattr; 

int pshared, ret; 

/* получить атрибут pshared для мьютекса */

ret = pthread_mutexattr_getpshared(&mattr, &pshared);



2004-06-22

next up previous contents
Next: Запирание мьютекса Up: Синхронизация потоков Previous: Область видимости мьютекса   Contents

Инициализация мьютекса

Функция pthread_mutex_init() предназначена для инициализации мьютекса:

int pthread_mutex_init(pthread_mutex_t *mp,

    const pthread_mutexattr_t *mattr);

Здесь мьютекс, указанный mp, инициализируется значением по умолчанию, если mattr равен NULL, или определенными атрибутами, которые уже установлены с помощью pthread_mutexattr_init().

Блокировка через мьютекс не должна повторно инициализироваться или удаляться, пока другие потоки могут его использовать. Если мьютекс инициализируется повторно или удаляется, приложение должно убедиться, что в настоящее время этот мьютекс не используется; pthread_mutex_init() возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка. Пример вызова:

#include <pthread.h>

pthread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER; 

pthread_mutexattr_t mattr; 

int ret; 

/* инициализация мьютекса значением по умолчанию */ 

ret = pthread_mutex_init(&mp, NULL);

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

/* инициализация атрибутов мьютекса по умолчанию*/ 

ret = pthread_mutexattr_init(&mattr);

/* смена значений mattr с помощью функций */

ret = pthread_mutexattr_*();

/* инициализация мьютекса произвольными значениями */

ret = pthread_mutex_init(&mp, &mattr);



2004-06-22

next up previous contents
Next: Захват мьютекса без блокирования Up: Синхронизация потоков Previous: Инициализация мьютекса   Contents

Запирание мьютекса

Функция pthread_mute_lock() используется для запирания мьютекса. Если мьютекс уже закрыт, вызывающий поток блокируется и мьютекс ставится в очередь приоритетов. Когда происходит возврат из pthread_mute_lock(), мьютекс запирается, а вызывающий поток становится его владельцем. pthread_mute_lock() возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка. Пример вызова:

#include <pthread.h> 

pthread_mutex_t mp; 

int ret; 

ret = pthread_mutex_lock(&mp);

Для открытия мьютекса используется функция pthread_mutex_
unlock
().

Мьютекс должен быть закрыт, а вызывающий поток должен быть владельцем, т. е. тем, кто запирал мьютекс. Пока любые другие потоки ждут доступа к мьютексу, поток в начале очереди не блокирован; pthread_mutex_unlock() возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка. Пример вызова:

#include <pthread.h> 

pthread_mutex_t mp; 

int ret; 

ret = pthread_mutex_unlock(&mp);



2004-06-22

next up previous contents
Next: Удаление мьютекса Up: Синхронизация потоков Previous: Запирание мьютекса   Contents

Захват мьютекса без блокирования

Функция pthread_mutex_trylock() пытается провести запирание мьютекса. Она является неблокирующей версией вызова
pthread_mutex_lock(). Если мьютекс уже закрыт, вызов возвращает ошибку. В противном случае, мьютекс закрывается, а вызывающий процесс становится его владельцем; pthread_mutex_trylock() возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка. Пример вызова:

#include <pthread.h> 

pthread_mutex_t mp;

int ret; ret = pthread_ mutex_trylock(&mp);



2004-06-22

next up previous contents
Next: Пример использования мьютексов Up: Синхронизация потоков Previous: Захват мьютекса без блокирования   Contents

Удаление мьютекса

Функция pthread_mutex_destroy() используется для удаления мьютекса в любом состоянии. Память для мьютекса не освобождается; pthread_mutex_destroy() возвращает 0 - после успешного завершения - или другое значение, если произошла ошибка. Пример вызова:

#include <pthread.h> 

pthread_mutex_t mp;

int ret; 

ret = pthread_mutex_destroy(&mp);



2004-06-22

next up previous contents
Next: Иерархическая блокировка Up: Синхронизация потоков Previous: Удаление мьютекса   Contents

Пример использования мьютексов

Функция increment_count() использует мьютекс, чтобы гарантировать атомарность модификации разделяемой переменной count.

Функция get_count() использует мьютекс, чтобы гарантировать, что переменная count атомарно считывается:

#include <pthread.h> 

pthread_mutex_t count_mutex; 

long long count; 

void increment_count() {

  pthread_mutex_lock(&count_mutex); 

  count = count + 1;

  pthread_mutex_unlock(&count_mutex); 

}

 

long long get_count() {

  long long c;

  pthread_mutex_lock(&count_mutex); 

  c = count; 

  pthread_mutex_unlock(&count_mutex);

  return (c); 

}



2004-06-22

next up previous contents
Next: Вложенные блокировки односвязного списка Up: Синхронизация потоков Previous: Пример использования мьютексов   Contents

Иерархическая блокировка

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

В приведенном ниже примере, два потока запирают мьютексы 1 и 2, и тогда тупик при попытке запереть другой мьютекс:

  Поток 1                             Поток 2

/* использует ресурс 1 */  |  /* использует ресурс 2 */

pthread_mutex_lock(&m1);   |  pthread_mutex_lock(&m2);

/* теперь захватывает      |  /* теперь захватывает

ресурсы 2 + 1 */           |   ресурсы 1 + 2 */

pthread_mutex_lock(&m2);   |  pthread_mutex_lock(&m1);

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

Если блокировка всегда выполняется в указанном порядке, тупик не возникнет. Однако, эта техника может использоваться не всегда. Иногда требуется запирать мьютексы в другом порядке, чем предписанный.

Чтобы предотвратить тупик в этой ситуации, лучше использовать функцию pthread_mutex_trylock(). Один из потоков должен освободить свой мьютекс, если он обнаруживает, что может возникнуть тупик.

Ниже проиллюстрирован подход условной блокировки:

Поток 1:

pthread_mutex_lock(&m1); 

pthread_mutex_lock(&m2); 

/* нет обработки */

pthread_mutex_unlock(&m2);

pthread_mutex_unlock(&m1);

Поток 2:

for (; ;) {

pthread_mutex_lock(&m2); 

if(pthread_mutex_trylock(&m1)==0) 

/* захват! */ 

break; 

/* уже заперт */ 

pthread_mutex_unlock(&m2); 

/* нет обработки */

pthread_mutex_unlock(&m1); 

pthread_mutex_unlock(&m2);

В примере, поток 1 запирает мьютексы в нужном порядке, а поток 2 пытается закрыть их по-своему. Чтобы убедиться, что тупик не возникнет, поток 2 должен аккуратно обращаться с мьютексом 1; если поток блокировался, ожидая мьютекс, который будет освобожден, он, вероятно, только что вызвал тупик с потоком 1. Чтобы гарантировать, что это не случится, поток 2 вызывает pthread_mutex_trylock(), который запирает мьютекс, если тот свободен. Если мьютекс уже заперт, поток 2 получает сообщение об ошибке. В этом случае поток 2 должен освободить мьютекс 2, чтобы поток 1 мог запереть его, а затем освободить оба мьютекса.



2004-06-22

next up previous contents
Next: Запуск процессов с помощью Up: Процессы Previous: Таблица процессов   Contents

Создание процессов с помощью вызова fork().

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

#include <sys/types>
#include <unistd.h>
pid_t fork(void);
pid_t является примитивным типом данных, который определяет идентификатор процесса или группы процессов. При вызове fork() порождается новый процесс (процесс-потомок), который почти идентичен порождающему процессу-родителю. Процесс-потомок наследует следующие признаки родителя:

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

Процесс-потомок и процесс-родитель получают разные коды возврата после вызова fork(). Процесс-родитель получает идентификатор (PID) потомка. Если это значение будет отрицательным, следовательно при порождении процесса произошла ошибка. Процесс-потомок получает в качестве кода возврата значение 0, если вызов fork() оказался успешным.

Таким образом, можно проверить, был ли создан новый процесс:

switch(ret=fork())
{
  case -1: /{*}при вызове fork() возникла ошибка{*}/
  case 0 : /{*}это код потомка{*}/
  default : /{*}это код родительского процесса{*}/
}
Пример порождения процесса через fork() приведен ниже:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
main()
{
  pid_t pid;
  int rv;
  switch(pid=fork()) {
  case -1:
          perror("fork"); /* произошла ошибка */
          exit(1); /*выход из родительского процесса*/
  case 0:
          printf(" CHILD: Это процесс-потомок!\n");
          printf(" CHILD: Мой PID -- %d\n", getpid());
          printf(" CHILD: PID моего родителя -- %d\n",
              getppid());
          printf(" CHILD: Введите мой код возврата 
                         (как можно меньше):");
          scanf(" %d");
          printf(" CHILD: Выход!\n");
          exit(rv);
  default:
          printf("PARENT: Это процесс-родитель!\n");
          printf("PARENT: Мой PID -- %d\n", getpid());
          printf("PARENT: PID моего потомка %d\n",pid);
          printf("PARENT: Я жду, пока потомок
                          не вызовет exit()...\n");
          wait();
          printf("PARENT: Код возврата потомка:%d\n",
                   WEXITSTATUS(rv));
          printf("PARENT: Выход!\n");
  }
}

Когда потомок вызывает exit(), код возврата передается родителю, который ожидает его, вызывая wait(). WEXITSTATUS() представляет собой макрос, который получает фактический код возврата потомка из вызова wait().

Функция wait() ждет завершения первого из всех возможных потомков родительского процесса. Иногда необходимо точно определить, какой из потомков должен завершиться. Для этого используется вызов waitpid() с соответствующим PID потомка в качестве аргумента. Еще один момент, на который следует обратить внимание при анализе примера, это то, что и родитель, и потомок используют переменную rv. Это не означает, что переменная разделена между процессами. Каждый процесс содержит собственные копии всех переменных.

Рассмотрим следующий пример:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
   char pid{[}255{]};
   fork();
   fork();
   fork();
   sprintf(pid, "PID : %d\n",getpid());
   write(STDOUT_FILENO, pid, strlen(pid));
   exit(0);
}

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

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



Subsections

2004-06-22

next up previous contents
Next: Переменные состояния Up: Синхронизация потоков Previous: Иерархическая блокировка   Contents

Вложенные блокировки односвязного списка

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

Структура для блокировки имеет вид:

typedef struct node1 { 

  int value; 

  struct node1 *link;

  pthread_mutex_t lock; 

} node1_t;

Теперь можно создать переменную для списка блокировок node1_t ListHead.

Чтобы удалить узел из списка, необходимо выполнить следующие действия:

Код для удаления элемента из списка с вложенной блокировкой:

node1_t *delete(int value) {

  node1_t *prev,

  *current; prev = &ListHead;

  pthread_mutex_lock(&prev->lock); 

  while ((current = prev->link) != NULL) {

      pthread_mutex_lock(&current->lock); 

      if (current->value == value) {

         prev->link = current->link;

         pthread_mutex_unlock(&current->lock); 

         pthread_mutex_unlock(&prev->lock);

         current->link = NULL;

         return(current); 

      } 

      pthread_mutex_unlock(&prev->lock);

      prev = current; 

  } 

  pthread_mutex_unlock(&prev->lock); 

  return(NULL); 

}



2004-06-22

next up previous contents
Next: Работа с атрибутами переменных Up: Потоки (threads) Previous: Вложенные блокировки односвязного списка   Contents

Переменные состояния

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

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



Subsections

2004-06-22

next up previous contents
Next: Видимость переменной состояния Up: Переменные состояния Previous: Переменные состояния   Contents

Работа с атрибутами переменных состояния

Функция pthread_condattr_init() инициализирует атрибуты, связанные с объектом значениями по умолчанию. Память для каждого объекта атрибутов cattr выделяется системой потоков в процессе выполнения; cattr является закрытым типом данных, который содержит созданный системой объект атрибутов. Возможные значения признаков видимости cattr - PTHREAD_PROCESS_PRIVATE и
PTHREAD_PROCESS_SHARED. Значение по умолчанию атрибута pshared, равное PTHREAD_PROCESS_PRIVATE, указывает, что инициализированная переменная состояния может использоваться в пределах процесса.

Прежде чем атрибут переменной состояния сможет использоваться повторно, он должен повторно инициализироваться функцией pthread_condattr_destroy(). Вызов pthread_condattr_init() возвращает указатель на закрытый объект. Если объект не будет удален, возникнет утечка памяти; pthread_condattr_init() возвращает 0 - после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример вызова функции:

#include <pthread.h>

pthread_condattr_t cattr; 

int ret; 

ret = pthread_condattr_init(&cattr);

Функция pthread_condattr_destroy() удаляет память и помечает недействительным объект атрибутов:

int pthread_condattr_destroy (pthread_condattr_t *cattr);
pthread_condattr_destroy() возвращает 0 - после успешного завершения. Любое другое значение указывает, что произошла ошибка.



2004-06-22

next up previous contents
Next: Инициализация переменной состояния Up: Переменные состояния Previous: Работа с атрибутами переменных   Contents

Видимость переменной состояния

Областью видимости переменной состояния может быть либо процесс, либо вся система, как и для мьютексов. Если переменная состояния создана с атрибутом pshared, установленным в состояние PTHREAD_PROCESS_SHARED, и она находится в разделяемой памяти, то эта переменная может разделяться среди потоков нескольких процессов. Если же атрибут pshared установлен в значение
PTHREAD_PROCESS_PRIVATE (по умолчанию), то лишь потоки, созданные тем же самым процессом, могут оперировать этой переменной.

Функция pthread_condattr_setpshared() используется, чтобы
установить область видимости переменной состояния. Она возвращает 0 - после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример использования функции:

#include <pthread.h> 

pthread_condattr_t cattr; 

int ret; 

/* Область видимости - все процессы */ 

ret = pthread_condattr_setpshared(&cattr,

      PTHREAD_PROCESS_SHARED); 

/* Внутренняя переменная для процесса */ 

ret = pthread_condattr_setpshared(&cattr,

      PTHREAD_PROCESS_PRIVATE);

Функция
int pthread_condattr_getpshared(
    const pthread_condattr_t *cattr, int *pshared)
используется для получения области видимости переменной состояния.



2004-06-22

next up previous contents
Next: Блокировка через переменную состояния Up: Переменные состояния Previous: Видимость переменной состояния   Contents

Инициализация переменной состояния

Функция pthread_cond_init() инициализирует переменную состояния:

int pthread_cond_init (pthread_cond_t *cv,

     const pthread_condattr_t *cattr);

Инициализируемая переменная состояния указана cv и устанавливается в значение по умолчанию, если cattr равен NULL, или на определенные cattr атрибуты, которые уже установлены через вызов pthread_condattr_init().

Статические переменные состояния могут инициализироваться непосредственно значениями по умолчанию с помощью макроса
PTHREAD_COND_INITIALIZER. Несколько потоков не должны одновременно инициализировать или повторно инициализировать ту же самую переменную состояния. Если переменная состояния повторно инициализируется или удаляется, приложение должно убедиться, что эта переменная состояния больше не используется;
pthread_cond_init() возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример использования функции:

#include <pthread.h> 

pthread_cond_t cv; 

pthread_condattr_t cattr; 

int ret; 

/* инициализация значениями по умолчанию */ 

ret = pthread_cond_init(&cv, NULL); 

/* инициализация определенными значениями */

ret = pthread_cond_init(&cv, &cattr);



2004-06-22

next up previous contents
Next: Сокеты Up: Переменные состояния Previous: Инициализация переменной состояния   Contents

Блокировка через переменную состояния

Функция pthread_cond_wait() используется, чтобы атомарно освободить мьютекс и заставить вызывающий поток блокироваться по переменной состояния. Функция pthread_cond_wait() возвращает 0 - после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример использования функции:

#include <pthread.h> 

pthread_cond_t cv; 

pthread_mutex_t mutex; 

int ret; 

ret = pthread_cond_wait(&cv, &mutex);

Блокированный поток пробуждается с помощью вызовов
pthread_cond_signal(), pthread_cond_broadcast(), или может быть прерван соответствующим сигналом. Любое изменение состояния, связанного с его переменной, не может быть вызвано возвратом из pthread_cond_wait(), и любое такое состояние должно быть перепроверено. Процедура pthread_cond_wait() всегда возвращает запертый мьютекс, который принадлежит вызывающему потоку, даже если возникла ошибка. Эта функция блокируется, пока не придет сообщение о нужном состоянии. Она атомарно освобождает связанный с ней закрытый мьютекс перед блокированием, и атомарно захватывает его снова, перед возвратом.

Проверка состояния обычно проводится в цикле while, который вызывает pthread_cond_wait():

pthread_mutex_lock();

while(condition_is_false) 

  pthread_cond_wait(); 

pthread_mutex_unlock();

Чтобы разблокировать определенный поток, используется функция
pthread_cond_signal():

int pthread_cond_signal(pthread_cond_t *cv);
Она разблокирует поток, заблокированный переменной состояния cv. Функция pthread_cond_signal() возвращает 0 - после успешного завершения. Любое другое значение указывает, что произошла ошибка.

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

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

pthread_mutex_t count_lock; 

pthread_cond_t count_nonzero; 

unsigned count; 

decrement_count() {

  pthread_mutex_lock(&count_lock); 

  while (count == 0) 

    pthread_cond_wait(&count_nonzero, &count_lock); 

  count = count - 1; 

  pthread_mutex_unlock(&count_lock); 

}

 

increment_count() {

  pthread_mutex_lock(&count_lock); 

  if (count == 0)

    pthread_cond_signal(&count_nonzero); 

  count = count + 1;

  pthread_mutex_unlock(&count_lock); 

}

Можно также блокировать поток до наступления определенного события. Для этих целей используется функция
pthread_cond_timedwait():

int pthread_cond_timedwait(pthread_cond_t *cv,

    pthread_mutex_t *mp,

    const struct timespec *abstime);

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

#include <pthread.h> 

#include <time.h> 

pthread_timestruc_t to;

pthread_cond_t cv; 

pthread_mutex_t mp;

timestruct_t abstime; 

int ret; 

/* ожидание переменной состояния */ 

ret = pthread_cond_timedwait(&cv, &mp, &abstime);

pthread_mutex_lock(&m); 

to.tv_sec = time(NULL) + TIMEOUT; 

to.tv_nsec = 0; 

while (cond == FALSE) {

  err = pthread_cond_timedwait(&c, &m, &to); 

  if (err == ETIMEDOUT) {

     /* таймаут */ 

     break;

  } 

pthread_mutex_unlock(&m);

Все блокированные потоки можно разблокировать функцией
pthread_cond_broadcast():

int pthread_cond_broadcast(pthread_cond_t *cv);
pthread_cond_broadcast() разблокирует все потоки, блокированные переменной состояния, на которую указывает cv, определенная
pthread_cond_wait(). Если ни один поток не блокирован этой переменной состояния, вызов pthread_cond_broadcast() не будет иметь никакого эффекта.

pthread_cond_broadcast() возвращает 0 - после успешного завершения - или любое другое значение в случае ошибки.

Поскольку pthread_cond_broadcast() заставляет все потоки, блокированные некоторым состоянием, бороться за мьютекс, ее нужно использовать аккуратно. Например, можно использовать
pthread_cond_broadcast(), чтобы позволить потокам бороться за изменение количества требуемых ресурсов, когда ресурсы освобождаются:

#include <pthread.h>

pthread_mutex_t rsrc_lock; 

pthread_cond_t rsrc_add;

unsigned int resources; 

get_resources(int amount) {

  pthread_mutex_lock(&rsrc_lock);

  while (resources < amount) 

    pthread_cond_wait(&rsrc_add, &rsrc_lock); 

  resources -= amount; 

  pthread_mutex_unlock(&rsrc_lock); 

}

 

add_resources(int amount) {

  pthread_mutex_lock(&rsrc_lock); 

  resources += amount;

  pthread_cond_broadcast(&rsrc_add); 

  pthread_mutex_unlock(&rsrc_lock); 

}

Функция pthread_cond_destroy() используется для удаления состояния, ассоциированного с переменной состояния:

#include <pthread.h> 

pthread_cond_t cv; 

int ret; 

/* Переменная состояния удалена */ 

ret = pthread_cond_destroy(&cv);

pthread_cond_destroy() возвращает 0 - после успешного завершения - или любое другое значение в случае ошибки.


next up previous contents
Next: Сокеты Up: Переменные состояния Previous: Инициализация переменной состояния   Contents
2004-06-22

next up previous contents
Next: Общие сведения о сокетах Up: Локальные и удаленные средства Previous: Блокировка через переменную состояния   Contents

Сокеты



Subsections

2004-06-22

next up previous contents
Next: Создание и именование сокетов Up: Сокеты Previous: Сокеты   Contents

Общие сведения о сокетах

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

Сокеты находятся в областях связи (доменах). Домен сокета - это абстракция, которая определяет структуру адресации и набор протоколов. Сокеты могут соединяться только с сокетами в том же домене. Всего выделено 23 класса сокетов (см. файл <sys/socket.h>), из которых обычно используются только UNIX-сокеты и Интернет-сокеты. Сокеты могут использоваться для установки связи между процессами на отдельной системе подобно другим формам IPC.

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

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

Поточный
- обеспечивает двухсторонний, последовательный, надежный, и недублированный поток данных без определенных границ. Тип сокета - SOCK_STREAM, в домене Интернета он использует протокол TCP.
Датаграммный
- поддерживает двухсторонний поток сообщений. Приложение, использующее такие сокеты, может получать сообщения в порядке, отличном от последовательности, в которой эти сообщения посылались. Тип сокета - SOCK_DGRAM, в домене Интернета он использует протокол UDP.
Сокет
последовательных пакетов - обеспечивает двухсторонний, последовательный, надежный обмен датаграммами фиксированной максимальной длины. Тип сокета - SOCK_SEQPACKET. Для этого типа сокета не существует специального протокола.
Простой
сокет - обеспечивает доступ к основным протоколам связи.
Все сокеты обычно ориентированы на применение датаграмм, но их точные характеристики зависят от интерфейса, обеспечиваемого протоколом. Обмен между сокетами происходит по следующей схеме:

Сервер   Клиент
Установка сокета socket()   Установка сокета socket()
$\Downarrow $   $\Downarrow $
Присвоение имени bind()   $\Downarrow $
$\Downarrow $   $\Downarrow $
Установка очереди запросов listen()   $\Downarrow $
$\Downarrow $   $\Downarrow $
Выбор соединения из очереди accept() $\Longleftrightarrow $ Установка соединения connect()
$\Downarrow $   $\Downarrow $
read() $\Longleftarrow $ write()
$\Downarrow $   $\Downarrow $
write() $\Longrightarrow $ read()


next up previous contents
Next: Создание и именование сокетов Up: Сокеты Previous: Сокеты   Contents
2004-06-22

next up previous contents
Next: Соединение сокетов Up: Сокеты Previous: Общие сведения о сокетах   Contents

Создание и именование сокетов

Для создания сокета определенного типа в определенном адресном пространстве используется функция socket():

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

Если протокол не определен, система сама определяет протокол по умолчанию, который поддерживает указанный тип сокета. Функция возвращает дескриптор сокета. Адресное пространство domain обычно принимает значения AF_UNIX или AF_INET, а тип сокета - SOCK_STREAM или SOCK_DGRAM. Если сокет не может быть создан, то функция вернет -1.

Удаленный процесс не может идентифицировать определенный сокет, пока ему не будет присвоен адрес. Процессы могут поддерживать связь только через адреса. В пространстве адресов UNIX соединение обычно определяется одним или двумя именами файлов. В пространстве адресов Интернета соединение определяется локальным и удаленным адресами и номерами портов.

Функция bind():

#include <sys/types.h>

#include <sys/socket.h>

int bind(int s, const struct sockaddr *name,

    int namelen);

осуществляет привязку пути к файлу или адреса Интернета к сокету. Первым аргументом является дескриптор соответствующего сокета, полученный ранее с помощью socket(). Вторым аргументом является структура, представляющая собой адрес. Она имеет вид:

struct sockaddr {

  ushort_t sa_family; /* семейство адресов */

  char sa_data[ 14]; /* 14 байт прямого адреса */

};

Различные варианты сокетов могут использовать различные структуры для адреса. Например, UNIX-сокет можно описать с помощью структуры:

struct sockaddr_un {

  ushort_t sun_family; /* AF_ UNIX */

  char sun_path[ 104]; /* путь к файлу */

};

Для сокета в пространстве адресов Интернет используется структура:

struct sockaddr_ in {

  uchar_t sin_len;

  sa_family_t sin_family; /* AF_ INET */ 

  in_port_t sin_port; /* 16-битный порт */

  struct in_addr sin_addr; /* Указатель на адрес */

  uchar_t sin_zero[ 8]; /* зарезервировано */ 

};



2004-06-22

next up previous contents
Next: Обмен данными через сокеты Up: Сокеты Previous: Создание и именование сокетов   Contents

Соединение сокетов

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

#include <sys/types.h>

#include <sys/socket.h>

int listen (int socket, int backlog );

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

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

#include <sys/types.h>

#include <sys/socket.h>

int accept( int socket, struct sockaddr *addr,

    int *addrlen );

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

Функция connect() используется процессом-клиентом для установления связи с сервером:

#include <sys/types.h>

#include <sys/socket.h>

int connect( int socket, struct sockaddr *name,

    int namelength );

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



2004-06-22

next up previous contents
Next: Сигналы Up: Создание процессов с помощью Previous: Создание процессов с помощью   Contents

Запуск процессов с помощью вызова exec()

Функция exec() (execute) загружает и запускает другую программу. Таким образом, новая программа полностью замещает текущий процесс. Новая программа начинает свое выполнение с функции main. Все файлы вызывающей программы остаются открытыми. Они также являются доступными новой программе. Используется шесть различных вариантов функций exec.

#include <unistd.h>
int execl(char *name, char *arg0, ... /*NULL*/);
int execv(char *name, char *argv[]);
int execle(char *name, char *arg0, ... /*,NULL,
           char *envp[]*/);
int execve(char *name, char *arv[], char *envp[]);
int execlp(char *name, char *arg0, ... /*NULL*/);
int execvp(char *name, char *argv[]);
Вызов exec происходит таким образом, что переданная в качестве аргумента программа загружается в память вместо старой, которая вызвала exec. Старой программе больше не доступны сегменты памяти, которые перезаписаны новой программой.

Суффиксы l, v, p, e в именах функций определяют формат и объем аргументов, а также каталоги, в которых нужно искать загружаемую программу:

Начнем с примера для execl(). Пусть используется следующая программа:

#include <stdio.h>
int main(int argc, char *argv[])
{
  int i=0;
  printf("%s\n",argv[0]);
  printf("Программа запущена и получила строку : ");
  while(argv[++i] != NULL)
  printf("%s ",argv[i]);
  return 0;
}
Эта программа выводит на экран строку, переданную ей в качестве аргумента. Пусть она называется hello. Она будет вызвана из другой программы с помощью функции execl(). Код вызывающей программы приведен ниже :

#include <stdio.h>
#include <unistd.h>
int main(int argc, int *argv[])
{
  printf("Будет выполнена программа %s...\n\n",
         argv[0]);
  printf("Выполняется %s", argv[0]);
  execl("hello"," ","Hello", "World!", NULL);
  return 0;
}
В строке execl() аргументы указаны в виде списка. Доступ к ним также осуществляется последовательно. Если использовать функцию execv(), то вместо списка будет указан вектор аргументов:

#include <stdio.h>
#include <unistd.h>
int main(int argc, int *argv[])
{
  printf("Программа %s будет выполнена...\n\n",
         argv[0]);
  printf("Выполняется %s", argv[0]);
  execv("hello",argv);
  return 0;
}



2004-06-22

next up previous contents
Next: Закрытие сокетов Up: Сокеты Previous: Соединение сокетов   Contents

Обмен данными через сокеты

Для обмена данными существуют две группы функций - для записи в сокет и для чтения из него. Функции для записи имеют вид:

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/uio.h>

int send( int socket, const char *msg, int len,

    int flags);

int sendto( int socket, const char *msg, int len,

    int flags, const struct sockaddr *to, int tolen );

int sendmsg( int socket, const struct msghdr *msg,

    int flags );

Аргумент socket определяет дескриптор сокета, в который записываются данные. Аргументы msg и len определяют, соответственно, адрес и длину буфера с записываемыми данными. В функции sendmsg() длина данных определяется автоматически, по структуре сообщения. Параметр flags содержит комбинацию битовых флагов, управляющих режимами записи. Если аргумент flags равен нулю, то запись в сокет (и соответственно - считывание) происходит в порядке поступления байтов. Если значение flags определено как MSG_OOB, то записываемые данные передаются потребителю вне очереди. Все функции возвращают число записанных в сокет байтов ( в нормальном случае оно должно быть равно значению параметра len или -1, в случае ошибки). Отметим, что запись в сокет не означает, что данные приняты на другом конце связи процессом-потребителем.

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

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/uio.h>

int recv( int socket, char *buffer, int len, int

    flags);

int recvfrom( int socket, char *buffer, int len,

    int flags, const struct sockaddr *from,

    int fromlen );

int recvmsg( int socket, const struct msghdr *msg,

    int flags );

Функции чтения и записи в сокет выполняются асинхронно. Первый аргумент функций - это дескриптор сокета, из которого читаются данные. Второй и третий аргументы (buffer и len соответственно) - адрес и длина буфера для записи читаемых данных. Четвертый аргумент - это комбинация битовых флагов, управляющих режимами чтения. Если аргумент flags равен нулю, то считанные данные удаляются из сокета. Если значение flags установлено в MSG_PEEK, то данные не удаляются и могут быть считаны последущим вызовом (или вызовами) функций чтения. Функция возвращает число считанных байтов или -1, в случае ошибки. Следует отметить, что нулевое значение не является ошибкой. Оно сигнализирует об отсутствии данных, записанных в сокет процессом-поставщиком.



2004-06-22

next up previous contents
Next: Приложение архитектуры клиент-сервер с Up: Сокеты Previous: Обмен данными через сокеты   Contents

Закрытие сокетов

Функция shutdown() используется для немедленного закрытия всех или некоторых связей для сокета:

#include <sys/socket.h>

#include <sys/uio.h>

int shutdown(int s, int how);

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

Функция close() закрывает сокет и разрывает все связи с ним. В отличие от функции shutdown() функция close может дожидаться окончания всех операций с сокетом, обеспечивая "нормальное", а не аварийное закрытие соединений.

#include <sys/socket.h>

#include <sys/uio.h>

int close (int s);

Аргумент функции - дескриптор закрываемого сокета.



2004-06-22

next up previous contents
Next: Удаленный вызов процедур Up: Сокеты Previous: Закрытие сокетов   Contents

Приложение архитектуры клиент-сервер с использованием сокетов

Пример-оболочка программы "Клиент":

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/un.h>

#include <stdio.h>

#define ADDRESS "mysocket" /* адрес для связи */

 

void main () {

  char c;

  int i, s, len;

  FILE *fp;

  struct sockaddr_un sa;

 

  /* получаем свой сокет-дескриптор: */

  if ((s = socket (AF_UNIX, SOCK_STREAM, 0))<0) {

    perror ("client: socket"); exit(1);

  }

  /* создаем адрес, по которому

   будем связываться с сервером: */

  sa.sun_family = AF_UNIX;

  strcpy (sa.sun_path, ADDRESS);

  /* пытаемся связаться с сервером: */

  len = sizeof ( sa.sun_family) + strlen ( sa.sun_path);

  if ( connect ( s, &sa, len) < 0 ){

    perror ("client: connect"); exit (1);

  }

  /* читаем сообщения сервера */

  fp = fdopen (s, "r");

  c = fgetc (fp);

  /* обрабатываем информацию от сервера

  ...................................

  */

  /* посылаем ответ серверу */

  send (s, "client", 7, 0);

  /* продолжаем диалог с сервером, пока в этом

  есть необходимость.....

  */

  /* завершаем сеанс работы */

  close (s);

  exit (0);

}

Пример-оболочка программы "Сервер":

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/un.h>

#include <stdio.h>

#define ADDRESS "mysocket" /* адрес для связи */

 

void main ()

{

  char c;

  int i, d, d1, len, ca_len;

  FILE *fp;

  struct sockaddr_un sa, ca;

 

  /* получаем свой сокет-дескриптор: */

  if((d = socket (AF_UNIX, SOCK_STREAM, 0)) < 0) {

    perror ("client: socket"); exit (1);

  }

 

  /* создаем адрес, c которым будут

  связываться клиенты */

  sa.sun_family = AF_UNIX;

  strcpy (sa.sun_path, ADDRESS);

  /* связываем адрес с сокетом;

  уничтожаем файл с именем ADDRESS,

  если он существует, для того, чтобы

  вызов bind завершился успешно */

  unlink (ADDRESS);

  len = sizeof ( sa.sun_family) + strlen (sa.sun_path);

  if ( bind ( d, &sa, len) < 0 ) {

     perror ("server: bind"); exit (1);

  }

 

  /* слушаем запросы на сокет */

  if ( listen ( d, 5) < 0 ) {

  perror ("server: listen"); exit (1);

  }

 

  /* связываемся с клиентом через неименованный сокет

    с дескриптором d1:*/

  if (( d1 = accept ( d, &ca, &ca_len)) < 0 ) {

    perror ("server: accept"); exit (1);

  }

 

  /* пишем клиенту: */

  send (d1, "server", 7, 0);

  /* читаем запрос клиента */

  fp = fdopen (d1, "r");

  c = fgetc (fp);

  /* ................................ */

  /* обрабатываем запрос клиента, посылаем ответ и т.д.

   ........................... */

  /* завершаем сеанс работы */

  close (d1);

  exit (0);

}


next up previous contents
Next: Удаленный вызов процедур Up: Сокеты Previous: Закрытие сокетов   Contents
2004-06-22

next up previous contents
Next: Общие сведения Up: Локальные и удаленные средства Previous: Приложение архитектуры клиент-сервер с   Contents

Удаленный вызов процедур



Subsections

2004-06-22

next up previous contents
Next: Разработка протокола взаимодействия Up: Удаленный вызов процедур Previous: Удаленный вызов процедур   Contents

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

Рассмотренные ранее методы синхронизации процессов и коммуникаций предполагали использование одного компьютера. Тем не менее часто приложения должны работать в пределах локальной или распределенной сети. Одним из методов реализации взаимодействия является удаленный вызов процедур (remote procedure calls - RPC). Вызов процедуры представляет собой классическую форму синхронной коммуникации: вызывающий процесс передает управление подпроцессу и ждет возвращения результатов. Используя RPC, программисты распределенных приложений могут не учитывать мелких деталей при обеспечении интерфейса с сетью. Транспортная независимость RPC изолирует приложение от физических и логических элементов механизма коммуникаций данных и позволяет ему использовать разнообразие транспортных протоколов.

RPC делает модель вычислений ``клиент - сервер'' более мощной и более простой для программирования. Использование компиляторов протоколов ONC RPCGEN позволяет клиентам прозрачно осуществлять удаленные вызовы через локальный интерфейс процедур.

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

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

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

Для разработки приложения RPC необходимо выполнить следующие шаги:



2004-06-22

next up previous contents
Next: Порядок компиляции приложения архитектуры Up: Удаленный вызов процедур Previous: Общие сведения   Contents

Разработка протокола взаимодействия

Самый простой способ определения и реализации протокола состоит в том, чтобы использовать компилятор протоколов rpcgen. Для создания протокола нужно идентифицировать имена сервисных процедур и типы данных для возвращаемых аргументов и параметров. Компилятор протокола считывает определения и автоматически создает коды для сервера и клиента; rpcgen использует собственный язык (язык RPC или RPCL), который очень похож на язык директив препроцессора С; rpcgen реализован в виде автономного компилятора, который работает со специальными файлами, обозначенными расширением .x.

Для обработки файла RPCL необходимо выполнить команду rpcgen~rpcprog.x~

При этом будут созданы четыре файла:

rpcprog_clnt.c - процедуры клиента;

rpcprog_svc.c - процедуры сервера;

rpcprog_xdr.c - фильтры XDR;

rpcprog.h - файл заголовка, необходимый для XDR фильтров.

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



2004-06-22

next up previous contents
Next: Интерфейсные процедуры RPC Up: Удаленный вызов процедур Previous: Разработка протокола взаимодействия   Contents

Порядок компиляции приложения архитектуры клиент-сервер

Пусть программа клиента называется rpcprog.c, а программа сервера - rpcsvc.c. Протокол определен в файле rpcprog.x. Этот файл обработан rpcgen, чтобы создать файлы фильтров и процедур: rpcprog_clnt.c, rpcprog_svc.c, rpcprog_xdr.c, rpcprog.h.

Программы клиента и сервера должны включать строку
#include"rpcprog.h"

После этого необходимо:

откомпилировать код клиента:
cc -c rpcprog.c

откомпилировать специальную клиентскую часть:
cc -c rpcprog_clnt.c

откомпилировать фильтр XDR:
cc -c rpcprog_xdr.c

построить выполняемый файл клиента:
cc -o rpcprog rpcprog.o rpcprog_clnt.o rpcprog_xdr.c

откомпилировать серверные процедуры:
cc -c rpcsvc.c

откомпилировать специальную серверную часть:
cc -c rpcprog_svc.c

построить выполняемый файл сервера:
cc -o rpcsvc rpcsvc.o rpcprog_svc.o rpcprog_xdr.c

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



2004-06-22

next up previous contents
Next: Упрощенный интерфейс RPC Up: Удаленный вызов процедур Previous: Порядок компиляции приложения архитектуры   Contents

Интерфейсные процедуры RPC

Здесь перечислены все процедуры RPC для всех уровней протокола удаленного вызова:

rpc_reg()
- регистрирует процедуру для использования программами RPC для всех транспортных служб указанного типа;
rpc_call()
- удаленный вызов указанной процедуры на указанном удаленном компьютере;
rpc_broadcast()
- передает сообщение вызова широковещательно для всех транспортных служб указанного типа;
clnt_create()
- обобщенное создание клиента. Программа сообщает clnt_create(), где расположен сервер и указывает тип используемого транспортного протокола;
clnt_create_timed()
- похожа на clnt_create(), но позволяет программисту определить максимальное время, допустимое для каждого типа транспортного протокола, который используется в течение попытки создания;
svc_create()
- создает дескрипторы сервера для всех транспортных служб указанного типа. Программа сообщает svc_create(), какую функцию диспетчера нужно использовать;
clnt_call()
- клиент вызывает эту процедуру, чтобы послать запрос серверу;
clnt_tp_create()
- создает дескриптор клиента для указанного
транспортного протокола;
clnt_tp_create_timed()
- подобна clnt_tp_create(), но позволяет программисту определять максимальное допустимое время;
svc_tp_create()
- создает дескриптор сервера для указанного
транспортного протокола;
clnt_tli_create()
- создает дескриптор клиента для указанного транспортного протокола;
svc_tli_create()
- создает дескриптор сервера для указанного
транспортного протокола;
rpcb_set()
- вызывает rpcbind, чтобы установить отображение между службой RPC и сетевым адресом;
rpcb_unset()
- удаляет отображение, установленное rpcb_set();
rpcb_getaddr()
- вызывает rpcbind, чтобы получить транспортные адреса указанных служб RPC;
svc_reg()
- связывает указанную программу и пару номера версии с указанной процедурой диспетчера;
svc_unreg()
- удаляет ассоциацию, установленную svc_reg();
clnt_dg_create()
- создает клиента RPC для указанной удаленной программы, используя транспортный протокол датаграмм;
svc_dg_create()
- создает дескриптор сервера RPC, используя транспортный протокол датаграмм;
clnt_vc_create()
- создает дескриптор клиента RPC для указанной удаленной программы, используя транспортный протокол вирутального канала;
svc_vc_create()
- создает дескриптор сервера RPC, используя транспортный протокол виртуального канала;



2004-06-22

next up previous contents
Next: Пример rusers.c Up: Удаленный вызов процедур Previous: Интерфейсные процедуры RPC   Contents

Упрощенный интерфейс RPC

Упрощенный интерфейс - это самый простой уровень использования RPC, потому что он не требует использования других процедур RPC. Он также ограничивает контроль над основными механизмами коммуникации. Разработка программ для этого уровня может осуществляться очень быстро и непосредственно поддерживается компилятором rpcgen. Для большинства приложений достаточно возможностей rpcgen. Некоторые службы RPC не доступны в виде функций C, но они доступны как программы RPC. Процедуры библиотеки упрощенного интерфейса обеспечивают прямой доступ к возможностям RPC для программ, которые не требуют детального управления.

Все процедуры находятся в библиотеке служб RPC librpcsvc.



Subsections

2004-06-22

next up previous contents
Next: Клиентская часть Up: Упрощенный интерфейс RPC Previous: Упрощенный интерфейс RPC   Contents

Пример rusers.c

Пример rusers.c, приведенный ниже, показывает число пользователей на удаленном компьютере. Он вызывает процедуру rusers из библиотеки RPC:

#include <rpc/rpc.h> 

#include <rpcsvc/rusers.h>

#include <stdio.h>

 

/*

* программа вызывает службу rusers()

*/

 

main(int argc,char **argv)

{

  int num;

  if (argc != 2) {

    fprintf(stderr, "Использование: %s hostname\n",

    argv[0]);

    exit(1);

  }

  if ((num = rnusers(argv[1])) < 0) {

    fprintf(stderr, "Ошибка вызова: rusers\n");

    exit(1);

  }

  fprintf(stderr, "%d пользователей на %s\n", num,

    argv[1] );

  exit(0);

}



2004-06-22

next up previous contents
Next: Понятие о сигналах Up: Локальные и удаленные средства Previous: Запуск процессов с помощью   Contents

Сигналы



Subsections

2004-06-22

next up previous contents
Next: Серверная часть Up: Упрощенный интерфейс RPC Previous: Пример rusers.c   Contents

Клиентская часть

Клиентская часть состоит из вызова функции rpc_call():

#include <stdio.h>

#include <utmp.h> 

#include <rpc/rpc.h>

#include <rpcsvc/rusers.h>

 

/* программа вызывает удаленную программу RUSERSPROG */

 

main(int argc, char **argv)

{

  unsigned long nusers;

  enum clnt_stat cs;

  if (argc != 2) {

    fprintf(stderr, "Использование: rusers hostname\n");

    exit(1);

  }

  if( cs = rpc_call(argv[1], RUSERSPROG,

      RUSERSVERS, RUSERSPROC_NUM, xdr_void,

      (char *)0, xdr_u_long, (char *)&nusers,

      "visible") != RPC_SUCCESS ) {

         clnt_perrno(cs);

         exit(1);

  }

  fprintf(stderr, "%d пользователей на компьютере %s\n",

      nusers, argv[1] );

  exit(0);

}

Синтаксис функции rpc_call() приведен ниже:

int rpc_call (

     /* Имя сервера */

     char *host,

      /* Номер программы сервера */

     u_long prognum,

      /* Номер версии сервера */

     u_long versnum,

      /* фильтр XDR для кодирования arg */

     xdrproc_t inproc,

      /* Указатель на аргументы */

     char *in,

      /* Фильтр декодирования результата */

     xdr_proc_t outproc,

      /* Адрес сохранения результата */

     char *out,

      /* Выбор транспортной службы */)

     char *nettype

  );

Эта функция вызывает процедуру, указанную prognum, versnum, и procnum на нужном компьютере, указанном host. Аргументы, передаваемые удаленной процедуре, указывают параметром in, а inproc указывает фильтр XDR для кодирования этих аргументов. Параметр out - это адрес, куда помещается результат удаленной процедуры; outproc представляет фильтр XDR, который расшифрует результат и разместит его по этому адресу.

Клиент блокируется вызовом rpc_call() до тех пор, пока он не получит ответ от сервера. Если сервер отвечает, то возвращается RPC_SUCCESS со значением 0. Если запрос был неудачен, возвращается значение, отличное от 0. Это значение можно преобразовать к типу clnt_stat - перечислимому типу, определенному в файле RPC (<rpc/rpc.h>) и интерпретируемому функцией clnt_sperrno(). Эта функция возвращает указатель на стандартное сообщение RPC об ошибке, соответствующее коду ошибки. В примере испытываются все "видимые" транспортные службы, внесенные в /etc/netconfig. Настройка количества повторов требует использования более низких уровней библиотеки RPC. Множественные аргументы и результаты обрабатываются с помощью объединения их в структуры.

Поскольку типы данных могут быть представлены на различных машинах различным образом, то для rpc_call() нужно указать и тип аргумента, и указатель на него (аналогично и для результата). Возвращаемое значение для RUSERSPROC_NUM - unsigned long, поэтому первым возвращаемым параметром rpc_call() будет xdr_u_long, а вторым - *nusers. Поскольку RUSERSPROC_NUM не имеет аргументов, функцией шифрования XDR для rpc_call() будет xdr_void(), а ее аргумент имеет значение NULL.



2004-06-22

next up previous contents
Next: Передача произвольных типов данных Up: Упрощенный интерфейс RPC Previous: Клиентская часть   Contents

Серверная часть

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

Прототип rpc_reg() представлен ниже:

int rpc_reg(

    /* Номер программы сервера */

   u_long prognum,

    /* Номер версии сервера */

   u_long versnum,

    /* Номер процедуры сервера */

   u_long procnum,

    /* Имя удаленной функции */

   char *procname,

    /* Фильтр для кодирования аргумента arg */

   xdrproc_t inproc,

    /* Фильтр декодирования результата*/

   xdrproc_t outproc,

    /* Выбор транспортной службы */)

   char *nettype;

Функция svc_run() вызывает сервисные процедуры в ответ на вызовы RPC. Диспетчер в rpc_reg() заботится о расшифровывании аргументов удаленных процедур и о зашифровывании результатов, с использованием фильтров XDR, определенных при регистрации удаленной процедуры.

Некоторые замечания относительно программы сервера:

Иногда программа, написанная вручную, более компактна, чем созданная с помощью rpcgen. Ниже приведен пример процедуры регистрации. Он регистрирует единственную процедуру и вызывает svc_run(), чтобы обслуживать запросы:

#include <stdio.h> 

#include <rpc/rpc.h>

#include <rpcsvc/rusers.h>

 

void *rusers();

 

main()

{

  if(rpc_reg(RUSERSPROG, RUSERSVERS,

     RUSERSPROC_NUM, rusers,

     xdr_void, xdr_u_long,

     "visible") == -1) {

        fprintf(stderr, "Невозможно зарегистрировать\n");

        exit(1);

  }

  svc_run(); /* Процедура без возврата */

  fprintf(stderr, "Ошибка: Выход из svc_run!\n");

  exit(1);

}

rpc_reg() можно вызвать сколько угодно раз, чтобы зарегистрировать все различные программы, версии, и процедуры.



2004-06-22

next up previous contents
Next: Разработка высокоуровневых приложений RPC Up: Удаленный вызов процедур Previous: Серверная часть   Contents

Передача произвольных типов данных

Типы данных, передаваемые и получаемые из удаленных процедур, могут быть любыми из множества предопределенных, либо типом, определенным программистом. RPC работает с произвольными структурами данных, независимо от различий в структуре типов на различных машинах, преобразуя типы к стандартному формату передачи, который называется внешним представлением данных (XDR). Преобразование из машинного представления в XDR называют сериализацией, а обратный процесс - десериализацией. Аргументы транслятора для rpc_call() и rpc_reg() могут определять примитивную процедуру XDR, например xdr_u_long(), или специальную процедуру пользователя, которая обрабатывает полную структуру аргументов. Процедуры обработки аргументов должны принимать только два аргумента: указатель на результат и указатель на обработчик XDR.

Доступны следующие примитивные процедуры XDR для обработки типов данных:

xdr_int() xdr_netobj() xdr_u_long() xdr_enum()

xdr_long() xdr_float() xdr_u_int() xdr_bool()

xdr_short() xdr_double() xdr_u_short() xdr_wrapstring()

xdr_char() xdr_quadruple() xdr_u_char() xdr_void()

Непримитивная xdr_string(), которая принимает больше, чем два параметра, вызывается из xdr_wrapstring().

В случае собственной процедуры программиста, структура

struct simple {

  int a;

  short b;

} simple;

содержит аргументы вызова процедуры. Процедура xdr_simple() преобразует структуру аргумента так, как показано ниже:

#include <rpc/rpc.h>

#include "simple.h"

bool_t xdr_simple(XDR *xdrsp, struct simple *simplep)

{

  if (!xdr_int(xdrsp, &simplep->a))

    return (FALSE);

  if (!xdr_short(xdrsp, &simplep->b))

    return (FALSE);

  return (TRUE);

}

Эквивалентную процедуру можно создать автоматически с помощью rpcgen.

Процедура XDR возвращает результат, отличный от нуля, если она завершается успешно, либо 0 - в случае ошибки.

Для более сложных структур данных используют готовые процедуры XDR:

xdr_array() xdr_bytes() xdr_reference()

xdr_vector() xdr_union() xdr_pointer()

xdr_string() xdr_opaque()

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

struct varintarr {

  int *data;

  int arrlnth;

} arr;

Массив транслируется через xdr_array(), как показано ниже:

bool_t xdr_varintarr(XDR *xdrsp, struct varintarr *arrp)

{

  return(xdr_array(xdrsp, (caddr_t)&arrp->data, 

  (u_int *)&arrp->arrlnth, MAXLEN, sizeof(int),

  xdr_int));

}

Аргументы xdr_array() - обработчик XDR, указатель на массив, указатель на размер массива, максимальный размер массива, размер каждого элемента массива и указатель на процедуру XDR для преобразования каждого элемента массива. Если размер массива известен заранее, более эффективным является использование
xdr_vector():

int intarr[SIZE];

bool_t xdr_intarr(XDR *xdrsp, int intarr[])

{

  return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int),

  xdr_int));

}

При сериализации XDR преобразует величины к четырехбайтным значениям. В массивах символов каждый символ занимает 32 бита; xdr_bytes() упаковывает символы. Он имеет четыре параметра, схожие с первыми четырьмя параметрами функции xdr_array().

Строки, законченные пустым указателем, транслируются с помощью функции xdr_string(). Она сходна с xdr_bytes(), но не содержит параметра длины. При сериализации процедура получает длину строки из strlen(), а при десериализации создает строку, заканчивающуюся пустым указателем.

xdr_reference() вызывает встроенные функции xdr_string() и xdr_reference(), которые преобразуют указатели для передачи строки, и struct simple из предыдущего примера. Пример использования xdr_reference():

struct finalexample {

  char *string;

  struct simple *simplep;

} finalexample;

 

bool_t xdr_finalexample(XDR *xdrsp,

    struct finalexample *finalp)

{

  if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN))

    return (FALSE);

  if (!xdr_reference( xdrsp, &finalp->simplep,

      sizeof(struct simple), xdr_simple))

      return (FALSE);

  return (TRUE);

}

Процедура thatxdr_simple() должна вызываться вместо
xdr_reference().


next up previous contents
Next: Разработка высокоуровневых приложений RPC Up: Удаленный вызов процедур Previous: Серверная часть   Contents
2004-06-22

next up previous contents
Next: Определение протокола Up: Удаленный вызов процедур Previous: Передача произвольных типов данных   Contents

Разработка высокоуровневых приложений RPC

В качестве примера высокоуровневого приложения приведен удаленный аналог команды чтения оглавления каталога.

Вначале рассматривается локальная версия. Программа состоит из двух файлов.

Файл lls.c содержит основную программу, которая вызывает процедуру в локальном модуле read_dir.c:

#include <stdio.h>

#include <strings.h>

#include "rls.h"

 

main (int argc, char **argv)

{

  char dir[DIR_SIZE];

  /* вызов локальной процедуры */

  strcpy(dir, argv[1]);

  /* char dir[DIR_SIZE] это имя каталога */

  read_dir(dir);

  /* вывод результата */

  printf("%s\n", dir);

  exit(0);

}

read_dir.c - файл локальной процедуры read_dir().

/* процедуры, совместимые с RPC принимают один входной

   аргумент и возвращают один результат. Оба передаются

   через указатели. Возвращаемые значения должны

   указывать на статические данные. */

#include <stdio.h>

#include <sys/types.h>

#include <sys/dir.h> 

#include "rls.h"

 

read_dir(char *dir)  /* char dir[DIR_SIZE] */

{

  DIR * dirp;

  struct direct *d;

  printf("начало");

 

  /* открывает каталог */

  dirp = opendir(dir);

  if (dirp == NULL)

  return(NULL);

 

  /* сохраняет имена файлов в буфер каталога */

  dir[0] = NULL;

  while (d = readdir(dirp))

  sprintf(dir, "%s%s\n", dir, d->d_name);

 

  /* выводит результат */

  printf("выход ");

  closedir(dirp);

  return((int)dir); 

}

Заголовочный файл rls.h содержит строку #define DIR_SIZE 8192.

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

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



Subsections

2004-06-22

next up previous contents
Next: Разделение данных Up: Разработка высокоуровневых приложений RPC Previous: Разработка высокоуровневых приложений RPC   Contents

Определение протокола

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

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

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

Номера программы определяются стандартным способом:

0x00000000 - 0x1FFFFFFF: Определены Sun

0x20000000 - 0x3FFFFFFF: Пользовательские 

0x40000000 - 0x5FFFFFFF: Переходные

0x60000000 - 0xFFFFFFFF: Резервированные 

Для номера программы выбирается пользовательский диапазон. Номера версии и процедуры установлены согласно стандартной практике.

DIR_SIZE определяет размер буфера для каталога в программах сервера и клиента.

Теперь файл rls.h содержит:

#define DIR_SIZE 8192

 /* номер программы сервера */

#define DIRPROG ((u_long) 0x20000001)

#define DIRVERS ((u_long) 1) /* номер версии */

#define READDIR ((u_long) 1) /* номер процедуры */



2004-06-22

next up previous contents
Next: Серверная часть Up: Разработка высокоуровневых приложений RPC Previous: Определение протокола   Contents

Разделение данных

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

Файл XDR, rls_xrd.c, выглядит так:

#include <rpc/rpc.h>

#include "rls.h"

bool_t xdr_dir(XDR *xdrs, char *objp)

{ return ( xdr_string(xdrs, &objp, DIR_SIZE) ); }



2004-06-22

next up previous contents
Next: Клиентская часть Up: Разработка высокоуровневых приложений RPC Previous: Разделение данных   Contents

Серверная часть

Для нее можно использовать оригинальный файл read_dir.c. Необходимо лишь зарегистрировать процедуру и запустить сервер.

Процедура регистрируется с помощью функции registerrpc():

int registerrpc(

  u_long prognum /* Номер программы сервера */,

  u_long versnum /* Номер версии сервера */,

  u_long procnum /* Номер процедуры сервера */,

  char *procname /* Имя удаленной функции */,

   /* Фильтр для кодирования аргументов */

  xdrproc_t inproc,

   /* Фильтр декодирования результата */

  xdrproc_t outproc);

Полный код rls_svc.c:

#include <rpc/rpc.h>

#include "rls.h"

main()

{

  extern bool_t xdr_dir();

  extern char * read_dir();

  registerrpc(DIRPROG, DIRVERS, READDIR,

  read_dir, xdr_dir, xdr_dir);

  svc_run();

}



2004-06-22

next up previous contents
Next: Компиляция протоколов и низкоуровневое Up: Разработка высокоуровневых приложений RPC Previous: Серверная часть   Contents

Клиентская часть

На клиентской стороне просто производится вызов удаленной процедуры. Для этого используется функция callrpc():

int callrpc(char *host /* Имя сервера */,

   u_long prognum /* Номер программы сервера */,

   u_long versnum /* Номер версии сервера */,

   char *in /* Указатель на аргументы */,

    /* Фильтр XDR для кодирования аргумента */

   xdrproc_t inproc,

   char *out /* Адрес для сохранения результата */,

    /* Фильтр декодирования результата */

   xdr_proc_t outproc);

Локально вызывается функция read_dir(), которая использует
callrpc() для вызова удаленной процедуры, зарегистрированной на сервере как READDIR.

Программа rls.c выглядит так:

/*

* rls.c: клиент удаленного чтения каталога

*/

#include <stdio.h>

#include <strings.h>

#include <rpc/rpc.h>

#include "rls.h"

 

main (argc, argv)

int argc; char *argv[];

{

  char dir[DIR_SIZE];

  /* вызов удаленной процедуры */

  strcpy(dir, argv[2]);

  read_dir(argv[1], dir); /* read_dir(host, directory) */

  /* вывод результата */

  printf("%s\n", dir);

  exit(0);

}

 

read_dir(host, dir)

char *dir, *host;

{

  extern bool_t xdr_dir();

  enum clnt_stat clnt_stat;

  clnt_stat = callrpc ( host, DIRPROG, DIRVERS, READDIR,

      xdr_dir, dir, xdr_dir, dir);

  if (clnt_stat != 0) clnt_perrno (clnt_stat);

}



2004-06-22

next up previous contents
Next: Преобразование локальных процедур в Up: Удаленный вызов процедур Previous: Клиентская часть   Contents

Компиляция протоколов и низкоуровневое программирование RPC

Программа rpcgen создает модули интерфейса удаленной программы. Она компилирует исходный код, написанный на языке RPC. Язык RPC подобен по синтаксису и структуре на C; rpcgen создает один или несколько исходных модулей на языке C, которые затем обрабатываются компилятором.

Результатом работы rpcgen являются:

С помощью rpcgen можно также создавать:



2004-06-22

next up previous contents
Next: Передача сложных структур данных Up: Удаленный вызов процедур Previous: Компиляция протоколов и низкоуровневое   Contents

Преобразование локальных процедур в удаленные

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

Однопроцессная версия printmesg.c:

/* printmsg.c: выводит сообщение на терминал */

#include <stdio.h>

main(int argc, char *argv[])

{

  char *message;

  if (argc != 2) {

     fprintf(stderr, "usage: %s <message>\n",argv[0]);

     exit(1);

  }

  message = argv[1];

  if (!printmessage(message)) {

     fprintf(stderr,"%s: невозможно вывести сообщение\n",

         argv[0]); exit(1);

  }

  printf("Сообщение выведено!\n");

  exit(0);

}

/* Вывод сообщения на терминал.

* Возвращает логическое значение, показывающее

* выведено ли сообщение. */

printmessage(char *msg)  {

  FILE *f;

  f = fopen("/dev/console", "w");

  if (f == (FILE *)NULL)  return (0);

  fprintf(f, "%s\n", msg);

  fclose(f);

  return(1);

}

Если функцию printmessage() превратить в удаленную процедуру, ее можно вызывать на любой машине сети.

Сначала необходимо определить типы данных всех аргументов вызова процедуры и результата. Аргумент вызова printmessage() представляет собой строку, а результат - целое число. Теперь можно написать спецификацию протокола на языке RPC, который будет описывать удаленную версию printmessage(). Исходный код RPC для данной спецификации:

/* msg.x: Удаленный протокол вывода сообщения */

program MESSAGEPROG {

  version PRINTMESSAGEVERS {

    int PRINTMESSAGE(string) = 1;

  } = 1;

} = 0x20000001;

Удаленные процедуры всегда объявляются как часть удаленных программ. Код выше описывает полную удаленную программу, которая содержит единственную процедуру PRINTMESSAGE.

В этом примере PRINTMESSAGE - это процедура номер 1 в версии 1 удаленной программы MESSAGEPROG с номером программы 0x20000001.

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

Необходимо разработать еще две дополнительные программы. Одной из них является сама удаленная процедура. Версия printmsg.c для RPC:

/*

* msg_proc.c: реализация удаленной процедуры 

* "printmessage" */

#include <stdio.h>

#include "msg.h" /* msg.h сгенерированный rpcgen */

 

int * printmessage_1(char **msg, struct svc_req *req) {

  static int result; /* должен быть static! */

  FILE *f;

  f = fopen("/dev/console", "w");

  if (f == (FILE *)NULL) {

     result = 0;

     return (&result);

  }

  fprintf(f, "%s\n", *msg);

  fclose(f);

  result = 1;

  return (&result);

}

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

Пример клиентской программы, которая вызывает процедуру:

/*

* rprintmsg.c: удаленная версия "printmsg.c"

*/

#include <stdio.h>

#include "msg.h" /* msg.h сгенерирован rpcgen */

 

main(int argc, char **argv)

{

  CLIENT *clnt;

  int *result;

  char *server;

  char *message;

  if (argc != 3) {

     fprintf(stderr, "usage: %s host

     message\n", argv[0]);

     exit(1);

  }

  server = argv[1];

  message = argv[2];

 

  /*

  * Создает клиентский "обрабочик", используемый

  * для вызова MESSAGEPROG на сервере

  */

  clnt = clnt_create(server, MESSAGEPROG,

       PRINTMESSAGEVERS, "visible");

  if (clnt == (CLIENT *)NULL) {

    /*

    * Невозможно установить соединение с сервером.

    */

    clnt_pcreateerror(server);

    exit(1);

  }

 

  /*

  * Вызов удаленной процедуры

  * "printmessage" на сервере

  */

  result = printmessage_1(&message, clnt);

  if (result == (int *)NULL) {

     /*

     * Ошибка при вызове сервера

     */

     clnt_perror(clnt, server);

     exit(1);

  }

 

  /* Успешный вызов удаленной процедуры.

  */

  if (*result == 0) {

     /*

     * Сервер не может вывести сообщение.

     */

     fprintf(stderr, "%s: невозможно вывести

           сообщение\n", argv[0]);

     exit(1);

  }

 

  /* Сообщение выведено на терминал сервера

  */

  printf("Сообщение доставлено %s\n", server);

  clnt_destroy( clnt );

  exit(0);

}

Следует отметить следующие особенности клиентской программы вызова printmsg.c:

Вызов удаленной процедуры может завершиться неудачно по двум причинам: либо произойдет ошибка в механизме RPC, либо ошибка в выполнении удаленной процедуры. В первом случае удаленная процедура printmessage_1 возвращает NULL. Во втором случае сообщение об ошибке зависит от приложения. Здесь ошибка возвращается через *result.

Для компиляции примера удаленного rprintmsg:

  1. Откомпилируйте протокол, определенный в msg.x: rpcgen msg.x. При этом должны быть созданы заголовочный файл (msg.h), клиентская часть (msg_clnt.c), и серверная часть (msg_svc.c).

  2. Откомпилируйте исполняемый файл клиента:

    cc rprintmsg.c msg_clnt.c -o rprintmsg -lnsl 
  3. Откомпилируйте исполняемый файл сервера:
    cc msg_proc.c msg_svc.c -o msg_server -lnsl
Объектные файлы C должны быть скомпонованы с библиотекой libnsl, которая содержит все сетевые функции, включая версии для RPC и XDR.

В этом примере не было создано никаких процедур XDR, потому что приложение использует только основные типы, которые включены в libnsl. Теперь нужно рассмотреть, что создано rpcgen на основе входного файла msg.x:

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


next up previous contents
Next: Передача сложных структур данных Up: Удаленный вызов процедур Previous: Компиляция протоколов и низкоуровневое   Contents
2004-06-22