Операционные системы/IPC

Материал из eSyr's wiki.

Версия от 20:29, 22 января 2010; Nexor (Обсуждение | вклад)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

IPC (Inter-Process Communication) – средства межпроцессорного взаимодействия в ОС Unix.

IPC включают в себя:

  • Очереди сообщений
  • Разделяемую память
  • Семафоры

Содержание

Именование разделяемых объектов

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

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

Генерация ключей: функция ftok()

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

Для решения этой задачи служит функция ftok():

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(char *filename, char proj);

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

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

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

Общие принципы работы с разделяемыми ресурсами

Рассмотрим некоторые моменты, общие для работы со всеми разделяемыми ресурсами IPC. Как уже говорилось, общим для всех ресурсов является механизм именования. Кроме того, для каждого IPC-ресурса поддерживается идентификатор его владельца и структура, описывающая права доступа к нему. Подобно файлам, права доступа задаются отдельно для владельца, его группы и всех остальных пользователей; однако, в отличие от файлов, для разделяемых ресурсов поддерживается только две категории доступа: по чтению и записи. Априори считается, что возможность изменять свойства ресурса и удалять его имеется только у процесса, эффективный идентификатор пользователя которого совпадает с идентификатором владельца ресурса. Владельцем ресурса назначается пользователь, от имени которого выполнялся процесс, создавший ресурс, однако создатель может передать права владельца другому пользователю. В заголовочном файле <sys/ipc.h> определен тип struct ipc_perm, который описывает права доступа к любому IPC-ресурсу. Поля в этой структуре содержат информацию о создателе и владельце ресурса и их группах, правах доступа к ресурсу и его ключе.

Для создания разделяемого ресурса с заданным ключом, либо подключения к уже существующему ресурсу с таким ключом используются ряд системных вызовов, имеющих общий суффикс get. Общими параметрами для всех этих вызовов являются ключ и флаги. В качестве значения ключа при создании любого IPC-объекта может быть указано значение IPC_PRIVATE. При этом создается ресурс, который будет доступен только породившему его процессу. Такие ресурсы обычно порождаются родительским процессом, который затем сохраняет полученный дескриптор в некоторой переменной и порождает своих потомков. Так как потомкам доступен уже готовый дескриптор созданного объекта, они могут непосредственно работать с ним, не обращаясь предварительно к «get»-методу. Таким образом, созданный ресурс может совместно использоваться родительским и порожденными процессами. Однако, важно понимать, что если один из этих процессов повторно вызовет «get»-метод с ключом IPC_PRIVATE, в результате будет получен другой, совершенно новый разделяемый ресурс, так как при обращении к «get»-методу с ключом IPC_PRIVATE всякий раз создается новый объект нужного типа.

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

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

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

Изображение:Queue.jpg

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

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

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

#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 байт из тела сообщения, а остальные данные отбрасываются.

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

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

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

#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("\nПроцесс-приемник А: %s", Message.Data);
	}	
	return 0;
}

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

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

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

server

  1. include <sys/types.h>
  2. include <sys/ipc.h>
  3. include <sys/msg.h>
  4. 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

  1. include <sys/types.h>
  2. include <sys/ipc.h>
  3. include <sys/msg.h>
  4. include <unistd.h>
  5. 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\n", messagefrom.mes); return 0; }

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

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

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

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

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

#include <sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid, struct sembuf *semop, size_t nops)

Этому вызову передаются следующие аргументы:

  • semid – дескриптор массива семафоров;
  • semop – массив из объектов типа struct sembuf, каждый из которых задает одну операцию над семафором;
  • nops – длина массива semop. Количество семафоров, над которыми процесс может одновременно производить операцию в одном вызове *semop(), ограничено константой SEMOPM, описанной в файле <sys/sem.h>. Если процесс попытается вызвать semop() с параметром nops, большим этого значения, этот вызов вернет неуспех.

Структура имеет sembuf вид:

