что такое named pipes
Что такое Named Pipes и как с ними бороться.
Сервером канала называется процесс, создающий именованный канал.
Клиентом канала называется процесс, подключающийся к созданному именованному каналу.
От других аналогичных объектов именованные каналы отличает гарантированная доставка сообщений, возможность асинхронного ввода/вывода, возможность коммуникации между процессами на разных компьютерах в локальной вычислительной сети и относительная простота использования.
По своему назначению они похожи на каналы операционной системы UNIX.
При создании именованного канала ему назначается уникальное имя, определяется максимальное количество одновременных соединений с клиентами канала и режим работы канала (должен ли канал быть односторонним или двусторонним (дуплексным), ведется ли передача пакетами или потоком байтов. При передаче пакетами данные одной операции записи отделяются в буфере канала от данных другой операции записи).
Базовым объектом для реализации именованных каналов служит объект «файл», поэтому для посылки и приема сообщений по именованным каналам используются те же самые функции Windows API, что и при работы с файлами (ReadFile, WriteFile).
Для каждого процесса-клиента канала создается свой экземпляр канала, с собственными буферами и дескрипторами (handles) и с собственным механизмом передачи данных, не влияющим на остальные экземпляры.
Экземпляры одного канала имеют общее имя, указанное при создании, сервер назначает имя канала в соответствии с универсальными правилами именования (Universal Naming Convention, UNC), которые обеспечивают независимый от протоколов способ идентификации каналов в Windows-сетях [1].
Именованные каналы, также как и файлы, наследуют стандартную защиту объектов Windows, что позволяет разграничить участников коммуникации и обеспечить запрет несанкционированного доступа к каналу.
Создание именованных каналов возможно только в NT-системах, подключение к созданному каналу возможно как в NT-системах, так и в Win9x. Кроме того, API работы с каналами в Win9x не поддерживает асинхронных операций ввода/вывода.
Именованные каналы широко используются внутри самой системы. Например, взаимодействие менеджера сервисов с самими сервисами осуществляется через несколько именованных каналов. Для связи с сервисами RunAs, с планировщиком событий и с сервером локальной аутентификации также используются именованные каналы.
Именованные каналы являются наиболее простым способом организации связи между сервисами и пользовательскими приложениями, нуждающимися в такой связи.
Одним из полезных (и довольно уникальных) свойств именованного канала является возможность сервера заменять права своей учетной записи правами учетной записи клиента, соединившегося с каналом. Эта возможность служит преимущественно для ограничения прав сервера при выполнении операций доступа к различным объектам системы.
Для работы с именованными каналами Windows API предоставляет следующие функции:
CreateNamedPipe | Создание именованного канала или нового экземпляра канала. Функция доступна только серверу. |
ConnectNamedPipe или CreateFile | Подключение к экземпляру именованного канала со стороны клиента. Функция доступна только клиенту. |
WaitNamedPipe | Ожидание клиентом появления свободного экземпляра именованного канала для подключения к нему. |
ConnectNamedPipe | Ожидание сервером подключения клиента к экземпляру именованного канала. |
ReadFile, ReadFileEx | Чтение данных из именованного канала. Функция доступна как клиенту, так и серверу. |
WriteFile, WriteFileEx | Запись данных в именованный канал. Функция доступна как клиенту, так и серверу. |
PeekNamedPipe | Чтение данных из именованного канала без удаления прочитанных данных из буфера канала. Функция доступна как клиенту, так и серверу. |
TransactNamedPipe | Запись и чтение из именованного канала одной операцией. Функция доступна как клиенту, так и серверу. |
DisconnectNamedPipe | Отсоединение сервера от экземпляра именованного канала. |
GetNamedPipeInfo | Получение информации об именованном канале. |
GetNamedPipeHandleState | Получение текущего режима работы именованного канала и количества созданных экземпляров канала. |
SetNamedPipeHandleState | Установка текущего режима работы именованного канала. |
CloseHandle | Закрытие дескриптора экземпляра именованного канала, освобождение связанных с объектом ресурсов. |
FlushFileBuffers | Сброс данных из кэша в буфер канала. |
Рассмотрим алгоритм работы сервера двустороннего именованного канала, обслуживающего одновременно нескольких клиентов с использованием асинхронного ввода-вывода. Работа данного сервера заключается в пересылке сообщений, полученных от каждого клиента всем клиентам, подключенным к каналу (некий аналог чата).
После инициализации, процесс сервера запускает поток, обслуживающий клиентское соединение. Поток создает именованный канал, используя функцию CreateNamedPipe, указывает максимальное количество экземпляров канала (равное максимально возможному количеству одновременно обслуживаемых клиентов) и режимы работы канала, одновременно с созданием канала создается первый его экземпляр.
Затем поток сервера, обслуживающий этот экземпляр именованного канала, вызывает функцию ConnectNamedPipe для ожидания клиентского подключения.
После того, как клиент подключился к экземпляру именованного канала, сервер инициализирует структуры для обмена данными с клиентом, читает из канала имя подсоединившегося клиента, регистрирует его и запускает копию этого же потока для обслуживания следующего клиентского подключения. Далее, поток обслуживания экземпляра канала входит в цикл чтения канала и рассылки прочитанных сообщений всем подключенным клиентам до тех пор, пока соединение с клиентом не разорвется.
После разрыва соединения поток отключается от экземпляра канала, чтобы освободить ресурсы, выделенные системой под обслуживание этого экземпляра, и завершает свою работу.
Последующие потоки при вызове функции CreateNamedPipe создают дополнительные экземпляры канала, при этом параметры, задающие режимы работы канала функцией игнорируются.
Алгоритм работы клиента следующий: После запуска клиент запрашивает у пользователя имя сервера и свое имя для идентификации, подключается к экземпляру канала вызовом функции CreateFile с указанием имени канала, регистрируется на сервере с помощью посылки в канал своего имени и запускает поток асинхронного чтения сообщений из канала. Основной поток принимает сообщения пользователя и передает их в канал серверу. Поток чтения работает до тех пор, пока не разорвется соединение с сервером, или пока пользователь не завершит клиентский процесс.
Рассмотрим реализацию потока сервера, обслуживающего экземпляр именованного канала:
и процедуры рассылки сообщения клиентам:
Рассмотрим реализацию клиента, взаимодействующего с сервером именованного канала:
И клиентского потока асинхронного чтения данных из канала:
Полный текст примера сервера и клиента, использующих именованный канал для коммуникации можно найти в приложении к статье.
Как уже отмечалось ранее, базовым объектом для реализации именованных каналов является объект «Файл». Это позволяет перечислить созданные в системе именованные каналы программно средствами Native API: открыть корневой каталог файловой системы именованных каналов (\Device\NamedPipe) и перечислить его содержимое. Пример программы перечисления созданных именованных каналов можно найти на сайте http://www.sysinternals.com (PipeList) или в приложении к статье.
Автор выражает признательность Екатерине Субботиной, Дмитрию Заварзину и Александру Жигалину за конструктивную критику в процессе написания статьи.
Что такое named pipes
Сервером канала называется процесс, создающий именованный канал.
Клиентом канала называется процесс, подключающийся к созданному именованному каналу.
От других аналогичных объектов именованные каналы отличает гарантированная доставка сообщений, возможность асинхронного ввода/вывода, возможность коммуникации между процессами на разных компьютерах в локальной вычислительной сети и относительная простота использования.
По своему назначению они похожи на каналы операционной системы UNIX.
При создании именованного канала ему назначается уникальное имя, определяется максимальное количество одновременных соединений с клиентами канала и режим работы канала (должен ли канал быть односторонним или двусторонним (дуплексным), ведется ли передача пакетами или потоком байтов. При передаче пакетами данные одной операции записи отделяются в буфере канала от данных другой операции записи).
Базовым объектом для реализации именованных каналов служит объект «файл», поэтому для посылки и приема сообщений по именованным каналам используются те же самые функции Windows API, что и при работы с файлами (ReadFile, WriteFile).
Для каждого процесса-клиента канала создается свой экземпляр канала, с собственными буферами и дескрипторами (handles) и с собственным механизмом передачи данных, не влияющим на остальные экземпляры.
Экземпляры одного канала имеют общее имя, указанное при создании, сервер назначает имя канала в соответствии с универсальными правилами именования (Universal Naming Convention, UNC), которые обеспечивают независимый от протоколов способ идентификации каналов в Windows-сетях [1].
Именованные каналы, также как и файлы, наследуют стандартную защиту объектов Windows, что позволяет разграничить участников коммуникации и обеспечить запрет несанкционированного доступа к каналу.
Создание именованных каналов возможно только в NT-системах, подключение к созданному каналу возможно как в NT-системах, так и в Win9x. Кроме того, API работы с каналами в Win9x не поддерживает асинхронных операций ввода/вывода.
Именованные каналы широко используются внутри самой системы. Например, взаимодействие менеджера сервисов с самими сервисами осуществляется через несколько именованных каналов. Для связи с сервисами RunAs, с планировщиком событий и с сервером локальной аутентификации также используются именованные каналы.
Именованные каналы являются наиболее простым способом организации связи между сервисами и пользовательскими приложениями, нуждающимися в такой связи.
Одним из полезных (и довольно уникальных) свойств именованного канала является возможность сервера заменять права своей учетной записи правами учетной записи клиента, соединившегося с каналом. Эта возможность служит преимущественно для ограничения прав сервера при выполнении операций доступа к различным объектам системы.
Для работы с именованными каналами Windows API предоставляет следующие функции:
CreateNamedPipe | Создание именованного канала или нового экземпляра канала. Функция доступна только серверу. |
ConnectNamedPipe или CreateFile | Подключение к экземпляру именованного канала со стороны клиента. Функция доступна только клиенту. |
WaitNamedPipe | Ожидание клиентом появления свободного экземпляра именованного канала для подключения к нему. |
ConnectNamedPipe | Ожидание сервером подключения клиента к экземпляру именованного канала. |
ReadFile, ReadFileEx | Чтение данных из именованного канала. Функция доступна как клиенту, так и серверу. |
WriteFile, WriteFileEx | Запись данных в именованный канал. Функция доступна как клиенту, так и серверу. |
PeekNamedPipe | Чтение данных из именованного канала без удаления прочитанных данных из буфера канала. Функция доступна как клиенту, так и серверу. |
TransactNamedPipe | Запись и чтение из именованного канала одной операцией. Функция доступна как клиенту, так и серверу. |
DisconnectNamedPipe | Отсоединение сервера от экземпляра именованного канала. |
GetNamedPipeInfo | Получение информации об именованном канале. |
GetNamedPipeHandleState | Получение текущего режима работы именованного канала и количества созданных экземпляров канала. |
SetNamedPipeHandleState | Установка текущего режима работы именованного канала. |
CloseHandle | Закрытие дескриптора экземпляра именованного канала, освобождение связанных с объектом ресурсов. |
FlushFileBuffers | Сброс данных из кэша в буфер канала. |
Рассмотрим алгоритм работы сервера двустороннего именованного канала, обслуживающего одновременно нескольких клиентов с использованием асинхронного ввода-вывода. Работа данного сервера заключается в пересылке сообщений, полученных от каждого клиента всем клиентам, подключенным к каналу (некий аналог чата).
После инициализации, процесс сервера запускает поток, обслуживающий клиентское соединение. Поток создает именованный канал, используя функцию CreateNamedPipe, указывает максимальное количество экземпляров канала (равное максимально возможному количеству одновременно обслуживаемых клиентов) и режимы работы канала, одновременно с созданием канала создается первый его экземпляр.
Затем поток сервера, обслуживающий этот экземпляр именованного канала, вызывает функцию ConnectNamedPipe для ожидания клиентского подключения.
После того, как клиент подключился к экземпляру именованного канала, сервер инициализирует структуры для обмена данными с клиентом, читает из канала имя подсоединившегося клиента, регистрирует его и запускает копию этого же потока для обслуживания следующего клиентского подключения. Далее, поток обслуживания экземпляра канала входит в цикл чтения канала и рассылки прочитанных сообщений всем подключенным клиентам до тех пор, пока соединение с клиентом не разорвется.
После разрыва соединения поток отключается от экземпляра канала, чтобы освободить ресурсы, выделенные системой под обслуживание этого экземпляра, и завершает свою работу.
Последующие потоки при вызове функции CreateNamedPipe создают дополнительные экземпляры канала, при этом параметры, задающие режимы работы канала функцией игнорируются.
Алгоритм работы клиента следующий: После запуска клиент запрашивает у пользователя имя сервера и свое имя для идентификации, подключается к экземпляру канала вызовом функции CreateFile с указанием имени канала, регистрируется на сервере с помощью посылки в канал своего имени и запускает поток асинхронного чтения сообщений из канала. Основной поток принимает сообщения пользователя и передает их в канал серверу. Поток чтения работает до тех пор, пока не разорвется соединение с сервером, или пока пользователь не завершит клиентский процесс.
Рассмотрим реализацию потока сервера, обслуживающего экземпляр именованного канала:
и процедуры рассылки сообщения клиентам: Рассмотрим реализацию клиента, взаимодействующего с сервером именованного канала: И клиентского потока асинхронного чтения данных из канала:
Полный текст примера сервера и клиента, использующих именованный канал для коммуникации можно найти в приложении к статье.
Как уже отмечалось ранее, базовым объектом для реализации именованных каналов является объект «Файл». Это позволяет перечислить созданные в системе именованные каналы программно средствами Native API: открыть корневой каталог файловой системы именованных каналов (\Device\NamedPipe) и перечислить его содержимое. Пример программы перечисления созданных именованных каналов можно найти на сайте http://www.sysinternals.com (PipeList) или в приложении к статье.
Автор выражает признательность Екатерине Субботиной, Дмитрию Заварзину и Александру Жигалину за конструктивную критику в процессе написания статьи.
Знакомство с межпроцессным взаимодействием на Linux
Межпроцессное взаимодействие (Inter-process communication (IPC)) — это набор методов для обмена данными между потоками процессов. Процессы могут быть запущены как на одном и том же компьютере, так и на разных, соединенных сетью. IPC бывают нескольких типов: «сигнал», «сокет», «семафор», «файл», «сообщение»…
Отступление: данная статья является учебной и расчитана на людей, только еще вступающих на путь системного программирования. Ее главный замысел — познакомиться с различными способами взаимодействия между процессами на POSIX-совместимой ОС.
Именованный канал
Для передачи сообщений можно использовать механизмы сокетов, каналов, D-bus и другие технологии. Про сокеты на каждом углу можно почитать, а про D-bus отдельную статью написать. Поэтому я решил остановиться на малоозвученных технологиях отвечающих стандартам POSIX и привести рабочие примеры.
Рассмотрим передачу сообщений по именованным каналам. Схематично передача выглядит так:
Для создания именованных каналов будем использовать функцию, mkfifo():
Примечание: mode используется в сочетании с текущим значением umask следующим образом: (mode &
umask). Результатом этой операции и будет новое значение umask для создаваемого нами файла. По этой причине мы используем 0777 (S_IRWXO | S_IRWXG | S_IRWXU), чтобы не затирать ни один бит текущей маски.
Как только файл создан, любой процесс может открыть этот файл для чтения или записи также, как открывает обычный файл. Однако, для корректного использования файла, необходимо открыть его одновременно двумя процессами/потоками, одним для получение данных (чтение файла), другим на передачу (запись в файл).
Пример
mkfifo.c
Мы открываем файл только для чтения (O_RDONLY). И могли бы использовать O_NONBLOCK модификатор, предназначенный специально для FIFO файлов, чтобы не ждать когда с другой стороны файл откроют для записи. Но в приведенном коде такой способ неудобен.
Компилируем программу, затем запускаем ее:
В соседнем терминальном окне выполняем:
В результате мы увидим следующий вывод от программы:
Разделяемая память
Следующий тип межпроцессного взаимодействия — разделяемая память (shared memory). Схематично изобразим ее как некую именованную область в памяти, к которой обращаются одновременно два процесса:
Для выделения разделяемой памяти будем использовать POSIX функцию shm_open():
Функция возвращает файловый дескриптор, который связан с объектом памяти. Этот дескриптор в дальнейшем можно использовать другими функциями (к примеру, mmap() или mprotect()).
Целостность объекта памяти сохраняется, включая все данные связанные с ним, до тех пор пока объект не отсоединен/удален (shm_unlink()). Это означает, что любой процесс может получить доступ к нашему объекту памяти (если он знает его имя) до тех пор, пока явно в одном из процессов мы не вызовем shm_unlink().
После создания общего объекта памяти, мы задаем размер разделяемой памяти вызовом ftruncate(). На входе у функции файловый дескриптор нашего объекта и необходимый нам размер.
Пример
Следующий код демонстрирует создание, изменение и удаление разделяемой памяти. Так же показывается как после создания разделяемой памяти, программа выходит, но при следующем же запуске мы можем получить к ней доступ, пока не выполнен shm_unlink().
shm_open.c
После создания объекта памяти мы установили нужный нам размер shared memory вызовом ftruncate(). Затем мы получили доступ к разделяемой памяти при помощи mmap(). (Вообще говоря, даже с помощью самого вызова mmap() можно создать разделяемую память. Но отличие вызова shm_open() в том, что память будет оставаться выделенной до момента удаления или перезагрузки компьютера.)
Компилировать код на этот раз нужно с опцией -lrt:
Смотрим что получилось:
Аргумент «create» в нашей программе мы используем как для создания разделенной памяти, так и для изменения ее содержимого.
Зная имя объекта памяти, мы можем менять содержимое разделяемой памяти. Но стоит нам вызвать shm_unlink(), как память перестает быть нам доступна и shm_open() без параметра O_CREATE возвращает ошибку «No such file or directory».
Семафор
Семафор — самый часто употребляемый метод для синхронизации потоков и для контролирования одновременного доступа множеством потоков/процессов к общей памяти (к примеру, глобальной переменной). Взаимодействие между процессами в случае с семафорами заключается в том, что процессы работают с одним и тем же набором данных и корректируют свое поведение в зависимости от этих данных.
Семафор со счетчиком
Смысл семафора со счетчиком в том, чтобы дать доступ к какому-то ресурсу только определенному количеству процессов. Остальные будут ждать в очереди, когда ресурс освободится.
Итак, для реализации семафоров будем использовать POSIX функцию sem_open():
В функцию для создания семафора мы передаем имя семафора, построенное по определенным правилам и управляющие флаги. Таким образом у нас получится именованный семафор.
Имя семафора строится следующим образом: в начале идет символ «/» (косая черта), а следом латинские символы. Символ «косая черта» при этом больше не должен применяться. Длина имени семафора может быть вплоть до 251 знака.
Если нам необходимо создать семафор, то передается управляющий флаг O_CREATE. Чтобы начать использовать уже существующий семафор, то oflag равняется нулю. Если вместе с флагом O_CREATE передать флаг O_EXCL, то функция sem_open() вернет ошибку, в случае если семафор с указанным именем уже существует.
Параметр mode задает права доступа таким же образом, как это объяснено в предыдущих главах. А переменной value инициализируется начальное значение семафора. Оба параметра mode и value игнорируются в случае, когда семафор с указанным именем уже существует, а sem_open() вызван вместе с флагом O_CREATE.
Для быстрого открытия существующего семафора используем конструкцию:
, где указываются только имя семафора и управляющий флаг.
Пример семафора со счетчиком
Рассмотрим пример использования семафора для синхронизации процессов. В нашем примере один процесс увеличивает значение семафора и ждет, когда второй сбросит его, чтобы продолжить дальнейшее выполнение.
sem_open.c
В одной консоли запускаем:
В соседней консоли запускаем:
Бинарный семафор
Вместо бинарного семафора, для которого так же используется функция sem_open, я рассмотрю гораздо чаще употребляемый семафор, называемый «мьютекс» (mutex).
Мьютекс по существу является тем же самым, чем является бинарный семафор (т.е. семафор с двумя состояниями: «занят» и «не занят»). Но термин «mutex» чаще используется чтобы описать схему, которая предохраняет два процесса от одновременного использования общих данных/переменных. В то время как термин «бинарный семафор» чаще употребляется для описания конструкции, которая ограничивает доступ к одному ресурсу. То есть бинарный семафор используют там, где один процесс «занимает» семафор, а другой его «освобождает». В то время как мьютекс освобождается тем же процессом/потоком, который занял его.
Без мьютекса не обойтись в написании, к примеру базы данных, к которой доступ могут иметь множество клиентов.
Для использования мьютекса необходимо вызвать функцию pthread_mutex_init():
Функция инициализирует мьютекс (перемнную mutex) аттрибутом mutexattr. Если mutexattr равен NULL, то мьютекс инициализируется значением по умолчанию. В случае успешного выполнения функции (код возрата 0), мьютекс считается инициализированным и «свободным».
Функция pthread_mutex_lock(), если mutex еще не занят, то занимает его, становится его обладателем и сразу же выходит. Если мьютекс занят, то блокирует дальнейшее выполнение процесса и ждет освобождения мьютекса.
Функция pthread_mutex_trylock() идентична по поведению функции pthread_mutex_lock(), с одним исключением — она не блокирует процесс, если mutex занят, а возвращает EBUSY код.
Фунция pthread_mutex_unlock() освобождает занятый мьютекс.
Пример mutex
mutex.c
Данный пример демонстрирует совместный доступ двух потоков к общей переменной. Один поток (первый поток) в автоматическом режиме постоянно увеличивает переменную counter на единицу, при этом занимая эту переменную на целую секунду. Этот первый поток дает второму доступ к переменной count только на 10 миллисекунд, затем снова занимает ее на секунду. Во втором потоке предлагается ввести новое значение для переменной с терминала.
Если бы мы не использовали технологию «мьютекс», то какое значение было бы в глобальной переменной, при одновременном доступе двух потоков, нам не известно. Так же во время запуска становится очевидна разница между pthread_mutex_lock() и pthread_mutex_trylock().
Компилировать код нужно с дополнительным параметром -lpthread:
Запускаем и меняем значение переменной просто вводя новое значение в терминальном окне:
Вместо заключения
В следующих статьях я хочу рассмотреть технологии d-bus и RPC. Если есть интерес, дайте знать.
Спасибо.
UPD: Обновил 3-ю главу про семафоры. Добавил подглаву про мьютекс.