• Семафоры
  • Описание семафора
  • Теоретический пример
  • Реализация семафоров в Linux
  • Применение семафоров
  • Совместно используемая память
  • shmget
  • shmat
  • shmdt
  • shmctl
  • Очереди сообщений
  • msgget
  • msgsnd
  • msgrcv
  • msgctl
  • Приложение для работы с базой данных компакт-дисков
  • Пересмотр функций сервера
  • Пересмотр функций клиента
  • Команды состояния IPC
  • Отображение состояния семафора
  • Отображение состояния совместно используемой памяти
  • Отображение состояния очереди сообщений
  • Резюме
  • Глава 14

    Семафоры, совместно используемая память и очереди сообщений

    В этой главе мы обсудим набор средств, обеспечивающих взаимодействие процессов и первоначально введенных в версии ОС UNIX AT&T System V.2. Поскольку все эти средства появились в одном выпуске системы и обладают одинаковым программным интерфейсом, их часто называют средствами IPC (Inter-Process Communication, взаимодействие между процессами) или более полно System V IPC. Как вы уже видели, это далеко не единственный способ установления связи между процессами, но термин "System V IPC" обычно применяется для обозначения именно этих конкретных средств.

    В данной главе мы рассмотрим следующие темы:

    □ семафоры для управления доступом к ресурсам;

    □ совместно используемая память для эффективного использования общих данных разными программами;

    □ обмен сообщениями как легкий способ передачи данных между программами.

    Семафоры

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

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

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

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

    Примечание

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

    Написать программный код общего назначения, который гарантирует одной программе монопольный доступ к конкретному ресурсу, на удивление сложно, несмотря на то, что существует решение, известное как алгоритм Деккера (Dekker's Algorithm). К сожалению, этот алгоритм полагается на состояние активного ожидания или спин-блокировки, в котором процесс выполняется непрерывно, ожидая изменения адреса памяти. В многозадачной среде, какой является ОС Linux, это нежелательные расходы ресурсов ЦПУ. Ситуация существенно облегчается, когда для обеспечения монопольного доступа есть аппаратная поддержка, обычно в виде специальных команд ЦПУ. Примером аппаратной поддержки могла бы быть команда обращения к ресурсу и приращения регистра атомарным образом, так чтобы никакая другая команда (даже прерывание) не могла появиться между операциями чтения/инкремента/записи.

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

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

    Важный шаг вперед в сфере параллельного программирования был сделан, когда голландский специалист в области компьютерных наук Эдсгер Дейкстра (Edsger Dijkstra) предложил идею семафоров. Как уже кратко упоминалось в главе 12, семафор — это специальная переменная, которая принимает только целые положительные значения и с помощью которой программы могут действовать только атомарно. В этой главе мы расширим данное ранее упрощенное определение. Будет более подробно рассказано, как действуют семафоры и как для взаимодействия отдельных процессов применяются функции общего назначения вместо особого случая многопоточных программ, которые рассматривались в главе 12.

    Определяя более строго, семафор — это специальная переменная, для которой разрешены только две операции, формально именуемые ожиданием или приостановкой (wait) и оповещением (signal). Поскольку в программировании Linux у приостановки и оповещения уже есть специальные значения, мы будем применять оригинальное обозначение:

    □ 

    P
    (переменная-семафор) для приостановки (wait);

    □ 

    V
    (переменная-семафор) для оповещения (signal).

    Эти буквы взяты из голландских слов для приостановки (passeren — проходить, пропускать как в случае контрольной точки перед критической секцией) и для оповещения (vrijgeven — предоставлять или освобождать, как в случае отказа от контроля критической секции). Вы можете встретить термины "вверх" (up) и "вниз" (down), применяемые в отношении семафоров по аналогии с использованием сигнальных флажков.

    Описание семафора

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

    Определения операций

    P
    и
    V
    удивительно просты. Предположим, что у вас есть переменная-семафор
    sv
    . В этом случае обе операции определяются так, как представлено в табл. 14.1.


    Таблица 14.1

    Операция Описание
    Р(sv)
    Если
    sv
    больше нуля, она уменьшается на единицу. Если sv равна 0, выполнение данного процесса приостанавливается
    V(sv)
    Если какой-то другой процесс был приостановлен в ожидании семафора
    sv
    , переменная заставляет его возобновить выполнение. Если ни один процесс не приостановлен в ожидании семафора
    sv
    , значение переменной увеличивается на единицу

    Другой способ описания семафора — считать, что переменная

    sv
    , равная
    true
    , когда доступна критическая секция, уменьшается на единицу с помощью
    P(sv)
    и становится равна
    false
    , когда критическая секция занята, и увеличивается на единицу операцией
    V(sv)
    , когда критическая секция снова доступна. Имейте в виду, что обычная переменная, которую вы уменьшаете и увеличиваете на единицу, не годится, т.к. в языках С, С++, C# или практически в любом традиционном языке программирования у вас нет возможности сформировать единую атомарную операцию, проверяющую, равна ли переменная
    true
    , и если это так, изменяющую ее значение на
    false
    . Именно эта функциональная возможность делает операции с семафором особенными.

    Теоретический пример

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

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

    Оба процесса совместно используют переменную-семафор

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

    Требуемый псевдокод у обоих процессов идентичен:

    semaphore sv = 1;

    loop forever {

     P(sv);

     critical code section;

     V(sv);

     noncritical code section;

    }

    Код на удивление прост, потому что определение операций

    P
    и
    V
    наделяет их большими функциональными возможностями.

    Рис. 14.1 


    На рис. 14.1 показана схема действующих операций

    P
    и
    V
    , напоминающих ворота в критических секциях программного кода.

    Реализация семафоров в Linux

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

    Далее приведены объявления функций семафоров:

    #include <sys/sem.h>

    int semctl(int sem_id, int sem_num, int command, ...);

    int semget(key_t key, int num_sems, int sem_flags);

    int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

    Примечание

    Обычно заголовочный файл sys/sem.h опирается на два других заголовочных файла: sys/types.h и sys/ipc.h. Как правило, они автоматически включаются в программу файлом sys/sem.h и вам не нужно задавать их явно в директивах

    #include
    .

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

    Обратите внимание на то, что параметр

    key
    действует во многом как имя файла, т.к. он тоже представляет ресурс, который программы могут использовать и кооперироваться при этом, если соблюдают соглашение об общем имени для него. Аналогичным образом идентификатор, возвращаемый функцией
    semget
    и применяемый другими функциями, совместно использующими память, очень похож на файловый поток
    FILE*
    , возвращаемый функцией
    fopen
    и представляющий собой значение, применяемое процессом для доступа к совместно используемому файлу. Как и в случае файлов, у разных процессов будут разные идентификаторы семафоров, несмотря на то, что они ссылаются на один и тот же семафор. Такое применение ключа и идентификаторов — общее для всех средств IPC, обсуждаемых здесь, несмотря на то, что каждое средство применяет независимые ключи и идентификаторы.

    semget

    Функция

    semget
    создает новый семафор или получает ключ существующего семафора.

    int semget(key_t key, int num_sems, int sem_flags);

    Первый параметр

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

    Существует особое значение ключа семафора

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

    Параметр

    num_sems
    определяет количество требуемых семафоров. Почти всегда он равен 1.

    Параметр

    sem_flags
    — набор флагов, очень похожих на флаги функции open. Младшие девять байтов — права доступа к семафору, ведущие себя, как права доступа к файлу. Кроме того, для создания нового семафора с помощью поразрядной операции
    OR
    их можно объединить со значением
    IPC_CREAT
    . Не считается ошибкой наличие флага
    IPC_CREAT
    и задание ключа существующего семафора. Флаг
    IPC_CREAT
    безмолвно игнорируется, если в нем нет нужды. Можно применять флаги
    IPC_CREAT
    и
    IPC_EXCL
    для гарантированного получения нового уникального семафора. Если семафор уже существует, функция вернет ошибку.

    Функция

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

    semop

    Функция semop применяется для изменения значения семафора.

    int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

    Первый параметр

    sem_id
    — идентификатор семафора, возвращенный функцией
    semget
    . Второй параметр
    sem_ops
    — указатель на массив структур, у каждой из которых есть, по крайней мере, следующие элементы:

    struct sembuf {

     short sem_num;

     short sem_op;

     short sem_flg;

    }

    Первый параметр

    sem_num
    — номер семафора, обычно 0, если вы не работаете с массивом семафоров. Элемент
    sem_op
    — значение, на которое должен изменяться семафор. (Вы можете увеличивать и уменьшать семафор на значения, не равные 1.) Как правило, применяются только два значения: -1 для операции
    P
    , заставляющей ждать, пока семафор не станет доступен, и +1 для операции
    V
    , оповещающей о том, что в данный момент семафор доступен.

    Последний элемент

    sem_flg
    обычно задается равным
    SEM_UNDO
    . Это значение заставляет операционную систему отслеживать изменения значения семафора, сделанные текущим процессом, и, если процесс завершается, не освободив семафор, позволяет операционной системе автоматически освободить семафор, если он удерживался этим процессом. Хорошо взять за правило установку
    sem_flg
    , равным
    SEM_UNDO
    , если вам не требуется иного поведения. Если же вы все-таки решили, что вам нужно значение, отличное от
    SEM_UNDO
    , очень важно быть последовательным, иначе вы можете оказаться в замешательстве относительно попыток ядра системы "убрать" ваши семафоры, когда ваш процесс завершается.

    Все действия, предусмотренные

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

    semctl

    Функция

    semctl
    позволяет напрямую управлять данными семафора.

    int semctl (int sem_id, int sem_num, int command, ...);

    Первый параметр sem_id — идентификатор семафора, полученный от функции

    semget
    . Параметр
    sem_num
    — номер семафора. Он применяется при работе с массивом семафоров. Обычно этот параметр равен 0, первый и единственный семафор. Параметр
    command
    — предпринимаемое действие, и четвертый параметр, если присутствует, —
    union
    (объединение) типа
    semun
    , которое в соответствии со стандартом X/Open должно содержать как минимум следующие элементы:

    union semun {

     int val;

     struct semid_ds *buf;

     unsigned short *array;

    }

    В большинстве версий ОС Linux определение объединения

    semun
    включено в заголовочный файл (обычно sem.h), несмотря на то, что стандарт X/Open настаивает на том, что вы должны привести собственное объявление. Если вы поймете, что должны объявить его самостоятельно, проверьте, нет ли объявления этого объединения на страницах интерактивного справочного руководства, относящихся к функции
    semctl
    . Если вы найдете его, мы полагаем, что вы примените определение из вашего справочного руководства, даже если оно отличается от приведенного на страницах этой книги.

    Существует множество разных значений параметра command, допустимых в функции

    semctl
    . Обычно применяются два из них, которые описаны далее. Более подробную информацию о функции
    semctl
    см. в интерактивном справочном руководстве.

    Два часто используемых значения

    command
    таковы:

    □ 

    SETVAL
    — применяется для инициализации семафора с заданным значением. Это значение передается как элемент
    val
    объединения
    semun
    . Такое действие необходимо для того, чтобы увеличить значение семафора перед первым его применением;

    □ 

    IPC_RMID
    — применяется для удаления идентификатора семафора, когда он больше не нужен.

    Функция

    semctl
    возвращает разные значения, зависящие от параметра
    command
    . Если значение команды —
    IPC_RMID
    , функция в случае успешного завершения вернет 0 и -1 в противном случае.

    Применение семафоров

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

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

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

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

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

    Упражнение 14.1. Семафоры

    1. После системных директив

    #include
    вы включаете файл semun.h. Он определяет объединение типа
    semun
    в соответствии со стандартом X/Open, если оно уже не описано в системном файле sys/sem.h. Далее следуют прототипы функций и глобальная переменная, расположенные перед входом в функцию
    main
    . В ней создается семафор с помощью вызова
    semget
    , который возвращает ID семафора. Если программа вызывается первый раз (т.е. вызывается с параметром и
    argc > 1
    ), выполняется вызов
    set_semvalue
    для инициализации семафора и переменной
    op_char
    присваивается значение
    O
    .

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <sys/sem.h>

    #include "semun.h"


    static int set_semvalue(void);

    static void del_semvalue(void);

    static int semaphore_p(void);

    static int semaphore_v(void);

    static int sem_id;


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

     int i;

     int pause_time;

     char op_char = 'О';

     srand((unsigned int)getpid());

     sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

     if (argc >1) {

      if (!set_semvalue()) {

       fprintf(stderr, "Failed to initialize semaphore\n");

       exit(EXIT_FAILURE);

      }

      op_char = 'X';

      sleep(2);

     }

    2. Далее следует цикл, в котором 10 раз выполняется вход в критическую секцию и выход из нее. Вы сначала выполняете вызов функции

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

     for (i = 0; i < 10; i++) {

      if (!semaphore_p()) exit(EXIT_FAILURE);

      printf("%c", op_char);

      fflush(stdout);

      pause_time = rand() % 3;

      sleep(pause_time);

      printf("%c", op_char);

      fflush(stdout);

    3. После критической секции вы вызываете функцию

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

      if (!semaphore_v()) exit(EXIT_FAILURE);

      pause_time = rand() % 2;

      sleep(pause_time);

     }

     printf("\n%d - finished\n", getpid());

     if (argc > 1) {

      sleep(10);

      del_semvalue();

     }

     exit(EXIT_SUCCESS);

    }

    4. Функция

    set_semvalue
    инициализирует семафор с помощью команды
    SETVAL
    в вызове
    semctl
    . Это следует сделать перед использованием семафора.

    static int set_semvalue(void) {

     union semun sem_union;

     sem_union.val = 1;

     if (semctl(sem_id, 0, SETVAL, sem_union) == -1) return(0);

     return(1);

    }

    5. У функции

    del_semvalue
    почти та же форма за исключением того, что в вызове
    semctl
    применяется команда
    IPC_RMID
    для удаления ID семафора.

    static void del_semvalue(void) {

     union semun sem_union;

     if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)

      fprintf(stderr, "Failed to delete semaphore\n");

    }

    6. Функция semaphore_p изменяет счетчик семафора на -1. Это операция ожидания или приостановки процесса.

    static int semaphore_p(void) {

     struct sembuf sem_b;

     sem_b.sem_num = 0;

     sem_b.sem_op = -1; /* P() */

     sem_b.sem_flg = SEM_UNDO;

     if (semop(sem_id, &sem_b, 1) == -1) {

      fprintf(stderr, "semaphore_p failed\n");

      return(0);

     }

     return(1);

    }

    7. Функция

    semaphore_v
    аналогична за исключением задания элемента
    sem_op
    структуры
    sembuf
    , равного 1. Это операция "освобождения", в результате которой семафор снова становится доступен.

    static int semaphore_v(void) {

     struct sembuf sem_b;

     sem_b.sem_num = 0;

     sem_b.sem_op = 1; /* V() */

     sem_b.sem_flg = SEM_UNDO;

     if (semop(sem_id, &sem_b, 1) == -1) {

      fprintf(stderr, "semaphore_v failed\n");

      return(0);

     }

     return(1);

    }

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

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

    Далее приведен примерный вывод для двух запущенных экземпляров программы:

    $ cc sem1.с -о sem1

    $ ./sem1 1 &

    [1] 1082

    $ ./sem1

    OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX

    1083 - finished

    1082 - finished

    Напоминаем, что символ

    О
    представляет первый запущенный экземпляр программы, а символ
    X
    — второй экземпляр выполняющейся программы. Поскольку каждый экземпляр программы выводит символ при входе в критическую секцию и при выходе из нее, каждый символ должен появляться только попарно. Как видите, символы
    О
    и
    Х
    на самом деле образуют пары, указывая на корректную обработку критических секций. Если программа не работает на вашей системе, можно применить команду
    stty -tostop
    перед запуском программы, чтобы гарантировать, что фоновая программа, генерирующая вывод на
    tty
    , не вызывает возбуждение сигнала.

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

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

    semget
    . Флаг
    IPC_CREAT
    приводит к созданию семафора, если он нужен.

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

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

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

    semaphore_p
    и
    semaphore_v
    , упрощенных интерфейсов функции более общего вида
    semop
    .

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

    Совместно используемая память

    Совместно используемая или разделяемая память — вторая разновидность средств IPC. Она позволяет двум несвязанным процессам обращаться к одной и той же логической памяти. Хотя стандарт X/Open не требует этого, надо полагать, что большинство реализаций разделяемой памяти размещают память, совместно используемую разными процессами, так, что она ссылается на одну и ту же физическую память.

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

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

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

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

    Рис. 14.2


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

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

    #include <sys/shm.h>

    void *shmat(int shm_id, const void *shm_addr, int shmflg);

    int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

    int shmdt(const void *shm_addr);

    int shmget(key_t key, size_t size, int shmflg);

    Как и в случае семафоров, заголовочные файлы sys/types.h и sys/ipc.h автоматически включаются в программу файлом shm.h.

    shmget

    Создается совместно используемая память с помощью функции

    shmget
    :

    int shmget(key_t key, size_t size, int shmflg);

    Как и для семафоров, программа предоставляет

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

    Второй параметр

    size
    задает требуемый объем памяти в байтах.

    Третий параметр

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

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

    Если совместно используемая память создана успешно,

    shmget
    вернет неотрицательное целое, идентификатор совместно используемой памяти. В случае аварийного завершения функция вернет -1.

    shmat

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

    shmat
    :

    void *shmat(int shm_id, const void *shm_addr, int shmflg);

    Первый параметр

    shm_id
    — идентификатор совместно используемой области памяти, возвращаемый функцией
    shmget
    .

    Второй параметр

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

    Третий параметр

    shmflg
    — набор поразрядных флагов. Два возможных значения:
    SHM_RND
    , в сочетании с
    shm_addr
    управляющее адресом, по которому присоединяется к процессу совместно используемая память, и
    SHM_RDONLY
    , которое делает присоединенную память доступной только для чтения. Очень редко возникает необходимость управлять адресом присоединения совместно используемой памяти. Как правило, следует позволить системе выбрать для вас адрес, поскольку в противном случае приложение станет в значительной степени аппаратно-зависимым.

    Если вызов

    shmat
    завершился успешно, он вернет указатель на первый байт совместно используемой памяти. В случае аварийного завершения возвращается -1.

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

    Исключение из этого правила возникает, если выражение

    shmflg & SHM_RDONLY
    равно
    true
    . В этом случае в совместно используемую память нельзя писать, даже если права доступа предоставляют такую возможность.

    shmdt

    Функция

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

    shmctl

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

    int shmctl(int shm_id, int command, struct shmid_ds *buf);

    У структуры типа

    shmid_ds
    есть, как минимум, следующие элементы:

    struct shmid_ds {

     uid_t shm_perm.uid;

     uid_t shm_perm.gid;

     mode_t shm_perm.mode;

    }

    Первый параметр

    shm_id
    — идентификатор, возвращаемый функцией
    shmget
    .

    Второй параметр

    command
    содержит предпринимаемое действие. Он может принимать три значения, перечисленные в табл. 14.2.


    Таблица 14.2

    Значение Описание
    IPC_STAT
    Задаёт данные в структуре
    shmid_ds
    , отображающие значения, связанные с совместно используемой памятью
    IPC_SET
    Устанавливает значения, связанные с совместно используемой памятью в соответствии с данными из структуры типа
    shmid_ds
    , если у процесса есть право на это действие
    IPC_RMID
    Удаляет сегмент совместно используемой памяти

    Третий параметр

    buf
    — указатель на структуру, содержащую режимы и права доступа для совместно используемой памяти.

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

    Выполните упражнение 14.2.

    Упражнение 14.2. Совместно используемая память

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

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

    #define TEXT_SZ 2048


    struct shared_use_st {

     int written_by_you;

     char some_text[TEXT_SZ];

    };

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

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

    2. Первая программа shm1.c — потребитель. После заголовочных файлов создается сегмент совместно используемой памяти (размер равен вашей структуре, описывающей совместно используемую память) с помощью вызова

    shmget
    с заданным битом
    IPC_CREAT
    .

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <string.h>

    #include <sys/shm.h>

    #include "shm_com.h"


    int main() {

     int running = 1;

     void *shared_memory = (void *)0;

     struct shared_use_st *shared_stuff;

     int shmid;

     srand((unsigned int)getpid());

     shmid = shmget((key_t)1234, sizeof(struct shared_use_st),

      0666 | IPC_CREAT);

     if (shmid == -1) {

      fprintf(stderr, "shmget failed\n");

      exit(EXIT_FAILURE);

     }

    3. Теперь вы делаете совместно используемую память доступной программе.

     shared_memory = shmat(shmid, (void *)0, 0);

     if (shared memory == (void *)-1) {

      fprintf(stderr, "shmat failed\n");

      exit(EXIT_FAILURE);

     }

     printf("Memory attached at %X\n", (int)shared_memory);

    4. В следующем фрагменте программы сегмент

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

     shared_stuff = (struct shared_use_st *)shared_memory;

     shared_stuff->written_by_you = 0;

     while (running) {

      if (shared_stuff->written_by_you) {

       printf("You wrote: %s", shared_stuff->some_text);

       sleep(rand() % 4);

       /* Заставляет другой процесс ждать нас! */

       shared_stuff->written_by_you = 0;

       if (strncmp(shared_stuff->some_text, "end", 3) == 0) {

        running = 0;

       }

      }

     }

    5. В заключение совместно используемая память отсоединяется и удаляется.

     if (shmdt(shared_memory) == -1) {

      fprintf(stderr, "shmdt failed\n");

      exit(EXIT_FAILURE);

     }

     if (shmctl(shmid, IPC_RMID, 0) == -1) {

      fprintf(stderr, "shmctl(IPC_RMID) failed\n");

      exit(EXIT_FAILURE);

     }

     exit(EXIT_SUCCESS);

    }

    6. Вторая программа shm2.c — поставщик; она позволяет вводить данные для потребителей. Программа очень похожа на shm1.c и выглядит следующим образом.

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <string.h>

    #include <sys/shm.h>

    #include "shm_com.h"


    int main() {

     int running = 1;

     void *shared_memory = (void *)0;

     struct shared_use_st *shared_stuff;

     char buffer[BUFSIZ];

     int shmid;

     shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);

     if (shmid == -1) {

      fprintf(stderr, "shmget failed\n");

      exit(EXIT_FAILURE);

     }

     shared_memory = shmat(shmid, (void *)0, 0);

     if (shared_memory == (void *)-1) {

      fprintf(stderr, "shmat failed\n");

      exit(EXIT_FAILURE);

     }

     printf("Memory attached at %X\n", (int)shared_memory);

     shared_stuff = (struct shared_use_st *)shared_memory;

     while (running) {

      while (shared_stuff->written_by_you == 1) {

       sleep(1);

       printf("waiting for client...\n");

      }

      printf("Enter same text: ");

      fgets(buffer, BUFSIZ, stdin);

      strncpy(shared_stuff->some_text, buffer, TEXT_SZ);

      shared_stuff->written_by_you = 1;

      if (strncmp(buffer, "end", 3) == 0) {

       running = 0;

      }

     }

     if (shmdt(shared_memory) == -1) {

      fprintf(stderr, "shmdt failed\n");

      exit(EXIT_FAILURE);

     }

     exit(EXIT_SUCCESS);

    }

    Когда вы выполните эти программы, то получите образец вывода, подобный следующему:

    $ ./shm1 &

    [1] 294

    Memory attached at 40017000

    $ ./shm2

    Memory attached at 40017000

    Enter some text: hello

    You wrote: hello

    waiting for client...

    waiting for client...

    Enter some text: Linux!

    You wrote: Linux!

    waiting for client...

    waiting for client...

    waiting for client...

    Enter some text: end

    You wrote: end

    $

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

    Первая программа shm1 создает сегмент совместно используемой памяти и затем присоединяет его к своему адресному пространству. Вы накладываете структуру

    shared_use_st
    на начальную область совместно используемой памяти. У нее есть флаг
    written_by_you
    , который устанавливается, когда данные доступны. Если флаг установлен, программа считывает текст, выводит его и сбрасывает флаг, чтобы показать, что данные прочитаны. Для корректного выхода из цикла примените специальную строку
    end
    . Далее программа отсоединяет сегмент совместно используемой памяти и удаляет его.

    Вторая программа shm2 получает и присоединяет тот же самый сегмент совместно используемой памяти, поскольку она применяет тот же ключ 1234. Затем она просит пользователя ввести текст. Если флаг

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

    Обратите внимание на то, что вы вынуждены с помощью флага

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

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

    Теперь рассмотрим третье и последнее средство System V IPC: очереди сообщений. Во многом очереди сообщений похожи на именованные каналы, но без сложностей, сопровождающих открытие и закрытие канала. Однако применение очереди сообщений не избавляет вас от проблем, возникающих при использовании именованных каналов, например блокировки заполненных каналов.

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

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

    Наложив эти ограничения, стандарт X/Open не позаботился о способе выяснения их числовых значений за исключением того, что превышение ограничений — достаточное основание для аварийного завершения функций обработки очереди сообщений. В ОС Linux есть два определения:

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

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

    #include <sys/msg.h>

    int msgctl(int msqid, int cmd, struct msqid_ds *buf);

    int msgget(key_t key, int msgflg);

    int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

    int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);

    Как и в случае семафоров или совместно используемой памяти, заголовочные файлы sys/types.h и sys/ipc.h обычно автоматически включаются заголовочным файлом msg.h.

    msgget

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

    msgget
    :

    int msgget(key_t key, int msgflg);

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

    key
    , которое, как и в других средствах IPC, задает имя конкретной очереди сообщений. С помощью специального значения
    IPC_PRIVATE
    создается скрытая или частная очередь, которая теоретически доступна только текущему процессу. Как и в случае семафоров и совместно используемой памяти, в некоторых системах Linux такая очередь может не быть частной. Поскольку от скрытой или частной очереди очень мало пользы, это не слишком важная проблема. Как и раньше, второй параметр
    msgflg
    состоит из девяти флагов прав доступа. Для создания новой очереди сообщений специальный бит со значением
    IPC_CREAT
    должен быть объединен с правами доступа поразрядной операцией
    OR
    . Не считается ошибкой установка флага
    IPC_CREAT
    и задание ключа уже существующей очереди сообщений. Если очередь уже есть, флаг
    IPC_CREAT
    безмолвно игнорируется.

    Функция

    msgget
    вернет положительное число, идентификатор очереди; в случае успешного завершения и -1 в случае сбоя.

    msgsnd

    Функция

    msgsnd
    позволяет добавить сообщение в очередь сообщений:

    int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);

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

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

    struct my_message {

     long int message_type;

     /* Данные, которые вы собираетесь передавать */

    }

    Поскольку элемент

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

    Первый параметр

    msqid
    — идентификатор очереди сообщений, возвращаемый функцией
    msgget
    .

    Второй параметр

    msg_ptr
    — указатель на отправляемое сообщение, которое должно начинаться с элемента типа
    long int
    , как описывалось ранее.

    Третий параметр

    msg_sz
    — объем сообщения, на которое указывает
    msg_ptr
    . Этот объем не должен включать элемент типа
    long int
    , содержащий тип сообщения.

    Четвертый параметр

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

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

    msgrcv

    Функция

    msgrcv
    извлекает сообщения из очереди сообщений:

    int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

    Первый параметр

    msqid
    — идентификатор очереди сообщений, возвращенный функцией msgget.

    Второй параметр

    msg_ptr
    — указатель на получаемое сообщение, которое должно начинаться с элемента типа
    long int
    , как описывалось ранее в функции
    msgsnd
    .

    Третий параметр

    msg_sz
    — размер сообщения, на которое указывает
    msg_ptr
    , без элемента типа
    long int
    , содержащего тип сообщения.

    Четвертый параметр

    msgtype
    типа
    long int
    позволяет реализовать простую форму приоритетного получения. Если значение
    msgtype
    равно 0, извлекается первое доступное сообщение в очереди. Если значение параметра больше нуля, извлекается первое сообщение с таким же типом сообщения. Если оно меньше нуля, извлекается первое сообщение с таким же типом сообщения или со значением, по абсолютной величине меньшим, чем
    msgtype
    .

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

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

    Четвертый параметр

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

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

    msg_ptr
    , и данные удаляются из очереди сообщений. В случае ошибки функция вернет -1.

    msgctl

    Последняя функция обработки очереди сообщений

    msgctl
    очень похожа на функцию управления для совместно используемой памяти:

    int msgctl(int msqid; int command, struct msqid_ds *buf);

    Структура

    msqid_ds
    содержит, как минимум, следующие элементы:

    struct msqid_ds {

     uid_t msg_perm.uid;

     uid_t msg_perm.gid;

     mode_t msg_perm.mode;

    }

    Первый параметр

    msqid
    — идентификатор, возвращаемый функцией
    msgget
    .

    Второй параметр

    command
    задает предпринимаемое действие. Он может принимать три значения, перечисленные в табл. 14.3.


    Таблица 14.3

    Значение Описание
    IPC_STAT
    Задает данные в структуре
    msqid_ds
    , отображающие значения, связанные с очередью сообщений
    IPC_SET
    Если у процесса есть на это право, это действие устанавливает значения, связанные с очередью сообщений, в соответствии с данными структуры
    msqid_ds
    IPC_RMID
    Удаляет очередь сообщений

    В случае успешного завершения возвращает 0, в случае аварийного — -1. Если очередь сообщений удаляется, когда процесс ожидает в функции

    msgsnd
    или
    msgrcv
    , функция отправки или получения сообщения завершается аварийно.

    Выполните упражнение 14.3.

    Упражнение 14.3. Очереди сообщений

    Теперь, когда вы познакомились с объявлениями, относящимися к очередям сообщений, можно посмотреть, как они действуют на практике. Как и раньше, вы напишите две программы: msg1.c для получения и msg2.c для отправки сообщений. Вы разрешите обеим программам создавать очередь сообщений, но используете для удаления очереди программу-приемник после того, как она получит последнее сообщение.

    1. Далее приведена программа-приемник msg1 .с:

    #include <stdlib.h>

    #include <stdio.h>

    #include <string.h>

    #include <errno.h>

    #include <unistd.h>

    #include <sys/msg.h>


    struct my_msg_st {

     long int my_msg_type;

     char some_text[BUFSIZ];

    };


    int main() {

     int running = 1;

     int msgid;

     struct my_msg_st some_data;

     long int msg_to_receive = 0;

    2. Прежде всего, задайте очередь сообщений:

     msgid = msgget((key_t)1234, 0666 | IPC_CREAT);

     if (msgid == -1) {

      fprintf(stderr, "msgget failed with error: %d\n", errno);

      exit(EXIT_FAILURE);

     }

    3. Далее сообщения извлекаются из очереди до тех пор, пока не будет обнаружено сообщение

    end
    . В конце очередь сообщений удаляется.

     while (running) {

      if (msgrcv(msgid, (void *)&some_data, BUFSIZ, msg_to_receive, 0) == -1) {

       fprintf(stderr, "msgrcv failed with error: %d\n", errno);

       exit(EXIT_FAILURE);

      }

      printf("You wrote: %s", some_data.some_text);

      if (strncmp(some_data.some_text, "end", 3) == 0) {

       running = 0;

      }

     }

     if (msgctl(msgid, IPC_RMID, 0) == -1) {

      fprintf(stderr, "msgctl(IPC_RMID) failed\n");

      exit(EXIT_FAILURE);

     }

     exit(EXIT_SUCCESS);

    }

    4. Программа-отправитель msg2.c очень похожа на программу msg1.с. В функции main удалите объявление

    msg_to_receive
    и замените его переменной
    buffer[BUFSIZ]
    . Уберите из программы удаление очереди и внесите следующие изменения в цикл с управляющей переменной
    running
    . Теперь у вас появился вызов функции msgsnd для отправки введенного текста в очередь сообщений. Далее приведена программа msg2.c с отличиями от программы msg1.с, выделенными цветом.

    #include <stdlib.h>

    #include <stdio.h>

    #include <string.h>

    #include <errno.h>

    #include <unistd.h>

    #include <sys/msg.h>

    #define MAX_TEXT 512


    struct my_msg_st {

     long int my_msg_type;

     char some_text[MAX_TEXT];

    };


    int main() {

     int running = 1;

     struct my_msg_st some_data;

     int msgid;

     char buffer = [BUFSIZ];

     msgid = msgget((key_t)1234, 0666 | IPC_CREAT);

     if (msgid == -1) {

      fprintf(stderr, "msgget failed with error: %d\n", errno);

      exit(EXIT_FAILURE);

     }

     while (running) {

      printf("Enter some text: ");

      fgets(buffer, BUFSIZ, stdin);

      some_data.my_msg_type = 1;

      strcpy(some_data.some_text, buffer);

      if (msgsnd(msgid, (void*)&some_data, MAX_TEXT, 0)) == -1) {

       fpintf(stderr, "msgsnd failed\n");

       exit(EXIT_FAILURE);

      }

      if (strncmp(buffer, "end", 3) == 0) {

       running = 0;

      }

     }

     exit(EXIT_SUCCESS);

    }

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

    Если в очереди сообщений есть место, отправитель может создать очередь, поместить в нее какие-либо данные и завершить выполнение еще до того, как начнет выполняться приемник. Первой следует запускать программу-отправителя msg2. Далее приведен пример вывода:

    $ ./msg2

    Enter some text: hello

    Enter some text: How are you today?

    Enter some text: end

    $ ./msg1

    You wrote: hello

    You wrote: How are you today?

    You wrote: end

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

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

    msgget
    ; далее она добавляет сообщения в очередь, применяя функцию
    msgsnd
    . Программа-приемник получает идентификатор очереди сообщений с помощью функции
    msgget
    и получает сообщения до тех пор, пока не будет найден специальный текст
    end
    . Затем программа приводит все в порядок, удаляя очередь сообщений с помощью функции
    msgctl
    .

    Приложение для работы с базой данных компакт-дисков

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

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

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

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

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

    Для преобразования приложения, работающего с базой данных компакт-дисков, с помощью средств IPC вам придется заменить только файл pipe_imp.c из сопроводительного программного кода к главе 13. Далее мы рассмотрим важные разделы замещающего файла ipc_imp.c.

    Пересмотр функций сервера

    Сначала нужно обновить серверные функции.

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

    #include "cd_data.h"

    #include "cliserv.h"

    #include <sys/msg.h>


    #define SERVER_MQUEUE 1234

    #define CLIENT_MQUEUE 4321


    struct msg_passed {

     long int msg_key; /* Используется для клиентского pid */

     message_db_t real message;

    };

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

    msgget
    :

    static int serv_qid = -1;

    static int cli_qid = -1;

    3. Сделайте сервер ответственным за создание обеих очередей сообщений:

    int server starting() {

    #if DEBUG_TRACE

     printf("%d :- server_starting()\n", getpid());

    #endif

     serv_qid = msgget((key_t)SERVER_MQUEUE, 0666 | IPC_CREAT);

     if (serv_qid == -1) return(0);

     cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666 | IPC_CREAT);

     if (cli_qid == -1) return(0);

     return(1);

    }

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

    server_ending
    :

    void server_ending() {

    #if DEBUG_TRACE

     printf("%d :- server_ending()\n", getpid());

    #endif

     (void)msgctl(serv_qid, IPC_RMID, 0);

     (void)msgctl(cli_qid, IPC_RMID, 0);

     servqid = -1;

     cliqid = -1;

    }

    5. Серверная функция

    read
    читает из очереди сообщение любого типа (т.е. от любого клиента) и возвращает часть сообщения с данными (пропуская тип сообщения):

    int read_request_from_client(message_db_t *rec_ptr) {

     struct msg_passed my_msg;

    #if DEBUG_TRACE

     printf("%d :- read_request_from_client()\n", getpid());

    #endif

     if (msgrcv(serv_qid, (void *)&my_msg, sizeof(*rec_ptr), 0, 0) == -1) {

      return(0);

     }

     *rec_ptr = my_msg.real_message;

     return(1);

    }

    6. При отправке сообщения для его адресации используется ID клиентского процесса, хранящийся в запросе:

    int send_resp_to_client(const message_db_t mess_to_send) {

     struct msg_passed my_msg;

    #if DEBUG_TRACE

     printf("%d :- send_resp_to_client()\n", getpid());

    #endif

     my_msg.real_message = mess_to_send;

     my_msg.msg_key = mess_to_send.client_pid;

     if (msgsnd(cli_qid, (void *)&my_msg, sizeof(mess_to_send), 0) == -1) {

      return(0);

     }

     return(1);

    }

    Пересмотр функций клиента

    Теперь нужно внести изменения в клиентские функции.

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

    int client starting() {

    #if DEBUG_TRACE

     printf("%d :- client_starting\n", getpid());

    #endif

     serv_qid = msgget((key_t)SERVER_MQUEUE, 0666);

     if (serv_qid == -1) return(0);

     cli_qid = msgget((key_t)CLIENT_MQUEUE, 0666);

     if (cli_qid == -1) return(0);

     return(1);

    }

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

    client_ending
    .

    void client_ending() {

    #if DEBUG_TRACE

     printf("%d :- client_ending()\n", getpid());

    #endif

     serv_qid = -1;

     cli_qid = -1;

    }

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

    int send_mess_to_server(message_db_t mess_to_send) {

     struct msg_passed my_msg;

    #if DEBUG_TRACE

     printf("%d send_mess_to_server()\n", getpid());

    #endif

     my_msg.real_message = mess_to_send;

     my_msg.msg_key = mess_to_send.client_pid;

     if (msgsnd(serv_qid, (void *)&my_msg, sizeof(mess_to_send) , 0) == -1) {

      perror("Message send failed");

      return(0);

     }

     return(1);

    }

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

    int read_resp_from_server(message_db_t *rec_ptr) {

     struct msg_passed mymsg;

    #if DEBUG_TRACE

     printf("%d :- read_resp_from_server()\n", getpid());

    #endif

     if (msgrcv(cli_qid, (void *)&my_msg, sizeof(*rec_ptr), getpid(), 0) == -1) {

      return(0);

     }

     *rec_ptr = my_msg.real_message;

     return(1);

    }

    5. Для сохранения совместимости с файлом pipe_imp.c необходимо объявить четыре дополнительные функции. Но в вашей программе они будут пустыми. Операции, которые они реализовывали в случае применения каналов, больше не нужны.

    int start_resp_to_client(const message_db_t mess_to_send) {

     return(1);

    }

    void end_resp_to_client(void) {}

    int start_resp_from_server(void) {

     return(1);

    }

    void end_resp_from_server(void) {}

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

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

    Команды состояния IPC

    Несмотря на то, что для соответствия требованиям X/Open этого не требуется, большинство систем Linux предоставляет набор команд, обеспечивающих доступ к данным IPC в режиме командной строки и удаление потерянных средств IPC. Существуют команды

    ipcs
    и
    ipcrm
    , очень полезные при разработке программ.

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

    ipcs
    ) и удаления (
    ipcrm
    ) позволяют проверить систему и очистить ее от ненужных средств IPC.

    Отображение состояния семафора

    Для проверки состояния семафоров в системе примените команду

    ipcs -s
    . Если какие-то семафоры присутствуют, вывод команды будет выглядеть следующим образом:

    $ ipcs -s

    ------ Semaphore Arrays ------

    key        semid owner perms nsems

    0x4d00df1a 768   rick  666   1

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

    ipcrm
    . Для удаления только что отображенного семафора примените (в Linux) следующую команду:

    $ ipcrm -s 768

    В некоторых более старых системах Linux используется несколько иной синтаксис команды:

    $ ipcrm sem 768

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

    Отображение состояния совместно используемой памяти

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

    ipcs -m
    и
    ipcrm -m <id>
    (или
    ipcrm shm <id>
    ).

    Далее приведен пример вывода команды

    ipcs -m
    :

    $ ipcs -m

    ------ Shared Memory Segments ------

    key        shmid owner perms bytes nattch status

    0x00000000 384   rick  666   4096  2      dest

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

    Команда

    ipcrm -m <id>
    позволяет удалить совместно используемую память. Она бывает полезной, когда программа завершается аварийно при попытке убрать такую память.

    Отображение состояния очереди сообщений

    Для очередей сообщений предназначены команды

    ipcs -q
    и
    ipcrm -q <id
    > (или
    ipcrm msg <id>
    ).

    Далее приведен пример вывода команды

    ipcs -q
    :

    $ ipcs -q

    ------ Message Queues ------

    key        msqid owner perms used-bytes messages

    0x000004d2 3384  rick  666   2048       2

    В нем показаны в очереди сообщений два сообщения общим объемом 2048 байтов. Команда

    ipcrm -q <id>
    позволяет удалить очередь сообщений.

    Резюме

    В этой главе вы познакомились с тремя разновидностями средств взаимосвязи процессов, которые стали широко применяться в ОС UNIX System V.2 и были доступны в системе Linux, начиная с ранних версий ее дистрибутивов. Вы рассмотрели предлагаемые ими сложные функциональные возможности и, после того как поняли принципы их функционирования, оценили обеспечиваемое ими эффективное решение для удовлетворения многих потребностей межпроцессного взаимодействия. 








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх