Logo Море(!) аналитической информации!
IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware
VPS/VDS серверы. 30 локаций на выбор

Серверы VPS/VDS с большим диском

Хорошие условия для реселлеров

4VPS.SU - VPS в 17-ти странах

2Gbit/s безлимит

Современное железо!

Бесплатный конструктор сайтов и Landing Page

Хостинг с DDoS защитой от 2.5$ + Бесплатный SSL и Домен

SSD VPS в Нидерландах под различные задачи от 2.6$

✅ Дешевый VPS-хостинг на AMD EPYC: 1vCore, 3GB DDR4, 15GB NVMe всего за €3,50!

🔥 Anti-DDoS защита 12 Тбит/с!

Руководство программиста для Linux

Sven Goldt, Sven van der Meer, Skott Burkett, Matt Welsh

Перевод: Алексей Паутов,
Russian LDP: http://www.botik.ru/~rldp, ftp://ftp.botik.ru/rented/rldp

6. Межпроцессовые коммуникации LINUX

6.1. Введение

Система Linux IPC (Inter-process communication) предоставляет средства для взаимодействия процессов между собой.

В распоряжении программистов есть несколько методов IPC:

  • полудуплексные каналы UNIX
  • FIFO (именованные каналы)
  • Очереди сообщений в стиле SYSV
  • Множества семафоров в стиле SYSV
  • Разделяемые сегменты памяти в стиле SYSV
  • Сетевые сокеты (в стиле Berkeley) (не охватывается этой статьей)
  • Полнодуплексные каналы (каналы потоков) (не охватывается этой статьей)

Если эти возможности эффективно используются, то они обеспечивают солидную базу для поддержания идеологии клиент/сервер в любой UNIX-системе, включая Linux.

6.2. Полудуплексные каналы UNIX

6.2.1. Основные понятия

Канал - это средство связи стандартного вывода одного процесса со стандартным вводом другого. Каналы - это старейший из инструментов IPC, существующий приблизительно со времени появления самых ранних версий оперативной системы UNIX. Они предоставляют метод односторонних коммуникаций (отсюда термин half-duplex) между процессами.

Эта особенность широко используется даже в командной строке UNIX (в shell-е).

   ls | sort | lp

Приведенный выше канал принимает вывод ls как ввод sort, и вывод sort за ввод lp. Данные проходят через полудуплексный канал, перемещаясь (визуально) слева направо.

Хотя большинство из нас использует каналы в программировании на shell-е довольно часто, мы редко задумываемся о том, что происходит на уровне ядра.

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

                     in     <-----
           Process                     Kernel
                     out    ----->

Из этого рисунка легко увидеть, как файловые дескрипторы связаны друг с другом. Если процесс посылает данные через канал (fd0), он имеет возможность получить эту информацию из fd1. Однако этот простенький рисунок отображает и более глобальную задачу. Хотя канал первоначально связывает процесс с самим собой, данные, идущие через канал, проходят через ядро. В частности, в Linux-е каналы внутренне представлены корректным inode-ом. Конечно, этот inode существует в пределах самого ядра, а не в какой-либо физической файловой системе. Эта особенность откроет нам некоторые привелекательные возможности для ввода/вывода, как мы увидим немного позже.

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

                     in   <-----            ----->   in
    Parent Process                 Kernel                  Child Process
                     out  ----->            <-----   out

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

                     in   <-----                     in
    Parent Process                 Kernel                  Child Process
                     out                    <-----   out

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

Чтобы послать данные в канал, мы используем системный вызов write(), а чтобы получить данные из канала - системный вызов read(). Вспомним, что системные вызовы ввода/вывода в файл или из файла работают с файловыми дескрипторами! (Однако, не забывайте, что некоторые системные вызовы, как, например, lseek(), не работают с дескрипторами.)

6.2.2. Создание каналов на Си

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

SYSTEM CALL: pipe();
PROTOTYPE: int pipe( int fd[2] );
  RETURNS: 0 в случае успеха
           -1 в случае ошибки:
              errno = EMFILE (нет свободных дескрипторов)
                      EMFILE (системная файловая таблица переполнена)
                      EFAULT (массив fd некорректен)
     NOTES: fd[0] устанавливается для чтения, fd[1] - для записи.

Первое целое в массиве (элемент 0) установлено и открыто для чтения, в то время как второе целое (элемент 1) установлено и открыто для записи. Наглядно говоря, вывод fd1 становится вводом для fd0. Еще раз отметим, что все данные, проходящие через канал, перемещаются через ядро.

   #include
   #include
   #include
   main()
   {
           int     fd[2];
           pipe(fd);
           .
           .
   }

Вспомните, что имя массива decays в указатель на его первый член. fd - это эквивалент &fd[0]. Раз мы установили канал, то ответвим нашего нового потомка:

   #include
   #include
   #include
   main()
   {
           int     fd[2];
           pid_t   childpid;
           pipe(fd);
           if((childpid = fork()) == -1)
           {
                   perror("fork");
                   exit(1);
           }
           .
           .
   }

Если родитель хочет получить данные от потомка, то он должен закрыть fd1, а потомок должен закрыть fd0. Если родитель хочет послать данные потомку, то он должен закрыть fd0, а потомок - fd1. С тех пор как родитель и потомок делят между собой дескрипторы, мы должны всегда быть уверены, что не используемый нами в данный момент конец канала закрыт; EOF никогда не будет возвращен, если ненужные концы канала не закрыты.

   #include
   #include
   #include
   main()
   {
           int     fd[2];
           pid_t   childpid;

2. Ядро LINUX

           if((childpid = fork()) == -1)
           {
                   perror("fork");
                   exit(1);
           }
           if(childpid == 0)
           {
                   /* Потомок закрывает вход */
                   close(fd[0]);
           }
           else
           {
                   /* Родитель закрывает выход */
                   close(fd[1]);
           }
           .
           .
   }

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

