Межпроцессное взаимодействие (Inter-process communication (IPC) ) - это набор методов для обмена данными между потоками процессов. Процессы могут быть запущены как на одном и том же компьютере, так и на разных, соединенных сетью. IPC бывают нескольких типов: «сигнал», «сокет», «семафор», «файл», «сообщение»…
В данной статье я хочу рассмотреть всего 3 типа IPC:
Рассмотрим передачу сообщений по именованным каналам. Схематично передача выглядит так:
Для создания именованных каналов будем использовать функцию, mkfifo()
:
#include
Функция создает специальный FIFO файл с именем pathname
, а параметр mode
задает права доступа к файлу.
В случае успешного создания FIFO файла, mkfifo() возвращает 0 (нуль). В случае каких либо ошибок, функция возвращает -1 и выставляет код ошибки в переменную errno .
Типичные ошибки, которые могут возникнуть во время создания канала:
Мы открываем файл только для чтения (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
Целостность объекта памяти сохраняется, включая все данные связанные с ним, до тех пор пока объект не отсоединен/удален (shm_unlink() ). Это означает, что любой процесс может получить доступ к нашему объекту памяти (если он знает его имя) до тех пор, пока явно в одном из процессов мы не вызовем shm_unlink() .
Переменная oflag является побитовым «ИЛИ» следующих флагов:
После создания общего объекта памяти, мы задаем размер разделяемой памяти вызовом ftruncate() . На входе у функции файловый дескриптор нашего объекта и необходимый нам размер.
После создания объекта памяти мы установили нужный нам размер 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».
Есть два типа семафоров:
Итак, для реализации семафоров будем использовать POSIX функцию sem_open()
:
#include
В функцию для создания семафора мы передаем имя семафора, построенное по определенным правилам и управляющие флаги. Таким образом у нас получится именованный семафор.
Имя семафора строится следующим образом: в начале идет символ "/" (косая черта), а следом латинские символы. Символ «косая черта» при этом больше не должен применяться. Длина имени семафора может быть вплоть до 251 знака.
Если нам необходимо создать семафор, то передается управляющий флаг O_CREATE . Чтобы начать использовать уже существующий семафор, то oflag равняется нулю. Если вместе с флагом O_CREATE передать флаг O_EXCL , то функция sem_open() вернет ошибку, в случае если семафор с указанным именем уже существует.
Параметр mode задает права доступа таким же образом, как это объяснено в предыдущих главах. А переменной value инициализируется начальное значение семафора. Оба параметра mode и value игнорируются в случае, когда семафор с указанным именем уже существует, а sem_open() вызван вместе с флагом O_CREATE .
Для быстрого открытия существующего семафора используем конструкцию:
#include
В одной консоли запускаем:
$ ./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.
Мьютекс по существу является тем же самым, чем является бинарный семафор (т.е. семафор с двумя состояниями: «занят» и «не занят»). Но термин «mutex» чаще используется чтобы описать схему, которая предохраняет два процесса от одновременного использования общих данных/переменных. В то время как термин «бинарный семафор» чаще употребляется для описания конструкции, которая ограничивает доступ к одному ресурсу. То есть бинарный семафор используют там, где один процесс «занимает» семафор, а другой его «освобождает». В то время как мьютекс освобождается тем же процессом/потоком, который занял его.
Без мьютекса не обойтись в написании, к примеру базы данных, к которой доступ могут иметь множество клиентов.
Для использования мьютекса необходимо вызвать функцию pthread_mutex_init():
#include
Функция инициализирует мьютекс (перемнную mutex
) аттрибутом mutexattr
. Если mutexattr
равен NULL
, то мьютекс инициализируется значением по умолчанию. В случае успешного выполнения функции (код возрата 0), мьютекс считается инициализированным и «свободным».
Типичные ошибки, которые могут возникнуть:
Коды возврата для pthread_mutex_lock() :
Данный пример демонстрирует совместный доступ двух потоков к общей переменной. Один поток (первый поток) в автоматическом режиме постоянно увеличивает переменную 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
UPD: Обновил 3-ю главу про семафоры. Добавил подглаву про мьютекс.
Теги: Добавить метки
Операционная система, имея доступ ко всем областям памяти, играет роль посредника в информационном обмене прикладных потоков. При необходимости в обмене данными поток обращается с запросом к ОС. ОС, пользуясь своими привилегиями, создает различные системные средства связи. Многие из средств межпроцессного обмена данными выполняют также и функции синхронизации: в том случае, когда данные для процесса-получателя отсутствуют, последний переводится в состояние ожидания средствами ОС, а при поступлении данных от процесса-отправителя процесс-получатель активизируется.
Передача может осуществляться несколькими способами:
— разделяемая память;
— канал (pipe, конвейер) — псевдофайл, в который один процесс пишет, а другой читает;
— сокеты — поддерживаемый ядром механизм, скрывающий особенности среды и позволяющий единообразно взаимодействовать процессам, как на одном компьютере, так и в сети;
— почтовые ящики (только в Windows), однонаправленные, возможность широковещательной рассылки;
— вызов удаленной процедуры, процесс А Может вызвать процедуру в процессе В , и получить обратно данные.
Конвейеры (каналы)
Существует два способа организовать двунаправленное соединение с помощью каналов: безымянные и именованные каналы. Канал представляет собой буфер в оперативной памяти, поддерживающий очередь байт по алгоритму FIFO. Для программиста этот буфер выглядит как безымянный файл, в который можно писать и читать, осуществляя тем самым обмен данными. Обмениваться данными могут только родственные процессы, точнее процессы, которые имеют общего прародителя, создавшего данный конвейер. Связано это с тем, что конвейер не имеет имени, обращение к нему происходит по дескриптору, который имеет локальное для каждого процесса значение.
Безымянные (или анонимные) каналы позволяют связанным процессам передавать информацию друг другу. Обычно, безымянные каналы используются для перенаправления стандартного ввода/вывода дочернего процесса так, чтобы он мог обмениваться данными с родительским процессом. Чтобы производить обмен данными в обоих направлениях, необходимо создать два безымянных канала. Родительский процесс записывает данные в первый канал, используя его дескриптор записи, в то время как дочерний процесс считывает данные из канала, используя дескриптор чтения. Аналогично, дочерний процесс записывает данные во второй канал и родительский процесс считывает из него данные. Безымянные каналы не могут быть использованы для передачи данных по сети и для обмена между несвязанными процессами.
Именованные конвейеры. Такие конвейеры имеют имя, которое является записью в каталоге файловой системы ОС, поэтому пригодны для обмена данными между двумя произвольными процессами или потоками этих процессов. Именованный конвейер является специальным файлом типа FIFO и не имеет области данных на диске. Создается именованный конвейер с помощью того же системного вызова, который используется для создания файлов любого типа, но только с указанием в качестве типа файла параметра FIFO. Системный вызов порождает в каталоге запись о файле FIFO с заданным именем, после чего любой процесс может открыть этот файл и передавать данные другому процессу, также открывшему файл с этим именем.
Именованные каналы используются для передачи данных между независимыми процессами или между процессами, работающими на разных компьютерах. Обычно, процесс сервера именованных каналов создает именованный канал с известным именем или с именем, которое будет передано клиентам. Процесс клиента именованных каналов, зная имя созданного канала, открывает его на своей стороне с учетом ограничений, указанных процессом сервера. После этого между сервером и клиентом создается соединение, по которому может производиться обмен данными в обоих направлениях. В дальнейшем наибольший интерес для нас будут представлять именованные каналы.
Рис. 2.22 Схема реализации канала
Очереди сообщений
Механизм очередей сообщений похож на механизм конвейеров с тем отличием, что он позволяет процессам и потока обмениваться структурированными сообщениями. Очереди сообщений являются глобальными средствами коммуникаций для процессов операционной системы, так как каждая очередь в пределах ОС имеет уникальное имя.
Разделяемая память
Разделяемая память представляет собой сегмент физической памяти, отображенной в виртуальное адресное пространство двух или более процессов.
Рис. 2.23 а) файл, отображенный на память; б) разделяемая память
Одно из преимуществ файлов, отображаемых в память, заключается в том, что их легко использовать совместно. Присвоение имени объекту «отображение файла» делает возможным совместное использование файла несколькими процессами. В этом случае его содержимое отображено на совместно используемую физическую память.
Почтовые ящики
Почтовые ящики обеспечивают только однонаправленные соединения. Каждый процесс, который создает почтовый ящик, является «сервером почтовых ящиков». Другие процессы, называемые «клиентами почтовых ящиков», посылают сообщения серверу, записывая их в почтовый ящик. Входящие сообщения всегда дописываются в почтовый ящик и сохраняются до тех пор, пока сервер их не прочтет. Каждый процесс может одновременно быть и сервером и клиентом почтовых ящиков, создавая, таким образом, двунаправленные коммуникации между процессами.
Как использовать очереди сообщений, семафоры и разделяемую память, чтобы объединить приложения
Процессы, которыми управляет ядро UNIX, выполняются автономно, что ведет к более стабильной работе системы. Тем не менее каждый разработчик в конце концов попадает в ситуацию, когда одна группа процессов должна сообщаться с другой группой, например, для обмена данными или передачи команд. Это метод обмена сообщениями называется Inter-Process Communication (IPC ). Спецификация System V (SysV) UNIX определяет три механизма для IPC, которые обычно называют SysV IPC:
В дополнение к этому процессы могут взаимодействовать и другими способами, такими как:
Последнюю группу достаточно часто относят к IPC. В этой статье основной акцент делается на IPC-методах SysV ввиду их простоты и эффективности.
Три IPC-метода SysV имеют сходный синтаксис, несмотря на то, что цели их различны. Обычно следует выполнить следующие действия:
Каждый экземпляр 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 байт.
Получатель сообщения использует тип сообщения. Когда вы получаете сообщение из очереди, вы можете выбрать первое доступное сообщение или можете найти определенный тип сообщений. Типы сообщений для использования являются специфичными для приложений, так как создают очереди, отличающиеся от других форм IPC тем, что ядро распознает передачу данных приложений, читая поле type .
Показывает пример постановки сообщения в очередь.
Код в импортирует необходимые заголовочные файлы и затем определяет переменные для использования в пределах функции main . Первым делом нужно определить IPC-ключ, используя /tmp/foo как общий файл и число 42 как ID. Для просмотра это число выводится на дисплей с помощью printf(3c) . Затем очередь сообщений создается с помощью msgget . Первым параметром для msgget является IPC-ключ, а вторым - набор флажков. В примере флажки имеют восьмеричные разрешения, что позволяет любому, кто имеет IPC-ключ, полностью использовать этот IPC, а также флажок IPC_CREAT , который вызывает msgget для создания очереди. Опять же результат выводится на экран.
Отправлять сообщение в очередь просто. После обнуления пространства памяти в сообщении обычная строка копируется в текстовую часть буфера. Тип сообщения определяется как 1, и затем вызывается msgsnd . msgsnd ожидает передачи ID очереди, указателя на данные, размер данных и флажок, который показывает, должен ли блокироваться вызов или нет. Если выставлен флажок IPC_NOWAIT , вызов возвращается, даже если очередь заполнена. Если же стоит флажок 0, то вызов блокируется до тех пор, пока в очереди не будет свободного места, очередь удаляется либо приложение получает сигнал.
Клиентская часть этого уравнения устроена похожим образом. В приведен пример кода, который извлекает сообщение, отправленное сервером.
Процедура для получения IPC-ключа и идентификатора очереди сообщений аналогична коду сервера. Вызов msgget не определяет никаких флажков, потому что сервер уже создал очередь. Если бы приложения были разработаны так, что клиент мог бы быть запущен раньше сервера, тогда и клиент, и сервер должны были бы определять разрешения на доступ и флажок IPC_CREAT так, чтобы любое приложение, запущенное раньше других, создавало бы очередь.
mq_client.c затем вызывает msgrcv , чтобы получить сообщение из очереди. Первые три переменные определяют идентификатор очереди сообщений, указатель на объем памяти для сообщения и размер буфера. Четвертый параметр - параметр типа, который позволяет вам выбирать то, какие сообщения получать:
Пятым параметром msgrcv опять же является блокирующий флажок. показывает, как работают клиент и сервер.
Результаты работы клиента и сервера показывают, что оба они получили одинаковый IPC-ключ, поскольку они ссылались на один и тот же файл и идентификатор. Сервер создал IPC-экземпляр, для которого ядро определило значение 2, а приложение клиента об это узнало. Поэтому не стоит удивляться, что клиент получает "Hello, world!" из очереди сообщений.
Данный пример показывает простейшую из ситуаций. Очередь сообщений может пригодиться для кратковременных процессов, таких как Web-транзакция, которая определяет работу для "тяжелого" приложения back-end, например, как выполнение пакета заданий. Клиент также может быть сервером, и многочисленные приложения могут отправлять сообщения в очередь. Поле типа сообщения позволяет приложениям отправлять сообщения конкретным адресатам.
Связь между процессами не обязательно должна включать в себя пересылку большого количества данных. На самом деле одного бита может оказаться достаточно, чтобы показать, что процесс использует какой-то определенный ресурс. Рассмотрим два процесса, которым нужен доступ к какому-то аппаратному обеспечению, но только один из которых может использовать его в определенный момент времени. Они могут попеременно использовать счетчик. Если один процесс считывает данные счетчика и видит 1, то понятно, что другой процесс использует аппаратуру. Если значение счетчика равняется 0, то процесс может использовать аппаратуру до тех пор, пока значение счетчика будет установлено как 1 во время выполнения операции и по завершении операции значение будет сброшено до 0.
В данной ситуации существует две проблемы. Первая состоит в настройке общего счетчика и определении того, где он будет находиться, что является наибольшим из неудобств. Вторая проблема состоит в том, что операции запроса и присваивания, необходимые для блокировки ресурсов аппаратуры, не атомарны. Если один процесс должен считал бы значение счетчика как 0, но его опередил бы другой процесс до того, как первый мог бы установить значение счетчика как 1, второй процесс мог бы считывать и присваивать значение счетчика. Оба процесса посчитали бы, что они могут использовать аппаратуру. Нет никакой возможности узнать, присвоил ли значение счетчику другой процесс (процессы). Это называется гонка состояний . Семафоры решают обе эти проблемы, предоставляя общий интерфейс для приложений и используя атомарный тест или операцию присвоения.
В SysV реализация семафоров является более общей, чем описанная выше. Прежде всего, значение семафора не обязательно должно быть 0 или 1; оно может быть 0 или любым положительным числом. Во-вторых, ряд операций с семафорами возможен аналогично параметру type , использованному с msgrcv . Эти операции даются как набор инструментов для ядра, и они либо запускаются все вместе, либо не запускаются вообще. Ядро требует того, чтобы эти команды осуществлялись в структуре, называемой sembuf , которая включает в себя компоненты (в порядке следования):
В sem_op имеется большое количество конфигураций:
Пример в разъясняет использование семафоров проверкой программы, которую можно запустить одновременно несколько раз, но гарантирует, что в один момент времени только один процесс будет в критическом разделе. Используется простой вариант семафоров; ресурс свободен, если значение семафора равняется 0.
Показывает способ так же, как и пример с очередью сообщений. Как msgget определяет размер очереди сообщений во втором параметре, так semget определяет размер набора семафоров. Набор семафоров - это группа семафоров, имеющих общий IPC-экземпляр. Количество семафоров в наборе не может быть изменено. Если был создан набор семафоров, то второй параметр для semget игнорируется. Если semget возвращает отрицательное целое число, говорящее о неудаче, распечатывается причина и осуществляется выход из программы.
Прямо перед основным циклом while инициализируются sem_num и sem_flg , поскольку они остаются постоянными на протяжении всего этого примера. SEM_UNDO определяет, что если владелец семафора выйдет до того, как освобожден семафор, то все другие приложения не будут блокированы.
В этом цикле статусное сообщение печатается для того, чтобы показать, что приложение начало ждать семафор. Это сообщение снабжается первым аргументом командной строки, чтобы отличить его от других экземпляров. Прежде чем войти в критический раздел, приложение блокирует семафор. Прописаны две инструкции семафора. Первая - 0, что означает, что приложение ждет, пока значение семафора не будет сброшено до 0. Вторая - это 1, означающая, что после того, как значение семафора обнулится, к нему добавится 1. Приложение вызывает semop , чтобы запустить команды, передавая их ID семафора, а также адрес структуры данных и количество sembuf команд, которые должны быть использованы.
Когда semop возвращается, приложение узнает, что оно заблокировало семафор и печатает сообщение, чтобы показать это. Затем запускается критический раздел, который в этом случае является паузой на заданное случайно число секунд. В итоге семафор освобождается запуском единой команды sembuf со значением semop равным -1, что имеет эффект вычитания 1 из значения семафора и его обнуления. Распечатывается большее количество отладочного вывода, приложение останавливается на некоторое количество времени, и продолжается выполнение. показывает вывод двух экземпляров этого приложения.
Показывает, как запущены два экземпляра под именами a и b соответственно. Сначала a получает семафор, и в это же время b пытается получить блокировку. Как только a освобождает семафор, то b разрешается блокировка. Ситуация обратная в случае, когда a ждет завершения работы b. В итоге a снова присваивает семафор после того, как отключается от него, поскольку b не находится в состоянии ожидания.
Последнее, о чем стоит упомянуть в связи с семафорами, - это то, что они известны как рекомендуемые блокировки . Это означает, что семафоры сами по себе не предотвращают одновременное использование двумя процессами одного ресурса; скорее они нужны для того, чтобы сообщать о том, что ресурсы уже используется.
Общая память является, пожалуй, самым мощным IPC-методом SysV и самым простым для выполнения. Как и подразумевает название, блок памяти совместно используется несколькими процессами. показывает программу, которая вызывает fork(2) , чтобы разделиться на порождающий и дочерний процессы, которые сообщаются между собой, используя сегмент общей памяти.
Параметры для shmget должны быть известны к этому времени: ключ, размер и флажки. Размер участка общей памяти в данном примере является одиночным целым числом. отличается от предыдущих примеров в использовании IPC_PRIVATE для IPC-ключа. Когда используется IPC_PRIVATE , гарантируется уникальный IPC ID, и предполагается, что приложение само представит ID. В примере shmid является и порождающим, и дочерним процессом, поскольку они являются копиями друг друга. Системное обращение fork производит вторую копию текущего процесса, называемого дочерним, которая фактически является идентичной порождающему процессу. Выполнение обоих процессов возобновляется после fork . Результат выполнения fork используется, чтобы определить является ли текущий процесс порождающим или дочерним.
И порождающий процесс, и дочерний выглядят одинаково. Во-первых, системное обращение shmat используется для того, чтобы указатель находился в сегменте общей памяти. shmat требует ID общей памяти, указатель и некоторые флажки. Указатель используется, чтобы запросить определенный адрес памяти. Передавая 0, ядро может выбрать все что угодно. Флажки в основном присущи производителю, тем не менее SHM_RDONLY является общим флажком, который показывает, что сегмент является незаписываемым. Как это показано в , shmat часто используется, чтобы разрешить ядру решать все.
shmat возвращает указатель в сегмент общей, который для отладки выдается на экран. Затем каждый процесс изменяет сегмент общей памяти и выводит значение. В итоге порождающий процесс удаляет сегмент общей памяти, используя shmctl(2) . показывает результат выполнения этой программы.
На основе выведенных данных вы можете увидеть совместное использование одного и того же участка общей памяти. Во-первых, значение, которое установил дочерний процесс и прочитал порождающий, в общей памяти равняется 1. Затем порождающий процесс установил значение 42, которое читает дочерний процесс. Заметьте, что у порождающего и дочернего процессов были различные указатели на адреса сегмента общей памяти, хотя доступ они имели к одной и той же физической памяти. Это создает проблему для некоторых структур данных, таких как связанные списки, когда используется физический адрес, так что вы можете использовать соответствующий адрес, если создаете сложные структуры в разделяемой памяти.
Этот пример основывается на том, что один процесс приостановлен, в то время как другой пишет в сегмент разделяемой памяти. В настоящем же приложении это было бы непрактично, поэтому если у вашего приложения несколько процессов осуществляют запись в одну область памяти, продумайте блокировку этой области при помощи семафора.
UNIX предоставляет некоторые методы для IPC. IPC-методы SysV - это очереди сообщений, семафоры и общая память. Очереди сообщений позволяют одному приложению отправлять сообщение, которое другие приложения могут получить позднее, даже после завершения работы приложения. Семафоры гарантируют, что разные приложения могут блокировать ресурсы и избегать гонки состояний. Общая память позволяет разным приложениям совместно использовать общий сегмент памяти, что обеспечивает быстрый способ сообщения между ними и передачи большого количества данных. Эти методы можно использовать в сочетании. Например, вы можете использовать семафор, чтобы контролировать доступ к сегменту разделяемой памяти.
IPC-методы полезны для разработчиков приложений, поскольку они предоставляют стандартный способ сообщения между приложения и доступны в разных версиях UNIX. В следующий раз, когда нужно будет заблокировать ресурсы или обеспечить передачу данных между процессами, попробуйте IPC-механизмы SysV.
Межпроцессное взаимодействие (Inter-process communication (IPC) ) - это набор методов для обмена данными между потоками процессов. Процессы могут быть запущены как на одном и том же компьютере, так и на разных, соединенных сетью. IPC бывают нескольких типов: «сигнал», «сокет», «семафор», «файл», «сообщение»…
В данной статье я хочу рассмотреть всего 3 типа IPC:
Рассмотрим передачу сообщений по именованным каналам. Схематично передача выглядит так:
Для создания именованных каналов будем использовать функцию, mkfifo()
:
#include
Функция создает специальный FIFO файл с именем pathname
, а параметр mode
задает права доступа к файлу.
В случае успешного создания FIFO файла, mkfifo() возвращает 0 (нуль). В случае каких либо ошибок, функция возвращает -1 и выставляет код ошибки в переменную errno .
Типичные ошибки, которые могут возникнуть во время создания канала:
Мы открываем файл только для чтения (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
Целостность объекта памяти сохраняется, включая все данные связанные с ним, до тех пор пока объект не отсоединен/удален (shm_unlink() ). Это означает, что любой процесс может получить доступ к нашему объекту памяти (если он знает его имя) до тех пор, пока явно в одном из процессов мы не вызовем shm_unlink() .
Переменная oflag является побитовым «ИЛИ» следующих флагов:
После создания общего объекта памяти, мы задаем размер разделяемой памяти вызовом ftruncate() . На входе у функции файловый дескриптор нашего объекта и необходимый нам размер.
После создания объекта памяти мы установили нужный нам размер 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».
Есть два типа семафоров:
Итак, для реализации семафоров будем использовать POSIX функцию sem_open()
:
#include
В функцию для создания семафора мы передаем имя семафора, построенное по определенным правилам и управляющие флаги. Таким образом у нас получится именованный семафор.
Имя семафора строится следующим образом: в начале идет символ "/" (косая черта), а следом латинские символы. Символ «косая черта» при этом больше не должен применяться. Длина имени семафора может быть вплоть до 251 знака.
Если нам необходимо создать семафор, то передается управляющий флаг O_CREATE . Чтобы начать использовать уже существующий семафор, то oflag равняется нулю. Если вместе с флагом O_CREATE передать флаг O_EXCL , то функция sem_open() вернет ошибку, в случае если семафор с указанным именем уже существует.
Параметр mode задает права доступа таким же образом, как это объяснено в предыдущих главах. А переменной value инициализируется начальное значение семафора. Оба параметра mode и value игнорируются в случае, когда семафор с указанным именем уже существует, а sem_open() вызван вместе с флагом O_CREATE .
Для быстрого открытия существующего семафора используем конструкцию:
#include
В одной консоли запускаем:
$ ./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.
Мьютекс по существу является тем же самым, чем является бинарный семафор (т.е. семафор с двумя состояниями: «занят» и «не занят»). Но термин «mutex» чаще используется чтобы описать схему, которая предохраняет два процесса от одновременного использования общих данных/переменных. В то время как термин «бинарный семафор» чаще употребляется для описания конструкции, которая ограничивает доступ к одному ресурсу. То есть бинарный семафор используют там, где один процесс «занимает» семафор, а другой его «освобождает». В то время как мьютекс освобождается тем же процессом/потоком, который занял его.
Без мьютекса не обойтись в написании, к примеру базы данных, к которой доступ могут иметь множество клиентов.
Для использования мьютекса необходимо вызвать функцию pthread_mutex_init():
#include
Функция инициализирует мьютекс (перемнную mutex
) аттрибутом mutexattr
. Если mutexattr
равен NULL
, то мьютекс инициализируется значением по умолчанию. В случае успешного выполнения функции (код возрата 0), мьютекс считается инициализированным и «свободным».
Типичные ошибки, которые могут возникнуть:
Коды возврата для pthread_mutex_lock() :
Данный пример демонстрирует совместный доступ двух потоков к общей переменной. Один поток (первый поток) в автоматическом режиме постоянно увеличивает переменную 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
UPD: Обновил 3-ю главу про семафоры. Добавил подглаву про мьютекс.
Теги: