Обмен данными между процессами с помощью общих областей памяти. Обмен данными между приложениями с помощью механизма из IPC UNIX System V

16.04.2019

Межпроцессное взаимодействие (Inter-process communication (IPC) ) - это набор методов для обмена данными между потоками процессов. Процессы могут быть запущены как на одном и том же компьютере, так и на разных, соединенных сетью. IPC бывают нескольких типов: «сигнал», «сокет», «семафор», «файл», «сообщение»…

В данной статье я хочу рассмотреть всего 3 типа IPC:

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

Именованный канал

Для передачи сообщений можно использовать механизмы сокетов, каналов, D-bus и другие технологии. Про сокеты на каждом углу можно почитать, а про D-bus отдельную статью написать. Поэтому я решил остановиться на малоозвученных технологиях отвечающих стандартам POSIX и привести рабочие примеры.

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

Для создания именованных каналов будем использовать функцию, mkfifo() :
#include int mkfifo(const char *pathname, mode_t mode);
Функция создает специальный FIFO файл с именем pathname , а параметр mode задает права доступа к файлу.

Примечание: mode используется в сочетании с текущим значением umask следующим образом: (mode & ~umask) . Результатом этой операции и будет новое значение umask для создаваемого нами файла. По этой причине мы используем 0777 (S_IRWXO | S_IRWXG | S_IRWXU ), чтобы не затирать ни один бит текущей маски.
Как только файл создан, любой процесс может открыть этот файл для чтения или записи также, как открывает обычный файл. Однако, для корректного использования файла, необходимо открыть его одновременно двумя процессами/потоками, одним для получение данных (чтение файла), другим на передачу (запись в файл).

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

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

  • EACCES - нет прав на запуск (execute) в одной из директорий в пути pathname
  • EEXIST - файл pathname уже существует, даже если файл - символическая ссылка
  • ENOENT - не существует какой-либо директории, упомянутой в pathname , либо является битой ссылкой
  • ENOSPC - нет места для создания нового файла
  • ENOTDIR - одна из директорий, упомянутых в pathname , на самом деле не является таковой
  • EROFS - попытка создать FIFO файл на файловой системе «только-на-чтение»
Чтение и запись в созданный файл производится с помощью функций read() и write() .

Пример

mkfifo.c
#include #include #include #include #define NAMEDPIPE_NAME "/tmp/my_named_pipe" #define BUFSIZE 50 int main (int argc, char ** argv) { int fd, len; char buf; if (mkfifo(NAMEDPIPE_NAME, 0777)) { perror("mkfifo"); return 1; } printf("%s is created\n", NAMEDPIPE_NAME); if ((fd = open(NAMEDPIPE_NAME, O_RDONLY)) <= 0) { perror("open"); return 1; } printf("%s is opened\n", NAMEDPIPE_NAME); do { memset(buf, "\0", BUFSIZE); if ((len = read(fd, buf, BUFSIZE-1)) <= 0) { perror("read"); close(fd); remove(NAMEDPIPE_NAME); return 0; } printf("Incomming message (%d): %s\n", len, buf); } while (1); } [скачать ]

Мы открываем файл только для чтения (O_RDONLY ). И могли бы использовать O_NONBLOCK модификатор, предназначенный специально для FIFO файлов, чтобы не ждать когда с другой стороны файл откроют для записи. Но в приведенном коде такой способ неудобен.

Компилируем программу, затем запускаем ее:
$ gcc -o mkfifo mkfifo.c $ ./mkfifo
В соседнем терминальном окне выполняем:
$ echo "Hello, my named pipe!" > /tmp/my_named_pipe
В результате мы увидим следующий вывод от программы:
$ ./mkfifo /tmp/my_named_pipe is created /tmp/my_named_pipe is opened Incomming message (22): Hello, my named pipe! read: Success

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

Следующий тип межпроцессного взаимодействия - разделяемая память (shared memory ). Схематично изобразим ее как некую именованную область в памяти, к которой обращаются одновременно два процесса:


Для выделения разделяемой памяти будем использовать POSIX функцию shm_open() :
#include int shm_open(const char *name, int oflag, mode_t mode);
Функция возвращает файловый дескриптор, который связан с объектом памяти. Этот дескриптор в дальнейшем можно использовать другими функциями (к примеру, mmap() или mprotect() ).

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

Переменная oflag является побитовым «ИЛИ» следующих флагов:

  • O_RDONLY - открыть только с правами на чтение
  • O_RDWR - открыть с правами на чтение и запись
  • O_CREAT - если объект уже существует, то от флага никакого эффекта. Иначе, объект создается и для него выставляются права доступа в соответствии с mode.
  • O_EXCL - установка этого флага в сочетании с O_CREATE приведет к возврату функцией shm_open ошибки, если сегмент общей памяти уже существует.
Как задается значение параметра mode подробно описано в предыдущем параграфе «передача сообщений».

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

Пример

