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

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

Методы и средства параллельной организации процессов

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





2003-12-09

next up previous contents
Next: Contents   Contents

Методы и средства параллельной организации процессов





2003-12-09

next up previous
Next: Введение. Up: Методы и средства параллельной Previous: Методы и средства параллельной


Contents



2003-12-09

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

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

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

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

Отдельные сигналы разделяются на три различных класса:

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

Существует три основных варианта реакции на сигналы:

Чтобы реагировать на те или другие сигналы, необходимо понимать концепции обработки сигнала. Процесс должен организовать так называемый обработчик сигнала. Эти обработчики срабатывают в случае прихода сигнала. Для этого используется функция 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.

Номер Значение Реакция программы по умолчанию

Переменная 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);
Пример использования обработчика сигнала (рис. 6):

#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;

}

Рис. 6. Пример обработки сигнала.



2003-12-09

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

Преобразование локальных процедур в удаленные.

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

Однопроцессная версия printmesg.c (рис. 38):

/* 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);

}

Рис. 38. Однопроцессная версия printmesg.c

Если функцию 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 (рис. 39):

/*

* 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);

}

Рис. 39. Версия printmsg.c для RPC.

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

Пример клиентской программы, которая вызывает процедуру (рис. 40):

/*

* 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);

}

Рис. 40. Клиент для обращения к printmsg.c

Следует отметить следующие особенности клиентской программы вызова printmsg.c:

Вызов удаленной процедуры может завершиться неудачно двумя способами. Либо произойдет ошибка в механизме RPC, либо может быть ошибка в выполнении удаленной процедуры. В первом случае удаленная процедура printmessage_1 возвращает NULL. Во втором случае сообщение об ошибке зависит от приложения. Здесь ошибка возвращается через *result.

Для компиляции примера удаленного rprintmsg:

Откомпилируйте протокол, определенный в msg.x: rpcgen msg.x. При этом будут созданы заголовочный файл (msg.h), клиентская часть (msg_clnt.c), и серверная часть (msg_svc.c).

Откомпилируйте исполняемый файл клиента:

cc rprintmsg.c msg_clnt.c -o rprintmsg -lnsl 
Откомпилируйте исполняемый файл сервера:

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
2003-12-09

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 показана ниже (рис. 41):

/*

* 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);

}

Рис. 41. Сервер процедуры readdir

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

/*

* 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);

}

Рис. 42. Клиентская часть для вызова readdir.

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


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

next up previous contents
Next: About this document ... Up: Удаленный вызов процедур. Previous: Передача сложных структур данных.   Contents

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

rpcgen поддерживает препроцессор C. При этом препроцессор C применяется к входным файлам rpcgen перед компиляцией. В исходных файлах .x поддерживаются все стандартные директивы препроцессора C. В зависимости от типа генерируемого выходного файла, пять символов определяются самим rpcgen. 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



2003-12-09

next up previous contents
Up: Методы и средства параллельной 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 2003-12-09


2003-12-09

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, чтобы завершить ее (рис. 7).

#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;

}

Рис. 7. Использование raise().

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

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int signalnumber);

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



2003-12-09

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

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

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

В новой концепции сигналов вначале вводится переменная примитивного типа данных sigset_t.

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);

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

Функция позволяет использовать три предопределенные константы:

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

#include <signal.h>

int sigsuspend(const sigset_t *sig_m);

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



2003-12-09

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

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

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

#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;

}

Рис. 8. Взаимодействие через обмен сигналами.



2003-12-09

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

Трубы (pipes)



Subsections

2003-12-09

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(). Процесс-потомок наследует от родителя оба открытых дескриптора файлов. Необходимо указать обоим процессам, кто куда пишет и кто что читает, при этом закрыв ненужные дескрипторы.

В приведенном ниже примере (рис. 9) процесс-родитель будет записывать данные в трубу. Таким образом, дескриптор чтения (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);

}

Рис. 9. Простое взаимодействие через трубы.



2003-12-09

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()(рис. 10):

#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);

}

Рис. 10. Взаимодействие с помощью popen().



2003-12-09

next up previous contents
Next: Блокировка файлов. Up: Трубы (pipes) Previous: Функция popen().   Contents

FIFO - именованные каналы.

С помощью труб могут общаться только родственные друг другу процессы, полученные с помощью fork(). Именованные каналы FIFO дают возможность обмена данными с абсолютно чужим процессом.

С точки зрения ядра ОС FIFO является одним из вариантов реализации трубы. Системный вызов mkfifo() предоставляет именованную трубу в виде объекта файловой системы. Как и для любого другого объекта, необходимо предоставлять процессам права доступа в FIFO, чтобы определить, кто может писать что-либо в 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. Пример использования (рис. 11):

#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;

}

Рис. 11. Обмен через объект FIFO.

Если в системе отсутствует функция mkfifo(), можно воспользоваться общей функцией для создания файла

int mknod(char *pathname, int mode, int dev);
Pathname указывает обычное имя каталога Unix и имя 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 для чтения.При чтении канал снова блокируется до тех пор, пока другой процесс не запишет в FIFO.

Флаг O_NONBLOCK может использоваться только при доступе для чтения. При попытке открыть FIFO с O_NONBLOCK для записи возникает ошибка открытия. Если FIFO закрыть для записи через close или fclose, это значит, что для чтения в FIFO помещается EOF.

Если несколько процессов пишут в один и тот же FIFO, необходимо обратить внимание на то, чтобы сразу не записывалось больше чем PIPE_BUF байтов. Это необходимо, чтобы данные не смешивались друг с другом. Установить пределы записи можно следующей программой (рис. 12):

#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;

}

Рис. 12. Ограничение записи в FIFO.

При попытке записи в FIFO, который не открыт в данный момент для чтения ни одним процессом, генерируется сигнал SIGPIPE.

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

#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); /*Ждем окончания потомка*/

    read(r_fifo, &buf, sizeof(buf)); /*Читаем из FIFO*/

    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); }

    write(w_fifo, buffer, strlen(buffer)); /*Записываем в FIFO*/

    close(w_fifo); /*EOF*/

    exit(0);

  }

  return 0;

}