/*************************************************************************
 Excerpt from "Linux Programmer's Guide - Chapter 6"
 (C)opyright 1994-1995, Scott Burkett
 *************************************************************************
 MODULE: pipe.c
 *************************************************************************/
   #include
   #include
   #include
   int main(void)
   {
           int     fd[2], nbytes;
           pid_t   childpid;
           char    string[] = "Hello, world!\n";
           char    readbuffer[80];
           pipe(fd);
           if((childpid = fork()) == -1)
           {
                   perror("fork");
                   exit(1);
           }
           if(childpid == 0)
           {
                   /* Потомок закрывает вход */
                   close(fd[0]);
                   /* Посылаем "string" через выход канала */
                   write(fd[1], string, strlen(string));
                   exit(0);
           }
           else
           {
                   /* Родитель закрывает выход */
                   close(fd[1]);
                   /* Чтение строки из канала */
                   nbytes = read(fd[0], readbuffer, sizeof(readbuffer));
                   printf("Received string: %s", readbuffer);
           }
           return(0);
   }

Часто дескрипторы потомка раздваиваются на стандартный ввод или вывод. Потомок может затем exec() другую программу, которая наследует стандартные потоки. Давайте посмотрим на системный вызов dup():

SYSTEM CALL: dup();
PROTOTYPE: int dup( int oldfd );
  RETURNS: new descriptor on success
           -1 on error: errno = EBADF (oldfd некорректен)
                                EBADF ($newfd is out of range$)
                                EMFILE (слишком много дескрипторов для
                                        процесса)

NOTES: старый дескриптор не закрыт! Оба работают совместно!

Несмотря на то, что старый и новосозданный дескрипторы взаимозаменяемы, мы будем сначала закрывать один из стандартных потоков. Системный вызов dup() использует наименьший по номеру неиспользуемый дескриптор для нового.

Рассмотрим:

        .
        .
        childpid = fork();
        if(childpid == 0)
        {
                /* Закрываем стандартный ввод потомка */
                close(0);
                /* Дублируем вход канала на stdin */
                dup(fd[0]);
                execlp("sort", "sort", NULL);
                .
        }

Поскольку файловый дескриптор 0 (stdin) был закрыт, вызов dup() дублировал дескриптор ввода канала (fd0) на его стандартный ввод. Затем мы сделали вызов execlp(), чтобы покрыть код потомка кодом программы sort. Поскольку стандартные потоки exec()-нутой программы наследуются от родителей, это означает, что вход канала ста для потомка стандартным вводом! Теперь все, что первоначальный процесс-родитель посылает в канал, идет в sort.

Существует другой системный вызов, dup2(), который также может использоваться. Этот особенный вызов произошел с Version 7 of UNIX и был поддержан BSD, и теперь требуется по стандарту POSIX.

SYSTEM CALL: dup2();
PROTOTYPE: int dup2( int oldfd, int newfd );
  RETURNS: новый дескриптор в случае успеха
           -1 в случае ошибки: errno = EBADF (oldfd некорректен)
                                EBADF ($newfd is out of range$)
                                EMFILE (слишком много дескрипторов для
                                       процесса)
NOTES: старый дескриптор закрыл dup2()!

Благодаря этому особенному вызову мы имеем закрытую операцию и действующую копию за один системный вызов. Вдобавок, он гарантированно неделим, что означает, что он никогда не будет прерван поступающим сигналом. С первым системным вызовом dup() программисты были вынуждены предварительно выполнять операцию close(). Это приводило к наличию двух системных вызовов с малой степенью защищенности в краткий промежуток времени между ними. Если бы сигнал поступил в течение этого интервала времени, копия дескриптора не состоялась бы. dup2() разрешает для нас эту проблему.

Рассмотрим:

   .
   .
   childpid = fork();
   if(childpid == 0)
   {
      /* Закрываем стандартный ввод, дублируем вход канала на
          стандартный ввод */
      dup2(0, fd[0]);
      execlp("sort", "sort", NULL);
      .
      .
   }

6.2.3. Каналы - легкий путь!

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

LIBRARY FUNCTION: popen();
PROTOTYPE: FILE *popen ( char *command, char *type );
   RETURNS: новый файловый поток в случае успеха
            NULL при неудачном fork() или pipe()
NOTES: создает канал, и выполняет fork/exec, используя command

Эта стандартная библиотечная функция создает полудуплексный канал посредством вызывания pipe() внутренне. Затем она порождает дочерний процесс, запускает Bourne shell и исполняет аргумент command внутри shell-а. Управление потоком данных определяется вторым аргументом, type. Он может быть "r" или "w" - для чтения или записи, но не может быть и то, и другое! Под Linux-ом канал будет открыт в виде, определенном первой литерой аргумента "type". Поэтому, если вы попытаетесь ввести "rw", канал будет открыт только в виде "read".

Каналы, созданные popen(), должны быть закрыты pclose(). К этому моменту вы, вероятно, уже использовали [реализовали] popen/pclose share, удивительно похожий на стандартный файловый поток I/O функций

fopen() и fclose().LIBRARY FUNCTION: pclose();
PROTOTYPE: int pclose( FILE *stream )
   RETURNS: выход из статуса системного вызова wait4()
            -1, если "stream" некорректен или облом с wait4()
NOTES: ожидает окончания связанного каналом процесса, затем закрывает поток.

Функция pclose() выполняет wait4() над процессом, порожденным popen()-ом. Когда она возвращается, то уничтожает канал и файловый поток. Повторим еще раз, что этот эффект аналогичен эффекту, вызываемому функцией fclose() для нормального, основанного на потоке файлового ввода/вывода.

Рассмотрим пример, который открывает канал для команды сортировки и начинает сортировать массив строк:

/****************************************************************************
 Excerpt from "Linux Programmer's Guide - Chapter 6"
 (C)opyright 1994-1995, Scott Burkett
 ****************************************************************************
 MODULE: popen1.c
 ****************************************************************************/
#include
#define MAXSTRS 5
int main(void)
{
   int cntr;
   FILE *pipe_fp;
   char *strings[MAXSTRS] = {"echo", "bravo", "alpha", "charlie", "delta"};
   /* Создаем односторонний канал вызовом popen() */
   if (( pipe_fp = popen("sort", "w")) == NULL)
   {
      perror("popen");
      exit(1);
   }
   /* Цикл */
   for(cntr=0; cntr /tmp/foo", "w")
popen("sort | uniq | more", "w");

В качестве другого примера popen()-а, рассмотрим маленькую программу, открывающую два канала (один - для команды ls, другой - для сортировки):

****************************************************************************
 Excerpt from "Linux Programmer's Guide - Chapter 6"
 (C)opyright 1994-1995, Scott Burkett
 ****************************************************************************
 MODULE: popen2.c
 ****************************************************************************/
#include
int main(void)
{
   FILE *pipein_fp, *pipeout_fp;
   char readbuf[80];
   /* Создаем односторонний канал вызовом popen() */
   if (( pipein_fp = popen("ls", "r")) == NULL)
   {
      perror("popen");
      exit(1);
   }
   /* Создаем односторонний канал вызовом popen() */
   if (( pipeout_fp = popen("sort", "w")) == NULL)
   {
      perror("popen");
      exit(1);
   }
   /* Цикл */
   while(fgets(readbuf, 80, pipein_fp))
      fputs(readbuf, pipeout_fp);
   /* Закрываем каналы */
   pclose(pipein_fp);
   pclose(pipeout_fp);
   return(0);
}

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

/****************************************************************************
 Excerpt from "Linux Programmer's Guide - Chapter 6"
 (C)opyright 1994-1995, Scott Burkett
 ****************************************************************************
 MODULE: popen3.c
 ****************************************************************************/
#include
int main(int argc, char *argv[])
{
   FILE *pipe_fp, *infile;
   char readbuf[80];
   if( argc != 3 ) {
      fpintf(stderr, "USAGE": popen3 [command] [filename]\n);
      exit(1);
   }
   /* Открываем вводимый файл */
   if (( infile = popen(argv[2], "rt")) == NULL)
   {
      perror("fopen");
      exit(1);
   }
   /* Создаем односторонний канал вызовом popen() */
   if (( pipe_fp = popen(argv[1], "w")) == NULL)
   {
      perror("popen");
      exit(1);
   }
   /* Цикл */
   do {
      fgets(readbuf, 80, infile);
      if(feof(infile)) break;
      fputs(readbuf, pipe_fp);
   } while(!feof(infile));
   fclose(infile);
   pclose(pipe_fp);
   return(0);
}

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

popen3 sort popen3.c
popen3 cat popen3.c
popen3 more popen3.c
popen3 cat popen3.c | grep main

6.2.4. Атомарные (неделимые) операции с каналами

Для того чтобы операция рассматривалась как "атомарная", она не должна прерываться ни по какой причине. Неделимая операция выполняется сразу. POSIX стандарт говорит в /usr/include/posix_lim.h, что максимальные размеры буфера для атомарной операции в канале таковы:

#define _POSIX_PIPE_BUF      512

Атомарно по каналу может быть получено или записано до 512 байт. Все, что выходит за эти пределы, будет разбито и не будет выполняться атомарно. Однако, под Linux-ом этот атомарный операционный лимит определен в "linux/limits.h" следующим образом:

#define PIPE_BUF      4096

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

6.2.5. Примечания к полудуплексным каналам

  • Двусторонние каналы могут быть созданы посредством открывания двух каналов и правильным переопределением файловых дескрипторов в процессе-потомке.
  • Вызов pipe() должен быть произведен ПЕРЕД вызовом fork(), или дескрипторы не будут унаследованы процессом-потомком! (то же для popen()).
  • С полудуплексными каналами любые связанные процессы должны разделять происхождение. Поскольку канал находится в пределах ядра, любой процесс, не состоящий в родстве с создателем канала, не имеет способа адресовать его. Это не относится к случаю с именованными каналами (FIFOS).

6.3. Именованные каналы (FIFOs - First In First Out)

6.3.1. Основные понятия

Именованные каналы во многом работают так же, как и обычные каналы, но все же имеют несколько заметных отличий.

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

6.3.2. Создание FIFO

Есть несколько способов создания именованного канала. Первые два могут быть осуществлены непосредственно из shell-а.

mknod MYFIFO p
mkfifo a=rw MYFIFO

Эти две команды выполняют идентичные операции, за одним исключением. Команда mkfifo предоставляет возможность для изменения прав доступа к файлу FIFO непосредственно после создания. При использовании mknod будет необходим вызов команды chmod.

Файлы FIFO могут быть быстро идентифицированы в физической файловой системе посредством индикатора "p", представленного здесь в длинном листинге директории.

$ ls -1 MYFIFO
^prw-r--r--  1 root  root      0 Dec 14 22:15 MYFIFO| ...

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

Linux, eh?

Чтобы создать FIFO на Си, мы можем прибегнуть к использованию системного вызова mknod():

LIBRARY FUNCTION: mknod();
PROTOTYPE: int mknod( char *pathname, mode_t mode, dev_t dev );
   RETURNS: 0 в случае успеха,
            -1 в случае ошибки:
               errno = EFAULT (ошибочно указан путь)
                       EACCESS (нет прав)
                       ENAMETOOLONG (слишком длинный путь)
                       ENOENT (ошибочно указан путь)
                       ENOTDIR (ошибочно указан путь)
                       (остальные смотрите в man page для mknod)
     NOTES: Создает узел файловой системы (файл, файл устройства или
FIFO)

Оставим более детальное обсуждение mknod()-а man page, а сейчас давайте рассмотрим простой пример создания FIFO на Си:

   mknod("/tmp/MYFIFO", S_IFIFO|0666, 0);

В данном случае файл "/tmp/MYFIFO" создан как FIFO-файл. Требуемые права - это "0666", хотя они находятся под влиянием установки umask, как например:

   final_umask = requested_permissions & ~original_umask ...

Общая хитрость - использовать системный вызов umask() для того, чтобы временно устранить значение umask-а:

   umask(0);
   mknod("/tmp/MYFIFO", S_IFIFO|0666, 0);

Кроме того, третий аргумент mknod()-а игнорируется, в противном случае мы создаем файл устройства. В этом случае он должен отметить верхнее и нижнее числа файла устройства.

6.3.3. Операции FIFO

Операции ввода/вывода FIFO, по существу, такие же, как для обычных каналов, за одним исключением. Чтобы физически открыть проход к каналу, должен быть использован системный вызов "open" или библиотечная функция. С полудуплексными каналами это невозможно, поскольку канал находится в ядре, а не в физической файловой системе. В нашем примере мы будем трактовать канал как поток, открывая его fopen()-ом и закрывая fclose()-ом.

Рассмотрим простой сервер-процесс:

/****************************************************************************
 Excerpt from "Linux Programmer's Guide - Chapter 6"
 (C)opyright 1994-1995, Scott Burkett
 ****************************************************************************
 MODULE: fifoserver.c
 ****************************************************************************
#include
#include #include
#include
#include
#define FIFO_FILE   "MYFIFO"
int main(void)
{
   FILE *fp;
   char readbuf[80];
   /* Создаем FIFO, если он еще не существует */
   umask(0);
   mknod(FIFO_FILE, S_IFIFO|0666, 0);
   while(1)
   {
      fp = fopen(FIFO_FILE, "r");
      fgets(readbuf, 80, fp);
      printf("Received string: %s\n", readbuf);
      fclose(fp);
   }
   return(0);
}

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

   $ fifoserver&

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

/****************************************************************************
 Excerpt from "Linux Programmer's Guide - Chapter 6"
 (C)opyright 1994-1995, Scott Burkett
 ****************************************************************************
 MODULE: fifoclient.c
 ****************************************************************************
#include
#include
#define FIFO_FILE   "MYFIFO"int main(int argc, char *argv[])
{
   FILE *fp;
   if ( argc != 2 ) {
      printf("USAGE: fifoclient [string]\n");
      exit(1);
   }
   fputs(argv[1], fp);
   fclose(fp);
   return(0);
}

6.3.4. Действие блокирования над FIFO

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

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

6.3.5. Неизвестный SIGPIPE

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

6.4 System V IPC

6.4.1. Базовые понятия

Вместе с System V AT&T предложил три новых типа IPC средств (очереди сообщений, семафоры и разделяемая память). POSIX еще не стандартизировал эти средства, но большинство разработок их уже поддерживает. Впрочем, Беркли (BSD) в качестве базовой формы IPC использует скорее сокеты, чем элементы System V. Linux имеет возможность использовать оба вида IPC (BSD и System V), хотя мы не будем обсуждать сокеты в этой главе.

Версия System V IPC для LINUX-а авторизована Кришной Баласубраманьяном (Krishna Balasubramanian), balasub@cis.ohio-state.edu.

Идентификаторы IPC

Каждый объект IPC имеет уникальный IPC идентификатор. (Когда мы говорим "объект IPC", мы подразумеваем очередь единичных сообщений, множество семафоров или разделяемый сегмент памяти.) Этот идентификатор требуется ядру для однозначного определения объекта IPC. Например, чтобы сослаться на определенный разделяемый сегмент, единственное, что вам потребуется, это уникальное значение ID, которое привязано к этому сегменту.

Идентификатор IPC уникален только для своего типа объектов. То есть, скажем, возможна только одна очередь сообщений с идентификатором "12345", так же как номер "12345" может иметь какое-нибудь одно множество семафоров или (и) какой-то разделяемый сегмент.

Ключи IPC

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

(Чтобы позвонить кому-либо по телефону, вы должны знать его номер. Кроме того, телефонная компания должна знать как провести ваш вызов к адресату. И только когда этот адресат ответит, связь состоится.)

В случае System V IPC "телефон" соединяет объекты IPC одного типа. Под "телефонной компанией", или методом маршрутизации, следует понимать ключ IPC.

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

   LIBRARY FUNCTION: ftok();
   PROTOTYPE: key_t ftok( char *pathname, char proj );
     RETURNS: новый IPC ключ в случае успеха
              -1 в случае неудачи, errno устанавливается как значение вызова
                  stat()

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

         key_t   mykey;
         mykey = ftok ("/tmp/myapp", 'a');

В предложенном выше куске директория /tmp/myapp смешивается с однолитерным идентификатором 'a'. Другой распространенный пример - использовать текущую директорию.

         key_t   mykey;
         mykey = ftok(".", 'a');

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

Итак, значение ключа, когда оно получено, используется в последующих системных вызовах IPC для создания или улучшения доступа к объектам IPC.

Команда ipcs выдает статус всех объектов System V IPC.

LINUX-версия ipcs также была авторизована Кришной Баласубраманьяном.

   ipcs      -q:   показать только очереди сообщений
   ipcs      -s:   показать только семапхоры
   ipcs      -m:   показать только разделяемую память
   ipcs  --help:   для любознательных

По умолчанию показывают все три категории объектов. Посмотрим на следующий незатейливый вывод ipcs-а:

------ Shared Memory Segments --------
shmid     owner     perms     bytes     nattch     status
------ Semaphore Arrays --------
^semid     owner     perms     nsems     status
------ Message Queues --------
msqid     owner     perms     used-butes     messages
0         root      660       5              1

Здесь мы видим одинокую очередь с идентификатором "0". Она принадлежит пользователю root и имеет восьмеричные права доступа 660, или -rw-rw---. Очередь содержит одно пятибайтное сообщение.

Команда ipcs - это очень мощное средство, позволяющее подсматривать за механизмом ядреной памяти для IPC-объектов. Изучайте его, пользуйтесь им, благоговейте перед ним.

Команда ipcrm

Команда ipcrm удаляет объект IPC из ядра. Однако, поскольку объекты IPC можно удалить через системные вызовы в программе пользователя (как это делать мы увидим чуть позднее), часто нужды удалять их "вручную" нет. Особенно это касается всяких программных оболочек.

Внешний вид ipcrm прост:

         ipcrm

Требуется сказать, является ли удаляемый объект очередью сообщений (msg), набором семафоров (sem), или сегментом разделяемой памяти (shm). IPC ID может быть получен через команду ipcs. Напомним, что ID уникален в пределах одного из трех типов объектов IPC, поэтому мы обязаны назвать этот тип.

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

Базовые принципы

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

Внутренние и пользовательские структуры данных

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

Буфер сообщения

Первой структурой, которую мы рассмотрим, будет msgbuf. Его можно понимать как шаблон для данных сообщения. Поскольку данные в сообщении программист определяет сам, он обязан понимать, что на самом деле они являются структурой msgbuf. Его описание находится в linux/msg.h:

/* буфер сообщения для вызовов msgsnd и msgrcv*/
struct msgbuf {
    long mtype;          /* тип сообщения */
    char mtext[1];       /* текст сообщения */
};
   mtype

Тип сообщения, представленный натуральным числом. Он обязан быть натуральным!

   mtext

Собственно сообщение.

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

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

struct my_msgbuf {
   long   mtype;         /* тип сообщения */
   long   request_id;    /* идентификатор запроса */
   struct client info;   /* информация о клиенте */
}

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

Существует, однако, ограничение на максимальный размер сообщения.В LINUX-е он определен в linux/msg.h:

#define MSGMAX 4056 /* <= 4056 */ /* максимальный размер сообщения,
                                     в байтах*/

Сообщения не могут быть больше, чем 4056 байт, сюда входит и элемент mtype, который занимает 4 байта (long).

Структура msg ядра

Ядро хранит сообщение в очереди структуры msg. Она определена в linux/msg.h следующим образом:

struct msg {
   struct msg *msg_next;   /* следующее сообщение в очереди */
   long   msg_type;
   char  *msg_spot;        /* адрес текста сообщения */
   short  msg_ts;          /* размер текста */
};
   msg_next

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

   msg_type
     Тип сообщения, каким он был объявлен в msgbuf.
   msg_spot
     Указатель на начало тела сообщения.
   msg_ts
     Длина текста (или тела) сообщения.
     Структура msqid_ds ядра

Каждый из трех типов IPC-объектов имеет внутреннее представление, которое поддерживается ядром. Для очередей сообщений это структура msqid_ds. Ядро создает, хранит и сопровождает образец такой структуры для каждой очереди сообщений в системе. Она определена в linux/msg.h следующим образом:

/* структура msqid для каждой очереди в системе */
struct msqid_ds {
   struct ipc_perm msg_perm;
   struct msg *msg_first;     /* первое сообщение в очереди */
   struct msg *msg_last;      /* последнее сообщение в очереди */
   time_t msg_stime;          /* время последнего вызова msgsnd */
   time_t msg_rtime;          /* время последнего вызова msgrcv */
   time_t msg_ctime;          /* время последнего изменения */
   struct wait_queue *wwait;
   struct wait_queue *rwait;
   ushort msg_cbytes;
   ushort msg_qnum;
   ushort msg_qbytes;         /* максимальное число байтов на очередь */
   ushort msg_lspid;          /* pid последнего испустившего msgsnd */
   ushort msg_lrpid;          /* последний полученный pid */
};

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

msg_perm

Экземпляр структуры ipc_perm, определенной в linux/ipc.h. Она содержит информацию о доступе для очереди сообщений, включая права доступа и информацию о создателе сообщения (uid и т.п.).

msg_first

Ссылка на первое сообщение в очереди (голова списка).

msg_last

Ссылка на последний элемент списка (хвост списка).

msg_stime

Момент времени (time_t) посылки последнего сообщения из очереди.

msg_rtime

Момент времени последнего изъятия элемента из очереди.

msg_ctime

Момент времени последнего изменения, проделанного в очереди (подробнее об этом позже).

wwait и rwait

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

msg_cbytes

Число байт, стоящих в очереди (суммарный размер всех сообщений).

msg_qnum

Количество сообщений в очереди на настоящий момент.

msg_qbytes

Максимальный размер очереди.

msg_lspid

PID процесса, пославшего последнее в очереди сообщение.

msg_lrpid

PID последнего процесса, взявшего из очереди сообщение.

Структура ipc_perm ядра

Информацию о доступе к IPC-объектам ядро хранит в структуре ipc_perm. Например, описанная выше структура очереди сообщений содержит одну структуру типа ipc_perm в качестве элемента. Следующее ее определение дано в linux/ipc.h.

struct ipc_perm {
   key_t  key;
   ushort uid;    /* euid и egid владельца */
   ushort gid;   ushort cuid;   /* euid и egid создателя */
   ushort cgid;
   ushort mode;   /* режим доступа, см. режимные флаги ниже */
   ushort seq;    /* порядковый номер использования гнезда */
};

Все приведенное выше говорит само за себя. Сохраняемая отдельно вместе с ключом IPC-объекта информация содержит данные о владельце и создателе этого объекта (они могут различаться). Режимы восьмеричного доступа также хранятся здесь, как unsigned short. Наконец, сохраняется порядковый номер использования гнезда. Каждый раз когда IPC объект закрывается через системный вызов (уничтожается), этот номер уменьшается на максимальное число объектов IPC, которые могут находиться в системе. Касается вас это значение? Нет.

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

SYSTEM CALL: msgget()
PROTOTYPE: int msgget( key_t key, int msgflg );
  RETURNS: идентификатор очереди сообщений в случае успеха;
           -1 в случае ошибки. При этом
                errno = EACCESS (доступ отклонен)
                        EEXIST (такая очередь уже есть, создание невозможно)
                        EIDRM (очередь помечена как удаляемая)
                        ENOENT (очередь не существует)
                        ENOMEM (не хватает памяти для создания новой очереди)
                        ENOSPC (исчерпан лимит на количество очередей)

NOTES:

Первый аргумент msgget() значение ключа (мы его получаем при помощи ftok()). Этот ключ сравнивается с ключами уже существующих в ядре очередей. При этом операция открытия или доступа к очереди зависит от содержимого аргумента msgflg:

IPC_CREAT

Создает очередь, если она не была создана ранее.

IPC_EXCL

При использовании совместно с IPC_CREAT, приводит к неудаче если очередь уже существует.

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

Восьмеричный режим может быть OR-нут в маску доступа. Каждый IPC-объект имеет права доступа, аналогичные правам доступа к файлу в файловой системе UNIX-а.

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

int open_queue( key_t keyval )
{
   int  qid;
   if ((qid = msgget ( keyval, IPC_CREAT | 0660 )) == -1)
   {
      return (-1);
   }
   return (qid);
}

Отметьте использование точного ограничителя доступа 0660. Эта небольшая функция возвращает идентификатор очереди (int) или -1 в случае ошибки. Единственный требуемый аргумент - ключевое значение.

Системный вызов msgsnd()

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

SYSTEM CALL: msgsnd();
PROTOTYPE: int msgsnd( int msqid, struct msgbuf *msgp, int msgsz,
                       int msgflg );
  RETURNS: 0 в случае успеха
           -1 в случае ошибки:
              errno =
     EAGAIN (очередь переполнена, и установлен IPC_NOWAIT)
     EACCES (доступ отклонен, нет разрешения на запись)
     EFAULT (адрес msgp недоступен, неверно...)
     EIDRM (очередь сообщений удалена)
     EINTR (получен сигнал во время ожидания печати)
     EINVAL (ошибочный идентификатор очереди сообщений,
неположительный тип сообщения или неправильный размер сообщения)
     ENOMEM (не хватает памяти для копии буфера сообщения)
     NOTES:

Первый аргумент msgsnd - идентификатор нашей очереди, возвращенный предварительным вызовом msgget. Второй аргумент, msgp - это указатель на редекларированный и загруженный буфер сообщения. Аргумент msgsz содержит длину сообщения в байтах, не учитывая тип сообщения (long 4 байта).

     Аргумент msgflg может быть нулем или:
     IPC_NOWAIT

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

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

int send_message( int qid, struct mymsgbuf *qbuf )
{
   int  result, length;
   /* Длина есть в точности размер структуры минус sizeof(mtype) */
   length = sizeof(struct mymsgbuf) - sizeof(long);
   if((result = msgsnd( qid, qbuf, length, 0)) == -1)
   {
       return(-1);
   }
   return(result);
}

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

#include
#include
#include
#include
main()
{
   int qid;
   key_t msgkey;
   struct mymsgbuf {
      long   mtype;     /* тип сообщения */
      int    request;   /* рабочий номер запроса */
      double salary;    /* зарплата */
   } msg;
   /* Генерируем IPC-ключ */
   msgkey = ftok(".", 'm');
   /* Открываем/создаем очередь */
   if (( qid = open_queue( msgkey)) == -1) {
      perror("open_queue");
      exit(1);
   }
   /* Заполняем сообщение произвольными тестовыми данными */
   msg.type    = 1;  /* тип сообщения должен быть положительным! */
   msg.request = 1;  /* элемент данных 1 */
   msg.salary  = 1000.00; /* элемент данных 2
                             (моя годовая зарплата! - авт.) */
   /* Бомбим! */
   if((send_message( qid, &msg )) == -1) {
     perror("send_message");
     exit(1);
   }
}

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

Теперь, когда мы имеем сообщение в очереди, попытайтесь при помощи ipcs посмотреть на статус нашей очереди. Обсудим, как забрать из очереди сообщение. Для этого используется системный вызов msgrcv():

SYSTEM CALL: msgrcv();
PROTOTYPE: int msgrcv( int msqid, struct msgbuf *msgp, int msgsz,
                       long mtype, $$)
  RETURNS: число байт, скопированных в буфер сообщения
           -1 в случае ошибки:
              errno = E2BIG (длина сообщения больше, чем msgsz, $$)
                      EACCES (нет права на чтение)
                      EFAULT (адрес, на который указывает msgp, ошибочен)
                      EIDRM (очередь была уничтожена в период изъятия
                             сообщения)
                      EINTR (прервано поступившим сигналом)
                      EINVAL (msgqid ошибочен или msgsz меньше 0)
                      ENOMSG (установлен IPC_NOWAIT, но в очереди нет
                              ни одного сообщения, удовлетворяющего запросу)
NOTES:

Конечно, первый аргумент определяет очередь, из которой будет взято сообщение (должен быть возвращен сделанным предварительно вызовом msgget). Второй аргумент (msgp) представляет собой адрес буфера, куда будет положено изъятое сообщение. Третий аргумент, msgsz, ограничивает размер структуры-буфера без учета длины элемента mtype.

Еще раз повторимся, это может быть легко вычислено:

msgsz = sizeof(struct mymsgbuf) - sizeof(long);

Четвертый аргумент, mtype - это тип сообщения, изымаемого из очереди. Ядро будет искать в очереди наиболее старое сообщение такого типа и вернет его копию по адресу, указанному аргументом msgp. Существует один особый случай: если mtype = 0, то будет возвращено наиболее старое сообщение, независимо от типа.

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

Давайте рассмотрим функцию-переходник для изъятия сообщения из нашей очереди.

int read_message( int qid, long type, struct mymsgbuf *qbuf )
{
   int   result, length;
   /* Длина есть в точности размер структуры минус sizeof(mtype) */
   length = sizeof(struct mymsgbuf) - sizeof(long);
   if((result = msgrcv( qid, qbuf, length, type, 0 )) == -1)
   {
      return(-1);
   }
   return(result);
}

После успешного изъятия сообщения удаляется из очереди и его ярлык.

Бит MSG_NOERROR в msgflg предоставляет некоторые дополнительные возможности. Если физическая длина сообщения больше, чем msgsz, и MSG_NOERROR установлен, то сообщение обрезается и возвращается только msgsz байт. Нормальный же msgrcv() возвращает -1 (E2BIG), и сообщение остается в очереди до последующих запросов. Такое поведение можно использовать для создания другой оберточной функции, которая позволит нам "подглядывать" внутрь очереди, чтобы узнать, пришло ли сообщение, удовлетворяющее нашему запросу.

int peek_message( int qid, long type )
{
   int   result, length;
   if((result = msgrcv( qid, NULL, 0, type, IPC_NOWAIT )) == -1)
   {      if(errno == E2BIG)
         return(TRUE);
   }
   return(FALSE);
}

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

Системный вызов msgсtl()

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

Для осуществления контроля над очередью предназначен системный вызов msgсtl.

SYSTEM CALL: msgctl()
PROTOTYPE: int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
  RETURNS: 0 в случае успеха
           -1 в случае неудачи
              errno = EACCES (нет прав на чтение и cmd есть IPC_STAT)
                      EFAULT (адрес, на который указывает buf, ошибочен
                              для команд IPC_SET и IPC_STAT)
                      EIDRM  (очередь была уничтожена во время запроса)
                      EINVAL (ошибочный msqid или msgsz меньше 0)
                      EPERM  (IPC_SET- или IPC_RMID-команда была
                              послана процессом, не имеющим прав на запись
                              (в очередь))
NOTES:

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

IPC_STAT

Сохраняет по адресу buf структуру msqid_ds для очереди сообщений.

IPC_SET

Устанавливает значение элемента ipc_perm структуры msqid. Значения выбирает из буфера.

IPC_RMID

Удаляет очередь из ядра.

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

int get_queue_ds( int qid, struct msgqid_ds *qbuf )
{
   if( msgctl( qid, IPC_STAT, qbuf) == -1 )
   {
      return(-1);
   }
   return(0);
}

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

Что же мы можем делать с полученной копией структуры? Единственное, что можно поменять, это элемент ipc_perm. Это права доступа очереди, информация о создателе и владельце очереди. Однако и отсюда менять позволено только mode, uid и gid.

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

int change_queue_mode( int qid, char *mode )
{
   struct msqid_ds tmpbuf;
   /* Берем текущую копию внутренней структуры */
   get_queue_ds( qid, &tmpbuf );
   /* Применяем уже известный прикол для изменения прав доступа */
   sscanf(mode, "%ho", &tmpbuf.msg_perm.mode);   /* Модернизируем внутреннюю структуру */
   if( msgctl( qid, IPC_SET, &tmpbuf ) == -1 )
   {
      return(-1);
   }
   return(0);
}

Мы взяли текущую копию внутренней структуры данных посредством вызова нашей get_queue_ds; затем sscanf() меняет элемент mode структуры msg_perm. Однако ничего не произойдет, пока msgctl c IPC_SET не обновил внутреннюю версию.

ОСТОРОЖНО! Изменяя права доступа, можно случайно лишить прав себя самого! Помните, что IPC-объекты не исчезают, пока они не уничтожены должным образом или не перезагружена система. Поэтому то, что вы не видите очереди ipcs-ом, не означает, что ее нет на самом деле.

После того, как сообщение взято из очереди, оно удаляется. Однако, как отмечалось ранее, IPC-объекты остаются в системе до персонального удаления или перезапуска всей системы. Поэтому наша очередь сообщений все еще существует в ядре и пригодна к употреблению в любое время, несмотря на то, что последнее его соообщение уже давно на небесах. Чтобы и душа нашей очереди с миром отошла к богам, нужен вызов msgctl(), использующий команду IPC_RMID:

int remove_queue( int qid )
{
   if( msgctl( qid, IPC_RMID, 0) == -1)
   {
      return(-1)
   }
   return(0);
}

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

msgtool: интерактивный обработчик очередей сообщений

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

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

Описание

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

     Синтаксис командной строки
     Посылка сообщений
     msgtool s (type) "text"
     Изъятие сообщений
     msgtool к (type)
     Изменение прав доступа
     msgtool (mode)
     Уничтожение очереди
     msgtool d
              Примеры
   msgtool  s  1 test
   msgtool  s  5 test
   msgtool  s  1 "This is test"
   msgtool  r  1
   msgtool  d
   msgtool  m  660

Код

Следующее, что мы рассмотрим, это исходный текст msgtool. Его следует компилировать в версии системы, которая поддерживает System V IPC. Убедитесь в наличии System V IPC в ядре, когда будете пересобирать программу!

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

Замечание. Поскольку msgtool использует ftok() для генерации ключей IPC, вы можете нарваться на конфликты, связанные с директориями. Если вы где-то в скрипте меняете директории, то все это наверняка не сработает. Это обходится путем более явного указания пути в msgtool, вроде "/tmp/msgtool", или даже запроса пути из командной строки вместе с остальными аргументами.

/****************************************************************************
 Excerpt from "Linux Programmer's Guide - Chapter 6"
 (C)opyright 1994-1995, Scott Burkett
 ****************************************************************************
 MODULE: msgtool.c
 ****************************************************************************
 Средство командной строки для работы со очередями сообщений в стиле SysV
 ****************************************************************************/
#include
#include
#include
#include
#include
#include
#define MAX_SEND_SIZE 80
struct mymsgbuf {
   long mtype;
   char mtext[MAX_SEND_SIZE];
};
void send_message(int qid, struct mymsgbuf *qbuf, long type, char *text);
void read_message(int qid, struct mymsgbuf *qbuf, long type);
void remove_queue(int qid);
void change_queue_mode(int qid, char *mode);
void usage(void);
int main(int argc, char *argv[])
{
   key_t key;
   int msgqueue_id;
   struct mymsgbuf qbuf;
   if(argc == 1)
      usage();
   /* Создаем уникальный ключ через вызов ftok() */
   key = ftok(".",'m');
   /* Открываем очередь - при необходимости создаем */
   if((msgqueue_id = msgget(key, IPC_CREAT|0660)) == -1) {
      perror("msgget");
      exit(1);
   }
   switch(tolower(argv[1][0]))
   {
      case 's': send_message(msgqueue_id, (struct mymsg buf *)&qbuf,
                             atol(argv[2]), argv[3]);
                break;
      case 'r': read_message(msgqueue_id, &qbuf, atol(argv[2]));
                break;
      case 'd': remove_queue(msgqueue_id);
                break;
      case 'm': change_queue_mode(msgqueue_id, argv[2]);
                break;
      default:  usage();
   }   return(0);
}
void send_message(int qid, struct mymsgbuf *qbuf, long type, char *text)
{
   /* Посылаем сообщение в очередь */
   printf("Sending a message ...\n");
   qbuf->mtype = type;
   strcopy(qbuf->mtext, text);
   if((msgsnd(qid, (struct msgbuf *)qbuf,
          strlen(qbuf->mtext)+1, 0)) == -1)
   {
      perror("msgsnd");
      exit(1);
   }
}
void read_message(int qid, struct mymsgbuf *qbuf, long type)
{
  /* Вычитываем сообщение из очереди */
  printf("Reading a message ...\");
  qbuf->mtype = type;
  msgrcv(qid, (struct msgbuf *)qbuf, MAX_SEND_SIZE, type, 0);
  printf("Type: %ld Text: %s\n", qbuf->mtype, qbuf->mtext);
}
void remove_queue(int qid)
{
   /* Удаляем очередь */
   msgctl(qid, IPC_RMID, 0);
}
void change_queue_mode(int qid, char *mode)
{
   struct msqid_ds myqueue_ds;
   /* Получаем текущее состояние */
   msgctl(qid, IPC_STAT, &myqueue_ds);
   /* Меняем состояние в копии внутренней структуры данных */
   sscanf(mode, "%ho", &myqueue_ds.msg_perm.mode)
   /* Обновляем состояние в самой внутренней структуре данных */
   msgctl(qid, IPC_SET, &myqueue_ds);
}
void usage(void)
{
   fprintf(stderr, "msgtool - A utility for tinkering with msg queues\n");
   fprintf(stderr, "\nUSAGE: msgtool (s)end  \n");
   fprintf(stderr, "                 (r)ecv \n");
   fprintf(stderr, "                 (d)elete\n");
   fprintf(stderr, "                 (m)ode \n");
   exit(1);
}

Назад | Содержание | Вперед

 

VPS в России, Европе и США

Бесплатная поддержка и администрирование

Оплата российскими и международными картами

🔥 VPS до 5.7 ГГц под любые задачи с AntiDDoS в 7 локациях

💸 Гифткод CITFORUM (250р на баланс) и попробуйте уже сейчас!

🛒 Скидка 15% на первый платеж (в течение 24ч)

Скидка до 20% на услуги дата-центра. Аренда серверной стойки. Colocation от 1U!

Миграция в облако #SotelCloud. Виртуальный сервер в облаке. Выбрать конфигурацию на сайте!

Виртуальная АТС для вашего бизнеса. Приветственные бонусы для новых клиентов!

Виртуальные VPS серверы в РФ и ЕС

Dedicated серверы в РФ и ЕС

По промокоду CITFORUM скидка 30% на заказ VPS\VDS

Новости мира IT:

Архив новостей

IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware

Информация для рекламодателей PR-акции, размещение рекламы — adv@citforum.ru,
тел. +7 495 7861149
Пресс-релизы — pr@citforum.ru
Обратная связь
Информация для авторов
Rambler's Top100 TopList This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2019 CIT Forum
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Подробнее...