Следующий код демонстрирует создание, изменение и удаление разделяемой памяти. Так же показывается как после создания разделяемой памяти, программа выходит, но при следующем же запуске мы можем получить к ней доступ, пока не выполнен shm_unlink() .
shm_open.c
#include #include #include #include #include #include #define SHARED_MEMORY_OBJECT_NAME "my_shared_memory" #define SHARED_MEMORY_OBJECT_SIZE 50 #define SHM_CREATE 1 #define SHM_PRINT 3 #define SHM_CLOSE 4 void usage(const char * s) { printf("Usage: %s ["text"]\n", s); } int main (int argc, char ** argv) { int shm, len, cmd, mode = 0; char *addr; if (argc < 2) { usage(argv); return 1; } if ((!strcmp(argv, "create") || !strcmp(argv, "write")) && (argc == 3)) { len = strlen(argv); len = (len<=SHARED_MEMORY_OBJECT_SIZE)?len:SHARED_MEMORY_OBJECT_SIZE; mode = O_CREAT; cmd = SHM_CREATE; } else if (! strcmp(argv, "print")) { cmd = SHM_PRINT; } else if (! strcmp(argv, "unlink")) { cmd = SHM_CLOSE; } else { usage(argv); return 1; } if ((shm = shm_open(SHARED_MEMORY_OBJECT_NAME, mode|O_RDWR, 0777)) == -1) { perror("shm_open"); return 1; } if (cmd == SHM_CREATE) { if (ftruncate(shm, SHARED_MEMORY_OBJECT_SIZE+1) == -1) { perror("ftruncate"); return 1; } } addr = mmap(0, SHARED_MEMORY_OBJECT_SIZE+1, PROT_WRITE|PROT_READ, MAP_SHARED, shm, 0); if (addr == (char*)-1) { perror("mmap"); return 1; } switch (cmd) { case SHM_CREATE: memcpy(addr, argv, len); addr = "\0"; printf("Shared memory filled in. You may run "%s print" to see value.\n", argv); break; case SHM_PRINT: printf("Got from shared memory: %s\n", addr); break; } munmap(addr, SHARED_MEMORY_OBJECT_SIZE); close(shm); if (cmd == SHM_CLOSE) { shm_unlink(SHARED_MEMORY_OBJECT_NAME); } return 0; } [скачать ]

После создания объекта памяти мы установили нужный нам размер shared memory вызовом ftruncate() . Затем мы получили доступ к разделяемой памяти при помощи mmap() . (Вообще говоря, даже с помощью самого вызова mmap() можно создать разделяемую память. Но отличие вызова shm_open() в том, что память будет оставаться выделенной до момента удаления или перезагрузки компьютера.)

Компилировать код на этот раз нужно с опцией -lrt :
$ gcc -o shm_open -lrt shm_open.c
Смотрим что получилось:
$ ./shm_open create "Hello, my shared memory!" Shared memory filled in. You may run "./shm_open print" to see value. $ ./shm_open print Got from shared memory: Hello, my shared memory! $ ./shm_open create "Hello!" Shared memory filled in. You may run "./shm_open print" to see value. $ ./shm_open print Got from shared memory: Hello! $ ./shm_open close $ ./shm_open print shm_open: No such file or directory
Аргумент «create» в нашей программе мы используем как для создания разделенной памяти, так и для изменения ее содержимого.

Зная имя объекта памяти, мы можем менять содержимое разделяемой памяти. Но стоит нам вызвать shm_unlink() , как память перестает быть нам доступна и shm_open() без параметра O_CREATE возвращает ошибку «No such file or directory».

Семафор

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

Есть два типа семафоров:

  1. семафор со счетчиком (counting semaphore), определяющий лимит ресурсов для процессов, получающих доступ к ним
  2. бинарный семафор (binary semaphore), имеющий два состояния «0» или «1» (чаще: «занят» или «не занят»)
Рассмотрим оба типа семафоров.

Семафор со счетчиком

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

Итак, для реализации семафоров будем использовать POSIX функцию sem_open() :
#include sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
В функцию для создания семафора мы передаем имя семафора, построенное по определенным правилам и управляющие флаги. Таким образом у нас получится именованный семафор.
Имя семафора строится следующим образом: в начале идет символ "/" (косая черта), а следом латинские символы. Символ «косая черта» при этом больше не должен применяться. Длина имени семафора может быть вплоть до 251 знака.

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

Параметр mode задает права доступа таким же образом, как это объяснено в предыдущих главах. А переменной value инициализируется начальное значение семафора. Оба параметра mode и value игнорируются в случае, когда семафор с указанным именем уже существует, а sem_open() вызван вместе с флагом O_CREATE .

Для быстрого открытия существующего семафора используем конструкцию:
#include sem_t *sem_open(const char *name, int oflag); , где указываются только имя семафора и управляющий флаг.

Пример семафора со счетчиком

Рассмотрим пример использования семафора для синхронизации процессов. В нашем примере один процесс увеличивает значение семафора и ждет, когда второй сбросит его, чтобы продолжить дальнейшее выполнение.
sem_open.c
#include #include #include #include #define SEMAPHORE_NAME "/my_named_semaphore" int main(int argc, char ** argv) { sem_t *sem; if (argc == 2) { printf("Dropping semaphore...\n"); if ((sem = sem_open(SEMAPHORE_NAME, 0)) == SEM_FAILED) { perror("sem_open"); return 1; } sem_post(sem); perror("sem_post"); printf("Semaphore dropped.\n"); return 0; } if ((sem = sem_open(SEMAPHORE_NAME, O_CREAT, 0777, 0)) == SEM_FAILED) { perror("sem_open"); return 1; } printf("Semaphore is taken.\nWaiting for it to be dropped.\n"); if (sem_wait(sem) < 0) perror("sem_wait"); if (sem_close(sem) < 0) perror("sem_close"); return 0; } [скачать ]

В одной консоли запускаем:
$ ./sem_open Semaphore is taken. Waiting for it to be dropped. <-- здесь процесс в ожидании другого процесса sem_wait: Success sem_close: Success
В соседней консоли запускаем:
$ ./sem_open 1 Dropping semaphore... sem_post: Success Semaphore dropped.

Бинарный семафор

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

