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

Программирование для Linux. ЧАСТЬ 2.

Linux API – Введение в межпроцессное

взаимодействие

Андрей Боровский

Наличие в Unix-системах простых и эффективных средств взаимодействия

между процессами оказало на программирование в Unix не менее важное

влияние, чем представление объектов системы в виде файлов. Благодаря

межпроцессному взаимодействию (Inter-Process Communication, IPC) разработчик

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

Последовательная обработка одной задачи несколькими простыми программами очень похожа на конвейерное производство (среди многих значений английского pipeline есть и «конвейер», но в этой статье мы для перевода слова pipe будем пользоваться принятым в отечественной литературе термином «канал» .

Альтернативой конвейерному подходу являются большие монолитные пакеты, построенные по принципу «все в одном». Использование набора простых утилит для решения одной сложной задачи требует несколько большей квалификации со стороны пользователя, но взамен предоставляет гибкость, не достижимую при использовании монолитных «монстров». Наборы утилит, использующих открытые протоколы IPC, легко наращивать и модифицировать. Разбиение сложных задач на сравнительно небольшие подзадачи также позволяет снизить количество ошибок, допускаемых программистами (см. врезку). Помимо всего этого у IPC есть еще одно важное преимущество. Программы, использующие IPC, могут «общаться»

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

Могущество скриптовых языков Unix и Linux во многом основано на возможностях IPC.

В этой статье мы ограничимся рассмотрением IPC с помощью каналов различных типов. Предполагается, что читатели статьи являются опытными пользователями Linux, и, во всяком случае, знают, как создаются каналы из нескольких программ с помощью командной строки. С точки зрения программиста, работа программ в канале, организованном с помощью символа “|”, выглядит очень просто. Данные со стандартного потока вывода одной программы перенаправляются на стандартный поток ввода другой программы, чей стандартный поток вывода может быть также перенаправлен. Но как быть в том случае, если необходимо использовать канал внутри самой программы?

Закон Брукса Фредерик Брукс, автор книги «Мифический человеко-месяц», высказал предположение (известное как «закон Брукса»), согласно которому количество ошибок в проекте пропорционально квадрату числа участников проекта, тогда как объем полезной работы связан с числом участников линейной зависимостью.

Если бы закон Брукса выполнялся, то на определенном этапе развития проекта любая попытка привлечь новых разработчиков приводила бы к лавинообразному росту числа ошибок, а необходимость исправлять их поглащала бы все ресурсы проекта. Иначе говоря, согласно закону Брукса для программных проектов существует некий порог сложности, превышение которого приводит к стремительному падению КПД. Что же касается открытой модели разработки ПО, то она, с точки зрения закона Брукса, должна быть невозможна в принципе. Для того, чтобы понять, в чем Ф. Брукс ошибался, следует рассмотреть исходные посылки его рассуждений. Закон Брукса основан двух предположениях: (а) ошибки чаще возникают на стыке элементов проекта, выполняемых разными разработчиками (соответственно, чем больше таких «швов», тем больше ошибок);

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

Неименованные каналы Чаще всего внутри-программные каналы использовуются тогда, когда программа запускает другую программу и считывает данные, которые та выводит в свой стандартный поток вывода. С помощью этого трюка разработчик может использовать в своей программе функциональность другой программы, не вмешиваясь во внутренние детали ее работы. Для решения этой задачи мы воспользуемся функциями popen(3) и pclose(3). Формально эти функции подобны функциям fopen(3) и fclose(3). Функция popen() запускает внешнюю программу и возвращает вызвавшему ее приложению указатель на структуру FILE, связанную либо со стандартным потоком ввода, либо со стандартным потоком вывода запущенного процесса. Первый параметр функции popen() - строка, содержащая команду, запускающую внешнюю программу. Второй параметр определяет, какой из стандартных потоков (вывода или ввода) будет возвращен. Аргумент “w” соответствует потоку ввода запускаемой программы, в этом случае приложение, вызвавшее popen(), записывает данные в поток. Аргумент “r” соответствует потоку вывода. Функция pclose() завершает работу с внешним приложением и закрывает канал. Для демонстрации работы с функциями popen()/pclose() мы напишем небольшую программу makelog (полный текст программы можно найти на диске в файле makelog.c) Программа makelog выполняет команду оболочки, переданную ей в качестве параметра, и записывает данные, выводимые этой командой, одновременно на стандартный терминал и в файл log.txt (аналогичными функциями обладает стандартная команда tee). Например, если скомпилировать программу:



gcc makelog.c -o makelog а затем скомандовать makelog "ls -al" на экране терминала будут распечатаны данные, выводимые командой оболочки ls -al, а в рабочей директории программы makelog будет создан файл log.txt, содержащий те же данные. Кавычки вокруг команды оболочки нужны для того, чтобы программа makelog получала строку вызова команды как один параметр командной строки.

Читатель наверняка уже догадался, что изюминка программы makelog заключается в использовании функции popen(). Рассмотрим фрагмент исходного текста программы:

f = popen(argv, "r");

Эта операция очень похожа на открытие обычного файла для чтения.

Переменная f имеет тип FILE *, но в параметре argv функции popen() передается не имя файла, а команда на запуск программы или команда оболочки, например, "ls -al". Если вызов popen() был успешен, мы можем считывать данные, выводимые запущенной командой, с помощью обычной функции fread(3):

fread(buf, 1, BUF_SIZE, f) Особенность функции popen() заключается в том, что эта функция не возвращает NULL, даже если переданная ей команда не является корректной.

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

write(1, buf, len);

Параллельно эти же данные записываются в файл на диске. По окончании чтения данных открытый канал нужно закрыть:

Следует иметь в виду, что pclose() вернет управление вызывающему потоку только после того как запущенное с помощью popen() приложение завершит свою работу.

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

Для обмена данными с внешним приложением функция popen() использует каналы неявным образом. В своих программах мы можем использовать каналы и непосредственно. Наиболее распространенный тип каналов, - неименованные однонаправленные каналы (anonymous pipes), создаваемые функцией pipe(2). На уровне интерфейса программирования такой канал представляется двумя дескрипторами файлов, один из которых служит для чтения данных, а другой - для записи. Каналы не поддерживают произвольный доступ, т. е. данные могут считываться только в том же порядке, в котором они записывались.

Неименованные каналы используются преимущественно вместе с функцией fork(2) и служат для обмена данными между родительским и дочерним процессами. Для организации подобного обмена данными, сначала, с помощью функции pipe(), создается канал. Функции pipe() передается единственный параметр - массив типа int, состоящий из двух элементов. В первом элементе массива функция возвращает дескриптор файла, служащий для чтения данных из канала (выход канала), а во втором - дескриптор для записи (вход). Затем, с помощью функции fork() процесс «раздваивается». Дочерний процесс наследует от родительского процесса оба дескриптора, открытых с помощью pipe(), но, также как и родительский процесс, он должен использовать только один из дескрипторов.

Направление передачи данных между родительским и дочерним процессом определяется тем, какой дескриптор будет использоваться родительским процессом, а какой - дочерним. Продемонстрируем изложенное на простом примере программы pipes.c, использующей функции pipe() и fork().

#include #include #include int main (int argc, char * argv) int pipedes;

char *str = "String passed via pipe\n";

write(pipedes, (void *) str, strlen(str) + 1);

while ((len = read(pipedes, buf, 1024)) != 0) write(2, buf, len);

Оба дескриптора канала хранятся в переменной pipedes. После вызова fork() процесс раздваивается и родительский процесс (тот, в котором fork() вернула ненулевое значение, равное PID дочернего процесса) закрывает дескриптор, открытый для чтения, и записывает данные в канал, используя дескриптор, открытый для записи (pipedes). Дочерний процесс (в котором fork() вернула 0) закрывает дескриптор, открытый для записи, и затем считывает данные из канала, используя дескриптор, открытый для чтения (pipedes). Назначение дескрипторов легко запомнить, сопоставив их с аббревиатурой I/O (первый дескриптор - для чтения (input), второй - для записи (output)). Стандарт POSIX предписывает, чтобы каждый процесс, получивший оба канальных дескриптора, закрывал тот дескриптор, который ему не нужен, перед тем, как начать работу с другим дескриптором, и хотя в системе Linux этим требованием можно пренебречь, лучше все же придерживаться строгих правил.

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

В этой ситуации нам пришлось бы выполнять синхронизацию передачи данных явным образом.

Как канал передает данные Для передачи данных с помощью каналов используются специальные области памяти (созданные ядром системы), называемые буферами каналов (pipe buffers).

Одна из важных особенностей буферов каналов заключается в том, что даже если предыдущая запись заполнила буфер не полностью, повторная запись данных в буфер становится возможной только после того, как прежде записанные данные будут прочитаны. Это означает, что если разные процессы, пишущие данные в один и тот же канал, передают данные блоками, размеры которых не превышают объем буферов, данные из блоков, записанных разными процессами, не будут перемешиваться между собой. Использование этой особенности каналов существенно упрощает синхронизацию передачи данных. Узнать размер буфера можно с помощью вызова функции fpathconf(pipedes, _PC_PIPE_BUF) где pipedes - дескриптор канала. На платформе IA32 размер одного буфера канала составляет 4 килобайта. Начиная с ядра версии 2.6.11, каждый канал Linux может использовать до 16 буферов, что существенно повышает производительность каналов.

Познакомившись с неименованными каналами, мы можем самостоятельно реализовать аналог функции popen() без «дополнительных расходов» (то есть, без запуска процесса-оболочки). Напишем небольшую программу, которая запускает утилиту netstat, читает данные, выводимые этой утилитой, и распечатывает их на экране. Если бы мы использовали для этой цели функцию popen(), то получили бы доступ к потоку вывода netstat с помощью вызова popen("netstat", "r");

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

Самое интересное происходит в дочернем процессе, где выполняется последовательность функций:

dup2(pipedes, 1);

execve("/bin/netstat", NULL, NULL);

С помощью функции dup2(2) мы перенаправляем стандартный поток вывода дочернего процесса (дескриптор стандартного потока вывода равен 1) в канал, используя дескриптор pipdes, открытый для записи. Далее с помощью функции execve(2) мы заменяем образ дочернего процесса процессом netstat (обратите внимание, что поскольку в нашем распоряжении нет оболочки с ее переменной окружения PATH, путь к исполнимому файлу netstat нужно указывать полностью).

Именованные каналы Хотя в приведенном выше примере неименованные каналы используются только для передачи данных между процессами, связанными «родственными узами», существует возможность использовать их и для передачи данных между совершенно разными процессами. Для этого нужно организовать передачу дескрипторов канала между неродственными процессами, как это описано, например, в . Однако, передача дескрипторов стороннему процессу носит скорее характер трюка (или «хака»), и мы на ней останавливаться не будем. Для передачи данных между неродственными процессами мы воспользуемся механизмом именованных каналов (named pipes), который позволяет каждому процессу получить свой, «законный» дескриптор канала. Передача данных в этих каналах (как, впрочем, и в однонаправленных неименованных каналах) подчиняется принципу FIFO (первым записано - первым прочитано), поэтому в англоязычной литературе иногда можно встретить названия FIFO pipes или просто FIFOs. Именованные каналы отличаются от неименованных наличием имени (странно, не правда ли?), то есть идентификатора канала, потенциально видимого всем процессам системы. Для идентификации именованного канала создается файл специального типа pipe. Это еще один представитель семейства виртуальных файлов Unix, не предназначенных для хранения данных (размер файла канала всегда равен нулю). Файлы именованных каналов являются элементами VFS, как и обычные файлы Linux, и для них действуют те же правила контроля доступа. Для создания файлов именованных каналов можно воспользоваться функцией mkfifo(3). Первый параметр этой функции - строка, в которой передается имя файла канала, второй параметр - маска прав доступа к файлу. Функции mkfifo() создает канал и файл соответствующего типа. Если указанный файл канала уже существует, mkfifo() возвращает -1, (переменная errno принимает значение EEXIST). После создания файла канала, процессы, участвующие в обмене данными, должны открыть этот файл либо для записи, либо для чтения. Обратите внимание на то, что после закрытия файла канала файл (и соответствующий ему канал) продолжают существовать. Для того чтобы закрыть сам канал, нужно удалить его файл, например с помощью последовательных вызовов unlink(2).

Рассмотрим работу именованного канала на примере простой системы клиентсервер. Программа-сервер создает канал и передает в него текст, вводимый пользователем с клавиатуры. Программа-клиент читает текст и выводит его на терминал. Программы из этого примера можно рассматривать как упрощенный вариант системы мгновенного обмена сообщениями между пользователями многопользовательской ОС. Исходный текст программы-сервера хранится в файле typeserver.c. Вызов функции mkfifo() создает файл-идентификатор канала в рабочей директории программы:

mkfifo(FIFO_NAME, 0600);

где FIFO_NAME - макрос, задающий имя файла канала (в нашем случае fifofile").

В качестве маски доступа мы используем восьмеричное значение 0600, разрешающее процессу с аналогичными реквизитами пользователя чтение и запись (можно было бы использовать маску 0666, но на мы на всякий случай воздержимся от упоминания Числа Зверя, пусть даже восьмеричного, в нашей программе). Для краткости мы не проверяем значение, возвращенное mkfifo(), на предмет ошибок. В результате вызова mkfifo() с заданными параметрами в рабочей директории программы должен появиться специальный файл fifofile. Файлменеджер KDE отображает файлы канала с помощью красивой пиктограммы, изображающей приоткрытый водопроводный кран. Далее в программе-сервере мы просто открываем созданный файл для записи:

f = fopen(FIFO_NAME, "w");

Считывание данных, вводимых пользователем, выполняется с помощью getchar(), а с помощью функции fputc() данные передаются в канал. Работа сервера завершается, когда пользователь вводит символ “q”. Исходный текст программы-клиента можно найти в файле typeclient.c. Клиент открывает файл fifofile для чтения как обычный файл:

f = fopen(FIFO_NAME, "r");

Символы, передаваемые по каналу, считываются с помощью функции fgetc() и выводятся на экран терминала с помощью putchar(). Каждый раз, когда пользователь сервера наживает ввод, функция fflush(), вызываемая сервером (см.

файл typeserver.c), выполняет принудительную очистку буферов канала, в результате чего клиент считывает все переданные символы. Получение символа “q” завершает работу клиента.

Скомпилируйте программы typeserver.c и typeclient.c в одной директории.

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

Создать файл FIFO можно и с помощью функции mknod(2), предназначенной для создания файлов различных типов (FIFO, сокеты, файлы устройств и обычные файлы для хранения данных). В нашем случае вместо mkfifo(fname, 0600);

можно было бы написать mknod(fname, S_IFIFO, 0);

Одной из сильных сторон Unix/Linux IPC является возможность организовывать взаимодействие между программами, которые не только ничего не знают друг о друге, но и используют разные механизмы ввода/вывода. Сравним нашу программу typeclient и команду ls. Казалось бы, между ними нет ничего общего - typeclient получает данные, используя именованный канал, а ls выводит содержимое директории в стандартный поток вывода. Однако, мы можем организовать передачу данных от ls к typeclient с помощью всего лишь пары команд bash! В директории программы typeclient дайте команду:

mknod fifofile p Эта команда создаст файл канала fifofile также, как это сделала бы программа typeserver. Запустите программу typeclient, а затем в другом окне терминала дайте команду, наподобие ls -al > /path/fifofile где /path/fifofile - путь к файлу FIFO. В результате, программа typeclient распечатает содержимое соответствующей директории. Главное, чтобы в потоке данных не встретился символ “q”, завершающий ее работу.

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

1. D. P Bovet, M. Cesati, Understanding the Linux Kernel, 3rd Edition, O"Reilly, 2. W. R. Stevens, S. A. Rago, Advanced Programming in the UNIX® Environment: Second Edition, Addison Wesley Professional, 3. Стивенс У., UNIX: Взаимодействие процессов. - СПб.: Питер,



Похожие работы:

«Министерство образования и науки Российской Федерации Внимание! Файл содержит ВСЕ программы по отраслям народного хозяйства! ПРОГРАММА - МИНИМУМ кандидатского экзамена по специальности 08.00.05 - Экономика и управление народным хозяйством (экономика, организация и управление предприятиями, отраслями, комплексами в АПК и сельском хозяйстве) по экономическим наукам Программа-минимум содержит 24 стр. 2007 2 Введение Основу настоящей программы составили ключевые положения следующих дисциплин:...»

«УТВЕРЖДЕН приказом директора ОГБПОУ Костромской политехнический колледж № 21п от 1 июля 2014 года ПОРЯДОК отчисления, восстановления и перевода студентов областного государственного бюджетного профессионально образовательного учреждения Костромской политехнический колледж 1. Общие положения 1.1. Настоящий Порядок отчисления, восстановления и перевода студентов областного государственного бюджетного профессионального образовательного учреждения Костромской политехнический колледж (далее –...»

«Указанный договор является публичным на основании ст. 633 Гражданского кодекса Украины и обязует Предприятие предоставлять услуги, которые являются предметом этого договора, каждому, кто к нему обратится при наличии у него возможности предоставления потребителю соответствующих услуг. ДОГОВОР предоставления услуг (редакция от 22.01.2013) г. Севастополь _201 г. ЧП Севтелекомсервис, в лице директора Привиденцева Евгения Александровича, действующего на основании Устава, именуемое в дальнейшем...»

«Публичный доклад за 2011-2012 учебный год муниципального бюджетного дошкольного образовательного учреждения города Кургана Детский сад №79 Ручеёк 1.Общие характеристики Полное наименование образовательного учреждения: заведения муниципальное бюджетное дошкольное образовательное учреждение города Кургана Детский сад № 79 Ручеёк. Официальное сокращённое наименование: МБДОУ Детский сад № 79. а) Дата создания ОУ: июнь 1964 года. Учредитель ОУ: муниципальное образование города Кургана в лице...»

«ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ Сочинский государственный университет туризма и курортного дела Филиал в г. Анапе УТВЕРЖДАЮ Директор АФ СГУТ и КД И.И. Ашкинадзе Инновации в социально-культурном сервисе и туризме рабочая программа дисциплины (ОЗФО) Закреплена за кафедрой: Социально-культурный сервис и туризм Учебный план: 100103 Специальность: Социально-культурный сервис и туризм 120 Часов по рабочему учебному плану: Часов по ГОСу (из РУП): 120 Часов по рабочей программе: Часов по прим....»

«Лютий 2013 СоЗдайте Свою иСторию уСПеха с Мэри Кэй Каким будет ваш успех – зависит только от вас. Мэри Кэй покажет вам путь к прекрасному! Знакомьтесь – новая Программа привлечения и развития бизнеса Новичков! Выгодное предложение Только для Звездных Консультантов II квартала Семинарского года 2012-2013! С 1 февраля по 31 марта 2013 года для всех Звездных Консультантов II квартала Семинарского года (ноябрь 2012 – январь 2013 года) Компания предлагает эксклюзивную возможность приобрести...»

«Министерство образования и науки РФ ФГБОУ ВПО Красноярский государственный педагогический университет им. В.П. Астафьева Факультет биологии, географии и химии Кафедра химии ОСНОВЫ СУПРАМОЛЕКУЛЯРНОЙ ХИМИИ М.2 УЧЕБНО-МЕТОДИЧЕСКИЙ КОМПЛЕКС ДИСЦИПЛИНЫ Направление подготовки: 050100.68 Педагогическое образование Магистерская программа Химическое образование Красноярск 2011 Рабочая программа составлена доц. кафедры химии, к.х.н. Бересневым В.А., асп. Кузнецовой А.С. Рабочая программа обсуждена на...»

«МИНИСТЕРСТВО СЕЛЬСКОГО ХОЗЯЙСТВА РОССИЙСКОЙ ФЕДЕРАЦИИ Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования КУБАНСКИЙ ГОСУДАРСТВЕННЫЙ АГРАРНЫЙ УНИВЕРСИТЕТ РАБОЧАЯ ПРОГРАММА Дисциплины ГСЭ. В.04 Инженерная психология для специальности 190601.65 Автомобили и автомобильное хозяйство факультет: механизации Ведущая кафедра: Педагогики и психологии Вид учебной работы Дневная форма обучения Всего часов Курс, семестр Лекции 3 курс, 16 5 семестр...»

«УТВЕРЖДАЮ Директор ИМКЭС СО РАН, д.ф.-м.н. _ В.А. Крутиков _ 2012 г. ОТЧЁТ о результатах исследований и об использовании средств, полученных на поддержку научного стационара Таёжный Института мониторинга климатических и экологических систем СО РАН в 2012 г. Комплексный экологический стационар “Таёжный” ИМКЭС СО РАН, созданный в 2004 г., располагается в п. Нибега Верхнекетского района Томской области, на территории Прикетья – одного из наиболее облесенных районов восточной части...»

«Пояснительная записка. Рабочая программа разработана на основе: Федерального базисного учебного плана (приказ МО РФ от 09 марта 2004 года) №1312; приказа Минобразования РФ от 05.03.2004 № 1089 (ред. от 19.10.2009) Об утверждении федерального компонента государственных образовательных стандартов начального общего, основного общего и среднего (полного) общего образования; приказа МО РБ от 19.08.13 № 1384 О рекомендуемых БУП и примерных учебных планах для образовательных учреждений РБ на...»

«. Муниципальное бюджетное общеобразовательное учреждение средняя общеобразовательная школа №1 Утверждено. Принято педсоветом. Рассмотрено МС (приказ №34от 30.08.2013г.) (протокол №11 от30.08.2013г.) учителей гуманитарного цикла Директор (протокол №1 от 29.08.2013г.) _Г.П.Хлюстова РАБОЧАЯ ПРОГРАММА по английскому языку 10 класс Учитель: Строкова Нина Степановна г. Кимовск 1 ПОЯСНИТЕЛЬНАЯ ЗАПИСКА Рабочая программа составлена на основе Федерального компонента государственного стандарта общего...»

«ПРАВИТЕЛЬСТВО ПЕНЗЕНСКОЙ ОБЛАСТИ ПОСТАНОВЛЕНИЕ от 19 декабря 2013 года № 972-пП г.Пенза О внесении изменений в государственную программу Развитие агропромышленного комплекса Пензенской области на 2014 - 2020 годы, утвержденную постановлением Правительства Пензенской области от 18.09.2013 № 691-пП В целях приведения нормативного правового акта в соответствие с действующим законодательством, руководствуясь Законом Пензенской области от 22.12.2005 № 906-ЗПО О Правительстве Пензенской области (с...»

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

«ПРИЛОЖЕНИЕ 2 ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО РЫБОЛОВСТВУ Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования Калининградский государственный технический университет (ФГБОУ ВПО КГТУ) УТВЕРЖДАЮ Проректор по НР А.В.Иванов от 2012 г. ПРОГРАММА ВСТУПИТЕЛЬНОГО ЭКЗАМЕНА в аспирантуру по научной специальности 01.01.05 – Теория вероятностей и математическая статистика Калининград 2012 16 Программу вступительного экзамена разработал: д.т.н., профессор В.А....»

«122 Правительство Российской Федерации Санкт-Петербургский государственный университет Регистрационный номер СМ/060201/1 ПРИЛОЖЕНИЕ ПО СПЕЦИАЛЬНОСТИ 060201 Стоматология К ОБРАЗОВАТЕЛЬНОМУ СТАНДАРТУ САНКТ-ПЕТЕРБУРГСКОГО ГОСУДАРСТВЕННОГО УНИВЕРСИТЕТА ПО УРОВНЮ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ ПОДГОТОВКА СПЕЦИАЛИСТА Нормативный срок освоения 5 лет Санкт-Петербург Приложения подготовка специалиста Настоящее приложение к образовательному стандарту СанктПетербургского государственного...»

«1. ОБЛАСТЬ ПРИМЕНЕНИЯ 1.1.Программа повышения квалификации Управление инновационной деятельностью в муниципальном образовании предназначена для муниципальных служащих, служащих муниципальных учреждений Краснодарского края. 1.2.Сферой применения слушателями полученных профессиональных знаний является деятельность служащих по реализации муниципальных функций и предоставлению муниципальных услуг. 2. ХАРАКТЕРИСТИКА ПОДГОТОВКИ ПО ПРОГРАММЕ 2.1.Нормативный срок освоения программы составляет 72 часа....»

«МЕЖДУНАРОДНОЕ БЮРО ТРУДА Административный совет 322-я сессия, Женева, 30 октября-13 ноября 2014 г. GB.322 Повестка дня и программа INS Секция по институциональным вопросам Повестка дня Утверждение протоколов 321-й сессии Административного совета. 1. Повестка дня Международной конференции труда. 2. Подготовка к проведению оценки резонанса Декларации МОТ о социальной 3. справедливости в целях справедливой глобализации к 105-й сессии Международной конференции труда (2016 г.). Вопросы, возникающие...»

«Шрайберг Я.Л., Гончаров М.В., Шлыкова О.В. Интернет ресурсы и услу ги для библиотек: учеб. пособие. - М.: Б.и., 2000. - 140 с. Шрейдер Ю.А. Лингвистический подход к теории информационных систем // Научно техническая информация, 1962. - № 9. Шрейдер Ю.А. Тезаурусы в информатике и теоретической семантике // НТИ. Сер. 2. - 1971. - № 3. Эко У. От Интернета к Гутенбергу: текст и гипертекст. - Режим досту па: http:// www.artinfo.ru/text Электронные изображения и визуальные искусства: Материалы меж...»

«География. Базовый уровень. 10-11 КЛАСС. (68 ЧАСОВ) Учебник: А.П. Кузнецов, Э.В. Ким География. Базовый уровень. 10-11 кл. М.: Дрофа, 2012 год. Программа: Программа основного общего образования по географии. 5-9 классы. Авторы А.И. Алексеев, О.А. Климанова, В.В. Климанов, В.А. Низовцев, Э.В. Ким. (источник Методическая лаборатория географии МИОО) Пояснительная записка: Курс География. Базовый уровень. согласуется с программами курсов Общее землеведение и Страноведение (О.А.Климанова,...»

«ПРОГРАММА ВЫСТАВКИ Содержание Вступительное слово Генерального Директора Metro Cash & Carry Россия WWW.METRO-EXPO.RU СОДЕРЖАНИЕ Вступительное слово Президента группы компаний Unilever в России, Украине и Беларуси. 5 План выставки Список участников Стенд М1. Ресторан la carte Стенд М2. Caf Стенд М3. Магазин у дома Фасоль Стенд М4. Офисный мир Стенд М5. Мясо и птица Стенд М6. Рыба и морепродукты Стенд М7. Сыры и деликатесы Стенд М8. Фрукты и овощи Стенд М9. Доставка Стенд М10. Контроль качества...»

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

Процессы, которыми управляет ядро 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.

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

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

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

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

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

Системные вызовы Unix/Linux

ОС Unix/Linux механизм разделяемых сегментов памяти обеспечивается четырьмя системными вызовами: shmget , shmctl , shmat , shmdt .

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

Системные вызовы shmat и shmdt выполняют присоединение и отсоединение сегмента соответственно.

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

Внимание!
В процессе отладки программы у Вас могут возникать ситуации, когда программа будет аварийно заканчиваться или прерываться Вами прежде, чем она уничтожит созданные ею общие области памяти. Такие области не удаляются в системе автоматически и могут накапливаться в течение многих дней. Накопление таких "забытых" общих областей может привести к тому, что будет исчерпан системный лимит на количество общих областей, и очередной вызов

Межпроцессное взаимодействие (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
  • программирование
Добавить метки