Системное программное обеспечение - Учебное пособие (Терехин А.Н.)

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

Итак, одним из типов объектов System V IPC являются очереди сообщений. Очередь сообщений представляет собой некое хранилище типизированных сообщений, организованное по принципу FIFO. Любой процесс может помещать новые сообщения в очередь и извлекать из очереди имеющиеся там сообщения. Каждое сообщение имеет тип, представляющий собой некоторое целое число. Благодаря наличию типов сообщений, очередь можно интерпретировать двояко — рассматривать ее либо как сквозную очередь неразличимых по типу сообщений, либо как некоторое объединение подочередей, каждая из которых содержит элементы определенного типа. Извлечение сообщений из очереди происходит согласно принципу FIFO – в порядке их записи, однако процесс-получатель может указать, из какой подочереди он хочет извлечь сообщение, или, иначе говоря, сообщение какого типа он желает получить – в этом случае из очереди будет извлечено самое «старое» сообщение нужного типа (см. Рис. 18).

 

 

Рис. 18 Типизированные очереди сообщений

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

Доступ к очереди сообщений.

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

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/message.h>

int msgget (key_t key, int msgflag)

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

Отправка сообщения.

 Для отправки сообщения используется функция msgsnd():

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgsnd (int msqid, const void *msgp, size_t msgsz, int msgflg)

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

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

long msgtype —  тип сообщения

char msgtext[ ]     —  данные (тело сообщения)

В заголовочном файле <sys/msg.h> определена константа MSGMAX, описывающая максимальный размер тела сообщения. При попытке отправить сообщение, у которого число элементов в массиве msgtext превышает это значение, системный вызов вернет –1.

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

В случае удачной записи возвращаемое значение вызова равно 0.

Получение сообщения.

Для получения сообщения имеется функция msgrcv():

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>   

int msgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)

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

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

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

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

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

Управление очередью сообщений.

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

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msgid_ds *buf)

Данный вызов используется для получения или изменения процессом управляющих параметров, связанных с очередью и уничтожения очереди. Ее аргументы — идентификатор ресурса, команда, которую необходимо выполнить, и структура, описывающая управляющие параметры очереди. Тип msgid_ds описан в заголовочном файле <sys/message.h>, и представляет собой структуру, в полях которой хранятся права доступа к очереди, статистика обращений к очереди, ее размер и т.п.

Возможные значения аргумента cmd:

IPC_STAT – скопировать структуру, описывающую управляющие параметры очереди по адресу, указанному в параметре buf

IPC_SET – заменить структуру, описывающую управляющие параметры очереди, на структуру, находящуюся по адресу, указанному в параметре buf

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

 Использование очереди сообщений.

Пример программы, где основной процесс читает некоторую текстовую строку из стандартного ввода, и в случае, если строка начинается с буквы 'a', эта строка в качестве сообщения будет передана процессу А, если 'b' - процессу В, если 'q' - то процессам А и В, затем будет осуществлен выход. Процессы А и В распечатывают полученные строки на стандартный вывод.

Основной процесс.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

#include <string.h>

#include <unistd.h>

#include <stdio.h>

 

struct {

          long mtype; /* тип сообщения */

          char Data[256];    /* сообщение */

} Message;

 

int main(int argc, char **argv)

{

          key_t key;  int msgid;  char str[256];

         

          key = ftok("/usr/mash",'s');  

/*получаем уникальный ключ, однозначно определяющий доступ к ресурсу */   

          msgid=msgget(key, 0666 | IPC_CREAT);

/*создаем очередь сообщений , 0666 определяет права доступа */   

         

          for(;;) {                                          

                   /* запускаем вечный цикл */

gets(str); /* читаем из стандартного ввода строку */

                   strcpy(Message.Data, str);     

                   /* и копируем ее в буфер сообщения */

                   switch(str[0]){

                             case 'a':

                             case 'A':

                                      Message.mtype = 1;

/* устанавливаем тип */

msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str) + 1, 0);

/* посылаем сообщение в очередь */

                                      break;

                             case 'b':

                             case 'B':

                                      Message.mtype = 2;

msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str) + 1, 0);

                                      break;

                             case 'q':

                             case 'Q':

                                      Message.mtype = 1;

msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str) + 1, 0);

                                      Message.mtype = 2;

msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str) + 1, 0);

                                      sleep(10);   

/* ждем получения сообщений процессами А и В */

                                       msgctl(msgid, IPC_RMID, NULL);

/* уничтожаем очередь*/

                                      return 0; 

                             default:

                                      break;

                   }

          }

}

Процесс-приемник А

/* процесс В аналогичен с точностью до четвертого параметра в msgrcv */

 

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

#include <stdio.h>

 

struct {      

                   long mtype;

                   char Data[256];

} Message;

 

int main(int argc, char **argv)

          key_t key;  int msgid;

         

          key = ftok("/usr/mash",'s');

/* получаем ключ по тем же параметрам */

          msgid = msgget(key, 0666 | IPC_CREAT);

/*подключаемся к очереди сообщений */

          for(;;) {

          /* запускаем вечный цикл */

msgrcv(msgid, (struct msgbuf*) (&Message), 256, 1, 0); 

                   /* читаем сообщение с типом 1*/

if (Message.Data[0]=='q' || Message.Data[0]=='Q') break;

printf(" Процесс-приемник А: \%s", Message.Data);

          }       

          return 0;

}

 

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

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

 Очередь сообщений. Модель «клиент-сервер»

server

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

#include <string.h>

 

int main(int argc, char **argv)

{

struct {

          long mestype;

          char mes [100];

} messageto;

struct {

     long mestype;

     long mes;      

     } messagefrom;

 

key_t key;

int mesid;

 

key = ftok("example",'r');                 

mesid = msgget (key, 0666 | IPC_CREAT);

 

while(1)

{

if (msgrcv(mesid, &messagefrom, sizeof(messagefrom), 1, 0) <= 0) continue;

messageto.mestype = messagefrom.mes;

strcpy( messageto.mes, "Message for client");

msgsnd (mesid, &messageto, sizeof(messageto), 0);

          }

msgctl (mesid, IPC_RMID, 0);

return 0;

}

 

client

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

#include <unistd.h>

#include <stdio.h>

 

int main(int argc, char **argv)

{

struct {

long mestype;   /*описание структуры сообщения*/

long mes;     

     } messageto;

struct {

long mestype;   /*описание структуры сообшения*/

     char mes[100];

     } messagefrom;

 

key_t key;

int mesid;

 

long pid = getpid();

key = ftok("example", 'r');                

mesid = msgget(key, 0);   /*присоединение к очереди сообщений*/

 

messageto.mestype = 1;

messageto.mes = pid;

 

msgsnd (mesid, &messageto, sizeof(messageto), 0); /* отправка */

 

while ( msgrcv (mesid, &messagefrom, sizeof(messagefrom), pid, 0) <= 0);  

/*прием сообщения */

 

printf("\%s ", messagefrom.mes);

return 0;

}       

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

 

Рис. 19 Разделяемая память

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

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

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

Создание общей памяти.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget (key_t key, int size, int shmemflg)

Аргументы этого вызова: key - ключ для доступа к разделяемой памяти; size задает размер области памяти, к которой процесс желает получить доступ. Если в результате вызова shmget() будет создана новая область разделяемой памяти, то ее размер будет соответствовать значению size. Если же процесс подключается к существующей области разделяемой памяти, то значение size должно быть не более ее размера, иначе вызов вернет –1. Заметим, что если процесс при подключении к существующей области разделяемой памяти указал в аргументе size значение, меньшее ее фактического размера, то впоследствии он сможет получить доступ только к первым size байтам этой области.

Отметим, что в заголовочном файле <sys/shm.h> определены константы SHMMIN и SHMMAX, задающий минимально возможный и максимально возможный размер области разделяемой памяти. Если процесс пытается создать область разделяемой памяти, размер которой не удовлетворяет этим границам, системный вызов shmget() окончится неудачей.

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

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

Доступ к разделяемой памяти.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

char *shmat(int shmid, char *shmaddr, int shmflg)

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

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

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

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

Открепление разделяемой памяти.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmdt(char *shmaddr)

Данный вызов позволяет отсоединить разделяемую память, ранее присоединенную посредством вызова shmat().

 Параметр shmaddr - адрес прикрепленной к процессу памяти, который был получен при вызове shmat(). 

В случае успешного выполнения функция возвращает 0, в случае неудачи -1

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

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf)

Данный вызов используется для получения или изменения процессом управляющих параметров, связанных с областью разделяемой памяти, наложения и снятия блокировки на нее и  ее уничтожения. Аргументы вызова — дескриптор области памяти, команда, которую необходимо выполнить, и структура, описывающая управляющие параметры области памяти. Тип shmid_ds описан в заголовочном файле <sys/shm.h>, и представляет собой структуру, в полях которой хранятся права доступа к области памяти, ее размер, число процессов, подсоединенных к ней в данный момент, и статистика обращений к области памяти.

Возможные значения аргумента cmd:

IPC_STAT – скопировать структуру, описывающую управляющие параметры области памяти по адресу, указанному в параметре buf

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

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

SHM_LOCK, SHM_UNLOCK – блокировать или разблокировать область памяти. Выполнить эту операцию может только процесс с правами привилегированного пользователя.

 Общая схема работы с общей памятью в рамках одного процесса.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

 

int putm(char *);

int waitprocess(void);

 

int main(int argc, char **argv)

{

key_t key;

int shmid;

char *shmaddr;

 

key = ftok(“/tmp/ter”,’S’);

shmid = shmget(key, 100, 0666|IPC_CREAT);

shmaddr = shmat(shmid, NULL, 0); /* подключение к памяти */

 

putm(shmaddr); /* работа с ресурсом */

waitprocess();

 

shmctl(shmid,IPC_RMID,NULL); /* уничтожение  ресурса */

return 0;

}

 

В данном примере считается, что putm() и waitprocess() – некие пользовательские функции, определенные в другом месте

Семафоры.

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

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

с каждым разделяемым ресурсом связывается один семафор из набора;

положительное значение семафора означает возможность доступа к ресурсу (ресурс свободен), неположительное – отказ в доступе (ресурс занят);

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

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

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

Рассмотрим набор вызовов для оперирования с семафорами в UNIX System V.

Доступ к семафору

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

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semget (key_t key, int nsems, int semflag);

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

В случае, если среди флагов указан IPC_CREAT, аргумент nsems должен представлять собой положительное число, если же этот флаг не указан, значение nsems игнорируется. Отметим, что в заголовочном файле <sys/sem.h> определена константа SEMMSL, задающая максимально возможное число семафоров в наборе. Если значение аргумента nsems больше этого значения, вызов semget() завершится неудачно.

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