Мьютекс по существу является тем же самым, чем является бинарный семафор (т.е. семафор с двумя состояниями: «занят» и «не занят»). Но термин «mutex» чаще используется чтобы описать схему, которая предохраняет два процесса от одновременного использования общих данных/переменных. В то время как термин «бинарный семафор» чаще употребляется для описания конструкции, которая ограничивает доступ к одному ресурсу. То есть бинарный семафор используют там, где один процесс «занимает» семафор, а другой его «освобождает». В то время как мьютекс освобождается тем же процессом/потоком, который занял его.

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

Для использования мьютекса необходимо вызвать функцию pthread_mutex_init():
#include Int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
Функция инициализирует мьютекс (перемнную mutex ) аттрибутом mutexattr . Если mutexattr равен NULL , то мьютекс инициализируется значением по умолчанию. В случае успешного выполнения функции (код возрата 0), мьютекс считается инициализированным и «свободным».

Типичные ошибки, которые могут возникнуть:

  • EAGAIN - недостаточно необходимых ресурсов (кроме памяти) для инициализации мьютекса
  • ENOMEM - недостаточно памяти
  • EPERM - нет прав для выполнения операции
  • EBUSY - попытка инициализировать мьютекс, который уже был инициализирован, но не унечтожен
  • EINVAL - значение mutexattr не валидно
Чтобы занять или освободить мьютекс, используем функции:
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
Функция pthread_mutex_lock() , если mutex еще не занят, то занимает его, становится его обладателем и сразу же выходит. Если мьютекс занят, то блокирует дальнейшее выполнение процесса и ждет освобождения мьютекса.
Функция pthread_mutex_trylock() идентична по поведению функции pthread_mutex_lock() , с одним исключением - она не блокирует процесс, если mutex занят, а возвращает EBUSY код.
Фунция pthread_mutex_unlock() освобождает занятый мьютекс.

Коды возврата для pthread_mutex_lock() :

  • EINVAL - mutex неправильно инициализирован
  • EDEADLK - мьютекс уже занят текущим процессом
Коды возврата для pthread_mutex_trylock() :
  • EBUSY - мьютекс уже занят
Коды возврата для pthread_mutex_unlock() :
  • EINVAL - мьютекс неправильно инициализирован
  • EPERM - вызывающий процесс не является обладателем мьютекса

Пример mutex

mutex.c
#include #include #include #include static int counter; // shared resource static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void incr_counter(void *p) { do { usleep(10); // Let"s have a time slice between mutex locks pthread_mutex_lock(&mutex); counter++; printf("%d\n", counter); sleep(1); pthread_mutex_unlock(&mutex); } while (1); } void reset_counter(void *p) { char buf; int num = 0; int rc; pthread_mutex_lock(&mutex); // block mutex just to show message printf("Enter the number and press "Enter" to initialize the counter with new value anytime.\n"); sleep(3); pthread_mutex_unlock(&mutex); // unblock blocked mutex so another thread may work do { if (gets(buf) != buf) return; // NO fool-protection ! Risk of overflow ! num = atoi(buf); if ((rc = pthread_mutex_trylock(&mutex)) == EBUSY) { printf("Mutex is already locked by another process.\nLet"s lock mutex using pthread_mutex_lock().\n"); pthread_mutex_lock(&mutex); } else if (rc == 0) { printf("WOW! You are on time! Congratulation!\n"); } else { printf("Error: %d\n", rc); return; } counter = num; printf("New value for counter is %d\n", counter); pthread_mutex_unlock(&mutex); } while (1); } int main(int argc, char ** argv) { pthread_t thread_1; pthread_t thread_2; counter = 0; pthread_create(&thread_1, NULL, (void *)&incr_counter, NULL); pthread_create(&thread_2, NULL, (void *)&reset_counter, NULL); pthread_join(thread_2, NULL); return 0; } [скачать ]

Данный пример демонстрирует совместный доступ двух потоков к общей переменной. Один поток (первый поток) в автоматическом режиме постоянно увеличивает переменную counter на единицу, при этом занимая эту переменную на целую секунду. Этот первый поток дает второму доступ к переменной count только на 10 миллисекунд, затем снова занимает ее на секунду. Во втором потоке предлагается ввести новое значение для переменной с терминала.

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

Компилировать код нужно с дополнительным параметром -lpthread :
$ gcc -o mutex -lpthread mutex.c
Запускаем и меняем значение переменной просто вводя новое значение в терминальном окне:
$ ./mutex Enter the number and press "Enter" to initialize the counter with new value anytime. 1 2 3 30 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 30 31 32 33 1 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 1 2 3

Вместо заключения

В следующих статьях я хочу рассмотреть технологии d-bus и RPC. Если есть интерес, дайте знать.
Спасибо.

UPD: Обновил 3-ю главу про семафоры. Добавил подглаву про мьютекс.

Теги: Добавить метки

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

Передача может осуществляться несколькими способами:

— разделяемая память;

— канал (pipe, конвейер) — псевдофайл, в который один процесс пишет, а другой читает;

— сокеты — поддерживаемый ядром механизм, скрывающий особенности среды и позволяющий единообразно взаимодействовать процессам, как на одном компьютере, так и в сети;

— почтовые ящики (только в Windows), однонаправленные, возможность широковещательной рассылки;

— вызов удаленной процедуры, процесс А Может вызвать процедуру в процессе В , и получить обратно данные.

Конвейеры (каналы)

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

Безымянные (или анонимные) каналы позволяют связанным процессам передавать информацию друг другу. Обычно, безымянные каналы используются для перенаправления стандартного ввода/вывода дочернего процесса так, чтобы он мог обмениваться данными с родительским процессом. Чтобы производить обмен данными в обоих направлениях, необходимо создать два безымянных канала. Родительский процесс записывает данные в первый канал, используя его дескриптор записи, в то время как дочерний процесс считывает данные из канала, используя дескриптор чтения. Аналогично, дочерний процесс записывает данные во второй канал и родительский процесс считывает из него данные. Безымянные каналы не могут быть использованы для передачи данных по сети и для обмена между несвязанными процессами.

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

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

Рис. 2.22 Схема реализации канала

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

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

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

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

Рис. 2.23 а) файл, отображенный на память; б) разделяемая память

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

Почтовые ящики

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

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

Процессы, которыми управляет ядро UNIX, выполняются автономно, что ведет к более стабильной работе системы. Тем не менее каждый разработчик в конце концов попадает в ситуацию, когда одна группа процессов должна сообщаться с другой группой, например, для обмена данными или передачи команд. Это метод обмена сообщениями называется Inter-Process Communication (IPC ). Спецификация System V (SysV) UNIX определяет три механизма для IPC, которые обычно называют SysV IPC:

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

В дополнение к этому процессы могут взаимодействовать и другими способами, такими как:

  • Чтение, запись и блокировка доступа к файлам
  • Сигналы
  • Сокеты
  • Каналы
  • FIFO (First In, First Out)

Последнюю группу достаточно часто относят к IPC. В этой статье основной акцент делается на IPC-методах SysV ввиду их простоты и эффективности.

Понимание модели SysV

Три IPC-метода SysV имеют сходный синтаксис, несмотря на то, что цели их различны. Обычно следует выполнить следующие действия:

  1. Определите подходящий ключ IPC для использования с ftok(3) .
  2. Поставьте IPC-специфичный идентификатор, связанный с ключом IPC, используя для этого msgget(2) , semget(2) или shmget(2) для очередей сообщений, семафоров или разделяемой памяти соответственно.
  3. Измените свойства экземпляра IPC при помощи msgctl(2) , semctl(2) или shmctl(2) .
  4. Используйте конкретный экземпляр IPC.
  5. В конце уничтожьте IPC-экземпляр при помощи msgctl(2) , semctl(2) или shmctl(2) и флажка IPC_RMID .

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

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

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

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

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

Передача сообщений через очереди

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

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

Листинг 1. С определение образца сообщения
struct mq_message { long type; /* The type or destination */ char text; /* Data */ };

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

Показывает пример постановки сообщения в очередь.

Листинг 2. Программа, которая ставит сообщения в очередь
#include #include #include #include #include int main (void) { key_t ipckey; int mq_id; struct { long type; char text; } mymsg; /* Generate the ipc key */ ipckey = ftok("/tmp/foo", 42); printf("My key is %d\n", ipckey); /* Set up the message queue */ mq_id = msgget(ipckey, IPC_CREAT | 0666); printf("Message identifier is %d\n", mq_id); /* Send a message */ memset(mymsg.text, 0, 100); /* Clear out the space */ strcpy(mymsg.text, "Hello, world!"); mymsg.type = 1; msgsnd(mq_id, &mymsg, sizeof(mymsg), 0); }

Код в импортирует необходимые заголовочные файлы и затем определяет переменные для использования в пределах функции main . Первым делом нужно определить IPC-ключ, используя /tmp/foo как общий файл и число 42 как ID. Для просмотра это число выводится на дисплей с помощью printf(3c) . Затем очередь сообщений создается с помощью msgget . Первым параметром для msgget является IPC-ключ, а вторым - набор флажков. В примере флажки имеют восьмеричные разрешения, что позволяет любому, кто имеет IPC-ключ, полностью использовать этот IPC, а также флажок IPC_CREAT , который вызывает msgget для создания очереди. Опять же результат выводится на экран.

Отправлять сообщение в очередь просто. После обнуления пространства памяти в сообщении обычная строка копируется в текстовую часть буфера. Тип сообщения определяется как 1, и затем вызывается msgsnd . msgsnd ожидает передачи ID очереди, указателя на данные, размер данных и флажок, который показывает, должен ли блокироваться вызов или нет. Если выставлен флажок IPC_NOWAIT , вызов возвращается, даже если очередь заполнена. Если же стоит флажок 0, то вызов блокируется до тех пор, пока в очереди не будет свободного места, очередь удаляется либо приложение получает сигнал.

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

Листинг 3. Код для извлечения сообщения из очереди
#include #include #include #include #include int main (void) { key_t ipckey; int mq_id; struct { long type; char text; } mymsg; int received; /* Generate the ipc key */ ipckey = ftok("/tmp/foo", 42); printf("My key is %d\n", ipckey); /* Set up the message queue */ mq_id = msgget(ipckey, 0); printf("Message identifier is %d\n", mq_id); received = msgrcv(mq_id, &mymsg, sizeof(mymsg), 0, 0); printf("%s (%d)\n", mymsg.text, received); }

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

mq_client.c затем вызывает msgrcv , чтобы получить сообщение из очереди. Первые три переменные определяют идентификатор очереди сообщений, указатель на объем памяти для сообщения и размер буфера. Четвертый параметр - параметр типа, который позволяет вам выбирать то, какие сообщения получать:

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

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

Листинг 4. Результаты работы клиентского и серверного кода
sunbox$ ./mq_server My key is 704654099 Message identifier is 2 sunbox$ ./mq_client My key is 704654099 Message identifier is 2 Hello, world! (104)