Рис. 13. Реализация архитектуры клиент/сервер через FIFO.


next up previous contents
Next: Блокировка файлов. Up: Трубы (pipes) Previous: Функция popen().   Contents
2003-12-09

next up previous contents
Next: Необходимость блокировки. Up: Локальные средства IPC. Previous: FIFO - именованные каналы.   Contents

Блокировка файлов.



Subsections

2003-12-09

next up previous contents
Next: Структура flock Up: Блокировка файлов. Previous: Блокировка файлов.   Contents

Необходимость блокировки.

Одной из проблем при организации параллельного доступа к файлам из нескольких процессов является непредсказуемость порядка доступа в файл. В следующем примере два процесса записывают в файл строку символов по одному символу (рис. 14):

#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;

}

Рис. 14. Параллельная запись в файл.

После отработки этой программы содержимое файла ``/tmp/file'' может быть следующим:

XxxXXxxXXxxXXxxXXxx
Для хранения данных предпочтительнее было бы, чтобы каждый процесс писал свои данные в отдельную часть файла. При этом содержимое файла "/tmp/file" должно быть следующим:

xxxxxxxxxxXXXXXXXXXX
Для разделения данных в файле используется функция:

int fcntl(int fd, int сommand,.../*struct flock *flockptr*/);



2003-12-09

next up previous contents
Next: Локальные средства IPC. Up: Методы и средства параллельной Previous: Contents   Contents

Введение.

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

Чтобы различные программы могли обмениваться информацией через файлы или общую базу данных, они, как правило, работают через соответствующий API (Application Programming Interface - интерфейс программирования приложения). Если программы находятся на различных компьютерах, то процесс взаимодействия сопряжен с определенными дополнительными трудностями, например, ограниченной пропускной способностью и сложностью синхронизации. Для организации коммуникации между одновременно работающими процессами применяются средства IPC (Interprocess Communication).

Выделяются три уровня 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 призваны решать проблемы, возникающие при организации параллельных вычислений. Сюда относятся:



2003-12-09

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;
При этом файл блокируется до конца файла.



2003-12-09

next up previous contents
Next: Блокировка частей файла и Up: Блокировка файлов. Previous: Структура flock   Contents

Режимы блокировки.

Функции блокирования можно осуществлять в следующих режимах.

Замечание: Если нужно предусмотреть для файла блокировку по записи (F_WRLCK), файл должен быть открыт для записи (O_WRONLY). И наоборот, если необходима блокировка по чтению (F_RDLCK), файл открывается в режиме чтения (O_RDONLY). Это означает также, что блокировки не должны одновременно устанавливаться на определенный байт. Пример для иллюстрации режимов блокировки (рис. 15):

#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("Fehler : 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;

}

Рис. 15. Упорядоченная запись с блокировкой.

Этот пример показывает возможности сериализации процессов. Тем не менее, здесь все равно существует возможность чтения и записи в процессе-потомке. Такой вариант блокировки называется 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
2003-12-09

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), образуя дедлок. Никакой из процессов не возобновит свое выполнение.

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

Пример программы, вызывающей тупик, приведен ниже (рис. 16):

#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) failed (%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);

}

Рис. 16. Программа, содержащая тупик.

Вначале создается файл данных 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
2003-12-09

next up previous contents
Next: Общие сведения об очередях. Up: Локальные средства IPC. Previous: Блокировка частей файла и   Contents

Очереди сообщений.



Subsections

2003-12-09

next up previous contents
Next: Использование очередей сообщений Up: Очереди сообщений. Previous: Очереди сообщений.   Contents

Общие сведения об очередях.

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

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

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

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

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



2003-12-09

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. Он позволяет выполнять следующие управляющие действия:



