Андрей Боровский
Термином IPC (inter-process communication), который обычно переводится как “межпроцессное взаимодействие”, обозначается передача данных между процессами в рамках одной системы. Механизмы взаимодействия, рассмотренные в этой статье, реализованы практически одинаково во всех современных Unix-системах (Solaris, Linux, FreeBSD и т. п.). По сравнению с другими ОС Unix-системы обладают весьма богатым набором механизмов
передачи данных. Отчасти это объясняется тем, что средства, изначально разрабатывавшиеся в разных ветвях Unix, внедрялись затем в другие версии в целях обеспечения совместимости.Далее будут рассмотрены такие средства взаимодействия
между процессами как однонаправленные каналы, сокеты, сообщения, разделяемые блоки памяти и семафоры. В этой статье не рассматриваются буферы обмена и Drag and Drop, так как в Unix-системах эти механизмы обычно реализованы на уровне графической оболочки, и их реализация зависит от того, какая оболочка используется.Однонаправленные каналы (pipes) позволяют передавать данные только в одном направлении. На уровне интерфейса программирования канал представляется двумя дескрипторами файлов, один из которых служит для чтения
данных, а другой – для записи. Каналы не поддерживают произвольный доступ, т. е. данные из канала могут считываться только в том же порядке, в котором они записывались. Однонаправленные каналы могут быть анонимными (anonymous pipes) и именованными (named pipes, FIFOs).Анонимные каналы отличаются тем, что оба файловых дескриптора открываются в контексте одного и того же процесса. Обычно такие каналы используются совместно с функцией fork и служат для передачи данных между двумя копиями процесса, полученными при вызове этой функции. Анонимный однонаправленный канал создается при помощи функции pipe (объявляется в файле unistd.h). В качестве параметра функции pipe передается массив типа int, состоящий из двух элементов. В первом элементе массива функция возвращает дескриптор файла, служащий для чтения данных, а во втором – дескриптор для записи. С полученными дескрипторами можно обращаться также, как и с обычными дескрипторами файлов. Ниже приводится пример использования функции pipe.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main () {
int pipedes[2];
int pid;
char *str = "String passed via pipe";
FILE *f;
pipe(pipe
В этом примере, как и в других примерах, приводимых в этой статье, проверка значений, возвращаемых функциями на предмет ошибок не производится с целью уменьшения размеров листинга.
В отличие от анонимных каналов именованные однонаправленные каналы позволяют передавать данные между независимыми процессами. Для идентификации именованного канала на диске создается файл специального типа pipe. Следует помнить о том, что файлы именованных каналов являются средством передачи, а не хранения, данных. В частности, размер файла канала всегда остается равным нулю. Для файлов именованных каналов действуют те же правила контроля доступа, что и для обычных файлов Unix. Для создания именованных каналов служит функция mkfifo. Первый параметр этой функции – строка, в которой передается имя файла, идентифицирующего канал, второй параметр – маска прав доступа к файлу. Функция mkfifo вызывается для создания файла только один раз. После создания файла канала оба приложения, участвующие в обмене данными, должны открыть этот файл. Одно приложение открывает файл канала для записи, другое для чтения. Далее следует пример программы, которая создает файл канала, открывает его для записи и записывает в него строку.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main () {
FILE * f;
char * str = "String passed
Программа, читающая данные из канала, может выглядеть следующим образом
:#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE * f;
char * fname = "~/fifo";
f = fopen(fname, "r"); // открываем файл для чтения
while ( putchar(getc(f)) != '\0' );
putchar
Программу, создающую файл канала, следует запускать первой. Следует отметить, что система синхронизирует процессы, связанные с каналами. Например, при вызове функции чтения или записи в канал выполнение процесса приостанавливается до тех пор, пока “на другом конце” канала не будет выполнена комплиментарная операция.
Среди средств работы с каналами особого внимания заслуживает функция
popen. Эта функция запускает внешнюю программу и возвращает вызвавшему ее приложению указатель, связанный либо со стандартным потоком ввода, либо со стандартным потоком вывода запущенного процесса. Первый параметр функции popen – строка, содержащая команду, запускающую внешнюю программу. Второй параметр определяет, какой из стандартных потоков (вывода или ввода) будет возвращен. Аргумент “w” соответствует потоку ввода запускаемой программы, в этом случае приложение, вызвавшее popen записывает данные в поток. Аргумент “r” соответствует потоку вывода. В следующем примере функция popen используется для запуска команды ls , так что данные, выданные этой командой направляются приложению, вызвавшему popen:f = popen("ls /usr/bin", "r");
act_size = fread(buf, 1, buf_size, f);
...
pclose(f);

Файлы сокетов и каналов в окне Konqueror
Сокеты, впервые реализованные в BSD Unix, поддерживаются всеми современными операционными
системами. Сокеты являются основой сетевых приложений, однако в Unix-системах они могут использоваться также и для обмена данными между локальными процессами. Следует уточнить, что речь здесь идет именно о специальных “несетевых” сокетах, так как локальные приложения, естественно, могут обмениваться данными, используя сетевые протоколы.Для упрощения взаимодействия локальных приложений при помощи сокетов в Unix-системах реализована концепция файлового пространства имен (file namespace). В рамках этой концепции сокеты могут использовать в качестве адресов имена файлов специального типа. Важной особенностью этих сокетов является то, что соединение с их помощью локального и удаленного приложений невозможно, даже если файловая система, в которой создан сокет, доступна удаленной операционной системе.
Для работы с адресами из файлового пространства используется специальная структура данных sockaddr_un, определенная в файле sys/un.h. В следующем фрагменте кода сокет связывается с файлом.
#include <sys/socket.h>
#include <sys/un.h>
...
struct sockaddr_un name;
int sock, size;
char * fn = "/tmp/filesocket";
sock = socket(PF_FILE, SOCK_DGRAM, 0);
name.sun_family = AF_FILE;
strcpy(name.sun_path, fn);
size = offsetof(struct sockaddr_un, sun_path) + strlen(name.sun_path) + 1;
bind(sock, (struct sockaddr *) &name, size);
...
В результате выполнения приведенного фрагмента на диске будет создан файл /tmp/filesocket. По окончании работы с сокетом этот файл должен быть удален. Значение, возвращенное функцией socket является файловым дескриптором и может быть использовано, например, для чтения данных:
act_size = read ( sock, buf, buf_size );
Приложение
-клиент может соединиться с уже созданным сокетом при помощи функции connect:#include <sys/socket.h>
#include <sys/un.h>
...
struct sockaddr_un name;
int sock;
char * fn = "/tmp/filesocket";
char * greeting = "How're you?";
sock = socket(PF_FILE, SOCK_DGRAM, 0);
name.sun_family = AF_FILE;
strcpy(name.sun_path, fn);
connect(sock, (struct sockaddr *) &name, sizeof(name));
write(sock, greeting, strlen(greeting));
...
Связанные сокеты (socket pairs) похожи на анонимные однонаправленные каналы. Разница между сокетами и однонаправленными каналами заключается в том, что сокеты могут передавать данные в обоих направлениях. Связанные сокеты создаются при помощи функции socketpair. Как и функция pipe, функция socketpair возвращает два файловых дескриптора, но, в отличие от pipe, оба эти дескриптора открыты и для записи, и для чтения. Далее следует программа, подобная приведенному выше примеру использования функции pipe.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <strings.h>
#define MAXLEN 512
int main () {
int sockets[2];
int size, pid;
char * greeting = "How are you?";
char * response = "I'm ok, thanks!";
char buf[MAXLEN];
socketpair(AF_FILE, SO
Механизм обмена данными при помощи сообщений был стандартизирован в рамках
System V Interface Definition (SVID) и в настоящее время поддерживается всеми основными Unix-системами (проверить, реализованы ли в конкретной системе механизмы обмена данными SVID можно при помощи команды ipcs). Сообщение определяется как “последовательность байтов, передаваемая от одного процесса другому”. Система сообщений SVID обладает следующими свойствами:К этому следует добавить, что одна очередь сообщений может использоваться совместно более чем двумя процессами.
Структура данных, использующаяся для передачи сообщений, может быть определена следующим образом:
struct msgp {
long mtype;
char mtext[SOMEVALUE];
};
Фактически, поле mtype является единственным обязательным полем в приведенной структуре. В этом поле хранится произвольный идентификатор сообщения. Кроме поля mtype структура данных сообщения может содержать любое количество других полей различных типов.
Все типы, константы и функции, использующиеся при работе с сообщениями объявлены в файлах sys/ipc.h и sys/msg.h. Очередь сообщений создается при помощи функции msgget. Первый параметр этой функции – ключ – уникальное число, идентифицирующее очередь. Для получения таких чисел можно использовать функцию ftok, однако руководство по работе с функциями SVID рекомендует выбирать значения самостоятельно. Второй параметр функции msgget представляет собой маску прав доступа к создаваемой очереди (аналогичную маске прав доступа к файлам) и несколько дополнительных флагов. Флаг IPC_CREATE указывает, что в результате вызова msgget должна быть создана новая очередь. При установке флага IPC_EXCL, функция msgget вернет сообщение об ошибке, если очередь с указанным ключом уже существует.
Передача и получение сообщений выполняется при помощи функций msgsnd и msgrcv соответственно. Первым параметром
обеих функций является идентификатор очереди, возвращенный функцией msgget. Во втором параметре передается размер структуры сообщения. Программа, читающая сообщения из очереди, должна определять размер сообщения на основе его идентификатора. Третьим параметром функции msgrcv является идентификатор сообщения. Если значение этого параметра больше нуля, из очереди будет извлечено сообщение с соответствующим значением идентификатора. Если этот параметр равен нулю, из очереди будет извлечено любое первое сообщение, а если параметр отрицательный, из очереди будет извлечено первое сообщение, чей идентификатор меньше либо равен абсолютному значению параметра. Последний параметр в обеих функциях позволяет задать дополнительные флаги. Обычно функции, работающие с сообщениями, приостанавливают выполнение программы до тех пор, пока помещение сообщения в очередь или извлечение сообщения из нее не будет выполнено. При указании флага IPC_NOWAIT функция вернет сообщение об ошибке, если соответствующая операция не может быть выполнена немедленно.Функция msgctl позволяет управлять очередью и получать данные о ее состоянии. Например, вызов
msgctl(msgid, IPC_RMID, 0);
где msgid – идентификатор очереди сообщений, возвращенный функцией msgget, удаляет ранее созданную очередь.
В качестве примера
рассмотрим две программы. Одна программа (сервер) создает очередь сообщений. Другая программа (клиент) использует эту очередь для отправки сообщений серверу и получения ответа. Программа-сервер выглядит следующим образом:#include <stdio.h>
#include <stdli
Ниже приводится исходный текст программы-клиента
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <strings.h>
#define KEY 1174 // "магическое" число
#define MAXLEN 512
struct msg_t {
long mtype;
int snd_pid;
char body[MAXLEN];
};
int main() {
int msgid;
int i;
struct msg_t message;
char buf[MAXLEN];
msgid = msgget(KEY, 0777); // получаем идентификатор очереди
i = 0;
while ( (i < (MAXLEN - 1)) && ((message.body[i++] = getchar()) != '\n') );
me
Сервер и клиент используют разные идентификаторы для посылаемых сообщений. Это сделано для того, чтобы программа
, вызывающая последовательно msgsnd и msgrcv, не извлекала из очереди свои собственные сообщения. Программу-сервер следует запустить до запуска программы-клиента. Запустив из другого окна консоли программу-клиент, наберите небольшой текст и нажмите ввод.Спецификация SVID описывает интерфейс для работы с разделяемыми блоками памяти. Разделяемые блоки памяти представляют собой область памяти, отображенную адресное пространство нескольких процессов. Функции для работы с разделяемой памятью объявлены в файлах sys/ipc.h sys/shm.h.
Разделяемый блок памяти выделяется при помощи функции shmget, которой передаются три параметра. В первом параметре передается ключ, идентифицирующий выделяемый блок памяти. Второй параметр позволяет указать размер блока памяти (в байтах). В третьем параметре передается маска прав доступа и флаги, аналогичные флагам msgget. Функция shmget возвращает идентификатор выделенного блока памяти (его не следует путать с указателем на блок). Отображение блока памяти в адресное пространство процесса выполняет функция shmat. У функции shmat три параметра. Первый параметр, это идентификатор, возвращенный функцией shmget. Во втором параметре передается желательный адрес начала области отображения блока. Функция shmat “постарается” отобразить блок в указанную область, хотя успешный результат негарантирован. Если в этом параметре передать нулевое значение, функция сама выберет начальный адрес области отображения. Третий параметр позволяет задать дополнительные флаги. Например, флаг SHM_RDONLY присваивает отображаемой области статус “только для чтения”. При успешном выполнении функция shmat возвращает указатель на начало области отображения. Функция shmdt удаляет область отображения (но не блок разделяемой памяти). Единственный параметр этой функции – указатель, возвращенный функцией shmat. Для удаления блока разделяемой памяти нужно вызвать функцию shmctl:
shmctl(shmid, IPC_RMID, 0);
где shmid – идентификатор, возвращенный функцией shmget.
Далее приводятся примеры двух программ. Программа-сервер выделяет блок разделяемой памяти считывает ее содержимое после того, как программа-клиент запишет в блок памяти свои данные. Для разнообразия воспользуемся функцией ftok при генерации ключа. В качестве “затравки” функция ftok использует имя некоторого файла (не обязательно связанного с данным процессом).
Программа
-сервер:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#define MAXLEN 512
struct shmem_t {
int locked;
char string[MAXLEN];
};
int main() {
Программа
-клиент:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <errno.h
Семафоры широко используются как средство синхронизации потоков
, принадлежащих одному или разным процессам, а также для разделения доступа к критическим ресурсам.Состояние семафора
определяется значением некоторой внутренней переменной. Функция, обращающаяся к семафору, либо изменяет значение этой переменной, либо приостанавливает выполнение вызвавшего ее потока до тех пор, пока это значение не будет изменено (другим потоком).Работу семафоров можно продемонстрировать на примере разделения доступа к некоему ресурсу, не допускающему одновременного использования несколькими потоками. Перед тем, как начать работу с таким ресурсом, поток должен проверить, не используется ли ресурс другим потоком. Если ресурс занят, поток ожидает его высвобождения. Если ресурс свободен, поток начинает работу с ресурсом, при этом поток сообщает системе, что ресурс занят и другие потоки
не могут получить к нему доступ. По окончании работы с ресурсом поток сигнализирует системе о высвобождении ресурса.SVID описывает функции
, предназначенные для работы с семафорами. Семафоры создаются при помощи функции semget, объявленной в файле sem.h. У функции semget три параметра. В первом параметре функции передается уникальный ключ, аналогичный по смыслу соответствующему параметру функций msgget и shmget. Второй параметр указывает, сколько семафоров необходимо создать. Дело в том, что часто для синхронизации процессов требуется более одного семафора, и функция semget позволяет создавать несколько семафоров за один вызов.Для управления состоянием семафоров служит функция semop. Первый
параметр этой функции – идентификатор, возвращенный функцией semget. Второй параметр функции semop представляет собой указатель на массив структур, содержащих данные об операции над семафорами. В третьем параметре передается число записей в массиве, которое должно соответствовать числу семафоров. Структура, служащая для передачи в semop данных об операциях над семафорами, называется sembuf. И структура и функция объявляются в файле sys/sem.h. В структуре sembuf определено много полей, из которых явным образом обязательно должны быть заданы три:Состояние семафора определяется значением некоторой внутренней переменной. Это значение может быть изменено при помощи поля sem_op. Следующая функция иллюстрирует логику работы функции semop в зависимости от значения переменной состояния (semvalue) и переменной sem_op.
void semaphore (int semvalue, int sem_op) {
if (sem_op != 0) {
if (sem_op < 0) while (semvalue < ABS(sem_op));
semvalue += sem_op;
}
else while (semvalue != 0);
}
Отрицательное значение sem_op соответствует операции проверки доступности ресурса и вызывает приостановку потока, если ресурс недоступен. Положительное значение сигнализирует о высвобождении ресурса.
В поле sem_flg устанавливаются флаги для функции semop. Обязательным является флаг SEM_UNDO. Кроме этого можно указать флаг IPC_NOWAIT. В этом случае при наступлении “блокирующей ситуации” функция semop не приостанавливает выполнение программы, а возвращает значение –1.
Для управления семафорами используется функция semctl, аналогичная
shmctl. У этой функции, однако, есть дополнительные возможности. Например, вызовsemctl(semid, semnum, SETVAL, value);
позволяет установить значение переменной, управляющей
состоянием семафора, равным value. Рассмотрим простой пример:#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#define KEY 1421
int main() {
int pid, semid;
struct sembuf buf;
semid = semget(KEY, 1, 0600|IPC_CREAT|IPC_EXCL);
buf.sem_num = 0;
buf.sem_flg = SEM_UNDO;
semctl(semid, 0, SETVAL, 0);
pid = fork();
if (pid == 0) {
sleep(1);
printf("Hello\n");
buf.sem_op = 1;
semop(semid, (struct sembuf*) &buf, 1);
buf.sem_op = -2;
semop(semid, (struct sembuf*) &buf, 1);
} else {
buf.sem_op = -1;
semop(semid, (struct sembuf*) &buf, 1);
printf("World!\n");
buf.sem_op = 2;
semop(semid, (struct sembuf*) &buf, 1);
semctl(semid, 0, IPC_RMID, 0);
}
return 0;
}
Сначала мы создаем два процесса. Родительский процесс печатает слово “World!” только после того, как дочерний процесс напечатает слово “Hello”. Вызов sleep(1) приводится для наглядности. Второй вызов функции
semop с параметром sem_op равным 2 производится для того, чтобы дочерний процесс не завершился до окончания родительского (при работе с семафорами следует уделять внимание синхронизации завершения работы).В заключение рассмотрим две консольные команды, служащие для управления IPC-объектами. Команда ipcs выводит данные о наличествующих в данный момент очередях сообщений, блоках разделяемой памяти и семафорах. Команда ipcrm позволяет удалять эти IPC-объекты. Данная команда может быть полезна в процессе отладки IPC-приложений при появлении “бесхозных” IPC-объектов.
Статья была опубликована в журнале "Программист", #11, 2001 г. Перепечатка статьи возможна только с разрешения редакции журнала.