Результаты работы клиента и сервера показывают, что оба они получили одинаковый IPC-ключ, поскольку они ссылались на один и тот же файл и идентификатор. Сервер создал IPC-экземпляр, для которого ядро определило значение 2, а приложение клиента об это узнало. Поэтому не стоит удивляться, что клиент получает "Hello, world!" из очереди сообщений.

Данный пример показывает простейшую из ситуаций. Очередь сообщений может пригодиться для кратковременных процессов, таких как Web-транзакция, которая определяет работу для "тяжелого" приложения back-end, например, как выполнение пакета заданий. Клиент также может быть сервером, и многочисленные приложения могут отправлять сообщения в очередь. Поле типа сообщения позволяет приложениям отправлять сообщения конкретным адресатам.

Блокировка ресурсов с помощью семафоров

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

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

В SysV реализация семафоров является более общей, чем описанная выше. Прежде всего, значение семафора не обязательно должно быть 0 или 1; оно может быть 0 или любым положительным числом. Во-вторых, ряд операций с семафорами возможен аналогично параметру type , использованному с msgrcv . Эти операции даются как набор инструментов для ядра, и они либо запускаются все вместе, либо не запускаются вообще. Ядро требует того, чтобы эти команды осуществлялись в структуре, называемой sembuf , которая включает в себя компоненты (в порядке следования):

  1. sem_num: описание того, над каким семафором из набора производится действие.
  2. sem_op: целое число со знаком, содержащее команду или тест, которые должны быть выполнены.
  3. sem_flg: комбинация обычного флажка IPC_NOWAIT , который показывает, нужно ли немедленно запустить тест или блокировать до его завершения, а также SEM_UNDO , которая отменяет операции с семафором в случае преждевременного завершения процесса.

В sem_op имеется большое количество конфигураций:

  • Если sem_op равняется 0, то sem_num тестируется, чтобы посмотреть равно ли значение 0. Если sem_num равняется 0, проводится следующий тест. Если sem_num не равно 0, операция либо блокируется до того момента, как значение семафора будет равно 0 и если IPC_NOWAIT не установлена, либо остальные тесты пропускаются, если установлено IPC_NOWAIT .
  • Если sem_op является положительным целым числом, значение sem_op добавляется к значению семафора.
  • Если sem_op является отрицательным целым числом и отрицательное значение семафора больше или равно абсолютному значению sem_op , то абсолютное значение вычитается из значения семафора.
  • Если sem_op является отрицательным целым числом и значение семафора менее абсолютного значения sem_op , то проведение тестов немедленно останавливается, если IPC_NOWAIT действительно, либо блокируется до того времени, пока значение семафора не станет больше абсолютного значения sem_op .

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

Листинг 5. Использование семафора для защиты критического раздела
#include #include #include #include /* For strerror(3c) */ #include /* For errno */ #include /* rand(3c) */ #include int main (int argc, char **argv) { key_t ipckey; int semid; struct sembuf sem; /* sembuf defined in sys/sem.h */ /* Generate the ipc key */ ipckey = ftok("/tmp/foo", 42); /* Set up the semaphore set. 4 == READ, 2 == ALTER */ semid = semget(ipckey, 1, 0666 | IPC_CREAT); if (semid < 0) { printf("Error - %s\n", strerror(errno)); _exit(1); } /* These never change so leave them outside the loop */ sem.sem_num = 0; sem.sem_num = 0; sem.sem_flg = SEM_UNDO; /* Release semaphore on exit */ sem.sem_flg = SEM_UNDO; /* Release semaphore on exit */ while(1) { /* loop forever */ printf("[%s] Waiting for the semaphore to be released\n", argv); /* Set up two semaphore operations */ sem.sem_op = 0; /* Wait for zero */ sem.sem_op = 1; /* Add 1 to lock it*/ semop(semid, sem, 2); printf("[%s] I have the semaphore\n", argv); sleep(rand() % 3); /* Critical section, sleep for 0-2 seconds */ sem.sem_op = -1; /* Decrement to unlock */ semop(semid, sem, 1); printf("[%s] Released semaphore\n", argv); sleep(rand() % 3); /* Sleep 0-2 seconds */ } }

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

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

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

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

Листинг 6. Две программы, использующие семафор для защиты критического раздела
sunbox$ ./sem_example a & ./sem_example b & [a] Waiting for the semaphore to be released [a] I have the semaphore [b] Waiting for the semaphore to be released [a] Released semaphore [b] I have the semaphore [a] Waiting for the semaphore to be released [b] Released semaphore [a] I have the semaphore [a] Released semaphore [a] Waiting for the semaphore to be released [a] I have the semaphore

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

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

Общая память

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

Листинг 7. Программа, иллюстрирующая использование общей памяти
#include #include #include #include #include #include int main(void) { pid_t pid; int *shared; /* pointer to the shm */ int shmid; shmid = shmget(IPC_PRIVATE, sizeof(int), IPC_CREAT | 0666); if (fork() == 0) { /* Child */ /* Attach to shared memory and print the pointer */ shared = shmat(shmid, (void *) 0, 0); printf("Child pointer %p\n", shared); *shared=1; printf("Child value=%d\n", *shared); sleep(2); printf("Child value=%d\n", *shared); } else { /* Parent */ /* Attach to shared memory and print the pointer */ shared = shmat(shmid, (void *) 0, 0); printf("Parent pointer %p\n", shared); printf("Parent value=%d\n", *shared); sleep(1); *shared=42; printf("Parent value=%d\n", *shared); sleep(5); shmctl(shmid, IPC_RMID, 0); } }