2003-12-09

next up previous contents
Next: Управление очередями сообщений Up: Очереди сообщений. Previous: Использование очередей сообщений   Contents

Создание очередей сообщений

В справочной статье для 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. Права доступа к очередям.

Права на операции Восьмеричное значение

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

Флаги определены во включаемом файле <sys/ipc.h>. В табл. 3 сведены мнемонические имена флагов и соответствующие им восьмеричные значения:

Табл. 3. Флаги работы с очередями.

Флаг Восьмеричное значение

Значение аргумента 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 (рис. 17) управляется посредством меню. Она позволяет поупражняться со всевозможными комбинациями в использовании системного вызова 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);

}

Рис. 17. Пример использования msgget().


next up previous contents
Next: Управление очередями сообщений Up: Очереди сообщений. Previous: Использование очередей сообщений   Contents
2003-12-09

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, требуется право на чтение.

Ниже приведена программа-пример (рис. 18), иллюстрирующая управление очередью. В программе использованы следующие переменные:

Программа ведет себя следующим образом. Прежде всего предлагается ввести допустимый идентификатор очереди сообщений, который заносится в переменную 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);

}

Рис. 18. Пример управления очередью.


next up previous contents
Next: Операции над очередями сообщений Up: Очереди сообщений. Previous: Создание очередей сообщений   Contents
2003-12-09

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.

В приведенном ниже примере (рис. 19) используются следующие переменные:

нужен ли флаг IPC_NOWAIT.

комбинацию флагов IPC_NOWAIT и MSG_NOERROR.

Структура данных 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);

 }

Рис. 19. Пример обмена через очередь сообщений.


next up previous contents
Next: Семафоры. Up: Очереди сообщений. Previous: Управление очередями сообщений   Contents
2003-12-09

next up previous contents
Next: Основные сведения о семафорах. Up: Локальные средства IPC. Previous: Операции над очередями сообщений   Contents

Семафоры.



Subsections

2003-12-09

next up previous contents
Next: Процессы Up: Методы и средства параллельной Previous: Введение.   Contents

Локальные средства IPC.



Subsections

2003-12-09

next up previous contents
Next: Использование семафоров. Up: Семафоры. Previous: Семафоры.   Contents

Основные сведения о семафорах.

Семафоры являются одним из классических примитивов синхронизации. Значение семафора - это целое число в диапазоне от 0 до 32767. Поскольку во многих приложениях требуется более одного семафора, ОС UNIX предоставляет возможность создавать множества семафоров. Их максимальный размер ограничен системным параметром SEMMSL. Множества семафоров создаются при помощи системного вызова semget.

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

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

Для выполнения первых двух операций у процесса должно быть право на изменение, для выполнения третьей достаточно права на чтение. Чтобы увеличить значение семафора, системному вызову semop следует передать требуемое число. Чтобы уменьшить значение семафора, нужно передать требуемое число, взятое с обратным знаком; если результат получается отрицательным, операция не может быть успешно выполнена. Для третьей операции нужно передать 0; если текущее значение семафора отлично от нуля, операция не может быть успешно выполнена.

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

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

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

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



2003-12-09

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 для выполнения управляющих действий.



2003-12-09

next up previous contents
Next: Управление семафорами с помощью Up: Семафоры. Previous: Использование семафоров.   Contents

Создание множеств семафоров.

Для создания множества семафоров служит системный вызов semget. В справочной статье 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 определяет максимально допустимое число семафоров в одном множестве.



2003-12-09

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 требуется право на изменение, а для выполнения остальных действий - право на чтение.

Пример работы с семафорами (рис. 20):