struct sembuf { 
short sem_num;	 /* номер семафора в векторе */
	short sem_op;	 /* производимая операция */	
	short sem_flg;	 /* флаги операции */
}

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

  1. Пусть значение семафора с номером sem_num равно sem_val.
    1. если значение операции не равно нулю:
    2. оценивается значение суммы sem_val + sem_op.
      • если эта сумма больше либо равна нулю, то значение данного семафора устанавливается равным этой сумме: sem_val = sem_val + sem_op
      • если же эта сумма меньше нуля, то действие процесса будет приостановлено до тех пор, пока значение суммы sem_val + sem_op не станет больше либо равно нулю, после чего значение семафора устанавливается равным этой сумме: sem_val = sem_val + sem_op
  2. Если код операции sem_op равен нулю:
    1. Если при этом значение семафора (sem_val) равно нулю, происходит немедленный возврат из вызова
    2. Иначе происходит блокирование процесса до тех пор, пока значение семафора не обнулится, после чего происходит возврат из вызова

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

Поле sem_flg в структуре sembuf содержит комбинацию флагов, влияющих на выполнение операции с семафором. В этом поле может быть установлен флаг IPC_NOWAIT, который предписывает соответствующей операции над семафором не блокировать процесс, а сразу возвращать управление из вызова semop(). Вызов semop() в такой ситуации вернет –1. Кроме того, в этом поле может быть установлен флаг SEM_UNDO, в этом случае система запомнит изменение значения семафора, произведенные данным вызовом, и по завершении процесса автоматически ликвидирует это изменение. Это предохраняет от ситуации, когда процесс уменьшил значение семафора, начав работать с ресурсом, а потом, не увеличив значение семафора обратно, по какой-либо причине завершился. В этом случае остальные процессы, ждущие доступа к ресурсу, оказались бы заблокированы навечно.

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

#include <sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semctl (int semid, int num, int cmd, union semun arg)

С помощью этого системного вызова можно запрашивать и изменять управляющие параметры разделяемого ресурса, а также удалять его. Первый параметр вызова – дескриптор массива семафоров. Параметр num представляет собой индекс семафора в массиве, параметр cmd задает операцию, которая должна быть выполнена над данным семафором. Последний аргумент имеет тип union semun и используется для считывания или задания управляющих параметров одного семафора или всего массива, в зависимости от значения аргумента cmd. Тип данных union semun определен в файле <sys/sem.h> и выглядит следующим образом:

union semun {
	int val;	// значение одного семафора
	struct semid_ds *buf;	/* параметры массива семафоров в целом */
	ushort	*array;		/* массив значений семафоров */
}

где struct semid_ds – структура, описанная в том же файле, в полях которой хранится информация о всем наборе семафоров в целом, а именно, количество семафоров в наборе, права доступа к нему и статистика доступа к массиву семафоров.

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

  • IPC_STAT – скопировать управляющие параметры набора семафоров по адресу arg.buf
  • IPC_SET – заменить управляющие параметры набора семафоров на те, которые указаны в arg.buf. Чтобы выполнить эту операцию, процесс должен быть владельцем или создателем массива семафоров, либо обладать правами привилегированного пользователя, при этом процесс может изменить только владельца массива семафоров и права доступа к нему.
  • IPC_RMID – удалить массив семафоров. Чтобы выполнить эту операцию, процесс должен быть владельцем или создателем массива семафоров, либо обладать правами привилегированного пользователя
  • GETALL, SETALL – считать/установить значения всех семафоров в массив, на который указывает arg.array
  • GETVAL – возвратить значение семафора с номером num. Последний аргумент вызова игнорируется.
  • SETVAL – установить значение семафора с номером num равным arg.val

В случае успешного завершения вызов возвращает значение, соответствующее конкретной выполнявшейся операции (0, если не оговорено иное), в случае неудачи – -1.

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

Программа будет оперировать с разделяемой памятью.

1 процесс – создает ресурсы “разделяемая память” и “семафоры”, далее он начинает принимать строки со стандартного ввода и записывает их в разделяемую память.