Параметры для shmget должны быть известны к этому времени: ключ, размер и флажки. Размер участка общей памяти в данном примере является одиночным целым числом. отличается от предыдущих примеров в использовании IPC_PRIVATE для IPC-ключа. Когда используется IPC_PRIVATE , гарантируется уникальный IPC ID, и предполагается, что приложение само представит ID. В примере shmid является и порождающим, и дочерним процессом, поскольку они являются копиями друг друга. Системное обращение fork производит вторую копию текущего процесса, называемого дочерним, которая фактически является идентичной порождающему процессу. Выполнение обоих процессов возобновляется после fork . Результат выполнения fork используется, чтобы определить является ли текущий процесс порождающим или дочерним.

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

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

Листинг 8. Пример вывода общей памяти
sunbox$ ./shared_memory Child pointer ff390000 Child value=1 Parent pointer ff380000 Parent value=1 Parent value=42 Child value=42

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

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

Заключение

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

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

Межпроцессное взаимодействие (Inter-process communication (IPC) ) - это набор методов для обмена данными между потоками процессов. Процессы могут быть запущены как на одном и том же компьютере, так и на разных, соединенных сетью. IPC бывают нескольких типов: «сигнал», «сокет», «семафор», «файл», «сообщение»…

В данной статье я хочу рассмотреть всего 3 типа IPC:

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

Именованный канал

Для передачи сообщений можно использовать механизмы сокетов, каналов, D-bus и другие технологии. Про сокеты на каждом углу можно почитать, а про D-bus отдельную статью написать. Поэтому я решил остановиться на малоозвученных технологиях отвечающих стандартам POSIX и привести рабочие примеры.

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

Для создания именованных каналов будем использовать функцию, mkfifo() :
#include int mkfifo(const char *pathname, mode_t mode);
Функция создает специальный FIFO файл с именем pathname , а параметр mode задает права доступа к файлу.

Примечание: mode используется в сочетании с текущим значением umask следующим образом: (mode & ~umask) . Результатом этой операции и будет новое значение umask для создаваемого нами файла. По этой причине мы используем 0777 (S_IRWXO | S_IRWXG | S_IRWXU ), чтобы не затирать ни один бит текущей маски.
Как только файл создан, любой процесс может открыть этот файл для чтения или записи также, как открывает обычный файл. Однако, для корректного использования файла, необходимо открыть его одновременно двумя процессами/потоками, одним для получение данных (чтение файла), другим на передачу (запись в файл).

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

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

  • EACCES - нет прав на запуск (execute) в одной из директорий в пути pathname
  • EEXIST - файл pathname уже существует, даже если файл - символическая ссылка
  • ENOENT - не существует какой-либо директории, упомянутой в pathname , либо является битой ссылкой
  • ENOSPC - нет места для создания нового файла
  • ENOTDIR - одна из директорий, упомянутых в pathname , на самом деле не является таковой
  • EROFS - попытка создать FIFO файл на файловой системе «только-на-чтение»
Чтение и запись в созданный файл производится с помощью функций read() и write() .

Пример

mkfifo.c
#include #include #include #include #define NAMEDPIPE_NAME "/tmp/my_named_pipe" #define BUFSIZE 50 int main (int argc, char ** argv) { int fd, len; char buf; if (mkfifo(NAMEDPIPE_NAME, 0777)) { perror("mkfifo"); return 1; } printf("%s is created\n", NAMEDPIPE_NAME); if ((fd = open(NAMEDPIPE_NAME, O_RDONLY)) <= 0) { perror("open"); return 1; } printf("%s is opened\n", NAMEDPIPE_NAME); do { memset(buf, "\0", BUFSIZE); if ((len = read(fd, buf, BUFSIZE-1)) <= 0) { perror("read"); close(fd); remove(NAMEDPIPE_NAME); return 0; } printf("Incomming message (%d): %s\n", len, buf); } while (1); } [скачать ]

Мы открываем файл только для чтения (O_RDONLY ). И могли бы использовать O_NONBLOCK модификатор, предназначенный специально для FIFO файлов, чтобы не ждать когда с другой стороны файл откроют для записи. Но в приведенном коде такой способ неудобен.

Компилируем программу, затем запускаем ее:
$ gcc -o mkfifo mkfifo.c $ ./mkfifo
В соседнем терминальном окне выполняем:
$ echo "Hello, my named pipe!" > /tmp/my_named_pipe
В результате мы увидим следующий вывод от программы:
$ ./mkfifo /tmp/my_named_pipe is created /tmp/my_named_pipe is opened Incomming message (22): Hello, my named pipe! read: Success

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

Следующий тип межпроцессного взаимодействия - разделяемая память (shared memory ). Схематично изобразим ее как некую именованную область в памяти, к которой обращаются одновременно два процесса:


Для выделения разделяемой памяти будем использовать POSIX функцию shm_open() :
#include int shm_open(const char *name, int oflag, mode_t mode);
Функция возвращает файловый дескриптор, который связан с объектом памяти. Этот дескриптор в дальнейшем можно использовать другими функциями (к примеру, mmap() или mprotect() ).

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

Переменная oflag является побитовым «ИЛИ» следующих флагов:

  • O_RDONLY - открыть только с правами на чтение
  • O_RDWR - открыть с правами на чтение и запись
  • O_CREAT - если объект уже существует, то от флага никакого эффекта. Иначе, объект создается и для него выставляются права доступа в соответствии с mode.
  • O_EXCL - установка этого флага в сочетании с O_CREATE приведет к возврату функцией shm_open ошибки, если сегмент общей памяти уже существует.
Как задается значение параметра mode подробно описано в предыдущем параграфе «передача сообщений».

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

Пример