/* Программа иллюстрирует

возможности системного вызова 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);

}

Рис. 20. Управление семафорами.


next up previous contents
Next: Операции над множествами семафоров. Up: Семафоры. Previous: Создание множеств семафоров.   Contents
2003-12-09

next up previous contents
Next: Разделяемые сегменты памяти Up: Семафоры. Previous: Управление семафорами с помощью   Contents

Операции над множествами семафоров.

В справочной статье 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 операций.

Пример работы с семафорами приведен ниже (рис. 21):

/* Программа иллюстрирует

возможности системного вызова 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);

}

Рис. 21. Операции над семафорами.


next up previous contents
Next: Разделяемые сегменты памяти Up: Семафоры. Previous: Управление семафорами с помощью   Contents
2003-12-09

next up previous contents
Next: Общие сведения о разделяемых Up: Локальные средства IPC. Previous: Операции над множествами семафоров.   Contents

Разделяемые сегменты памяти



Subsections

2003-12-09

next up previous contents
Next: Использование разделяемых сегментов памяти. Up: Разделяемые сегменты памяти Previous: Разделяемые сегменты памяти   Contents

Общие сведения о разделяемых сегментах памяти.

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

Разделение памяти обеспечивает наиболее быстрый обмен данными между процессами.

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

Когда разделяемый сегмент становится ненужным, его следует отсоединить, воспользовавшись системным вызовом shmdt().

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



2003-12-09

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. Состояния разделяемых сегментов памяти.

Бит удержания Бит подкачки Бит размещения Состояние

Состояния, упомянутые в таблице, таковы:

Неразмещенный
сегмент - разделяемый сегмент, ассоциированный с данным идентификатором, не размещен для использования.
В
памяти - сегмент размещен для использования. Это означает, что сегмент существует и в данный момент находится в оперативной памяти.
На
диске - сегмент в данный момент вытолкнут на устройство подкачки.
Удержан
в памяти - сегмент удержан в оперативной памяти и не будет рассматриваться в качестве кандидата на выталкивание, пока не будет снято удержание. Удерживать и освобождать разделяемые сегменты может только суперпользователь.
Не
используется - состояние в настоящий момент не используется и при работе обычного пользователя с разделяемыми сегментами памяти возникнуть не может.
Системный вызов shmget аналогичен вызову semget (разумеется, с заменой слов ``множество семафоров'' на ``разделяемый сегмент памяти''). Он также предназначен для получения нового или опроса существующего идентификатора, а нужное действие определяется значением аргумента key. В аналогичных ситуациях shmget терпит неудачу. Единственное отличие состоит в том, что задается не число семафоров в множестве, а размер сегмента в байтах.

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



2003-12-09

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. Данные системные параметры определяют, соответственно, минимальный и максимальный размеры разделяемого сегмента памяти.



2003-12-09

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 процессу требуется право на чтение (рис. 22).

/* Программа иллюстрирует

возможности системного вызова 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);

}

Рис. 22. Управление разделяемой памятью.


next up previous contents
Next: Операции над разделяемыми сегментами Up: Разделяемые сегменты памяти Previous: Создание разделяемых сегментов памяти.   Contents
2003-12-09

next up previous contents
Next: Основные сведения о процессах. Up: Локальные средства IPC. Previous: Локальные средства IPC.   Contents

Процессы



Subsections

2003-12-09

next up previous contents
Next: Потоки (threads). Up: Разделяемые сегменты памяти Previous: Управление разделяемыми сегментами памяти.   Contents

Операции над разделяемыми сегментами памяти.

В справочной статье shmop синтаксис системных вызовов 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() приведен ниже (рис. 23):

/* Программа иллюстрирует

возможности системных вызовов 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);

}

Рис. 23. Операции над сегментами разделяемой памяти.


next up previous contents
Next: Потоки (threads). Up: Разделяемые сегменты памяти Previous: Управление разделяемыми сегментами памяти.   Contents
2003-12-09

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

Потоки (threads).

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



Subsections

2003-12-09

next up previous contents
Next: Преимущества многопоточности. Up: Потоки (threads). Previous: Потоки (threads).   Contents

Различие между процессами и потоками.

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

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

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

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

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

При корректной реализации потоки имеют определенные преимущества над процессами. Им требуется:



2003-12-09

next up previous contents
Next: Уровни потоков. Up: Потоки (threads). Previous: Различие между процессами и   Contents

Преимущества многопоточности.

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



2003-12-09

next up previous contents
Next: Пользовательские потоки. Up: Потоки (threads). Previous: Преимущества многопоточности.   Contents

Уровни потоков.

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

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



Subsections

2003-12-09

next up previous contents
Next: Потоки уровня ядра. Up: Уровни потоков. Previous: Уровни потоков.   Contents

Пользовательские потоки.

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

Преимущества пользовательских потоков:

Недостатки:



2003-12-09

next up previous contents
Next: Создание потока. Up: Уровни потоков. Previous: Пользовательские потоки.   Contents

Потоки уровня ядра.

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

Преимущества потоков уровня ядра:

Недостатки:

Основной библиотекой для реализации пользовательских потоков является библиотека потоков POSIX, которая называется pthreads.



2003-12-09

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(), а затем использовать этот объект для создания самого потока. Пример создания потока приведен ниже (рис. 24):

#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);

Рис. 24. Создание потока.

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

Если вызов pthread_create() успешно завершен, идентификатор созданного потока сохраняется по адресу tid.

Создание потока с использованием аргумента атрибутов NULL оказывает тот же эффект, что и использование атрибута по умолчанию; оба создают поток по умолчанию. При инициализации tattr он получает поведение по умолчанию.

pthread_create() возвращает 0 при успешном завершении. Любое другое значение указывает, что произошла ошибка.



2003-12-09

next up previous contents
Next: Отделение потока. Up: Потоки (threads). Previous: Создание потока.   Contents

Ожидание завершения потока.

Функция pthread_join() используется для ожидания завершения потока:

int pthread_join(thread_t tid, void **status);
Пример использования функции (рис. 25):

#include <pthread.h>

pthread_t tid;

int ret;

int status;

/* ожидание завершения потока "tid" со статусом status */

ret = pthread_join(tid, &status);

/* ожидание завершения потока "tid" без статуса */

ret = pthread_join(tid, NULL);

Рис. 25. Завершение потока.

Функция pthread_join() блокирует вызывающий поток, пока указанный поток не завершится. Указанный поток должен принадлежать текущему процессу и не должен быть отделен. Если status не равен NULL, он указывает на переменную, которая принимает значение статуса выхода завершенного потока при успешном завершении pthread_join(). Несколько потоков не могут ждать завершения одного и того же потока. Если они пытаются выполнить это, один поток завершается успешно, а все остальные - с ошибкой ESRCH. После завершения pthread_join(), любое пространство стека, связанное с потоком, может быть использовано приложением.

В следующем примере (рис. 26) один поток верхнего уровня вызывает процедуру, которая создает новый вспомогательный поток, выполняющий сложный поиск в базе данных, требующий определенных затрат времени. Главный поток ждет результатов поиска, и в то же время может выполнять другую работу. Он ждет своего помощника с помощью функции 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];

}

Рис. 26. Скелет многопоточного приложения.



2003-12-09

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() не вызывает его завершения.

pthread_detach() возвращает 0 при успешном завершении. Любое другое значение указывает, что произошла ошибка.



2003-12-09

next up previous contents
Next: Таблица процессов Up: Процессы Previous: Процессы   Contents

Основные сведения о процессах.

В ОС Linux управление процессами является ключевой технологией при разработке многих программ.

Определение: Процесс - это находящаяся в состоянии выполнения программа вместе с ее средой выполнения.

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

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

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

В Linux реализована вытесняющая многозадачность. Это значит, что система сама решает, как долго конкретный процесс может использовать CPU, и когда наступит очередь следующего процесса. Если вы хотите вмешаться в процесс планирования, вы можете сделать это как 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 все процессы упорядочены иерархически, подобно генеалогическому дереву. Каждый процесс владеет информацией того процесса, от которого он был порожден. То же самое справедливо и для его родительского процесса и т.д.

Если вы хотите узнать, сколько времени CPU необходимо каждому процессу, Вы можете использовать команду top. Она показывает, какое время вычислений занимает определенная программа в процессоре. Обратите внимание на колонку '%CPU'.



2003-12-09

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. Вызывающий процесс должен гарантировать, что хранение и доступ к этому ключу должным образом синхронизированы. Дополнительная функция удаления, destructor, может использоваться, чтобы освободить ранее выделенную память. Если ключ имеет непустой указатель на функцию деструктора, и поток имеет непустое значение ключа, функция деструктора вызывается для значения, связанного с потоком, при завершении этого потока. Порядок, в котором вызываются функции деструктора, не играет роли.

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_setspecific() не освобождает память для хранения ключа. Если установлена новая привязка значения ключа, существующая привязка должна быть освобождена; иначе может произойти утечка памяти.

Чтобы получить привязку ключа для вызывающего потока, используется функция 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. 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
2003-12-09

next up previous contents
Next: Компиляция многопоточного приложения. Up: Потоки (threads). Previous: Работа с ключами потока.   Contents

Остановка потока.

Поток может прервать свое выполнение несколькими способами:

Функция

void pthread_exit(void *status);
прерывает выполнение потока точно так же, как функция exit() прерывает процесс:

#include <pthread.h>

int status;

