Операционная система, имея доступ ко всем областям памяти, играет роль посредника в информационном обмене прикладных потоков. При необходимости в обмене данными поток обращается с запросом к ОС. ОС, пользуясь своими привилегиями, создает различные системные средства связи. Многие из средств межпроцессного обмена данными выполняют также и функции синхронизации: в том случае, когда данные для процесса-получателя отсутствуют, последний переводится в состояние ожидания средствами ОС, а при поступлении данных от процесса-отправителя процесс-получатель активизируется.
Передача может осуществляться несколькими способами:
— разделяемая память;
— канал (pipe, конвейер) — псевдофайл, в который один процесс пишет, а другой читает;
— сокеты — поддерживаемый ядром механизм, скрывающий особенности среды и позволяющий единообразно взаимодействовать процессам, как на одном компьютере, так и в сети;
— почтовые ящики (только в Windows), однонаправленные, возможность широковещательной рассылки;
— вызов удаленной процедуры, процесс А Может вызвать процедуру в процессе В , и получить обратно данные.
Конвейеры (каналы)
Существует два способа организовать двунаправленное соединение с помощью каналов: безымянные и именованные каналы. Канал представляет собой буфер в оперативной памяти, поддерживающий очередь байт по алгоритму FIFO. Для программиста этот буфер выглядит как безымянный файл, в который можно писать и читать, осуществляя тем самым обмен данными. Обмениваться данными могут только родственные процессы, точнее процессы, которые имеют общего прародителя, создавшего данный конвейер. Связано это с тем, что конвейер не имеет имени, обращение к нему происходит по дескриптору, который имеет локальное для каждого процесса значение.
Безымянные (или анонимные) каналы позволяют связанным процессам передавать информацию друг другу. Обычно, безымянные каналы используются для перенаправления стандартного ввода/вывода дочернего процесса так, чтобы он мог обмениваться данными с родительским процессом. Чтобы производить обмен данными в обоих направлениях, необходимо создать два безымянных канала. Родительский процесс записывает данные в первый канал, используя его дескриптор записи, в то время как дочерний процесс считывает данные из канала, используя дескриптор чтения. Аналогично, дочерний процесс записывает данные во второй канал и родительский процесс считывает из него данные. Безымянные каналы не могут быть использованы для передачи данных по сети и для обмена между несвязанными процессами.
Именованные конвейеры. Такие конвейеры имеют имя, которое является записью в каталоге файловой системы ОС, поэтому пригодны для обмена данными между двумя произвольными процессами или потоками этих процессов. Именованный конвейер является специальным файлом типа FIFO и не имеет области данных на диске. Создается именованный конвейер с помощью того же системного вызова, который используется для создания файлов любого типа, но только с указанием в качестве типа файла параметра FIFO. Системный вызов порождает в каталоге запись о файле FIFO с заданным именем, после чего любой процесс может открыть этот файл и передавать данные другому процессу, также открывшему файл с этим именем.
Именованные каналы используются для передачи данных между независимыми процессами или между процессами, работающими на разных компьютерах. Обычно, процесс сервера именованных каналов создает именованный канал с известным именем или с именем, которое будет передано клиентам. Процесс клиента именованных каналов, зная имя созданного канала, открывает его на своей стороне с учетом ограничений, указанных процессом сервера. После этого между сервером и клиентом создается соединение, по которому может производиться обмен данными в обоих направлениях. В дальнейшем наибольший интерес для нас будут представлять именованные каналы.
Рис. 2.22 Схема реализации канала
Очереди сообщений
Механизм очередей сообщений похож на механизм конвейеров с тем отличием, что он позволяет процессам и потока обмениваться структурированными сообщениями. Очереди сообщений являются глобальными средствами коммуникаций для процессов операционной системы, так как каждая очередь в пределах ОС имеет уникальное имя.
Разделяемая память
Разделяемая память представляет собой сегмент физической памяти, отображенной в виртуальное адресное пространство двух или более процессов.
Рис. 2.23 а) файл, отображенный на память; б) разделяемая память
Одно из преимуществ файлов, отображаемых в память, заключается в том, что их легко использовать совместно. Присвоение имени объекту «отображение файла» делает возможным совместное использование файла несколькими процессами. В этом случае его содержимое отображено на совместно используемую физическую память.
Почтовые ящики
Почтовые ящики обеспечивают только однонаправленные соединения. Каждый процесс, который создает почтовый ящик, является «сервером почтовых ящиков». Другие процессы, называемые «клиентами почтовых ящиков», посылают сообщения серверу, записывая их в почтовый ящик. Входящие сообщения всегда дописываются в почтовый ящик и сохраняются до тех пор, пока сервер их не прочтет. Каждый процесс может одновременно быть и сервером и клиентом почтовых ящиков, создавая, таким образом, двунаправленные коммуникации между процессами.
Межпроцессное взаимодействие (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-ю главу про семафоры. Добавил подглаву про мьютекс.
Теги:
В операционных системах Unix и Linux имеется возможность создавать сегменты памяти, доступные нескольким процессам. При использовании этого средства участки виртуального адресного пространства разных процессов, отображаются на одни и те же адреса реальной памяти.
При работе с общим сегментом памяти один из процессов должен создать сегмент разделяемой (общей) памяти, указав требуемый размер сегмента, и получить его идентификатор. Этот идентификатор затем используется процессом-создателем для выполнения управляющих операций с разделяемой памятью.
Получив идентификатор разделяемого сегмента памяти (создав сегмент или получив его идентификатор от другого процесса), процесс должен "присоединить" сегмент. Операция присоединения возвращает адрес разделяемого сегмента в виртуальном адресном пространстве процесса. Виртуальный адрес одного и того же сегмента может быть разным для разных процессов. Далее процессы ведут чтение и запись данных в сегменте разделяемой памяти, используя для этого те же самые машинные команды, что и при работе с обычной памятью.
Процесс, закончивший работу с сегментом разделяемой памяти должен "отсоединить" его, а по окончании использования сегмента всеми процессами он должен быть уничтожен.
При одновременной работе процессов с общей областью памяти, возможно, требуется синхронизация их доступа к области. За обеспечение такой синхронизации полностью отвечает программист, который может использовать для этого алгоритмы, исключающие конфликты одновременного доступа или системные средства, например, семафоры.
ОС Unix/Linux механизм разделяемых сегментов памяти обеспечивается четырьмя системными вызовами: shmget , shmctl , shmat , shmdt .
Системный вызов shmctl позволяет выполнять управляющие операции над сегментом: получать информацию о его состоянии, изменять права доступа к нему, уничтожать сегмент.
Системные вызовы shmat и shmdt выполняют присоединение и отсоединение сегмента соответственно.
Разделяемые сегменты памяти в Unix/Linux (как и семафоры) не имеют внешних имен. При получении идентификатора сегмента процесс пользуется числовым ключом. Разработчики несвязанных процессов могут договориться об общем значении ключа, который они будут использовать, но у них нет гарантии в том, что это же значение ключа не будет использовано кем-то еще. Гарантированно уникальный сегмент можно создать с использованием ключа IPC_PRIVATE , но такой ключ не может быть внешним. Поэтому сегменты используются, как правило, родственными процессами, которые имеют возможность передавать друг другу их идентификаторы, например, через наследуемые ресурсы или через параметры вызова дочерней программы.
Внимание!
В процессе отладки программы у Вас могут возникать ситуации, когда программа будет аварийно заканчиваться или прерываться Вами прежде, чем она уничтожит созданные ею общие области памяти. Такие области не удаляются в системе автоматически и могут накапливаться в течение многих дней. Накопление таких "забытых" общих областей может привести к тому, что будет исчерпан системный лимит на количество общих областей, и очередной вызов |
Межпроцессное взаимодействие (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-ю главу про семафоры. Добавил подглаву про мьютекс.
Теги: Добавить метки