Следующий код демонстрирует создание, изменение и удаление разделяемой памяти. Так же показывается как после создания разделяемой памяти, программа выходит, но при следующем же запуске мы можем получить к ней доступ, пока не выполнен shm_unlink() .
shm_open.c
#include #include #include #include #include #include #define SHARED_MEMORY_OBJECT_NAME "my_shared_memory" #define SHARED_MEMORY_OBJECT_SIZE 50 #define SHM_CREATE 1 #define SHM_PRINT 3 #define SHM_CLOSE 4 void usage(const char * s) { printf("Usage: %s ["text"]\n", s); } int main (int argc, char ** argv) { int shm, len, cmd, mode = 0; char *addr; if (argc < 2) { usage(argv); return 1; } if ((!strcmp(argv, "create") || !strcmp(argv, "write")) && (argc == 3)) { len = strlen(argv); len = (len<=SHARED_MEMORY_OBJECT_SIZE)?len:SHARED_MEMORY_OBJECT_SIZE; mode = O_CREAT; cmd = SHM_CREATE; } else if (! strcmp(argv, "print")) { cmd = SHM_PRINT; } else if (! strcmp(argv, "unlink")) { cmd = SHM_CLOSE; } else { usage(argv); return 1; } if ((shm = shm_open(SHARED_MEMORY_OBJECT_NAME, mode|O_RDWR, 0777)) == -1) { perror("shm_open"); return 1; } if (cmd == SHM_CREATE) { if (ftruncate(shm, SHARED_MEMORY_OBJECT_SIZE+1) == -1) { perror("ftruncate"); return 1; } } addr = mmap(0, SHARED_MEMORY_OBJECT_SIZE+1, PROT_WRITE|PROT_READ, MAP_SHARED, shm, 0); if (addr == (char*)-1) { perror("mmap"); return 1; } switch (cmd) { case SHM_CREATE: memcpy(addr, argv, len); addr = "\0"; printf("Shared memory filled in. You may run "%s print" to see value.\n", argv); break; case SHM_PRINT: printf("Got from shared memory: %s\n", addr); break; } munmap(addr, SHARED_MEMORY_OBJECT_SIZE); close(shm); if (cmd == SHM_CLOSE) { shm_unlink(SHARED_MEMORY_OBJECT_NAME); } return 0; } [скачать ]

После создания объекта памяти мы установили нужный нам размер shared memory вызовом ftruncate() . Затем мы получили доступ к разделяемой памяти при помощи mmap() . (Вообще говоря, даже с помощью самого вызова mmap() можно создать разделяемую память. Но отличие вызова shm_open() в том, что память будет оставаться выделенной до момента удаления или перезагрузки компьютера.)

Компилировать код на этот раз нужно с опцией -lrt :
$ gcc -o shm_open -lrt shm_open.c
Смотрим что получилось:
$ ./shm_open create "Hello, my shared memory!" Shared memory filled in. You may run "./shm_open print" to see value. $ ./shm_open print Got from shared memory: Hello, my shared memory! $ ./shm_open create "Hello!" Shared memory filled in. You may run "./shm_open print" to see value. $ ./shm_open print Got from shared memory: Hello! $ ./shm_open close $ ./shm_open print shm_open: No such file or directory
Аргумент «create» в нашей программе мы используем как для создания разделенной памяти, так и для изменения ее содержимого.

Зная имя объекта памяти, мы можем менять содержимое разделяемой памяти. Но стоит нам вызвать shm_unlink() , как память перестает быть нам доступна и shm_open() без параметра O_CREATE возвращает ошибку «No such file or directory».

Семафор

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

Есть два типа семафоров:

  1. семафор со счетчиком (counting semaphore), определяющий лимит ресурсов для процессов, получающих доступ к ним
  2. бинарный семафор (binary semaphore), имеющий два состояния «0» или «1» (чаще: «занят» или «не занят»)
Рассмотрим оба типа семафоров.

Семафор со счетчиком

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

Итак, для реализации семафоров будем использовать POSIX функцию sem_open() :
#include sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
В функцию для создания семафора мы передаем имя семафора, построенное по определенным правилам и управляющие флаги. Таким образом у нас получится именованный семафор.
Имя семафора строится следующим образом: в начале идет символ "/" (косая черта), а следом латинские символы. Символ «косая черта» при этом больше не должен применяться. Длина имени семафора может быть вплоть до 251 знака.

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

Параметр mode задает права доступа таким же образом, как это объяснено в предыдущих главах. А переменной value инициализируется начальное значение семафора. Оба параметра mode и value игнорируются в случае, когда семафор с указанным именем уже существует, а sem_open() вызван вместе с флагом O_CREATE .

Для быстрого открытия существующего семафора используем конструкцию:
#include sem_t *sem_open(const char *name, int oflag); , где указываются только имя семафора и управляющий флаг.

Пример семафора со счетчиком

Рассмотрим пример использования семафора для синхронизации процессов. В нашем примере один процесс увеличивает значение семафора и ждет, когда второй сбросит его, чтобы продолжить дальнейшее выполнение.
sem_open.c
#include #include #include #include #define SEMAPHORE_NAME "/my_named_semaphore" int main(int argc, char ** argv) { sem_t *sem; if (argc == 2) { printf("Dropping semaphore...\n"); if ((sem = sem_open(SEMAPHORE_NAME, 0)) == SEM_FAILED) { perror("sem_open"); return 1; } sem_post(sem); perror("sem_post"); printf("Semaphore dropped.\n"); return 0; } if ((sem = sem_open(SEMAPHORE_NAME, O_CREAT, 0777, 0)) == SEM_FAILED) { perror("sem_open"); return 1; } printf("Semaphore is taken.\nWaiting for it to be dropped.\n"); if (sem_wait(sem) < 0) perror("sem_wait"); if (sem_close(sem) < 0) perror("sem_close"); return 0; } [скачать ]