pthread_exit(&status); /* выход возвращает статус 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 в случае успешного завершения, или другое значение в случае ошибки.



2003-12-09

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)



2003-12-09

next up previous contents
Next: Атрибуты потоков. Up: Потоки (threads). Previous: Компиляция многопоточного приложения.   Contents

Отладка многопоточного приложения.

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



2003-12-09

next up previous contents
Next: Состояние отделенного потока. Up: Потоки (threads). Previous: Отладка многопоточного приложения.   Contents

Атрибуты потоков.

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

Таким образом, обычно вызываются три функции:

Пример кода, выполняющего эти действия (рис. 27):

#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);

Рис. 27. Изменение атрибутов потока.

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

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

Функция pthread_attr_init() используется, чтобы инициализировать объект атрибутов значениями по умолчанию. Память распределяется системой потоков во время выполнения.

Пример вызова функции:

#include <pthread.h>

pthread_attr_t tattr;

int ret;

ret = pthread_attr_init(&tattr);

Значения по умолчанию для атрибутов (tattr) приведены в табл. 5:

Табл. 5. Атрибуты потока по умолчанию.

Атрибут Значение Смысл

Функция возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Код ошибки устанавливается в переменной errno.

Функция pthread_attr_destroy() используется, чтобы удалить память для атрибутов, выделенную во время инициализации. Объект атрибутов становится недействительным.

Пример вызова функции:

#include <pthread.h>

pthread_attr_t tattr;

int ret;

ret = pthread_attr_destroy(&tattr);

pthread_attr_destroy() возвращает 0 после успешного завершения или любое другое значение в случае ошибки.



Subsections

2003-12-09

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) потока очень важно, чтобы после того, как он завершится, к нему присоединился другой поток, - иначе ресурсы этого потока не будут освобождены для использования новыми потоками. Это обычно приводит к утечке памяти. Если не требуется создавать поток, который будет присоединен, нужно создавать его отделенным.

Следующий код иллюстрирует, как можно создать отделенный поток (рис. 28):

#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);

Рис. 28. Создание отделенного потока.

Функция pthread_attr_getdetachstate() позволяет определить состояние при создании потока, т.е. был ли он отделенным или присоединяемым. Она возвращает 0 после успешного завершения или любое другое значение в случае ошибки. Пример вызова:

#include <pthread.h>

pthread_attr_t tattr;

int detachstate;

int ret;

ret = pthread_attr_getdetachstate (&tattr, &detachstate);



2003-12-09

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 после успешного завершения или любое другое значение в случае ошибки.



2003-12-09

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) используется для получения приоритета текущего потока.



2003-12-09

next up previous contents
Next: Создание собственного стека потока. Up: Атрибуты потоков. Previous: Дисциплина планирования потока.   Contents

Размер стека в потоке.

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

Если определяется стек, то поток должен создаваться с типом
PTHREAD_CREATE_JOINABLE. Этот стек не может быть освобожден, пока не произойдет выход из pthread_join() этого потока, потому что стек потока не может быть освобожден, пока поток не закончится. Единственный надежный способ закончить такой поток - вызов pthread_join().

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

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



2003-12-09

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) используется для получения базового адреса стека текущего потока.



2003-12-09

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

Таблица процессов

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

/usr/include/linux/sched.h
Внутри структуры struct task_struct находятся все сведения о состоянии процесса. Они достаточно хорошо прокомментированы. Основными являются следующие сведения:



2003-12-09

next up previous contents
Next: Блоки взаимного исключения (мьютексы Up: Потоки (threads). Previous: Создание собственного стека потока.   Contents

Синхронизация потоков.

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

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

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

Основные ситуации, которые требуют использования синхронизации:



Subsections

2003-12-09

next up previous contents
Next: Инициализация и удаление объекта Up: Синхронизация потоков. Previous: Синхронизация потоков.   Contents

Блоки взаимного исключения (мьютексы - mutex).

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

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

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



2003-12-09

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);



2003-12-09

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);



2003-12-09

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);



2003-12-09

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);



2003-12-09

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);



2003-12-09

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);



2003-12-09

next up previous contents
Next: Иерархия блокировок. Up: Синхронизация потоков. Previous: Удаление мьютекса.   Contents

Пример использования мьютексов.

Функция increment_count() использует мьютекс, чтобы гарантировать атомарность модификации разделяемой переменной count.

Функция get_count() использует мьютекс, чтобы гарантировать, что переменная count атомарно считывается (рис. 29):

#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); 

}

Рис. 29. Атомарные операции с мьютексом.



2003-12-09

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 мог запереть его, а затем освободить оба мьютекса.



2003-12-09

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() приведен ниже (рис. 1):

#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");

  }

}

Рис. 1. Пример вызова fork().

Когда потомок вызывает exit(), код возврата передается родителю, который ждет его, вызвав wait(). WEXITSTATUS() представляет собой макрос, который получает фактический код возврата потомка из вызова wait().

Функция wait() ждет завершения первого из всех возможных потомков родительского процесса. Иногда необходимо точно определить, какой из потомков должен завершиться. Для этого используется вызов waitpid() с соответствующим PID потомка в качестве аргумента. Еще один момент, на который следует обратить внимание, это то, что и родитель и потомок используют переменную rv. Это не означает, что переменная разделена между процессами. Каждый процесс содержит собственные копии всех переменных.

Рассмотрим следующий пример (рис. 2).

#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);

}

Рис. 2. Порождение нескольких потомков.

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

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


next up previous contents
Next: Запуск процессов с помощью Up: Процессы Previous: Таблица процессов   Contents
2003-12-09

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.

Чтобы удалить узел из списка, необходимо:

Код для удаления элемента из списка с вложенной блокировкой (рис. 30):

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); 

}

Рис. 30. Доступ к списку с вложенной блокировкой.



2003-12-09

next up previous contents
Next: Работа с атрибутами переменных Up: Потоки (threads). Previous: Вложенные блокировки односвязного списка.   Contents

Переменные состояния.

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

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



Subsections

2003-12-09

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 после успешного завершения. Любое другое значение указывает, что произошла ошибка.



2003-12-09

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) используется для получения области видимости переменной состояния.



2003-12-09

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);



2003-12-09

next up previous contents
Next: Удаленные средства IPC. 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; 

/* Condition variable is destroyed */ 

ret = pthread_cond_destroy(&cv);

pthread_cond_destroy() возвращает 0 после успешного завершения, или любое другое значение в случае ошибки.


next up previous contents
Next: Удаленные средства IPC. Up: Переменные состояния. Previous: Инициализация переменной состояния.   Contents
2003-12-09

next up previous contents
Next: Сокеты. Up: Методы и средства параллельной Previous: Блокировка через переменную состояния.   Contents

Удаленные средства IPC.



Subsections

2003-12-09

next up previous contents
Next: Общие сведения о сокетах. Up: Удаленные средства IPC. Previous: Удаленные средства IPC.   Contents

Сокеты.



Subsections

2003-12-09

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. Для этого типа сокета не существует специального протокола.
Простой
сокет - обеспечивает доступ к основным протоколам связи.
Все сокеты обычно ориентированы на применение датаграмм, но их точные характеристики зависят от интерфейса, обеспечиваемого протоколом. Обмен между сокетами происходит по следующей схеме (рис. 31):

Сервер   Клиент
Установка сокета socket()
Рис. 31. Протокол обмена по сокетам.



2003-12-09

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]; /* зарезервировано */ 

};



2003-12-09

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

Запуск процессов с помощью вызова exec().

Функция exec() (execute) загружает и запускает другую программу. Таким образом, новая программа полностью замещает текущий процесс. Новая программа начинает свое выполнение с функции main. Все файлы, открытые вызывающей программой, остаются открытыми. Они также являются доступными новой программе. Существуют следующие 6 различных вариантов функций 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(). Пусть существует следующая программа (рис. 3):

#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;

}

Рис. 3. Программа, вызываемая через exec().

Эта программа выводит на экран строку, переданную ей в качестве аргумента. Пусть она называется hello. Она будет вызвана из другой программы с помощью функции execl(). Код вызывающей программы ниже (рис. 4.):

#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;

}

Рис. 4. Программа вызова exec().

В строке execl() указаны аргументы в виде списка. Доступ к ним также осуществляется последовательно. Если использовать функцию execv(), то вместо списка будет указан вектор аргументов (рис. 5):

#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;

}

Рис. 4. Программа вызова execv().



2003-12-09

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 в случае ошибки.



2003-12-09

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 в случае ошибки. Следует отметить, что нулевое значение не является ошибкой. Оно сигнализирует об отсутствии записанных в сокет процессом-поставщиком данных.



2003-12-09

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);

Аргумент функции - дескриптор закрываемого сокета.



2003-12-09

next up previous contents
Next: Удаленный вызов процедур. Up: Сокеты. Previous: Закрытие сокетов.   Contents

Приложение архитектуры клиент-сервер с использованием сокетов.

Пример-оболочка программы "Клиент" (рис. 32)

#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);

}

Рис. 32. Клиентская часть приложения.

Пример-оболочка программы "Сервер" (рис. 33)

#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);

}

Рис. 32. Серверная часть приложения.


next up previous contents
Next: Удаленный вызов процедур. Up: Сокеты. Previous: Закрытие сокетов.   Contents
2003-12-09

next up previous contents
Next: Общие сведения. Up: Удаленные средства IPC. Previous: Приложение архитектуры клиент-сервер с   Contents

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



Subsections

2003-12-09

next up previous contents
Next: Разработка протокола взаимодействия. Up: Удаленный вызов процедур. Previous: Удаленный вызов процедур.   Contents

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

Рассмотренные методы синхронизации процессов и коммуникаций предполагали использование одного компьютера. Однако, часто приложения должны работать в пределах локальной или распределенной сети. Одним из методов реализации взаимодействия является удаленный вызов процедур remote procedure calls (RPC). Вызов процедуры представляет собой классическую форму синхронной коммуникации: вызывающий процесс передает управление подпроцессу и ждет возвращения результатов. Используя RPC, программисты распределенных приложений могут не учитывать деталей при обеспечении интерфейса с сетью. Транспортная независимость RPC изолирует приложение от физических и логических элементов механизма коммуникаций данных и позволяет ему использовать разнообразие транспортных протоколов.

RPC делает модель вычислений клиент/сервер более мощной и более простой для программирования. Использование компиляторов протоколов ONC RPCGEN позволяет клиентам прозрачное осуществление удаленных вызовов через локальный интерфейс процедуры.

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

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

Удаленная процедура уникально идентифицируется тройкой: (номер программы, номер версии, номер процедуры). Номер программы идентифицирует группу соотносящихся удаленных процедур, каждая из которых имеет уникальный номер процедуры. Программа может состоять из одной или более версий. Каждая версия состоит из множества процедур, которые могут быть вызваны удаленно. Номера версии позволяют использоваться одновременно нескольким версиям RPC протокола. Каждая версия содержит множество процедур, которые можно вызвать удаленно. Каждая процедура имеет свой номер процедуры.

Для разработки приложения RPC необходимо выполнить следующие шаги:



2003-12-09

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 Representation) - абстракция данных, необходимая для машинно-независимой связи. Клиент и сервер могут быть машинами различных типов.