2 процесс – читает строки из разделяемой памяти.

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

  1. include <stdio.h>
  2. include <sys/types.h>
  3. include <sys/ipc.h>
  4. include <sys/sem.h>
  5. include <string.h>
  6. define NMAX 256

int main(int argc, char **argv) { key_t key; int semid, shmid; struct sembuf sops; char *shmaddr; char str[NMAX];

key = ftok(“/usr/ter/exmpl”, ’S’); /* создаем уникальный ключ */ semid = semget(key, 1, 0666 | IPC_CREAT); /* создаем один семафор с определенными правами доступа */ shmid = shmget(key, NMAX, 0666 | IPC_CREAT); /* создаем разделяемую память на 256 элементов */ shmaddr = shmat(shmid, NULL, 0); /* подключаемся к разделу памяти, в shaddr – указатель на буфер с разделяемой памятью */

    semctl(semid,0,SETVAL, (int) 0);

/* инициализируем семафор значением 0 */ sops.sem_num = 0; sops.sem_flg = 0; do { /* запуск цикла */ printf(“Введите строку:”); if (fgets(str, NMAX, stdin) == NULL) { /* окончание ввода */ /* пишем признак завершения – строку “Q” */ strcpy(str, “Q”); } /* в текущий момент семафор открыт для этого процесса */ strcpy(shmaddr, str); /* копируем строку в разд. память */ /* предоставляем второму процессу возможность войти */ sops.sem_op = 3; /* увеличение семафора на 3 */ semop(semid, &sops, 1); /* ждем, пока семафор будет открыт для 1го процесса - для следующей итерации цикла */ sops.sem_op = 0; /* ожидание обнуления семафора */ semop(semid, &sops, 1); } while (str[0] != ‘Q’); /* в данный момент второй процесс уже дочитал из разделяемой памяти и отключился от нее – можно ее удалять*/ shmdt(shmaddr) ; /* отключаемся от разделяемой памяти */ shmctl(shmid, IPC_RMID, NULL); /* уничтожаем разделяемую память */ semctl(semid, 0, IPC_RMID, (int) 0); /* уничтожаем семафор */ return 0; }


2й процесс: /* необходимо корректно определить существование ресурса, если он есть - подключиться */

  1. include <stdio.h>
  2. include <sys/types.h>
  3. include <sys/ipc.h>
  4. include <sys/sem.h>
  5. include <string.h>
  6. define NMAX 256

int main(int argc, char **argv) { key_t key; int semid, shmid; struct sembuf sops; char *shmaddr; char str[NMAX];

key = ftok(“/usr/ter/exmpl”,’S’); /* создаем тот же самый ключ */ semid = semget(key, 1, 0666 | IPC_CREAT); shmid = shmget(key, NMAX, 0666 | IPC_CREAT); /* аналогично предыдущему процессу - инициализации ресурсов */ shmaddr = shmat(shmid, NULL, 0); sops.sem_num = 0; sops.sem_flg = 0; /* запускаем цикл */ do { printf(“Waiting… \n”); /* ожидание на семафоре */ sops.sem_op = -2; /* будем ожидать, пока “значение семафора” + ”значение sem_op” не станет положительным, т.е. пока значение семафора не станет как минимум 3 (3-2=1 > 0) */

 		semop(semid, &sops, 1);

/* теперь значение семафора равно 1 */ strcpy(str, shmaddr); /* копируем строку из разд.памяти */ /*критическая секция - работа с разделяемой памятью - в этот момент первый процесс к разделяемой памяти доступа не имеет*/ if (str[0] == ‘Q’) { /*завершение работы - освобождаем разделяемую память */ shmdt(shmaddr); } /*после работы – обнулим семафор*/ sops.sem_op=-1; semop(semid, &sops, 1); printf(“Read from shared memory: %s\n”, str); } while (str[0] != ‘Q’); return 0; }

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

Личные инструменты
Разделы