В одной консоли запускаем:
$ ./sem_open Semaphore is taken. Waiting for it to be dropped. <-- здесь процесс в ожидании другого процесса sem_wait: Success sem_close: Success
В соседней консоли запускаем:
$ ./sem_open 1 Dropping semaphore... sem_post: Success Semaphore dropped.

Бинарный семафор

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

Мьютекс по существу является тем же самым, чем является бинарный семафор (т.е. семафор с двумя состояниями: «занят» и «не занят»). Но термин «mutex» чаще используется чтобы описать схему, которая предохраняет два процесса от одновременного использования общих данных/переменных. В то время как термин «бинарный семафор» чаще употребляется для описания конструкции, которая ограничивает доступ к одному ресурсу. То есть бинарный семафор используют там, где один процесс «занимает» семафор, а другой его «освобождает». В то время как мьютекс освобождается тем же процессом/потоком, который занял его.

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

Для использования мьютекса необходимо вызвать функцию pthread_mutex_init():
#include Int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
Функция инициализирует мьютекс (перемнную mutex ) аттрибутом mutexattr . Если mutexattr равен NULL , то мьютекс инициализируется значением по умолчанию. В случае успешного выполнения функции (код возрата 0), мьютекс считается инициализированным и «свободным».

Типичные ошибки, которые могут возникнуть:

  • EAGAIN - недостаточно необходимых ресурсов (кроме памяти) для инициализации мьютекса
  • ENOMEM - недостаточно памяти
  • EPERM - нет прав для выполнения операции
  • EBUSY - попытка инициализировать мьютекс, который уже был инициализирован, но не унечтожен
  • EINVAL - значение mutexattr не валидно
Чтобы занять или освободить мьютекс, используем функции:
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
Функция pthread_mutex_lock() , если mutex еще не занят, то занимает его, становится его обладателем и сразу же выходит. Если мьютекс занят, то блокирует дальнейшее выполнение процесса и ждет освобождения мьютекса.
Функция pthread_mutex_trylock() идентична по поведению функции pthread_mutex_lock() , с одним исключением - она не блокирует процесс, если mutex занят, а возвращает EBUSY код.
Фунция pthread_mutex_unlock() освобождает занятый мьютекс.

Коды возврата для pthread_mutex_lock() :

  • EINVAL - mutex неправильно инициализирован
  • EDEADLK - мьютекс уже занят текущим процессом
Коды возврата для pthread_mutex_trylock() :
  • EBUSY - мьютекс уже занят
Коды возврата для pthread_mutex_unlock() :
  • EINVAL - мьютекс неправильно инициализирован
  • EPERM - вызывающий процесс не является обладателем мьютекса

Пример mutex

mutex.c
#include #include #include #include static int counter; // shared resource static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void incr_counter(void *p) { do { usleep(10); // Let"s have a time slice between mutex locks pthread_mutex_lock(&mutex); counter++; printf("%d\n", counter); sleep(1); pthread_mutex_unlock(&mutex); } while (1); } void reset_counter(void *p) { char buf; int num = 0; int rc; pthread_mutex_lock(&mutex); // block mutex just to show message printf("Enter the number and press "Enter" to initialize the counter with new value anytime.\n"); sleep(3); pthread_mutex_unlock(&mutex); // unblock blocked mutex so another thread may work do { if (gets(buf) != buf) return; // NO fool-protection ! Risk of overflow ! num = atoi(buf); if ((rc = pthread_mutex_trylock(&mutex)) == EBUSY) { printf("Mutex is already locked by another process.\nLet"s lock mutex using pthread_mutex_lock().\n"); pthread_mutex_lock(&mutex); } else if (rc == 0) { printf("WOW! You are on time! Congratulation!\n"); } else { printf("Error: %d\n", rc); return; } counter = num; printf("New value for counter is %d\n", counter); pthread_mutex_unlock(&mutex); } while (1); } int main(int argc, char ** argv) { pthread_t thread_1; pthread_t thread_2; counter = 0; pthread_create(&thread_1, NULL, (void *)&incr_counter, NULL); pthread_create(&thread_2, NULL, (void *)&reset_counter, NULL); pthread_join(thread_2, NULL); return 0; } [скачать ]

Данный пример демонстрирует совместный доступ двух потоков к общей переменной. Один поток (первый поток) в автоматическом режиме постоянно увеличивает переменную counter на единицу, при этом занимая эту переменную на целую секунду. Этот первый поток дает второму доступ к переменной count только на 10 миллисекунд, затем снова занимает ее на секунду. Во втором потоке предлагается ввести новое значение для переменной с терминала.

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

Компилировать код нужно с дополнительным параметром -lpthread :
$ gcc -o mutex -lpthread mutex.c
Запускаем и меняем значение переменной просто вводя новое значение в терминальном окне:
$ ./mutex Enter the number and press "Enter" to initialize the counter with new value anytime. 1 2 3 30 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 30 31 32 33 1 <--- новое значение переменной Mutex is already locked by another process. Let"s lock mutex using pthread_mutex_lock(). New value for counter is 1 2 3

Вместо заключения

В следующих статьях я хочу рассмотреть технологии d-bus и RPC. Если есть интерес, дайте знать.
Спасибо.

UPD: Обновил 3-ю главу про семафоры. Добавил подглаву про мьютекс.

Теги:

  • linux
  • posix
  • ipc
  • программирование
Добавить метки