2003-12-09

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 на компьютерах клиента и сервера соответственно. Процедуры сервера должны быть зарегистрированы, прежде чем клиент сможет их вызвать.



2003-12-09

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 дескриптор сервера, используя транспортный протокол виртуального канала.



2003-12-09

next up previous contents
Next: Пример rusers.c. Up: Удаленный вызов процедур. Previous: Интерфейсные процедуры RPC.   Contents

Упрощенный интерфейс RPC.

Упрощенный интерфейс - это самый простой уровень использования RPC, потому что он не требует использования других процедур RPC. Он также ограничивает контроль над основными механизмами коммуникации. Разработка программ для этого уровня может осуществляться быстро и непосредственно поддерживается компилятором rpcgen. Для большинства приложений достаточно возможностей rpcgen. Некоторые службы RPC не доступны в виде функций C, но они доступны как программы RPC. Процедуры библиотеки упрощенного интерфейса обеспечивают прямой доступ к возможностям RPC для программ, которые не требуют детального управления.

Все процедуры находятся в библиотеке служб RPC librpcsvc.



Subsections

2003-12-09

next up previous contents
Next: Понятие о сигналах. Up: Локальные средства IPC. Previous: Запуск процессов с помощью   Contents

Сигналы



Subsections

2003-12-09

next up previous contents
Next: Клиентская часть. Up: Упрощенный интерфейс RPC. Previous: Упрощенный интерфейс RPC.   Contents

Пример rusers.c.

Пример rusers.c, приведенный ниже (рис. 34), показывает число пользователей на удаленном компьютере. Он вызывает процедуру библиотеки RPC rusers.

#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);

}

Рис. 34. Пример вызова rusers().



2003-12-09

next up previous contents
Next: Серверная часть. Up: Упрощенный интерфейс RPC. Previous: Пример rusers.c.   Contents

Клиентская часть.

Клиентская часть (рис. 35) состоит из вызова функции rpc_call(). Синтаксис функции приведен ниже:

int rpc_call (char *host /* Имя сервера */,

     u_long prognum /* Номер программы сервера */,

     u_long versnum /* Номер версии сервера */,

     xdrproc_t inproc /* фильтр XDR для кодирования arg */,

     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. Множественные аргументы и результаты обрабатываются с помощью объединения их в структуры.

#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);

}

Рис. 35. Клиентская часть приложения.

Так как типы данных могут быть представлены различным образом на различных машинах, rpc_call() нужно указать и тип аргумента, и указатель на него (аналогично и для результата). Возвращаемое значение для RUSERSPROC_NUM - unsigned long, поэтому первым возвращаемым параметром rpc_call() будет xdr_u_long, а вторым - *nusers. Поскольку RUSERSPROC_NUM не имеет аргументов, функцией шифрования XDR для rpc_call() будет xdr_void(), а ее аргумент имеет значение NULL.



2003-12-09

next up previous contents
Next: Передача произвольных типов данных Up: Упрощенный интерфейс RPC. Previous: Клиентская часть.   Contents

Серверная часть.

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

Прототип rpc_reg():

int rpc_reg(u_long prognum /* Номер программы сервера */,

      u_long versnum /* Номер версии сервера */,

      u_long procnum /* Номер процедуры сервера */,

      char *procname /* Имя удаленной функции */,

      xdrproc_t inproc /* Фильтр для кодирования аргумента arg */,

      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);

}

Рис. 36. Серверная часть приложения.

rpc_reg() можно вызвать сколько угодно раз, чтобы зарегистрировать все различные программы, версии, и процедуры.



2003-12-09

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() (рис. 37):

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);

}

Рис. 37. Пример использования xdr_reference().

Процедура thatxdr_simple(), должна вызываться вместо xdr_reference().



2003-12-09

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

2003-12-09

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) /* номер процедуры */



2003-12-09

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) ); }



2003-12-09

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();

}



2003-12-09

next up previous contents
Next: Компиляция протоколов и низкоуровневое Up: Разработка высокоуровневых приложений RPC. Previous: Серверная часть.   Contents

Клиентская часть.

На клиентской стороне просто производится вызов удаленной процедуры. Для этого используется функция callrpc():

int callrpc(char *host /* Имя сервера */,

   u_long prognum /* Номер программы сервера */,

   u_long versnum /* Номер версии сервера */,

   char *in /* Указатель на аргументы */,

   xdrproc_t inproc /* Фильтр XDR для кодирования аргумента */,

   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);

}



2003-12-09

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

Компиляция протоколов и низкоуровневое программирование RPC.

Программа rpcgen создает модули интерфейса удаленной программы. Она компилирует исходный код, написанный на языке RPC. Язык RPC подобен по синтаксису и структуре на C. rpcgen создает один или несколько исходных модулей на языке C, которые затем обрабатываются компилятором C.

Результатом работы rpcgen являются:

rpcgen может также создавать:



2003-12-09