Книга: UNIX: взаимодействие процессов



UNIX: взаимодействие процессов

Уильям Стивенс

UNIX: взаимодействие процессов

МАСТЕР-КЛАСС

Всему сообществу Usenet, где можно найти много ЧаВо с ответами на все вопросы и случаи жизни

Предисловие

Введение

Большинство нетривиальных программ пишутся с использованием одной из форм межпроцессного взаимодействия (IPC — Interprocess Communication). Это естественное следствие принципа разработки, заключающегося в том, что лучше создавать приложение, состоящее из отдельных взаимодействующих элементов, чем одну большую программу. Исторически разработка приложений пережила следующие этапы развития:

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

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

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

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

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

В этой книге подробно описываются четыре формы IPC:

1. Передача сообщений (каналы, FIFO, очереди сообщений).

2. Синхронизация (взаимные исключения, условные переменные, блокировки чтения-записи, блокировка файлов и записей, семафоры).

3. Разделяемая память (неименованная и именованная).

4. Удаленный вызов процедур (двери, Sun RPC).

Здесь не рассматриваются вопросы написания программ, взаимодействующих по сети. Такая форма взаимодействия обычно подразумевает использование интерфейса сокетов и стека протоколов TCP/IP; эти темы были подробно разобраны в первом томе книги ([24]).

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

В этой книге используется материал первого тома и других моих книг:

■ UNIX Network Programming, том 1, 1998 [24];

■ Advanced Programming in the UNIX Environment, 1992 [21];

■ TCP/IP Illustrated, том 1, 1994 [22];

■ TCP/IP Illustrated, том 2, написанной в соавторстве с Гари Райтом (Gary Wright),1995, [27];

■ TCP/IP Illustrated, том 3, 1996 [23].

Может показаться странным, что я описываю средства IPC в книге, заглавие которой содержит слова «Network Programming». Замечу, что IPC часто используется и в сетевых приложениях. Как говорилось в предисловии к книге «UNIX Network Programming» 1990 года издания, «для понимания методов разработки сетевых приложений необходимо понимание средств межпроцессного взаимодействия (IPC)».

Изменения со времени первого издания

Этот том содержит полностью переписанные главы 3 и 18 книги «UNIX Network Programming» 1990 года издания. Если подсчитать количество слов, объем материала увеличился в пять раз. Ниже перечислены основные отличия данного издания:

■ В дополнение к трем формам System V IPC (очереди сообщений, семафоры, разделяемая память) рассматриваются более новые функции Posix, реализующие эти же три формы IPC. О стандартах Posix более подробно говорится в разделе 1.7. В будущем можно ожидать перехода к использованию функций Posix, обладающих определенными преимуществами по сравнению с аналогами System V.

■ Рассматриваются средства синхронизации Posix: взаимные исключения, условные переменные, блокировки чтения-записи. Эти средства могут использоваться для синхронизации потоков или процессов и часто привлекаются для обеспечения синхронизации доступа к разделяемой памяти.

■ В этом томе предполагается наличие поддержки потоков Posix (Pthreads), и многие примеры написаны с использованием многопоточного (а не многопроцессного) программирования.

■ Описание именованных и неименованных каналов и блокировок записей основано на их определениях в стандарте Posix.

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

■ При описании RPC основное внимание уделяется пакету Sun RPC. Рассказ предваряется описанием нового интерфейса дверей в Solaris, который похож на RPC, но используется только в пределах одного узла. Описание дверей является как бы введением, в котором описываются важные вопросы вызова процедур в других процессах без необходимости учитывать особенности сетевой реализации.

Кому адресована эта книга?

Эта книга может использоваться как учебник по IPC или как справочник для опытных программистов. Текст разделен на четыре части:

■ передача сообщений;

■ синхронизация;

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

■ удаленный вызов процедур.

Возможно, некоторые читатели будут интересоваться содержимым конкретных подразделов. Большая часть глав может читаться совершенно независимо от остальных, хотя в главе 2 объединены многие общие особенности средств Posix IPC, в главе 3 — System V IPC, а глава 12 является введением в разделяемую память (как Posix, так и System V). Всем читателям настоятельно рекомендуется прочесть главу 1, в особенности раздел 1.6, в котором описываются используемые в книге функции-обертки. Главы, описывающие средства Posix IPC, могут читаться отдельно от глав, посвященных System V IPC. Описание каналов и блокировок записей стоит особняком. Две главы, посвященные удаленному вызову процедур, также могут читаться отдельно от прочих.

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

Исходный код и опечатки

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

Список замеченных опечаток можно также найти на домашней странице автора.

Благодарности

Хотя на обложке книги стоит имя только одного автора, в ее создании участвовало множество людей. Прежде всего это члены семьи автора, которые смирились с ушедшими на ее написание часами. Еще раз спасибо, Салли, Билл, Эллен и Дэвид.

Спасибо всем, кто помогал работать с содержимым книги. Ваша помощь была просто неоценимой (135 печатных страниц). Вы исправляли ошибки, отмечали недостаточную четкость пояснений, предлагали другие объяснения и варианты программ. Спасибо вам, Гевин Боуи, Аллен Бриггс, Дейв Бутенхов, Ван-Тех Чанг, Крис Клилэнд, Боб Фриснан, Эндрю Гиерт, Скотт Джонсон, Марти Леиснер, Ларри Мак-Вой, Крейг Метз, Боб Нельсон, Стив Рэго, Джим Рейд, Свами К. Ситарама, Джон К. Снейдер, Иан Ланс Тейлор, Рик Тир и Энди Такер.

Мне помогали и те, кто отвечал на мои электронные письма, в которых порой было множество вопросов. Ваши ответы помогли сделать книгу более точной и ясной: Дэвид Баусум, Дейв Бутенхов, Билл Голмейстер, Макеш Кэкер, Брайан Керниган, Ларри МакВой, Стив Рэго, Кейт Скорвран, Барт Смаалдерс, Энди Такер и Джон Уэйт.

Отдельная благодарность Ларри Рафски из GSquared. Спасибо, как обычно, говорю я NOAO, Сиднею Вульфу, Ричарду Вульфу и Стиву Гранди за возможность работать с их сетями и компьютерами. Джим Баунд, Мэтт Томас, Мэри Клаутер и Барб Гловер из Digital Equipment Corp. предоставили систему Alpha, на которой выполнялась большая часть примеров данной книги. Часть программ была протестирована и в других системах. Спасибо Майклу Джонсону из Red Hat Software (за новейшие версии Red Hat Linux), Дейву Маркуардту и Джесси Хауг за компьютер RS/6000 и доступ к последним версиям AIX.

Благодарю сотрудников Prentice Hall — редактора Мэри Франц вместе с Норин Регина, Софи Папаниколау и Патти Гуэрриери — за помощь, в особенности в соблюдении сроков.

Выходные сведения

Оригинал-макет этой книги был подготовлен на языке PostScript. Форматирование осуществлялось с помощью замечательного пакета groff (автор — Джеймс Кларк) на SparcStation под управлением Solaris 2.6. (Сведения о смерти groff сильно преувеличены). Я набил все 138 897 слов книги в редакторе vi, создал 72 рисунка с помощью программы gpic (используя макросы Гари Райта), сделал 35 таблиц с помощью программы gtbl, подготовил индекс (с помощью сценариев на языке awk, написанных Джоном Бентли и Брайаном Керниганом) и сверстал все это вместе. Программа Дейва Хэнсона loom, пакет GNU indent и сценарии Гари Райта помогли добавить в книгу 8046 строк исходного кода на языке С.

С нетерпением жду комментариев, предложений и сообщений о замеченных опечатках.

W. Richard Stevens Tucson, Arizona July 1998

rstevens@kohala.com http://www.kohala.com/~rstevens

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

Все исходные тексты, приведенные в книге, вы можете найти по адресу http://www.piter.com/download.

На web-сайте издательства http://www.piter.com вы найдете подробную информацию о наших книгах.

ЧАСТЬ 1

ВВЕДЕНИЕ В IPC UNIX

ГЛАВА 1

Обзор средств взаимодействия процессов Unix

1.1. Введение

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

В процессе развития операционных систем семейства Unix за последние 30 лет методы передачи сообщений эволюционировали следующим образом:

■ Каналы (pipes — глава 4) были первой широко используемой формой взаимодействия процессов, доступной программам и пользователю (из интерпретатора команд). Основным недостатком каналов является невозможность их использования между процессами, не имеющими общего родителя (ancestor), но этот недостаток был устранен с появлением именованных каналов (named pipes), или каналов FIFO (глава 4).

■ Очереди сообщений стандарта System V (System V message queues — глава 4) были добавлены к ядрам System V в начале 80-х. Они могут использоваться для передачи сообщений между процессами на одном узле вне зависимости от того, являются ли эти процессы родственными. Несмотря на сохранившийся префикс «System V», большинство современных версий Unix, включая и те, которые не произошли от System V, поддерживают эти очереди.

ПРИМЕЧАНИЕ

В отношении процессов Unix термин «родство» означает, что у процессов имеется общий предок. Подразумевается, что процессы, являющиеся родственниками, были созданы этим процессом-предком с помощью одной или нескольких «вилок» (forks). Простейшим примером будет вызов fork некоторым процессом дважды, что приведет к созданию двух порожденных процессов. Тогда можно говорить о родстве этих процессов между собой. Естественно, каждый порожденный процесс является родственником породившего. Родитель может позаботиться о возможности взаимодействия с порожденным процессом (создав канал или очередь сообщений) перед вызовом fork, и этот объект IPC будет унаследован порожденным процессом. Более подробно о наследовании объектов IPC рассказано в табл. 1.4. Нужно также отметить, что все процессы Unix теоретически являются потомками процесса init, который запускает все необходимое в процессе загрузки системы (bootstrapping). С практической точки зрения отсчет родства процессов лучше вести с оболочки (login shell) и всех процессов, ею созданных. В главе 9 [24] рассказано о сеансах и родственных отношениях процессов более подробно. 

ПРИМЕЧАНИЕ

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

■ Очереди сообщений Posix (Posix message queues — глава 5) были добавлены в стандарт Posix (1003.1b-1993, о котором более подробно рассказано в разделе 1.7). Они могут использоваться для взаимодействия родственных и неродственных процессов на каком-либо узле.

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

Интересно также взглянуть на эволюцию различных форм синхронизации в процессе развития Unix:

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

■ Возможность блокирования записей (record locking — глава 9) была добавлена к ядрам Unix в начале 80-х и стандартизована в версии Posix.1 в 1988.

■ Семафоры System V (System V semaphores — глава 11) были добавлены вместе с возможностью совместного использования памяти (System V shared memory — глава 14) и одновременно с очередями сообщений System V (начало 80-х). Эти IPC поддерживаются большинством современных версий Unix.

■ Семафоры Posix (Posix semaphores — глава 10) и разделяемая память Posix (Posix shared memory— глава 13) были также добавлены в стандарт Posix (1003.1b-1993, который ранее упоминался в связи с очередями сообщений Posix).

■ Взаимные исключения и условные переменные (mutex, conditional variable — глава 7) представляют собой две формы синхронизации, определенные стандартом программных потоков Posix (Posix threads, Pthreads — 1003.1с-1995). Хотя обычно они используются для синхронизации между потоками, их можно применять и при организации взаимодействия процессов.

■ Блокировки чтения-записи (read-write locks — глава 8) представляют собой дополнительную форму синхронизации. Она еще не включена в стандарт Posix, но, вероятно, скоро будет.



1.2. Процессы, потоки и общий доступ к информации

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

UNIX: взаимодействие процессов

Рис. 1.1. Совместное использование информации процессами


1. Два процесса в левой части совместно используют информацию, хранящуюся в одном из объектов файловой системы. Для доступа к этим данным каждый процесс должен обратиться к ядру (используя функции read, write, lseek, write, lseek и аналогичные). Некоторая форма синхронизации требуется при изменении файла, для исключения помех при одновременной записи в файл несколькими процессами и для защиты процессов, читающих из файла, от тех, которые пишут в него.

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

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

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

Потоки

Хотя концепция процессов в системах Unix используется уже очень давно, возможность использовать несколько потоков внутри одного процесса появилась относительно недавно. Стандарт потоков Posix.1, называемый Pthreads, был принят в 1995 году. С точки зрения взаимодействия процессов все потоки одного процесса имеют общие глобальные переменные (то есть поточной модели свойственно использование общей памяти). Однако потокам требуется синхронизация доступа к глобальным данным. Вообще, синхронизация, не являясь собственно формой IPC, часто используется совместно с различными формами IPC для управления доступом к данным.

В этой книге описано взаимодействие между процессами и между потоками. Мы предполагаем наличие среды, в которой поддерживается многопоточное программирование, и будем использовать выражения вида «если канал пуст, вызывающий поток блокируется до тех пор, пока какой-нибудь другой поток не произведет запись в канал». Если система не поддерживает потоки, можно в этом предложении заменить «потоки» на «процессы» и получится классическое определение блокировки в Unix, возникающей при считывании из пустого канала командой read. Однако в системе, поддерживающей потоки, блокируется только поток, запросивший данные из пустого канала, а все остальные потоки процесса будут продолжать выполняться. Записать данные в канал сможет другой поток этого же процесса или какой-либо поток другого процесса.

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

1.3. Живучесть объектов IPC

Можно определить живучесть (persistence) любого объекта IPC как продолжительность его существования. На рис. 1.2 изображены три возможные группы, к которым могут быть отнесены объекты по живучести.

UNIX: взаимодействие процессов

Рис. 1.2. Живучесть объектов IPC


1. Объект IPC, живучесть которого определяется процессом (process-persistent), существует до тех пор, пока не будет закрыт последним процессом, в котором он еще открыт. Примером являются неименованные и именованные каналы (pipes, FIFO).

2. Объект IPC, живучесть которого определяется ядром (kernel-persistent), существует до перезагрузки ядра или до явного удаления объекта. Примером являются очереди сообщений стандарта System V, семафоры и разделяемая память. Живучесть очередей сообщений Posix, семафоров и разделяемой памяти должна определяться по крайней мере ядром, но может определяться и файловой системой в зависимости от реализации.

3. Объект IPC, живучесть которого определяется файловой системой (filesystem-persistent), существует до тех пор, пока не будет удален явно. Его значение сохраняется даже при перезагрузке ядра. Очереди сообщений Posix, семафоры и память с общим доступом обладают этим свойством, если они реализованы через отображаемые файлы (так бывает не всегда).

Следует быть аккуратным при определении живучести объекта IPC, поскольку она не всегда очевидна. Например, данные в канале (pipe) обрабатываются ядром, но живучесть каналов определяется процессами, а не ядром, потому что после того, как последний процесс, которым канал был открыт на чтение, закроет его, ядро сбросит все данные и удалит канал. Аналогично, хотя каналы FIFO и обладают именами в файловой системе, живучесть их также определяется процессами, поскольку все данные в таком канале сбрасываются после того, как последний процесс, в котором он был открыт, закроет его.

В табл. 1.1 сведена информация о живучести перечисленных ранее объектов IPC.


Таблица 1.1. Живучесть различных типов объектов IPC

Тип IPC Живучесть определяет
Программный канал (pipe) Процесс
Именованный канал (FIFO) Процесс
Взаимное исключение Posix (mutex) Процесс
Условная переменная Posix (condition variable) Процесс
Блокировка чтения-записи Posix (lock) Процесс
Блокировка записи fcntl Процесс
Очередь сообщений Posix (message queue) Ядро
Именованный семафор Posix (named semaphore) Ядро
Семафор Posix в памяти (memory-based semaphore) Процесс
Разделяемая память Posix (shared memory) Ядро
Очередь сообщений System V Ядро
Семафор System V Ядро
Память с общим доступом System V Ядро
Сокет TCP (TCP socket) Процесс
Сокет UDP (UDP socket) Процесс
Доменный сокет Unix (Unix domain socket) Процесс

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

1.4. Пространства имен

Если два неродственных процесса используют какой-либо вид IPC для обмена информацией, объект IPC должен иметь имя или идентификатор, чтобы один из процессов (называемый обычно сервером — server) мог создать этот объект, а другой процесс (обычно один или несколько клиентов — client) мог обратиться к этому конкретному объекту.

Программные каналы (pipes) именами не обладают (и поэтому не могут использоваться для взаимодействия между неродственными процессами), но каналам FIFO сопоставляются имена в файловой системе, являющиеся их идентификаторами (поэтому каналы FIFO могут использоваться для взаимодействия неродственных процессов). Для других типов IPC, рассматриваемых в последующих главах, используются дополнительные соглашения об именовании (naming conventions). Множество возможных имен для определенного типа IPC называется его пространством имен (name space). Пространство имен — важный термин, поскольку для всех видов IPC, за исключением простых каналов, именем определяется способ связи клиента и сервера для обмена сообщениями.

В табл. 1.2 сведены соглашения об именовании для различных видов IPC.


Таблица 1.2. Пространства имен для различных типов IPC

Тип IPC Пространство имен для создания или открытия Идентификатор после открытия Posix.1 1996 Unix 98
Канал (Без имени) Дескриптор
FIFO Имя файла (pathname) Дескриптор
Взаимное исключение Posix (Без имени) Указатель типа pthread_mutex_t
Условная переменная Posix (Без имени) Указатель типа pthread_cond_t
Блокировка чтения-записи Posix (Без имени) Указатель типа pthread_rwlock_t
Блокировка записей fcntl Имя файла Дескриптор
Разделяемая память Posix Posix-имя IPC Дескриптор
Очередь сообщений System V Ключ key_t Идентификатор IPC System V
Семафор System V Ключ key_t Идентификатор IPC System V
Разделяемая память System V Ключ key_t Идентификатор IPC System V
Двери (doors) Имя файла Дескриптор
Удаленный вызов процедур (RPC) Sun Программа/версия Дескриптор (handle) RPC
Сокет TCP IP-адрес и порт TCP Дескриптор .1g
Сокет UDP IP-адрес и порт TCP Дескриптор .1g
Доменный сокет Unix (domain socket) Полное имя файла Дескриптор .1g

Здесь также указано, какие формы IPC содержатся в стандарте Posix.1 1996 года и какие были включены в стандарт Unix 98. Об обоих этих стандартах более подробно рассказано в разделе 1.7. Для сравнения мы включили в эту таблицу три типа сокетов, которые подробно описаны в [24]. Обратите внимание, что интерфейс сокетов (Application Program Interface — API) стандартизируется рабочей группой Posix.1g и должен в будущем стать частью стандарта Posix.1.

Хотя стандарт Posix. 1 и дает возможность использования семафоров, их поддержка не является обязательной для производителей. В табл. 1.3 сведены функции, описанные в стандартах Posix.1 и Unix 98. Каждая функция может быть обязательной (mandatory), неопределенной (not defined) или необязательной (дополнительной — optional). Для необязательных функций мы указываем имя константы (например, _POSIX_THREADS), которая будет определена (обычно в заголовочном файле <unistd.h>), если эта функция поддерживается. Обратите внимание, что Unix 98 содержит в себе Posix.1 в качестве подмножества.


Таблица 1.3. Доступность различных форм IPC

Тип IPC Posix.1 1996 Unix 98
Программный канал Обязателен Обязателен
FIFO Обязателен Обязателен
Взаимное исключение Posix _POSIX_THREADS Обязателен
Условная переменная Posix _POSIX_THREADS Обязателен
Взаимные исключения и условные переменные между процессами _POSIX_THREADS_PROCESS_SHARED Обязателен
Блокировка чтения-записи Posix (He определен) Обязателен
Блокировка записей fcntl Обязателен Обязателен
Очередь сообщений Posix _POSIX_MESSAGE_PASSING _XOPEN_REALTIME
Семафоры Posix _POSIX_SEMAPHORES_ _XOPEN_REALTIME
Память с общим доступом Posix _POSIX_SHARED_MEMORY_OBJECTS _XOPEN_REALTIME
Очередь сообщений System V (He определен) Обязателен
Семафор System V (He определен) Обязателен
Память с общим доступом System V (He определен) Обязателен
Двери (doors) (He определен) (Не определен)
Удаленный вызов процедур Sun (He определен) (Не определен)
Отображение памяти mmap _POSIX_MAPPED_FILES или POSIX_SHARED_MEMORY_OBJECTS Обязателен
Сигналы реального времени (realtime signals) _POSIX_REALTIME_SIGNALS _XOPEN_REALTIME

1.5. Действие команд fork, exec и exit на объекты IPC

Нам нужно достичь понимания действия функций fork, exec и _exit на различные формы IPC, которые мы обсуждаем (последняя из перечисленных функций вызывается функцией exit). Информация по этому вопросу сведена в табл. 1.4.

Большинство функций описаны далее в тексте книги, но здесь нужно сделать несколько замечаний. Во-первых, вызов fork из многопоточного процесса (multithreaded process) приводит к беспорядку в безымянных переменных синхронизации (взаимных исключениях, условных переменных, блокировках и семафорах, хранящихся в памяти). Раздел 6.1 книги [3] содержит необходимые детали. Мы просто отметим в добавление к таблице, что если эти переменные хранятся в памяти с общим доступом и создаются с атрибутом общего доступа для процессов, они будут доступны любому процессу, который может обращаться к этой области памяти. Во-вторых, три формы IPC System V не могут быть открыты или закрыты. Из листинга 6.6 и упражнений 11.1 и 14.1 видно, что все, что нужно знать, чтобы получить доступ к этим трем формам IPC, — это идентификатор. Поэтому они доступны всем процессам, которым известен этот идентификатор, хотя для семафоров и памяти с общим доступом требуется некая особая обработка.


Таблица 1.4. Действие fork, exec и _exit на IPC

Тип IPC fork exec _exit
Неименованные и именованные каналы Порожденный процесс получает копии всех дескрипторов родительского процесса Все открытые дескрипторы остаются открытыми, если для них не установлен бит FD_CLOEXEC Все открытые дескрипторы закрываются, данные из программного канала и FIFO удаляются после последнего закрытия
Очереди сообщений Posix Порожденный процесс получает копии всех открытых родительских процессов Все открытые дескрипторы очередей сообщений закрываются Все открытые дескрипторы очередей сообщений закрываются
Очереди сообщений System V Не действует Не действует Не действует
Взаимные исключения и условные переменные Posix Общий доступ, если используется разделяемая память с атрибутом разделения между процессами Исчезает, если не хранится в разделяемой памяти, которая остается открытой и имеет атрибут разделения Исчезает, если не находится в разделяемой памяти, которая остается открытой и имеет атрибут разделения
Блокировки чтения-записи Posix Общий доступ, если используется память с общим доступом и атрибутом разделения между процессами Исчезает, если не хранится в разделяемой памяти, которая остается открытой и имеет атрибут разделения Исчезает, если не хранится в разделяемой памяти, которая остается открытой и имеет атрибут разделения
Семафоры Posix, хранящиеся в памяти Общий доступ, если используется память с общим доступом и атрибутом разделения между процессами Исчезает, если не хранится в разделяемой памяти, которая остается открытой и имеет атрибут разделения Исчезает, если не хранится в разделяемой памяти, которая остается открытой и имеет атрибут разделения
Именованные семафоры Posix Все открытые в родительском процессе остаются открытыми в порожденном Все открытые закрываются Все открытые закрываются
Семафоры System V Все значения semadj в порожденном процессе устанавливаются в 0 Все значения semadj передаются новой программе Все значения semadj добавляются к значению соответствующего семафора
Блокировка записей fcntl Блокировки в родительском процессе не наследуются порожденным процессом Блокировки не изменяются до тех пор, пока не закроется дескриптор Все несброшенные блокировки, установленные процессом, снимаются
Отображение памяти Отображения памяти родительского процесса сохраняются в порожденном Отображения памяти сбрасываются (unmap) Отображения памяти сбрасываются
Разделяемая память Posix Отображения памяти родительского процесса сохраняются в порожденном Отображения памяти сбрасываются Отображения памяти сбрасываются
Разделяемая память System V Присоединенные сегменты разделяемой памяти остаются присоединенными в порожденном процессе Присоединенные сегменты разделяемой памяти отсоединяются Присоединенные сегменты разделяемой памяти отсоединяются
Двери (doors) Порожденный процесс получает копии всех открытых дескрипторов родительского процесса, но только родительский процесс является сервером при активизации дверей через дескрипторы Все дескрипторы дверей должны быть закрыты, потому что они создаются с установленным битом FD_CLOEXEC Все открытые дескрипторы закрываются

1.6. Обработка ошибок: функции-обертки

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



Sem_post(ptr);

Пример функции-обертки приведен в листинге 1.1[1] 

Листинг 1.1. Функция-обертка к функции sem_post

// lib/wrapunix.c

387 void

388 Sem_post(sem_t *sem)

389 {

390  if (sem_post(sem) == –1)

391   err_sys("sem_post error");

392 }

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

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

ПРИМЕЧАНИЕ

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

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

Хотя может показаться, что использовать такие функции-обертки не слишком выгодно, вы избавитесь от этого заблуждения в главе 7, где мы обнаружим, что функции для работы с потоками (thread functions) не присваивают значение стандартной переменной Unix errno при возникновении ошибки; вместо этого код ошибки просто возвращается функцией. Это означает, что при вызове функции pthread мы должны каждый раз выделять память под переменную, сохранять в ней возвращаемое функцией значение, а затем устанавливать значение переменной errno равным этой переменной, прежде чем вызывать функцию err_sys (листинг В.4). Чтобы не загромождать текст фигурными скобками, мы используем оператор языка Си «запятая» (comma) и совмещаем присваивание значения переменной errno и вызов err_sys в одном операторе, как в нижеследующем примере:

int n;

if ((n = pthread_mutex_lock(&ndone_mutex))!=0) errno=n, err_sys("pthread_mutex_lock error");

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

Pthread_mutex_lock(&ndone_mutex);

где используется наша собственная функция-обертка, приведенная в листинге 1.2.

Листинг 1.2. Реализация обертки к функции pthread_mutex_lock

//lib/wrappthread.c

125 void

126 Pthread_mutex_lock(pthread_mutex_t *mptr)

127 {

128  int n;

129  if ((n=pthread_mutex_lock(mptr))==0)

130   return;

131  errno=n;

132  err_sys("pthread_mutex_lock error");

133 }

ПРИМЕЧАНИЕ

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

Наше соглашение о замене первой буквы имени функции на заглавную является компромиссом. Рассматривалось много других форм записи: использование префикса е ([10, с. 182]), суффикса _е и т. д. Наш вариант кажется наименее отвлекающим внимание и одновременно дающим визуальное указание на то, что вызывается какая-то другая функция.

Этот метод имеет побочное полезное свойство: проверяются ошибки, возвращаемые функциями, код возврата которых обычно игнорируется, например close и pthread_ mutex_lock.

Далее в тексте книги мы будем использовать эти функции-обертки, если только не потребуется явно проверить наличие ошибки и обработать ее произвольным образом, отличным от завершения процесса. Мы не приводим в книге исходный код для всех оберток, но он свободно доступен в Интернете (см. предисловие).

Значение errno

При возникновении ошибки в функции Unix глобальной переменной errno присваивается положительное значение, указывающее на тип ошибки; при этом функция обычно возвращает значение –1. Наша функция err_sys выводит соответствующее коду ошибки сообщение (например, Resource temporarily unavailable — ресурс временно недоступен, — если переменная errno имеет значение EAGAIN).

Функция присваивает значение переменной errno только при возникновении ошибки. В случае нормального завершения работы значение этой переменной не определено. Все положительные значения соответствуют константам с именами из заглавных букв, начинающимися с Е, определяемым обычно в заголовочном файле <sys/errno.h>. Отсутствию ошибок соответствует значение 0. 

При работе с несколькими потоками в каждом из них должна быть собственная переменная errno. Выделение переменной каждому потоку происходит автоматически, однако обычно это требует указания компилятору на то, что должна быть возможность повторного входа в программу. Задается это с помощью ключей –D_REENTRANT или –D_POSIX_C_SOURCE=199506L или аналогичных. Часто в заголовке <errno.h> переменная errno определяется как макрос, раскрываемый в вызов функции, если определена константа _REENTRANT. Функция обеспечивает доступ к копии errno, относящейся к данному потоку.

Далее в тексте мы используем выражения наподобие «функция mq_send возвращает EMSGSIZE», означающие, что функция возвращает ошибку (обычно возвращаемое значение при этом равно –1) и присваивает переменной errno значение указанной константы.

1.7. Стандарты Unix

В настоящее время стандарты Unix определяются Posix и The Open Group.

Posix

Название Posix образовано от «Portable Operating System Interface», что означает приблизительно «интерфейс переносимых операционных систем». Это не один стандарт, а целое семейство, разработанное Институтом инженеров по электротехнике и радиоэлектронике (Institute for Electrical and Electronics Engineers — IEEE). Стандарты Posix были также приняты в качестве международных стандартов ISO (International Organization for Standardization, Международная организация по стандартизации) и IEC (International Electrotechnical Commission, Международная электротехническая комиссия), или ISO/IEC. Стандарты Posix прошли несколько стадий разработки.

■ Стандарт IEEE 1003.1-1988 (317 страниц) был первым стандартом Posix. Он определял интерфейс взаимодействия языка С с ядром Unix-типа в следующих областях: примитивы для реализации процессов (вызовы fork, exec, сигналы и таймеры), среда процесса (идентификаторы пользователей, группы процессов), файлы и каталоги (все функции ввода-вывода), работа с терминалом, базы данных системы (файлы паролей и групп), форматы архивов tar и cpio.

ПРИМЕЧАНИЕ

Первый стандарт Posix вышел в рабочем варианте под названием IEEEIX в 1986 году. Название Posix было предложено Ричардом Штолманом (Richard Stallman). 

■ Затем вышел стандарт IEЕЕ 1003.1-1990 (356 страниц). Он одновременно являлся и международным стандартом ISO/IEC 9945-1:1990. По сравнению с версией 1988 года изменения в версии 1990 года были минимальными. К заголовку было добавлено: «Part 1: System Application Program Interface (API) [C Language]» («Часть 1: Системный интерфейс разработки программ (API) [Язык С])», и это означало, что стандарт описывал программный интерфейс (API) языка С. 

■ IEEE 1003.2-1992 вышел в двух томах общим объемом около 1300 страниц, и его заголовок содержал строку «Part 2: Shell and Utilities» (Часть 2: «Интерпретатор и утилиты»). Эта часть определяла интерпретатор (основанный на Bourne shell в Unix System V) и около ста утилит (программ, обычно вызываемых из интерпретатора — от awk и basename до vi и уасс). В настоящей книге мы будем ссылаться на этот стандарт под именем Posix. 2.

■ IEEE 1003.1b-1993 (590 страниц) изначально был известен как IEEE P1003.4. Этот стандарт представлял собой дополнение к стандарту 1003.1-1990 и включал расширения реального времени, разработанные рабочей группой Р1003.4: синхронизацию файлов, асинхронный ввод-вывод, семафоры, управление памятью, планирование выполнения (scheduling), часы, таймеры и очереди сообщений.

■ IEEE 1003.1, издание 1996 года [8] (743 страницы), включает 1003.1-1990 (базовый интерфейс API), 1003.1b-1993 (расширения реального времени), 1003.1-1995 (Pthreads — программные потоки Posix) и 1003.1i-1995 (технические поправки к 1003.1b). Этот стандарт также называется ISO/IEC 9945-1: 1996. В него были добавлены три главы о потоках и дополнительные разделы, касающиеся синхронизации потоков (взаимные исключения и условные переменные), планирование выполнения потоков, планирование синхронизации. В настоящей книге мы называем этот стандарт Posix.1.

ПРИМЕЧАНИЕ

Более четверти из 743 страниц стандарта представляли собой приложение, озаглавленное «Rationale and Notes» («Обоснование и примечания»). Это обоснование содержит историческую информацию и объяснение причин, по которым некоторые функции были или не были включены в стандарт. Часто обоснование оказывается не менее полезным, чем собственно стандарт.

К сожалению, стандарты IEEE не являются свободно доступными через Интернет. Информация о том, где можно заказать книгу, дана в библиографии под ссылкой [8]. Обратите внимание, что семафоры были определены в стандарте расширений реального времени, отдельно от взаимных исключений и условных переменных (которые были определены в стандарте Pthreads), что объясняет некоторые различия в интерфейсах API этих средств.

Наконец, заметим, что блокировки чтения-записи не являются частью стандартов Posix. Об этом более подробно рассказано в главе 8. 

В будущем планируется выход новой версии IEEE 1003.1, включающей стандарт P1003.1g, сетевые интерфейсы (сокеты и XTI), которые описаны в первом томе этой книги.

В предисловии стандарта Posix.1 1996 года утверждается, что стандарт ISO/IEC 9945 состоит из следующих частей:

1. Системный интерфейс разработки программ (API) (язык С).

2. Интерпретатор и утилиты.

3. Администрирование системы (в разработке).

Части 1 и 2 представляют собой то, что мы называем Posix.1 и Posix.2. 

Работа над стандартами Posix постоянно продолжается, и авторам книг, с ними связанных, приходится заниматься стрельбой по движущейся мишени. О текущем состоянии стандартов можно узнать на сайте http://www.pasc.org/standing/sd11.html.

The Open Group

The Open Group (Открытая группа) была сформирована в 1996 году объединением X/Open Company (основана в 1984 году) и Open Software Foundation (OSF, основан в 1988 году). Эта группа представляет собой международный консорциум производителей и потребителей из промышленности, правительства и образовательных учреждений. Их стандарты тоже выходили в нескольких версиях:

■ В 1989 году Х/Open опубликовала 3-й выпуск X/Open Portability Guide (Руководство по разработке переносимых программ) — XPG3.

■ В 1992 году был опубликован четвертый выпуск (Issue 4), а в 1994 году — вторая его версия (Issue 4, Version 2). Последняя известна также под названием Spec 1170, где магическое число 1170 представляет собой сумму количества интерфейсов системы (926), заголовков (70) и команд (174). Есть и еще два названия: X/Open Single Unix Specification (Единая спецификация Unix) и Unix 95.

■ В марте 1997 года было объявлено о выходе второй версии Единой спецификации Unix. Этот стандарт программного обеспечения называется также Unix 98, и именно так мы ссылаемся на эту спецификацию далее в тексте книги. Количество интерфейсов в Unix 98 возросло с 1170 до 1434, хотя для рабочей станции это количество достигает 3030, поскольку в это число включается CDE (Common Desktop Environment — общее окружение рабочего стола), которое, в свою очередь, требует системы X Window System и пользовательского интерфейса Motif. Подробно об этом написано в книге [9]. Полезную информацию можно также найти по адресу http://www.UNIX-systems.org/version2.

ПРИМЕЧАНИЕ

С этого сайта можно свободно скачать единую спецификацию Unix практически целиком.

Версии Unix и переносимость

Практически все версии Unix, с которыми можно столкнуться сегодня, соответствуют какому-либо варианту стандарта Posix.1 или Posix.2. Мы говорим «какому-либо», потому что после внесения изменений в Posix (например, Добавление расширений реального времени в 1993 и потоков в 1996) производителям обычно требуется год или два, чтобы подогнать свои программы под эти стандарты.

Исторически большинство систем Unix являются потомками либо BSD, либо System V, но различия между ними постепенно стираются, по мере того как производители переходят к использованию стандартов Posix. Основные различия лежат в области системного администрирования, поскольку ни один стандарт Posix на данный момент не описывает эту область.

В большинстве примеров этой книги мы использовали операционные системы Solaris 2.6 и Digital Unix 4.0B. Дело в том, что на момент написания книги (конец 1997 — начало 1998 года) только эти две операционные системы поддерживали System V IPC, Posix IPC и программные потоки Posix (Pthreads).

1.8. Комментарий к примерам IPC

Чаще всего для иллюстрации различных функций в книге используются три шаблона (модели) взаимодействия:

1. Сервер файлов: приложение клиент-сервер, причем клиент посылает серверу запрос с именем файла, а сервер возвращает клиенту его содержимое.

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

3. Увеличение последовательного номера: один или несколько потоков или процессов увеличивают общий для всех индекс. Число это может храниться в файле с общим доступом или в совместно используемой области памяти.

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

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

1.9. Резюме

Взаимодействие процессов традиционно является одной из проблемных областей в Unix. По мере развития системы предлагались различные решения, и ни одно из них не было совершенным. Мы подразделяем IPC на четыре главных типа.

1. Передача сообщений (каналы, FIFO, очереди сообщений).

2. Синхронизация (взаимные исключения, условные переменные, блокировки чтения-записи, семафоры).

3. Разделяемая память (неименованная и именованная).

4. Вызов процедур (двери в Solaris, RPC Sun).

Мы рассматриваем взаимодействие как отдельных потоков одного процесса, так и нескольких независимых процессов.

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

Другим свойством каждого типа IPC является пространство имен, определяющее идентификацию объектов IPC процессами и потоками, использующими его. Некоторые объекты не имеют имен (каналы, взаимные исключения, условные переменные, блокировки чтения-записи), другие обладают именами в рамках файловой системы (каналы FIFO), третьи характеризуются тем, что в главе 2 названо «именами IPC стандарта Posix», а четвертые — еще одним типом имен, который описан в главе 3 (ключи или идентификаторы IPC стандарта System V). Обычно сервер создает объект IPC с некоторым именем, а клиенты используют это имя для получения доступа к объекту.

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

Стандарты IEEE Posix — Posix.1, определяющий основы интерфейса С в Unix, и Posix.2, определяющий основные команды, — это те стандарты, к которым движутся большинство производителей. Однако стандарты Posix в настоящее время быстро поглощаются (включаются в качестве части) и расширяются коммерческими стандартами, в частности The Open Group (Unix 98).


Таблица 1.5. Версии модели клиент-сервер  

Листинг Описание
4.1 Два канала между родительским и порожденным процессами
4.5 Использует popen и cat
4.6 Использует два канала FIFO между родительским и порожденным процессами
4.7 Два канала FIFO между независимым сервером и неродственным клиентом
4.10 Каналы FIFO между независимым последовательным сервером и несколькими клиентами
4.12 Программный канал или FIFO: формирование записей в потоке байтов
6.7 Две очереди сообщений System V
6.12 Одна очередь сообщений System V с несколькими клиентами
6.16 Одна очередь сообщений System V для каждого клиента; клиентов несколько
15.15 Передача дескриптора через дверь

Таблица 1.6. Версии модели производитель-потребитель

Листинг Описание
7.1 Взаимное исключение, несколько производителей, один потребитель
7.5 Взаимное исключение и условная переменная, несколько производителей, один потребитель
10.8 Именованные семафоры Posix, один производитель, один потребитель
10.11 Семафоры Posix в памяти, один производитель, один потребитель
10.12 Семафоры Posix в памяти, несколько производителей, один потребитель
10.15 Семафоры Posix в памяти, несколько производителей, несколько потребителей
10.18 Семафоры Posix в памяти, один производитель, один потребитель: несколько буферов

Таблица 1.7. Версии программы с увеличением последовательного номера

Листинг Описание
9.1 Индекс в файле, без блокировки
9.3 Индекс в файле, блокировка с помощью fcntl
9.9 Индекс в файле, блокировка с использованием функции open
10.10 Индекс в файле, блокировка с помощью именованного семафора Posix
12.2 Индекс в общей памяти mmap, блокировка с помощью именованного семафора Posix
12.3 Индекс в общей памяти mmap, блокировка с помощью семафора Posix в памяти
12.4 Индекс в неименованной общей памяти 4.4BSD, блокировка с помощью именованного семафора Posix
12.5 Индекс в общей памяти SVR4 /dev/zero, блокировка с помощью именованного семафора Posix
13.6 Индекс в общей памяти Posix, блокировка с помощью семафора Posix в памяти
А.19 Измерение производительности: блокировка взаимным исключением между потоками
А.22 Измерение производительности: блокировка чтения-записи между потоками
А.23 Измерение производительности: блокировка между потоками с помощью семафоров Posix в памяти
А.25 Измерение производительности: блокировка между потоками с помощью именованных семафоров Posix
А.28 Измерение производительности: блокировка между потоками с помощью семафоров System V
А.29 Измерение производительности: блокировка между потоками с помощью fcntl
А.33 Измерение производительности: блокировка между процессами с помощью взаимных исключений

Упражнения

1. На рис 1.1 изображены два процесса, обращающиеся к одному файлу. Если оба процесса только дописывают данные к концу файла (возможно, длинного), какой нужен будет тип синхронизации?

2. Изучите заголовочный файл <errno.h> в вашей системе и выясните, как определена errno.

3. Дополните табл. 1.3 используемыми вами функциями, поддерживаемыми Unix-системами.

ГЛАВА 2

Posix IPC

2.1. Введение

Из имеющихся типов IPC следующие три могут быть отнесены к Posix IPC, то есть к методам взаимодействия процессов, соответствующим стандарту Posix:

■ очереди сообщений Posix (глава 5);

■ семафоры Posix (глава 10);

■ разделяемая память Posix (глава 13).

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

Полный список функций, используемых для работы с данными типами IPC, приведен в табл. 2.1.


Таблица 2.1. Функции Posix IPC

Очереди сообщений Семафоры Общая память
Заголовочный файл <mqueue.h> <semaphore.h> <sys/mman.h>
Функции для создания, открытия и удаления mq_open mq_close mq_unlink sem_open sem_close sem_unlink sem_init sem_destroy shm_open shm_unlink
Операции управления mq_getattr mq_setattr ftruncate fstat
Операции IPC mq_send mq_receive mq_notify sem_wait sem_trywait sem_post sem_getvalue mmap munmap

2.2. Имена IPC

В табл. 1.2 мы отметили, что три типа IPC стандарта Posix имеют идентификаторы (имена), соответствующие этому стандарту. Имя IPC передается в качестве первого аргумента одной из трех функций: mq_open, sem_open и shm_open, причем оно не обязательно должно соответствовать реальному файлу в файловой системе. Стандарт Posix.1 накладывает на имена IPC следующие ограничения:

■ Имя должно соответствовать существующим требованиям к именам файлов (не превышать в длину РАТНМАХ байтов, включая завершающий символ с кодом 0).

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

■ Интерпретация дополнительных слэшей в имени зависит от реализации.

Таким образом, для лучшей переносимости имена должны начинаться со слэша (/) и не содержать в себе дополнительных слэшей. К сожалению, эти правила, в свою очередь, приводят к проблемам с переносимостью.

В системе Solaris 2.6 требуется наличие начального слэша и запрещается использование дополнительных. Для очереди сообщений, например, при этом создаются три файла в каталоге /tmp, причем имена этих файлов начинаются с .MQ. Например, если аргумент функции mq_open имеет вид /queue.1234, то созданные файлы будут иметь имена /tmp/.MQDqueue.1234, /tmp/.MQLqueue.1234 и /tmp/.MQPqueue.1234. В то же время в системе Digital Unix 4.0B просто создается файл с указанным при вызове функции именем.

Проблема с переносимостью возникает при указании имени с единственным слэшем в начале: при этом нам нужно иметь разрешение на запись в корневой каталог. Например, очередь с именем /tmp.1234 допустима стандартом Posix и не вызовет проблем в системе Solaris, но в Digital Unix возникнет ошибка создания файла, если разрешения на запись в корневой каталогу программы нет. Если мы укажем имя /tmp/test.1234, проблемы в Digital Unix и аналогичных системах, создающих файл с указанным именем, пропадут (предполагается существование каталога /tmp и наличие у программы разрешения на запись в него, что обычно для большинства систем Unix), однако в Solaris использование этого имени будет невозможно.

Для решения подобных проблем с переносимостью следует определять имя в заголовке с помощью директивы #define, чтобы обеспечить легкость его изменения при переносе программы в другую систему.

ПРИМЕЧАНИЕ

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

Стандарт Posix.1 определяет три макроса:

S_TYPEISMQ(buf)

S_TYPEISSEM(buf)

S_TYPEISSHM(buf)

которые принимают единственный аргумент — указатель на структуру типа stat, содержимое которой задается функциями fstat, lstat и stat. Эти три макроса возвращают ненулевое значение, если указанный объект IPC (очередь сообщений, семафор или сегмент разделяемой памяти) реализован как особый вид файла и структура stat ссылается на этот тип. В противном случае макрос возвращает 0.

ПРИМЕЧАНИЕ

К сожалению, проку от этих макросов мало, потому что нет никаких гарантий, что эти типы IPC реализованы как отдельные виды файлов. Например, в Solaris 2.6 все три макроса всегда возвращают 0.

Все прочие макросы, используемые для проверки типа файла, имеют имена, начинающиеся с S_IS, и принимают всегда единственный аргумент: поле st_mode структуры stat. Поскольку макросы, используемые для проверки типа IPC, принимают аргументы другого типа, их имена начинаются с S_TYPEIS. 

Функция px_ipc_name

Существует и другое решение упомянутой проблемы с переносимостью. Можно определить нашу собственную функцию px_ipc_name, которая добавляет требуемый каталог в качестве префикса к имени Posix IPC.

#include "unpipc.h"

char *px_ipc_name(const char *name);

/* Возвращает указатель при успешном завершении, NULL при возникновении ошибки */

ПРИМЕЧАНИЕ

Так выглядят листинги наших собственных функций, то есть функций, не являющихся стандартными системными. Обычно включается заголовочный файл unpipc.h (листинг B.1).

Аргумент пате (имя) не должен содержать слэшей. Тогда, например, при вызове px_ipc_name("test1") будет возвращен указатель на строку /test1 в Solaris 2.6 или на строку /tmp/test1 в Digital Unix 4.0B. Память для возвращаемой строки выделяется динамически и освобождается вызовом free. Можно установить произвольное значение переменной окружения PX_IPC_NAME, чтобы задать другой каталог по умолчанию.

В листинге 2.1[1] приведен наш вариант реализации этой функции.

ПРИМЕЧАНИЕ

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

Функция snprintf еще не является частью стандарта ANSI С, но планируется ее включение в обновленный стандарт, называющийся С9Х. Тем не менее многие производители включают ее в стандартную библиотеку С. Везде в тексте мы используем функцию snprintf в нашем собственном варианте, обеспечивающем вызов sprintf, если в системной библиотеке функция snprinft отсутствует. 

Листинг 2.1. Функция px_ipc_name в нашей реализации.

//lib/px_ipc_name.c

1  #include "unpipc.h"

2  char *

3  px_ipc_name(const char *name)

4  {

5   char *dir, *dst, *slash;

6   if ((dst = malloc(РАТН_МАХ)) == NULL)

7    return(NULL);

8   /* есть возможность задать другое имя каталога с помощью переменной окружения */

9   if ((dir = getenv("PX IPC_NAME")) == NULL) {

10 #ifdef POSIX_IPC_PREFIX

11   dir = POSIX_IPC_PREFIX; /* из "config.h" */

12 #else

13   dir = "/tmp/"; /* по умолчанию */

14 #endif

15  }

16  /* имя каталога должно заканчиваться символом '/' */

17  slash = (dir[strlen(dir) – 1] == '/') ? "" : "/";

18  snprintf(dst, PATH_MAX, "%s%s%s", dir, slash, name);

19  return(dst); /* для освобождения этого указателя можно вызвать free() */

20 }

2.3. Создание и открытие каналов IPC

Все три функции, используемые для создания или открытия объектов IPC: mq_open, sem_open и shm_open, — принимают специальный флаг oflag в качестве второго аргумента. Он определяет параметры открытия запрашиваемого объекта аналогично второму аргументу стандартной функции open. Все константы, из которых можно формировать этот аргумент, приведены в табл. 2.2.


Таблица 2.2. Константы, используемые при создании и открытии объектов IPC 

Описание mq_open sem_open shm_open
Только чтение О_RDONLY   О_RDONLY
Только запись О_WRONLY    
Чтение и запись О_RDWR   О_RDWR
Создать, если не существует О_CREAT О_CREAT О_CREAT
Исключающее создание О_EXCL О_EXCL О_EXCL
Без блокировки О_NONBLOCK    
Сократить (truncate) существующий     O_TRUNC 

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

Указание прочих флагов из табл. 2.2 не является обязательным.

O_CREAT — создание очереди сообщений, семафора или сегмента разделяемой памяти, если таковой еще не существует (см. также флаг O_EXCL, влияющий на результат).

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


Таблица 2.3. Константы режима доступа при создании нового объекта IPC 

Константа Описание
S_IRUSR Владелец — чтение
S_IWUSR Владелец — запись
S_IRGRP Группа — чтение
S_IWGRP Группа — запись
S_IROTH Прочие — чтение
S_IWOTH Прочие — запись 

Эти константы определены в заголовке <sys/stat.h>. Указанные биты разрешений изменяются наложением маски режима создания файлов для данного процесса (с. 83-85 [21]) или с помощью команды интерпретатора umask.

Как и со вновь созданным файлом, при создании очереди сообщений, семафора или сегмента разделяемой памяти им присваивается идентификатор пользователя, соответствующий действующему (effective) идентификатору пользователя процесса. Идентификатор группы семафора или сегмента разделяемой памяти устанавливается равным действующему групповому идентификатору процесса или групповому идентификатору, установленному по умолчанию для системы в целом. Групповой идентификатор очереди сообщений всегда устанавливается равным действующему групповому идентификатору процесса (на с. 77-78 [21] рассказывается о групповых и пользовательских идентификаторах более подробно).

ПРИМЕЧАНИЕ

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

O_EXCL — если этот флаг указан одновременно с O_CREAT, функция создает новую очередь сообщений, семафор или объект разделяемой памяти только в том случае, если таковой не существует. Если объект уже существует и указаны флаги O_CREAT | O_EXCL, возвращается ошибка EEXIST.

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

O_NONBLOCK — этот флаг создает очередь сообщений без блокировки. Блокировка обычно устанавливается для считывания из пустой очереди или записи в полную очередь. Об этом более подробно рассказано в подразделах, посвященных функциям mq_send и mq_receive раздела 5.4.

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

На рис. 2.1 показана реальная последовательность логических операций при открытии объекта IPC. Что именно подразумевается под проверкой разрешений доступа, вы узнаете в разделе 2.4. Другой подход к изображенному на рис. 2.1 представлен в табл. 2.4. 

UNIX: взаимодействие процессов

Рис. 2.1. Логика открытия объекта IPC


Обратите внимание, что в средней строке табл. 2.4, где задан только флаг O_CREAT, мы не получаем никакой информации о том, был ли создан новый объект или открыт существующий.


Таблица 2.4. Логика открытия объекта IPC 

Аргумент oflag Объект не существует Объект уже существует
Нет специальных флагов Ошибка, errno=ENOENT OK, открывается существующий объект
O_CREAT OK, создается новый объект OK, открывается существующий объект
O_CREAT|O_EXCL OK, создается новый объект Ошибка, errno=EEXIST 

2.4. Разрешения IPC

Новая очередь сообщений, именованный семафор или сегмент разделяемой памяти создается функциями mq_open, sem_open и shm_open, при условии, что аргумент oflag содержит константу O_CREAT. Согласно табл. 2.3, любому из данных типов IPC присваиваются определенные права доступа (permissions), аналогичные разрешениям доступа к файлам в Unix.

При открытии существующей очереди сообщений, семафора или сегмента разделяемой памяти теми же функциями (в случае, когда не указан флаг O_CREAT или указан O_CREAT без O_EXCL и объект уже существует) производится проверка разрешений:

1. Проверяются биты разрешений, присвоенные объекту IPC при создании.

2. Проверяется запрошенный тип доступа (O_RDONLY, O_WRONLY, O_RDWR).

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

Большинством систем Unix производятся следующие конкретные проверки:

1. Если действующий идентификатор пользователя для процесса есть 0 (привилегированный пользователь), доступ будет разрешен.

2. Если действующий идентификатор пользователя процесса совпадает с идентификатором владельца объекта IPC: если соответствующий бит разрешения для пользователя установлен, доступ разрешен, иначе в доступе отказывается.

Под соответствующим битом разрешения мы подразумеваем, например, бит разрешения на чтение, если процесс открывает объект только для чтения. Если процесс открывает объект для записи, должен быть установлен соответственно бит разрешения на запись для владельца (user-write).

3. Если действующий идентификатор группы процесса или один из дополнительных групповых идентификаторов процесса совпадает с групповым идентификатором объекта IPC: если соответствующий бит разрешения для группы установлен, доступ будет разрешен, иначе в доступе отказывается.

4. Если соответствующий бит разрешения доступа для прочих пользователей установлен, доступ будет разрешен, иначе в доступе будет отказано. 

Эти четыре проверки производятся в указанном порядке. Следовательно, если процесс является владельцем объекта IPC (шаг 2), доступ разрешается или запрещается на основе одних только разрешений пользователя (владельца). Разрешения группы при этом не проверяются. Аналогично, если процесс не является владельцем объекта IPC, но принадлежит к нужной группе, доступ разрешается или запрещается на основе разрешений группы — разрешения для прочих пользователей при этом не проверяются.

ПРИМЕЧАНИЕ

Согласно табл. 2.2, функция sem_open не использует флаги O_RDONLY, O_WRONLY, O_RDWR. В разделе 10.2, однако, будет сказано о том, что некоторые реализации Unix подразумевают наличие флага O_RDWR, потому что любое обращение к семафору подразумевает чтение и запись его значения.

2.5. Резюме

Три типа Posix IPC — очереди сообщений, семафоры и разделяемая память — идентифицируются их полными именами. Они могут являться или не являться реальными именами файлов в файловой системе, и это вызывает проблемы с переносимостью. Решение проблемы — использовать собственную функцию px_ipc_name. При создании или открытии объекта IPC требуется указать набор флагов, аналогичных указываемым при использовании функции open. При создании нового объекта IPC требуется указать разрешения для него, используя те же константы S_xxx, что и для функции open (табл. 2.3). При открытии существующего объекта IPC производится проверка разрешений процесса, аналогичная проверке при открытии файла.

Упражнения

1. Каким образом биты установки идентификатора пользователя (set-user-ID, SUID) и установки идентификатора группы (set-group-ID) (раздел 4.4 [21]) программы, использующей Posix IPC, влияют на проверку разрешений, описанную в разделе 2.4?

2. Когда программа открывает объект IPC, как она может определить, был ли создан новый объект IPC или производится обращение к существующему объекту? 

ГЛАВА 3

System V IPC

3.1. Введение

Из имеющихся типов IPC следующие три могут быть отнесены к System V IPC, то есть к методам взаимодействия процессов, соответствующим стандарту System V:

■ очереди сообщений System V (глава 6);

■ семафоры System V (глава 11);

■ общая память System V (глава 14).

Термин «System V IPC» говорит о происхождении этих средств: впервые они появились в Unix System V. У них много общего: схожи функции, с помощью которых организуется доступ к объектам; также схожи формы хранения информации в ядре. В этой главе описываются общие для трех типов IPC черты.

Информация о функциях сведена в табл. 3.1.


Таблица 3.1. Функции System V IPC

Очереди сообщений Семафоры Общая память
Заголовочный файл <sys/msg.h> <sys/sem.h> <sys/shm.h>
Создание или открытие msgget semget shmget
Операции управления msgctl semctl shmctl
Операции IPC msgsnd msgrcv semop shmat shmdt

ПРИМЕЧАНИЕ

Информация об истории разработки и развития функций System V IPC не слишком легко доступна. [16] предоставляет следующую информацию: очереди сообщений, семафоры и разделяемая память этого типа были разработаны в конце 70-х в одном из филиалов Bell Laboratories в городе Колумбус, штат Огайо, для одной из версий Unix, предназначенной для внутреннего использования. Версия эта называлась Columbus Unix, или CB Unix. Она использовалась в так называемых системах поддержки операций — системах обработки транзакций — для автоматизации управления и ведения записей в телефонной компании. System V IPC были добавлены в коммерческую версию Unix System V. приблизительно в 1983 году.

3.2. Ключи типа key_t и функция ftok

В табл. 1.2 было отмечено, что в именах трех типов System V IPC использовались значения key_t. Заголовочный файл <sys/types.h> определяет тип key_t как целое (по меньшей мере 32-разрядное). Значения переменным этого типа обычно присваиваются функцией ftok.

Функция ftok преобразовывает существующее полное имя и целочисленный идентификатор в значение типа key_t (называемое ключом IPC — IPC key):

#include <sys/ipc.h>

key_t ftok(const char *pathname, int id);

//Возвращает ключ IPC либо –1 при возникновении ошибки

На самом деле функция использует полное имя файла и младшие 8 бит идентификатора для формирования целочисленного ключа IPC.

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

Большинство реализаций ftok вызывают функцию stat, а затем объединяют:

■ информацию о файловой системе, к которой относится полное имя pathname (поле st_dev структуры stat);

■ номер узла (i-node) в файловой системе (поле st_ino структуры stat);

■ младшие 8 бит идентификатора (который не должен равняться нулю).

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

ПРИМЕЧАНИЕ

Номер узла всегда отличен от нуля, поэтому большинство реализаций определяют константу IPC_PRIVATE (раздел 3.4) равной нулю.

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

Пример

Программа в листинге 3.1 принимает полное имя в качестве аргумента командной строки, вызывает функции stat и ftok, затем выводит значения полей st_dev и st_ino структуры stat и получающийся ключ IPC. Эти три значения выводятся в шестнадцатеричном формате, поэтому легко видеть, как именно ключ IPC формируется из этих двух значений и идентификатора 0x57.

Листинг 3.1[1]. Получение и вывод информации о файле и созданного ключа IPC

//svipc/ftok.c

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   struct stat stat;

6   if (argc != 2)

7    err_quit("usage: ftok <pathname>");

8   Stat(argv[1], &stat);

9   printf("st_dev: &lx, st_ino: %Ix, key: %x\n",

10   (u_long) stat.st_dev, (u_long) stat.st_ino,

11   Ftok(argv[1], 0x57));

12  exit(0);

13 }

Выполнение этой программы в системе Solaris 2.6 приведет к следующим результатам:

solaris %ftok /etc/system

st_dev: 800018, st_ino: 4a1b, key: 57018a1b

solaris %ftok /usr/tmp

st_dev: 800015, st_ino: 10b78, key: 57015b78

solaris %ftok /home/rstevens/Mail.out

st_dev: 80001f, st_ino: 3b03, key: 5702fb03

Очевидно, идентификатор определяет старшие 8 бит ключа; младшие 12 бит st_dev определяют следующие 12 бит ключа, и наконец, младшие 12 бит st_ino определяют младшие 12 бит ключа.

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

ПРИМЕЧАНИЕ

В FreeBSD используются младшие 8 бит идентификатора, младшие 8 бит st_dev и младшие 16 бит st_ino.

Учтите, что отображение, производимое функцией ftok, — одностороннее, поскольку часть бит st_dev и st_ino не используются. По данному ключу нельзя определить полное имя файла, заданное для вычислений.

3.3. Структура ipc_perm

Для каждого объекта IPC, как для обычного файла, в ядре хранится набор информации, объединенной в структуру.

struct ipc_perm {

 uid_t uid; /*идентификатор пользователя владельца*/

 gid_t gid; /*идентификатор группы владельца */

 uid_t cuid; /*идентификатор пользователя создателя*/

 gid_t cgid; /*идентификатор группы создателя*/

 mode_t mode; /*разрешения чтения-записи*/

 ulong_t seq; /*последовательный номер канала*/

 key_t key; /* ключ IPC */

}

Эта структура вместе с другими переименованными константами для функций System V IPC определена в файле <sys/ipc.h>. В этой главе мы расскажем о полях структуры ipc_perm более подробно.


3.4. Создание и открытие каналов IPC

Три функции getXXX, используемые для создания или открытия объектов IPC (табл. 3.1), принимают ключ IPC (типа key_t) в качестве одного из аргументов и возвращают целочисленный идентификатор. Этот идентификатор отличается от того, который передавался функции ftok, как мы вскоре увидим. У приложения есть две возможности задания ключа (первого аргумента функций getXXX):

1. Вызвать ftok, передать ей полное имя и идентификатор.

2. Указать в качестве ключа константу IPCPRIVATE, гарантирующую создание нового уникального объекта IPC.

Последовательность действий иллюстрирует рис. 3.1. 

UNIX: взаимодействие процессов

Рис. 3.1. Вычисление идентификаторов IPC по ключам


Все три функции getXXX (табл. 3.1) принимают в качестве второго аргумента набор флагов oflag, задающий биты разрешений чтения-записи (поле mode структуры ipc_perm) для объекта IPC и определяющий, создается ли новый объект IPC или производится обращение к уже существующему. Для этого имеются следующие правила.

■ Ключ IPC_PRIVATE гарантирует создание уникального объекта IPC. Никакие возможные комбинации полного имени и идентификатора не могут привести к тому, что функция ftok вернет в качестве ключа значение IPC_PRIVATE.

■ Установка бита IPC_CREAT аргумента oflag приводит к созданию новой записи для указанного ключа, если она еще не существует. Если же обнаруживается существующая запись, возвращается ее идентификатор. 

Одновременная установка битов IPC_CREAT и IPC_EXCL аргумента oflag приводит к созданию новой записи для указанного ключа только в том случае, если такая запись еще не существует. Если же обнаруживается существующая запись, функция возвращает ошибку EEXIST (объект IPC уже существует).

Комбинация IPC_CREAT и IPC_EXCL в отношении объектов IPC действует аналогично комбинации O_CREAT и O_EXCL для функции open.

Установка только бита IPC_EXCL без IPC_CREAT никакого эффекта не дает.

Логическая диаграмма последовательности действий при открытии объекта IPC изображена на рис. 3.2. В табл. 3.2 показан альтернативный взгляд на этот процесс.

UNIX: взаимодействие процессов

Рис. 3.2. Диаграмма открытия объекта IPC


Обратите внимание, что в средней строке табл. 3.2 для флага IPC_CREAT без IPC_EXCL мы не получаем никакой информации о том, был ли создан новый объект или получен доступ к существующему. Для большинства приложений характерно создание сервером объекта IPC с указанием IPC_CREAT (если безразлично, существует ли уже объект) или IPC_CREAT | IPC_EXCL (если требуется проверка существования объекта). Клиент вообще не указывает флагов, предполагая, что сервер уже создал объект.

ПРИМЕЧАНИЕ

Функции System V IPC в отличие от функций Posix IPC определяют свои собственные константы IРС_ххх вместо использования O_CREAT и OEXCL, принимаемых стандартной функцией open (табл. 2.2).

Обратите также внимание на то, что функции System V IPC совмещают константы IРС_ххх с битами разрешений (описанными в следующем разделе) в едином аргументе oflag, тогда как для функции open и для Posix IPC характерно наличие двух аргументов: oflag, в котором задаются флаги вида О_ххх, и mode, определяющего биты разрешений доступа.


Таблица 3.2. Логика создания и открытия объектов IPC

Аргумент oflag Ключ не существует Ключ существует
Специальные флаги не установлены Ошибка, errno=ENOENT OK, открытие существующего объекта
IPC_CREAT OK, создается новая запись OK, открытие существующего
IPC CREAT|IPC_EXCL OK, создается новая запись Ошибка, errno=EEXIST

3.5. Разрешения IPC

При создании нового объекта IPC с помощью одной из функций getXXX, вызванной с флагом IPC_CREAT, в структуру ipc_perm заносится следующая информация (раздел 3.3):

1. Часть битов аргумента oflag задают значение поля mode структуры ipc_perm. В табл. 3.3 приведены биты разрешений для трех типов IPC (запись >>3 означает сдвиг вправо на три бита).

2. Поля cuid и cgid получают значения, равные действующим идентификаторам пользователя и группы вызывающего процесса. Эти два поля называются идентификаторами создателя.

3. Поля uid и gid структуры iрс_perm также устанавливаются равными действующим идентификаторам вызывающего процесса. Эти два поля называются идентификаторами владельца.


Таблица 3.3. Значения mode для разрешений чтения-записи IPC 

Число (восьмеричное) Очередь сообщений Семафор Разделяемая память Описание
0400 MSG_R SEM_R SHM_R Пользователь — чтение
0200 MSG_W SEM_A SHM_W Пользователь — запись
0040 MSG R>>3 SEM_R>>3 SHM_R>>3 Группа — чтение
0020 MSG_W>>3 SEM_A>>3 SHM_W>>3 Группа — запись
0004 MSG_R>>6 SEM_R>>6 SHM_R>>6 Прочие — чтение
0002 MSG_W>>6 SEM_A>>6 SHM_W>>6 Прочие — запись

Идентификатор создателя изменяться не может, тогда как идентификатор владельца может быть изменен процессом с помощью вызова функции ctlXXX для данного механизма IPC с командой IPC_SET. Три функции ctlXXX позволяют процессу изменять биты разрешений доступа (поле mode) объекта IPC.

ПРИМЕЧАНИЕ

В большинстве реализаций определены шесть констант: MSG_R, MSG_W, SEM_R, SEM_A, SHM_R и SHM_W, показанные в табл. 3.3. Константы эти определяются в заголовочных файлах <sys/msg.h>, <sys/sem.h> и <sys/shm.h>. Однако стандарт Unix 98 не требует их наличия. Суффикс А в SEM_A означает «alter» (изменение).

Тройка функций getXXX не используют стандартную маску создания файла Unix. Разрешения очереди сообщений, семафора и разделяемой памяти устанавливаются в точности равными аргументу функции.

Posix IPC не дает создателю IPC возможности изменить владельца объекта. В Posix нет аналогов команды IPC_SET. Однако в Posix IPC имя объекта принадлежит файловой системе, и потому владелец может быть изменен привилегированным пользователем с помощью команды chown.

Когда какой-либо процесс предпринимает попытку доступа к объекту IPC, производится двухэтапная проверка: первый раз при открытии файла (функция getXXX) и затем каждый раз при обращении к объекту IPC:

1. При установке доступа к существующему объекту IPC с помощью одной из функций getXXX производится первичная проверка аргумента oflag, вызывающего функцию процесса. Аргумент не должен указывать биты доступа, не установленные в поле mode структуры ipc_perm (нижний квадрат на рис. 3.2). Например, процесс-сервер может установить значение члена mode для своей очереди входящих сообщений, сбросив биты чтения для группы и прочих пользователей. Любой процесс, попытавшийся указать эти биты в аргументе oflag функции msgget, получит ошибку. Надо отметить, что от этой проверки, производимой функциями getXXX, мало пользы. Она подразумевает наличие у вызывающего процесса информации о том, к какой группе пользователей он принадлежит: он может являться владельцем файла, может принадлежать к той же группе или к прочим пользователям. Если создающий процесс сбросит некоторые биты разрешений, а вызывающий процесс попытается их установить, функция getXXX вернет ошибку. Любой процесс может полностью пропустить эту проверку, указав аргумент oflag, равный 0, если заранее известно о существовании объекта IPC.

2. При любой операции с объектами IPC производится проверка разрешений для процесса, эту операцию запрашивающего. Например, каждый раз когда процесс пытается поместить сообщение в очередь с помощью команды msgsnd, производятся нижеследующие проверки (при получении доступа последующие этапы пропускаются).

 □ Привилегированному пользователю доступ предоставляется всегда.

 □ Если действующий идентификатор пользователя совпадает со значением uid или cuid объекта IPC и установлен соответствующий бит разрешения доступа в поле mode объекта IPC, доступ будет разрешен. Под соответствующим битом разрешения доступа подразумевается бит, разрешающий чтение, если вызывающий процесс запрашивает операцию чтения для данного объекта IPC (например, получение сообщения из очереди), или бит, разрешающий запись, если процесс хочет осуществить ее.

 □ Если действующий идентификатор группы совпадает со значением gid или cgid объекта IPC и установлен соответствующий бит разрешения доступа в поле mode объекта IPC, доступ будет разрешен.

 □ Если доступ не был разрешен на предыдущих этапах, проверяется наличие соответствующих установленных битов доступа для прочих пользователей.

3.6. Повторное использование идентификаторов

Структура ipc_perm (раздел 3.3) содержит переменную seq, в которой хранится порядковый номер канала. Эта переменная представляет собой счетчик, заводимый ядром для каждого объекта IPC в системе. При удалении объекта IPC номер канала увеличивается, а при переполнении сбрасывается в ноль.

ПРИМЕЧАНИЕ

В этом разделе мы описываем характерную для SVR4 реализацию. Стандарт Unix 98 не исключает использование других вариантов.

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

Идентификатор IPC возвращается одной из функций getXXX: msgget, semget, shmget. Как и дескрипторы файлов, идентификаторы представляют собой целые числа, имеющие в отличие от дескрипторов одинаковое значение для всех процессов. Если два неродственных процесса (клиент и сервер) используют одну очередь сообщений, ее идентификатор, возвращаемый функцией msgget, должен иметь одно и то же целочисленное значение в обоих процессах, чтобы они получили доступ к одной и той же очереди. Такая особенность дает возможность какому-либо процессу, созданному злоумышленником, попытаться прочесть сообщение из очереди, созданной другим приложением, последовательно перебирая различные идентификаторы (если бы они представляли собой небольшие целые числа) и надеясь на существование открытой в текущий момент очереди, доступной для чтения всем. Если бы идентификаторы представляли собой небольшие целые числа (как дескрипторы файлов), вероятность найти правильный идентификатор составляла бы около 1/50 (предполагая ограничение в 50 дескрипторов на процесс).

Для исключения такой возможности разработчики средств IPC решили расширить возможный диапазон значений идентификатора так, чтобы он включал вообще все целые числа, а не только небольшие. Расширение диапазона обеспечивается путем увеличения значения идентификатора, возвращаемого вызывающему процессу, на количество записей в системной таблице IPC каждый раз, когда происходит повторное использование одной из них. Например, если система настроена на использование не более 50 очередей сообщений, при первом использовании первой записи процессу будет возвращен идентификатор 0. После удаления этой очереди сообщений при попытке повторного использования первой записи в таблице процессу будет возвращен идентификатор 50. Далее он будет принимать значения 100, 150 и т. д. Поскольку seq обычно определяется как длинное целое без знака (ulong — см. структуру ipc_perm в разделе 3.3), возврат к уже использовавшимся идентификаторам происходит, когда запись в таблице будет использована 85899346 раз (2³²/50 в предположении, что целое является 32-разрядным).

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

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

Листинг 3.2. Вывод идентификатора очереди сообщений десять раз подряд

//svmsg/slot.c

1  #include <unpipc.h>

2  int

3  main(int argc, char **argv)

4  {

5   int i, msqid;

6   for (i=0;i<10;i++) {

7    msqid=Msgget(IPC_PRIVATE, SVMSG_MODE|IPC_CREAT);

8    printf("msqid = %d\n", msqid);

9    Msgctl(msqid, IPC_RMID, NULL);

10  }

11  exit(0);

12 }

При очередном прохождении цикла msgget создает очередь сообщений, a msgctl с командой IPC_RMID в качестве аргумента удаляет ее. Константа SVMSG_MODE определяется в нашем заголовочном файле unpipc.h (листинг В.1) и задает разрешения по умолчанию для очереди сообщений System V. Вывод программы будет иметь следующий вид:

solaris %slot

msqid = 0

msqid = 50

msqid = 100

msqid = 150

msqid = 200

msqid = 250

msqid = 300

msqid = 350

msqid = 400

msqid = 450

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

solaris % slot

msqid = 500

msqid = 550

msqid = 600

msqid = 650

msqid = 700

msqid = 750

msqid = 800

msqid = 850

msqid = 900

msqid = 950

3.7. Программы ipcs и ipcrm

Поскольку объектам System V IPC не сопоставляются имена в файловой системе, мы не можем просмотреть их список или удалить их, используя стандартные программы ls и rm. Вместо них в системах, поддерживающих эти типы IPC, предоставляются две специальные программы: ipcs, выводящая различную информацию о свойствах System V IPC, и ipcrm, удаляющая очередь сообщений System V, семафор или сегмент разделяемой памяти. Первая из этих функций поддерживает около десятка параметров командной строки, управляющих отображением информации о различных типах IPC. Второй (ipcrm) можно задать до шести параметров. Подробную информацию о них можно получить в справочной системе.

ПРИМЕЧАНИЕ

Поскольку System V IPC не стандартизуется Posix, эти команды не входят в Posix.2. Они описаны в стандарте Unix 98.

3.8. Ограничения ядра

Большинству реализаций System V IPC свойственно наличие внутренних ограничений, налагаемых ядром. Это, например, максимальное количество очередей сообщений или ограничение на максимальное количество семафоров в наборе. Характерные значения этих ограничений приведены в табл. 6.2, 11.1 и 14.1. Большая часть ограничений унаследована от исходной реализации System V. 

ПРИМЕЧАНИЕ

Раздел 11.2 книги [1] и глава 8 [6] описывают реализацию очередей сообщений, семафоров и разделяемой памяти в System V. Некоторые из этих ограничений описаны уже там.

К сожалению, некоторые из накладываемых ограничений достаточно жестки, поскольку они унаследованы от исходной реализации, базировавшейся на системе с небольшим адресным пространством (16-разрядный PDP-11). К счастью, большинство систем позволяют администратору изменять некоторые из установленных по умолчанию ограничений, но необходимые для этого действия специфичны для каждой версии Unix. В большинстве случаев после внесения изменений требуется перезагрузка ядра. К сожалению, в некоторых реализациях для хранения некоторых параметров до сих пор используются 16-разрядные целые, а это уже устанавливает аппаратные ограничения.

В Solaris 2.6, например, таких ограничений 20. Их текущие значения можно вывести на экран, используя команду sysdef. Учтите, что вместо реальных значений будут выведены нули, если соответствующий модуль ядра не загружен (то есть средство не было ранее использовано). Это можно исключить, добавив к файлу /etc/system любой из нижеследующих операторов. Файл /etc/system считывается в процессе перезагрузки системы:

set msgsys:msginfo_msgseg = значение

set msgsys:msginfo_msgssz = значение

set msgsys:msginfo_msgtql = значение

set msgsys:msginfo_msgmap = значение

set msgsys:msginfo_msgmax = значение

set msgsys:msginfo_msgmnb = значение

set msgsys:msginfo_msgmni = значение


set semsys:seminfo_semopm = значение

set semsys:seminfo_semume = значение

set semsys:seminfo_semaem = значение

set semsys:seminfo_semmap = значение

set semsys:seminfo_semvmx = значение

set semsys:seminfo_semmsl = значение

set semsys:seminfo_semmni = значение

set semsys:seminfo_semmns = значение

set semsys:seminfo_semmnu = значение


set shmsys:shminfo_shmmin = значение

set shmsys:shminfo_shmseg = значение

set shmsys:shminfo_shmmax = значение

set shmsys:shminfo_shmmni = значение

Последние шесть символов имени слева от знака равенства представляют собой переменные, перечисленные в табл. 6.2, 11.1 и 14.1.

В Digital Unix 4.0B программа sysconfig позволяет узнать или изменить множество параметров и ограничений ядра. Ниже приведен вывод этой команды, запущенной с параметром –q. Команда выводит текущие ограничения для подсистемы ipc. Некоторые строки в выводе, не имеющие отношения к средствам IPC System V, были опущены:

alpha % /sbin/sysconfig –q ipc

ipc:

msg-max = 8192

msg-mnb = 16384

msg-mni = 64

msg-tql = 40


shm-max = 4194304

shm-min = 1

shm-mni = 128

shm-seg = 32


sem-mni = 16

sem-msl = 25

sem-opm = 10

sem-ume = 10

sem-vmx = 32767

sem-aem = 16384

num-of-sems = 60

Для этих параметров можно указать другие значения по умолчанию, изменив файл /etc/sysconfigtab. Делать это следует с помощью программы sysconfigdb. Этот файл также считывается в процессе начальной загрузки системы.

3.9. Резюме

Первым аргументом функций msgget, semget и shmget является ключ IPC System V. Эти ключи вычисляются по полному имени файла с помощью системной функции ftok. В качестве ключа можно также указать значение IPCPRIVATE. Эти три функции создают новый объект IPC или открывают существующий и возвращают идентификатор System V IPC — целое число, которое потом используется для распознавания объекта в прочих функциях, имеющих отношение к IPC. Эти идентификаторы имеют смысл не только в рамках одного процесса (как дескрипторы файлов), но и в рамках всей системы. Они могут повторно использоваться ядром, но лишь спустя некоторое время.

С каждым объектом System V IPC связана структура ipc_perm, содержащая информацию о нем, такую как идентификатор пользователя владельца, идентификатор группы, разрешения чтения и записи и др. Одним из отличий между System V и Posix IPC является то, что для объекта IPC System V эта информация доступна всегда (доступ к ней можно получить с помощью одной из функций XXXctl с аргументом IPC_STAT), а в Posix IPC доступ к ней зависит от реализации. Если объект Posix IPC хранится в файловой системе и мы знаем его имя в ней, мы можем получить доступ к этой информации, используя стандартные средства файловой системы.

При создании нового или открытии существующего объекта System V IPC функции getXXX передаются два флага (IPC_CREAT и IPC_EXCL) и 9 бит разрешений.

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

Упражнения

1. Прочитайте о функции msgctl в разделе 6.5 и измените программу в листинге 3.2 так, чтобы выводился не только идентификатор, но и поле seq структуры ipc_perm.

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

3. В разделе 3.5 было отмечено, что функции getXXX System V IPC не используют маску создания файла. Напишите тестовую программу, создающую канал FIFO (с помощью функции mkfifо, описанной в разделе 4.6) и очередь сообщений System V, указав для обоих разрешение 666 (в восьмеричном формате). Сравните разрешения для созданных объектов (FIFO и очереди сообщений). Перед запуском программы удостоверьтесь, что значение umask отлично от нуля.

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

5. Измените листинг 3.1 так, чтобы выводился только ключ IPC и путь к файлу. Запустите программу find, чтобы вывести список всех файлов вашей файловой системы, и передайте вывод вашей только что созданной программе. Скольким именам файлов будет соответствовать один и тот же ключ?

6. Если в вашей системе есть программа sar (system activity reporter — информация об активности системы), запустите команду

sar –m 5 6

На экран будет выведено количество операций в секунду с очередями сообщений и семафорами, замеряемыми каждые 5 секунд 6 раз. 

ЧАСТЬ 2

ОБМЕН СООБЩЕНИЯМИ

ГЛАВА 4

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

4.1. Введение

Неименованные каналы — это самая первая форма IPC в Unix, появившаяся еще в 1973 году в третьей версии (Third Edition [17]). Несмотря на полезность во многих случаях, главным недостатком неименованных каналов является отсутствие имени, вследствие чего они могут использоваться для взаимодействия только родственными процессами. Это было исправлено в Unix System III (1982) добавлением каналов FIFO, которые иногда называются именованными каналами. Доступ и к именованным каналам, и к неименованным организуется с помощью обычных функций read и write.

ПРИМЕЧАНИЕ

Программные (неименованные) каналы в принципе могут использоваться неродственными процессами, если предоставить им возможность передавать друг другу дескрипторы (см. раздел 15.8 этой книги или раздел 13.7 [24]). Однако на практике эти каналы обычно используются для осуществления взаимодействия между процессами, у которых есть общий предок.

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

4.2. Приложение типа клиент-сервер

Пример приложения модели клиент-сервер приведен на рис. 4.1. Именно на него мы будем ссылаться в тексте этой главы и главы 6 при необходимости проиллюстрировать использование программных каналов, FIFO и очередей сообщений System V.

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

UNIX: взаимодействие процессов

Рис. 4.1. Пример приложения типа клиент-сервер

4.3. Программные каналы

Программные каналы имеются во всех существующих реализациях и версиях Unix. Канал создается вызовом pipe и предоставляет возможность однонаправленной (односторонней) передачи данных:

#include <unistd.h>

int pipe(int fd[2]);

/* возвращает 0 в случае успешного завершения. –1 – в случае ошибки:*/

Функция возвращает два файловых дескриптора: fd[0] и fd[1], причем первый открыт для чтения, а второй — для записи.

ПРИМЕЧАНИЕ

Некоторые версии Unix, в частности SVR4, поддерживают двусторонние каналы (full-duplex pipes). В этом случае канал открыт на запись и чтение с обоих концов. Другой способ создания двустороннего канала IPC заключается в вызове функции socketpair, описанной в разделе 14.3 [24]. Его можно использовать в большинстве современных версий Unix. Однако чаще всего каналы используются при работе с интерпретатором команд, где уместно использование именно односторонних каналов.

Стандарты Posix.1 и Unix 98 требуют только односторонних каналов, и мы будем исходить из этого.

Для определения типа дескриптора (файла, программного канала или FIFO) можно использовать макрос S_ISFIFO. Он принимает единственный аргумент: поле st_mode структуры stat и возвращает значение «истина» (ненулевое значение) или «ложь» (ноль). Структуру stat для канала возвращает функция fstat. Для FIFO структура возвращается функциями fstat, lstat и stat.

На рис. 4.2 изображен канал при использовании его единственным процессом.

UNIX: взаимодействие процессов

Рис. 4.2. Канал в одиночном процессе


Хотя канал создается одним процессом, он редко используется только этим процессом (пример канала в одиночном процессе приведен в листинге 5.12). Каналы обычно используются для связи между двумя процессами (родительским и дочерним) следующим образом: процесс создает канал, а затем вызывает fork, создавая свою копию — дочерний процесс (рис. 4.3). Затем родительский процесс закрывает открытый для чтения конец канала, а дочерний, в свою очередь, — открытый на запись конец канала. Это обеспечивает одностороннюю передачу данных между процессами, как показано на рис. 4.4.

UNIX: взаимодействие процессов

Рис. 4.3. Канал после вызова fork

UNIX: взаимодействие процессов

Рис. 4.4. Канал между двумя процессами 


При вводе команды наподобие

who|sort|lp

в интерпретаторе команд Unix интерпретатор выполняет вышеописанные действия для создания трех процессов с двумя каналами между ними. Интерпретатор также подключает открытый для чтения конец каждого канала к стандартному потоку ввода, а открытый на запись — к стандартному потоку вывода. Созданный таким образом канал изображен на рис. 4.5.

UNIX: взаимодействие процессов

Рис. 4.5. Каналы между тремя процессами при конвейерной обработке 


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

1. Создаются каналы 1 (fd1[0] и fd1[1]) и 2 (fd2[0] и fd2[1]).

2. Вызов fork.

3. Родительский процесс закрывает доступный для чтения конец канала 1 (fd1[0]).

4. Родительский процесс закрывает доступный для записи конец канала 2 (fd2[1]).

5. Дочерний процесс закрывает доступный для записи конец канала 1 (fd1[1]).

6. Дочерний процесс закрывает доступный для чтения конец канала 2 (fd2[0]).

Текст программы, выполняющей эти действия, приведен в листинге 4.1. При этом создается структура каналов, изображенная на рис. 4.6.

UNIX: взаимодействие процессов

Рис. 4.6. Двусторонняя передача данных по двум каналам

Пример

Давайте напишем программу, описанную в разделе 4.2, с использованием каналов. Функция main создает два канала и вызывает fork для создания копии процесса. Родительский процесс становится клиентом, а дочерний — сервером. Первый канал используется для передачи полного имени от клиента серверу, а второй — для передачи содержимого файла (или сообщения об ошибке) от сервера клиенту. Таким образом мы получаем структуру, изображенную на рис. 4.7. 

UNIX: взаимодействие процессов

Рис. 4.7. Реализация рис. 4.1 с использованием двух каналов


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

В листинге 4.1[1] приведена функция main для данного примера.

Листинг 4.1. Функция main для приложения клиент-сервер, использующего два канала

//pipe/mainpipe.c

1  #include "unpipc.h"

2  void client(int, int), server(int, int);

3  int

4  main(int argc, char **argv)

5  {

6   int pipe1[2], pipe2[2]:

7   pid_t childpid;

8   Pipe(pipe1); /* создание двух каналов */

9   Pipe(pipe2);

10  if ((childpid = Fork()) == 0) { /* child */

11   Close(pipe1[1]);

12   Close(pipe2[0]);

13   server(pipe1[0], pipe2[1]);

14   exit(0);

15  }

16  /* родитель */

17  Close(pipel[0]);

18  Close(pipe2[1]);

19  client(pipe2[0], pipel[1]);

20  Waitpid(childpid, NULL, 0); /* ожидание завершения дочернего процесса */

21  exit(0);

22 }

Создание каналов, вызов fork

8-19 Создаются два канала и выполняются шесть шагов, уже упоминавшиеся в отношении рис. 4.6. Родительский процесс вызывает функцию client (листинг 4.2), а дочерний — функцию server (листинг 4.3).

Использование waitpid дочерним процессом

20 Процесс-сервер (дочерний процесс) завершает свою работу первым, вызывая функцию exit после завершения записи данных в канал. После этого он становится процессом-зомби. Процессом-зомби называется дочерний процесс, завершивший свою работу, родитель которого еще функционирует, но не получил сигнал о завершении работы дочернего процесса. При завершении работы дочернего процесса ядро посылает его родителю сигнал SIGCHLD, но родитель его не принимает и этот сигнал по умолчанию игнорируется. После этого функция client родительского процесса возвращает управление функции main, закончив Считывание данных из канала. Затем родительский процесс вызывает waitpid для получения информации о статусе дочернего процесса (зомби). Если родительский процесс не вызовет waitpid, а просто завершит работу, клиент будет унаследован процессом init, которому будет послан еще один сигнал SIGCHLD.

Функция client приведена в листинге 4.2.

Листинг 4.2. Функция client для приложения типа клиент-сервер с двумя каналами

//pipe/client.с

1  #include "unpipc.h"

2  void

3  client(int readfd, int writefd)

4  {

5   size_t len;

6   ssize_t n;

7   char buff[MAXLINE];

8   /* получение полного имени файла */

9   Fgets(buff, MAXLINE, stdin);

10  len = strlen(buff); /* fgets() гарантирует завершающий нулевой байт */

11  if (buff[Len-l] == ' \n' )

12   len--; /* удаление перевода строки из fgets() */

13  /* запись полного имени в канал IPC */

14  Write(writefd, buff, len);

15  /* считывание из канала, вывод в stdout */

16  while ((n = Read(readfd, buff, MAXLINE)) > 0)

17   Write(STDOUT_FILENO, buff, n);

18 }

Считывание полного имени из стандартного потока ввода

8-14 Полное имя файла считывается из стандартного потока ввода и записывается в канал после удаления завершающего символа перевода строки, возвращаемого функцией fgets.

Копирование из канала в стандартный поток вывода

15-17 Затем клиент считывает все, что сервер направляет в канал, и записывает эти данные в стандартный поток вывода. Ожидается, что это будет содержимое файла, но в случае его отсутствия будет принято и записано в стандартный поток вывода сообщение об ошибке.

В листинге 4.3 приведена функция server.

Листинг 4.3. Функция server для приложения клиент-сервер с двумя каналами

//pipe/server.c

1  #include "unpipc.h"

2  void

3  server(int readfd, int writefd)

4  {

5   int fd;

6   ssize_t n;

7   char buff[MAXLINE+1];

8   /* получение полного имени из канала IPC */

9   if ((n = Read(readfd, buff, MAXLINE)) == 0)

10   err_quit("end-of-file while reading pathname"):

11  buff[n] = '\0'; /* полное имя завершается 0 */

12  if ((fd = open(buff, O_RDONLY)) < 0) {

13   /* 4error: must tell client */

14   snprintf(buff + n, sizeof(buff) – n, ": can't open. %s\n".

15   strerror(errno)):

16   n = strlen(buff);

17   Write(writefd, buff, n);

18  } else {

19   /* файл успешно открыт и копируется в канал */

20   while ( (n = Read(fd, buff, MAXLINE)) > 0)

21    Write(writefd, buff, n);

22   Close(fd);

23  }

24 }

Считывание полного имени файла из канала

8-11 Записанное в канал клиентом имя файла считывается сервером и дополняется завершающим символом с кодом 0 (null-terminated). Обратите внимание, что функция read возвращает данные, как только они помещаются в поток, не ожидая накопления некоторого их количества (MAXLINE в данном примере).

Открытие файла, обработка возможной ошибки

12-17 Файл открывается для чтения и при возникновении ошибки сообщение о ней возвращается клиенту с помощью канала. Для получения строки с соответствующим значению переменной errno сообщением об ошибке вызывается функция strerror (в книге [24, с. 690-691] вы найдете более подробный рассказ об этой функции).

Копирование из файла в канал

18-23 При успешном завершении работы функции open содержимое файла копируется в канал.

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

solaris % mainpipe /etc/inet/ntp.conf файл, состоящий из двух строк

multicastclient 224.0.1.1

driftfile /etc/inet/ntp.drift

solaris % mainpipe /etc/shadow          фaйл, на чтение которого нет разрешения

/etc/shadow: can't open. Permission denied

solaris % mainpipe /no/such/file        несуществующий файл

/no/such/file: can't open. No such file or directory

4.4. Двусторонние каналы

В предыдущем разделе мы отметили, что во многих системах реализованы двусторонние каналы. В Unix SVR4 это обеспечивается самой функцией pipe, а во многих других ядрах — функцией socketpair. Но что в действительности представляет собой двусторонний канал? Представим себе сначала однонаправленный канал, изображенный на рис. 4.8.

UNIX: взаимодействие процессов

Рис. 4.8. Односторонний канал


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

UNIX: взаимодействие процессов

Рис. 4.9. Одна из возможных реализаций двустороннего канала (неправильная)


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

На рис. 4.10 изображена правильная реализация двустороннего канала.

UNIX: взаимодействие процессов

Рис. 4.10. Правильная реализация двустороннего канала


Здесь двусторонний канал получается из объединения двух односторонних. Все данные, записываемые в fd[1], будут доступны для чтения из fd[0], а данные, записываемые в fd[0], будут доступны для чтения из fd[1].

Программа в листинге 4.4 иллюстрирует использование одного двустороннего канала для двусторонней передачи информации.

Листинг 4.4. Двусторонняя связь через двусторонний канал

//pipe/fduplex.c

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   int fd[2], n;

6   char c;

7   pid_t childpid;

8   Pipe(fd); /* предполагается двусторонний канал (напр., SVR4) */

9   if ((childpid = Fork()) == 0) { /* child */

10   sleep(3):

11   if ((n = Read(fd[0], &c, 1)) != 1)

12    err_quit("child: read returned %d", n);

13   printf("child read %c\n", c):

14   Write(fd[0], "c", 1);

15   exit(0);

16  }

17  /* родитель */

18  Write(fd[1], "p", 1);

19  if ((n = Read(fd[1], &c, 1)) != 1)

20   err_quit("parent: read returned %d", n):

21  printf("parent read %c\n", c);

22  exit(0);

23 }

В этой программе сначала создается двусторонний канал, затем делается системный вызов fork. Породивший процесс записывает символ р в канал, а затем считывает из канала данные. Дочерний процесс ждет три секунды, считывает символ из канала, а потом записывает туда символ с. Задержка чтения для дочернего процесса позволяет породившему процессу вызвать read первым — таким образом мы можем узнать, не будет ли им считан обратно только что записанный символ.

При запуске этой программы в Solaris 2.6, в которой организована поддержка двусторонних каналов, мы получим ожидаемый результат:

solaris % fduplex

child read p

parent read с

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

При запуске этой программы в Digital Unix 4.0B, в которой по умолчанию создаются односторонние каналы (двусторонние каналы — как в SVR4 — будут создаваться в том случае, если при компиляции указать специальные параметры), мы увидим результат, ожидаемый для одностороннего канала:

alpha % fduplex

read error: Bad file number

alpha % child read p

write error: Bad file number

Родительский процесс записывает символ р, который успешно считывается дочерним процессом, однако при попытке считывания из канала (дескриптор fd[l]) родительский процесс прерывается с ошибкой, как и дочерний процесс, при попытке записи в канал (дескриптор fd[0]). Вспомните рис. 4.8. Функция read возвращает код ошибки EBADF, означающий, что дескриптор не открыт для чтения. Аналогично write возвращает тот же код ошибки, если дескриптор не был открыт на запись.

4.5. Функции popen и pclose

Другим примером использования каналов является имеющаяся в стандартной библиотеке ввода-вывода функция popen, которая создает канал и запускает другой процесс, записывающий данные в этот канал или считывающий их из него:

#include <stdio.h>

FILE *popen(const char *соmmаnd, const char *tуре);

/* Возвращает указатель FILE * в случае успешного выполнения, NULL – в случае ошибки */

int pclose(FILE *strеаm);

/* Возвращает код завершения команды интерпретатора или –1 – в случае ошибки */

Аргумент command представляет собой команду интерпретатора. Он обрабатывается программой sh (обычно это интерпретатор Bourne shell), поэтому для поиска исполняемого файла, вызываемого командой command, используется переменная PATH. Канал создается между вызывающим процессом и указанной командой. Возвращаемое функцией popen значение представляет собой обычный указатель на тип FILE, который может использоваться для ввода или для вывода в зависимости от содержимого строки type:

■ если type имеет значение r, вызывающий процесс считывает данные, направляемые командой command в стандартный поток вывода;

■ если type имеет значение w, вызывающий процесс записывает данные в стандартный поток ввода команды command.

Функция pclose закрывает стандартный поток ввода-вывода stream, созданный командой popen, ждет завершения работы программы и возвращает код завершения, принимаемый от интерпретатора.

ПРИМЕЧАНИЕ

Информацию о реализациях popen и pclose можно найти в разделе 14.3 [21].

Пример

В листинге 4.5 изображено еще одно решение задачи с клиентом и сервером, использующее функцию popen и программу (утилиту Unix) cat.

Листинг 4.5. Клиент-сервер с использованием popen

//pipe/mainpopen.c

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   size_t n;

6   char buff[MAXLINE], command[MAXLINE];

7   FILE *fp;

8   /* считывание полного имени */

9   Fgets(buff, MAXLINE, stdin);

10  n = strlen(buff); /* fgets() гарантирует завершающий ноль */

11  if (buff[n-1] == '\n')

12   n--; /* удаление перевода строки из возврата fgets() */

13  snprintf(command, sizeof(command), "cat %s", buff);

14  fp = Popen(command, "r");

15  /* копирование из канала в стандартный вывод */

16  while(Fgets(buff, MAXLINE, fp) != NULL)

17   Fputs(buff, stdout);

18  Pclose(fp);

19  exit(0);

20 }

8-17 Полное имя файла считывается из стандартного потока ввода, как и в программе в листинге 4.2. Формируется командная строка, которая передается popen. Вывод интерпретатора команд или команды cat копируется в стандартный поток вывода.

Одним из отличий этой реализации от приведенной в листинге 4.1 является отсутствие возможности формировать собственные сообщения об ошибках. Теперь мы целиком зависим от программы cat, а выводимые ею сообщения не всегда адекватны. Например, в системе Solaris 2.6 при попытке считать данные из файла, доступ на чтение к которому для нас запрещен, будет выведена следующая ошибка:

solaris % cat/etc/shadow

cat: cannot open /etc/shadow 

А в BSD/OS 3.1 мы получим более информативное сообщение в аналогичной ситуации:

bsdi % cat /etc/master.passwd

cat: /etc/master.passwd: cannot open [Permission denied]

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

4.6. Именованные каналы (FIFO)

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

Аббревиатура FIFO расшифровывается как «first in, first out» — «первым вошел, первым вышел», то есть эти каналы работают как очереди. Именованные каналы в Unix функционируют подобно неименованным — они позволяют передавать данные только в одну сторону. Однако в отличие от программных каналов каждому каналу FIFO сопоставляется полное имя в файловой системе, что позволяет двум неродственным процессам обратиться к одному и тому же FIFO.

FIFO создается функцией mkfifо:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *раthnаme, mode_t mоdе);

/* Возвращает 0 при успешном выполнении, –1 – при возникновении ошибок */

Здесь pathname — обычное для Unix полное имя файла, которое и будет именем FIFO.

Аргумент mode указывает битовую маску разрешений доступа к файлу, аналогично второму аргументу команды open. В табл. 2.3 приведены шесть констант, определенных в заголовке <sys/stat.h>. Эти константы могут использоваться для задания разрешений доступа и к FIFO.

Функция mkfifo действует как open, вызванная с аргументом O_CREAT | O_EXCL. Это означает, что создается новый канал FIFO или возвращается ошибка EEXIST, в случае если канал с заданным полным именем уже существует. Если не требуется создавать новый канал, вызывайте open вместо mkfifo. Для открытия существующего канала или создания нового в том случае, если его еще не существует, вызовите mkfifo, проверьте, не возвращена ли ошибка EEXIST, и если такое случится, вызовите функцию open.

Команда mkfifо также создает канал FIFO. Ею можно пользоваться в сценариях интерпретатора или из командной строки.

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

При записи в программный канал или канал FIFO вызовом write данные всегда добавляются к уже имеющимся, а вызов read считывает данные, помещенные в программный канал или FIFO первыми. При вызове функции lseek для программного канала или FIFO будет возвращена ошибка ESPIPE.

Пример

Переделаем программу, приведенную в листинге 4.1, таким образом, чтобы использовать два канала FIFO вместо двух программных каналов. Функции client и server останутся прежними; отличия появятся только в функции main, новый текст которой приведен в листинге 4.6.

Листинг 4.6. Функция main приложения клиент-сервер, использующего две очереди

//pipe/mainfifo.c

1  #include "unpipc.h"

2  #define FIFO1 "/tmp/fifo.1"

3  #define FIFO2 "/tmp/fifo.2"

4  void client(int, int), server(int. int);

5  int

6  main(int argc, char **argv)

7  {

8   int readfd, writefd;

9   pid_t childpid;

10  /* создание двух FIFO, если существуют – OK */

11  if ((mkfifo(FIF01, FILE_MODE) < 0) && (errno != EEXIST))

12   err_sys("can't create %s", FIF01);

13  if ((mkfifo(FIF02, FILE_MODE) < 0) && (errno != EEXIST)) {

14   unlink(FIF01);

15   err_sys("can't create %s", FIF02);

16  }

17  if ((childpid = Fork()) == 0) { /* child */

18   readfd = Open(FIF01, O_RDONLY, 0);

19   writefd = Open(FIF02, O_WRONLY, 0);

20   server(readfd, writefd);

21   exit(0);

22  }

23  /* родительский процесс */

24  writefd = Open(FIF01, O_WRONLY, 0);

25  readfd = Open(FIF02, O_RDONLY, 0);

26  client(readfd, writefd);

27  waitpid(childpid, NULL, 0); /* ожидание завершения дочернего процесса */

28  Close(readfd):

29  Close(writefd);

30  Unlink(FIF01);

31  Unlink(FIF02);

32  exit(0);

33 }

Создание двух FIFO

10-16 В файловой системе в каталоге /tmp создается два канала. Если какой-либо из них уже существует — ничего страшного. Константа FILE_MODE определена в нашем заголовке unpiрс.h (листинг В.1) как

#define FILEMODE(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

/* разрешения по умолчанию для вновь создаваемых файлов */

При этом владельцу файла разрешается чтение и запись в него, а группе и прочим пользователям — только чтение. Эти биты разрешений накладываются на маску режима доступа создаваемых файлов (file mode creation mask) процесса.

17-27 Далее происходит вызов fork, дочерний процесс вызывает функцию server (листинг 4.3), а родительский процесс вызывает функцию client (листинг 4.2). Перед вызовом этих функций родительский процесс открывает первый канал на запись, а второй на чтение, в то время как дочерний процесс открывает первый канал на чтение, а второй — на запись. Картина аналогична примеру с каналами и иллюстрируется рис. 4.11. 

UNIX: взаимодействие процессов

Рис. 4.11. Приложение клиент-сервер, использующее две очереди


Изменения по сравнению с примером, в которым использовались программные каналы, следующие:

■ Для создания и открытия программного канала требуется только один вызов — pipe. Для создания и открытия FIFO требуется вызов mkfifo и последующий вызов open.

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

В программах, некорректно использующих каналы FIFO, могут возникать неочевидные проблемы. Рассмотрим, например, листинг 4.6: если поменять порядок двух вызовов функции open в породившем процессе, программа перестанет работать. Причина в том, что чтение из FIFO блокирует процесс, если канал еще не открыт на запись каким-либо другим процессом. Действительно, если мы меняем порядок вызовов open в породившем процессе, и породивший, и порожденный процессы открывают канал на чтение, притом что на запись он еще не открыт, так что оба процесса блокируются. Такая ситуация называется блокированием, или зависанием (deadlock). Она будет рассмотрена подробно в следующем разделе.

Пример: неродственные клиент и сервер

В листинге 4.6 клиент и сервер все еще являлись родственными процессами. Переделаем этот пример так, чтобы родство между ними отсутствовало. В листинге 4.7 приведен текст программы-сервера. Текст практически идентичен той части программы из листинга 4.6, которая относилась к серверу.

Содержимое заголовка fifо.h приведено в листинге 4.8. Этот файл определяет имена двух FIFO, которые должны быть известны как клиенту, так и серверу.

В листинге 4.9 приведен текст программы-клиента, которая не слишком отличается от части программы из листинга 4.6, относящейся к клиенту. Обратите внимание, что именно клиент, а не сервер удаляет канал FIFO по завершении работы, потому что последние операции с этим каналом выполняются им.

Листинг 4.7. Функция main независимого сервера

//pipe/server_main.c

1  #include "fifo.h"

2  void server(int, int);

3  int

4  main(int argc, char **argv)

5  {

6   int readfd, writefd;

7   /* создание двух FIFO. OK, если они существуют */

8   if ((mkfifo(FIF01, FILE_MODE) < 0) && (errno != EEXIST))

9    err_sys("can't create %s", FIF01);

10  if ((mkfifo(FIF02, FILE MODE) < 0) && (errno != EEXIST)) {

11   unlink(FIF01);

12   err_sys("can't create %s", FIF02);

13  }

14  readfd = Open(FIF01, O_RDONLY, 0);

15  writefd = Open(FIFO2, O_WRONLY, 0);

16  server(readfd, writefd);

17  exit(0);

18 } 

Листинг 4.8. Заголовочный файл fifo.h, используемый и клиентом, и сервером

//pipe/fifo.h

1 #include "unpipc.h"

2 #define FIFO1 "/tmp/fifo.1"

3 #define FIFO2 "/tmp/fifo.2"

Листинг 4.9. Функция main независимого клиента

//pipe/client_main.c

1  #include "fifo.h"

2  void client(int, int);

3  int

4  main(int argc, char **argv)

5  {

6   int readfd, writefd;

7   writefd = Open(FIFO1, O_WRONLY, 0);

8   readfd = Open(FIFO2, O_RDONLY, 0);

9   client(readfd, writefd);

10  Close(readfd);

11  Close(writefd);

12  Unlink(FIFO1);

13  UnLink(FIFO2);

14  exit(0);

15 }

ПРИМЕЧАНИЕ

Для программных каналов и каналов FIFO ядро ведет подсчет числа открытых дескрипторов, относящихся к ним, поэтому безразлично, кто именно вызовет unlink — клиент или сервер. Хотя эта функция и удаляет файл из файловой системы, она не влияет на открытые в момент ее выполнения дескрипторы. Однако для других форм IPC, таких как очереди сообщений стандарта System V, счетчик отсутствует, и если сервер удалит очередь после записи в нее последнего сообщения, она может быть удалена еще до того, как клиент это сообщение считает.

Для запуска клиента и сервера запустите сервер в фоновом режиме:

% server_fifo &

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

4.7. Некоторые свойства именованных и неименованных каналов

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

1. При вызове open указать флаг O_NONBLOCK. Например, первый вызов open в листинге 4.9 мог бы выглядеть так:

writefd = Open(FIFO1, O_WRONLY | O_NONBLOCK, 0);

2. Если дескриптор уже открыт, можно использовать fcntl для включения флага O_NONBLOCK. Этот прием нужно применять для программных каналов, поскольку для них не вызывается функция open и нет возможности указать флаг O_NONBLOCK при ее вызове. Используя fcntl, мы сначала получаем текущий статус файла с помощью F_GETFL, затем добавляем к нему с помощью побитового логического сложения (OR) флаг O_NONBLOCK и записываем новый статус с помощью команды F_SETFL:

int flags;

if ((flags = fcntl(fd, F_GETFL, 0)) < 0) err_sys("F_GETFL error");

flags |= O_NONBLOCK;

if (fcntl(fd, F_SETFL, flags) < 0) err_sys("F_SETFL error");

Будьте аккуратны с программами, которые просто устанавливают требуемый флаг, поскольку при этом сбрасываются все прочие флаги состояния:

/* Неправильное отключение блокировки */

if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) err_sys("F_SETFL error");

Таблица 4.1 иллюстрирует действие флага, отключающего блокировку, при открытии очереди и при чтении данных из пустого программного канала или канала FIFO.


Таблица 4.1. Действие флага O_NONBLOCK на именованные и неименованные каналы 

Операция Наличие открытых каналов Блокировка включена (по умолчанию) Флаг O_NONBLOCK установлен
Открытие (open) FIFO только для чтения FIFO открыт на запись Возвращается код успешного завершения операции Возвращается код успешного завершения операции
Открытие (open) FIFO только для чтения FIFO не открыт на запись Процесс блокируется, пока FIFO не будет открыт на запись Возвращается код успешного завершения операции
Открытие (open) FIFO только для записи FIFO открыт на чтение Возвращает код успешного завершения операции Возвращает код успешного завершения операции
Открытие (open) FIFO только для записи FIFO не открыт на чтение Блокируется до тех пор, пока FIFO не будет открыт на чтение Возвращает ошибку с кодом ENXIO
Чтение (read) из пустого программного канала или FIFO Программный канал или FIFO открыт на запись Блокируется до тех пор, пока в программный канал или FIFO не будут помещены данные или они не будут закрыты всеми процессами, которыми они были открыты на запись Возвращает ошибку с кодом EAGAIN
Чтение (read) из пустого программного канала или FIFO Программный канал или FIFO не открыт на запись read возвращает 0 (конец файла) read возвращает 0 (конец файла)
Запись (write) в программный канал или FIFO Программный канал или FIFO открыт на чтение (См. в тексте) (См. в тексте)
Запись (write) в программный канал или FIFO Программный канал или FIFO не открыт на чтение Программному потоку посылается сигнал SIGPIPE Программному потоку посылается сигнал SIGPIPE 

Запомните несколько дополнительных правил, действующих при чтении и записи данных в программные каналы и FIFO.

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

■ Если количество байтов, направленных на запись функции write, не превышает значения PIPE_BUF (ограничение, устанавливаемое стандартом Posix, о котором более подробно рассказывается в разделе 4.11), то ядро гарантирует атомарность операции записи. Это означает, что если два процесса запишут данные в программный канал или FIFO приблизительно одновременно, то в буфер будут помещены сначала все данные от первого процесса, а затем от второго, либо наоборот. Данные от двух процессов при этом не будут смешиваться. Однако если количество байтов превышает значение PIPEBUF, атомарность операции записи не гарантируется.

ПРИМЕЧАНИЕ

Posix.1 требует, чтобы значение PIPE_BUF равнялось по меньшей мере 512. Характерные значения, встречающиеся на практике, лежат в диапазоне от 1024 (BSD/OS 3.1) до 5120 байт (Solaris 2.6). В разделе 4.11 приведен текст программы, выводящей значение этой константы.

■ Установка флага O_NONBLOCK не влияет на атомарность операции записи в про-грaммный канал или FIFO — она определяется исключительно объемом посылаемых данных в сравнении с величиной PIPE_BUF. Однако если для прогрaммнoгo канала или FIFO отключена блокировка, возвращаемое функцией write значение зависит от количества байтов, отправленных на запись, и наличия свободного места в пpoгрaммнoм канале или FIFO. Если количество байтов не превышает величины PIPE_BUF, то:

 □ Если в канале достаточно места для записи требуемого количества данных, они будут переданы все сразу.

 □ Если места в пpoгрaммнoм канале или FIFO недостаточно для записи требуемого объема данных, происходит немедленное завершение работы функции с возвратом ошибки EAGAIN. Поскольку установлен флаг O_NONBLOCK, процесс не может быть заблокирован, но в то же время ядро не может принять лишь часть данных, так как при этом невозможно гарантировать атомарность операции записи. Поэтому ядро возвращает ошибку, сообщающую процессу о необходимости попытаться произвести запись еще раз.

■ Если количество байтов превышает значение PIPE_BUF, то:

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

 □ Если в программном канале или FIFO свободное место отсутствует, происходит немедленное завершение работы с возвратом ошибки EAGAIN.

■ При записи в программный канал или FIFO, не открытый для чтения, ядро посылает сигнал SIGPIPE:

 □ Если процесс не принимает (catch) и не игнорирует SIGPIPE, выполняется действие по умолчанию — завершение работы процесса.

 □ Если процесс игнорирует сигнал SIGPIPE или перехватывает его и возвращается из подпрограммы его обработки, write возвращает ошибку с кодом EPIPE.

ПРИМЕЧАНИЕ

SIGPIPE считается синхронным сигналом, что означает, что он привязан к конкретному программному потоку, а именно тому, который вызвал функцию write. Простейшим способом обработки сигнала является его игнорирование (установка SIG_IGN) и предоставление функции write возможности вернуть ошибку с кодом EPIPE. В приложении всегда должна быть предусмотрена обработка ошибок, возвращаемых функцией write, а вот определить, что процесс был завершен сигналом SIGPIPE, сложнее. Если сигнал не перехватывается, придется посмотреть на статус завершения работы процесса (termination status) из интерпретатора команд, чтобы узнать, что процесс был принудительно завершен сигналом и каким именно сигналом. В разделе 5.13 [24] о сигнале SIGPIPE рассказывается более подробно.

4.8. Один сервер, несколько клиентов

Преимущества канала FIFO проявляются более явно в том случае, когда сервер представляет собой некоторый длительно функционирующий процесс (например, демон, наподобие описанного в главе 12 [24]), не являющийся родственным клиенту. Демон создает именованный канал с вполне определенным известным именем, открывает его на чтение, а запускаемые впоследствии клиенты открывают его на запись и отправляют демону команды и необходимые данные. Односторонняя связь в этом направлении (от клиента к серверу) легко реализуется с помощью FIFO, однако необходимость отправки данных в обратную сторону (от сервера к клиенту) усложняет задачу. Рисунок 4.12 иллюстрирует прием, применяемый в этом случае. 

UNIX: взаимодействие процессов

Рис. 4.12. Один сервер, несколько клиентов


Сервер создает канал с известным полным именем, в данном случае /tmp/fifо.serv. Из этого канала он считывает запросы клиентов. Каждый клиент при запуске создает свой собственный канал, полное имя которого определяется его идентификатором процесса. Клиент отправляет свой запрос в канал сервера с известным именем, причем запрос этот содержит идентификатор процесса клиента и имя файла, отправку которого клиент запрашивает у сервера. В листинге 4.10 приведен текст программы сервера.

Листинг 4.10. Сервер, обслуживающий несколько клиентов с помощью канала FIFO

//fifocliserv/mainserver.с

1  #include "fifo.h"

2  void server(int, int);

3  int

4  main(int argc, char **argv)

5  {

6   int readfifo, writefifo, dummyfd, fd;

7   char *ptr, buff[MAXLINE], fifoname[MAXLINE];

8   pid_t pid;

9   ssize_t n;

10  /* создание FIFO сервера с известным именем. ОК, если уже существует */

11  if ((mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST))

12   err_sys("can't create %s", SERV_FIFO);

13  /* открытие FIFO-cepвepa на чтение */

14  readfifo = Open(SERV_FIFO, O_RDONLY, 0);

15  dummyfd = Open(SERV_FIFO, O_WRONLY, 0); /* не используется */

16  while ((n = Readline(readfifo, buff, MAXLINE)) > 0) {

17   if (buff[n-1] == '\n')

18    n--; /* delete newline from readline() */

19   buff[n] = '\0'; /* полное имя, завершаемое 0 */

20   if ((ptr = strchr(buff, ' ')) == NULL) {

21    err_msg("bogus request: ls", buff);

22    continue;

23   }

24   *ptr++ = 0; /* идентификатор процесса, указатель на имя файла */

25   pid = atol(buff);

26   snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long) pid);

27   if ( (writefifo = open(fifoname, O_WRONLY, 0)) < 0) {

28    err_msg("cannot open: ls", fifoname);

29    continue;

30   }

31   if ((fd = open(ptr, O_RDONLY)) < 0) {

32    /* ошибка, нужно сообщить клиенту */

33    snprintf(buff + n, sizeof(buff) – n, ": can't open, %s\n",

34     strerror(errno));

35    n = strlen(ptr);

36    Write(writefifo, ptr, n);

37    Close(writefifo);

38

39   } else {

40    /* успешное открытие, копируем файл */

41    while ((n = Read(fd, buff, MAXLINE)) > 0)

42     Write(writefifo, buff, n);

43    Close(fd);

44    Close(writefifo);

45   }

46  }

47 }

Создание канала и открытие его только для записи и только для чтения

10-15 Сервер создает канал FIFO с известным именем, обрабатывая ситуацию, когда такой канал уже существует. Затем этот канал открывается дважды: один раз только для чтения, а второй — только для записи. Дескриптор readfifo используется для приема запросов от клиентов, а дескриптор dummyfd не используется вовсе. Причина, по которой нужно открыть канал для записи, видна из табл. 4.1. Если канал не открыть на запись, то при завершении работы очередного клиента этот канал будет опустошаться и сервер будет считывать 0, означающий конец файла. Пришлось бы каждый раз закрывать канал вызовом close, а затем заново открывать его с флагом O_RDONLY, что приводило бы к блокированию демона до подключения следующего клиента. Мы же всегда будем иметь дескриптор, открытый на запись, поэтому функция read не будет возвращать 0, означающий конец файла, при отсутствии клиентов. Вместо этого сервер просто будет блокироваться при вызове read, ожидая подключения следующего клиента. Этот трюк упрощает код программы-сервера и уменьшает количество вызовов open для канала сервера.

При запуске сервера первый вызов open (с флагом O_RDONLY) приводит к блокированию процесса до появления первого клиента, открывающего канал сервера на запись (см. табл. 4.1). Второй вызов open (с флагом O_WRONLY) не приводит к блокированию, поскольку канал уже открыт на запись. 

Считывание запроса от клиента

16 Каждый запрос, принимаемый от клиента, представляет собой одну строку, состоящую из идентификатора процесса, пробела и полного имени требуемого файла. Эта строка считывается функцией readline (приведенной в [24, с.79]).

Анализ запроса клиента

17-26 Символ перевода строки, возвращаемый функцией readline, удаляется. Этот символ может отсутствовать только в том случае, если буфер был заполнен, прежде чем был обнаружен символ перевода строки, либо если последняя введенная строка не была завершена этим символом. Функция strchr возвращает указатель на первый пробел в этой строке, который затем увеличивается на единицу, чтобы он указывал на первый символ полного имени файла, следующего за пробелом. Полное имя канала клиента формируется из его идентификатора процесса, и этот канал открывается сервером на запись.

Открытие файла и отправка его в FIFO клиента

27-44 Оставшаяся часть кода пpoгрaммы-cepвepa аналогична функции server из листинга 4.3. Программа открывает файл; если при этом возникает ошибка — клиенту отсылается сообщение о ней. Если открытие файла завершается успешно, его содержимое копируется в канал клиента. После завершения копирования открытый сервером «конец» (дескриптор) канала клиента должен быть закрыт с помощью функции close, чтобы функция read вернула пpoгрaммe-клиeнтy значение 0 (конец файла). Сервер не удаляет канал клиента; клиент должен самостоятельно позаботиться об этом после приема от сервера символа конца файла. Текст пpoгрaммы-клиeнтa приведен в листинге 4.11.

Листинг 4.11. Клиент, связывающийся с сервером (листинг 4.10) с помощью канала FIFO

//fifocliserv/mainclient.с

1  #include "fifo.h"

2  int

3  main(int argc, char **argv)

4  {

5   int readfifo, writefifo;

6   size_t len;

7   ssize_t n;

8   char *ptr, fifoname[MAXLINE], buff[MAXLINE];

9   pid_t pid;

10  /* создание FIFO с включением в его имя PID */

11  pid = getpid();

12  snprintf(fifoname, sizeof(fifoname), "/tmp/fifo,%ld", (long) pid):

13  if ((mkfifo(fifoname, FILE_MODE) < 0) && (errno != EEXIST))

14   err_sys("can't create %s", fifoname);

15  /* инициализация буфера PID и пробелом */

16  snprintf(buff, sizeof(buff), "%ld ", (long) pid);

17  len = strlen(buff);

18  ptr = buff + len;

19  /* считывание полного имени */

20  Fgets(ptr, MAXLINE – len, stdin);

21  len = strlen(buff); /* fgets() гарантирует завершающий 0 */

22  /* открытие FIFO сервера и запись в него полного имени и PID */

23  writefifo = Open(SERV_FIFO, O_WRONLY, 0);

24  Write(writefifo, buff, len);

25  /* открытие созданного FIFO; блокирование до открытия его сервером */

26  readfifo = Open(fifoname, O_RDONLY; 0);

27  /* считывание из канала IPC, запись в stdout */

28  while ((n = Read(readfifo, buff, MAXLINE)) > 0)

29   Write(STDOUT_FILENO, buff, n);

30  Close(readfifo);

31  Unlink(fifoname);

32  exit(0);

33 }

Создание канала

10-14 Идентификатор процесса клиента содержится в имени создаваемого им канала.

Формирование строки запроса

15-21 Запрос клиента состоит из его идентификатора процесса, одного пробела, полного имени запрашиваемого им файла и символа перевода строки. Строка запроса формируется в массиве buff, причем имя файла считывается из стандартного потока ввода.

Открытие канала сервера и отправка запроса

22-24 Клиент открывает канал сервера и записывает в него строку запроса. Если клиент окажется первым с момента запуска сервера, вызов open разблокирует сервер, заблокированный после сделанного им вызова open (с флагом O_RDONLY).

Прием содержимого файла или сообщения об ошибке от сервера

25-31 Ответ сервера считывается из канала и записывается в стандартный поток вывода, после чего канал клиента закрывается и* удаляется.

Сервер может быть запущен в одном из окон, а клиент — в другом, и программа будет работать так, как мы и рассчитывали. Ниже мы приводим только текст, выводимый клиентом:

solaris % mainclient /etc/shadow          файл, который нам нельзя читать

/etc/shadow: can't open. Permission denied

solaris % mainclient /etc/inet/ntp.conf  файл из двух строк

multicastclient 224.0.1.1

driftfile /etc/inet/ntp.drift 

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

solaris % Pid=$$

solaris % mkfifo /tmp/fifo.$Pid

solaris % echo "$Pid /etc/inet/ntp.conf" > /tmp/fifo.serv

solaris % cat < /tmp/fifo.$Pid

multicastclient 224.0.1.1

driftfile /etc/inet/ntp.drift

solaris % rm /tmp/fifo.$Pid

Мы отсылаем серверу идентификатор процесса текущей копии интерпретатора и полное имя файла одной командой интерпретатора (echo) и считываем из канала сервера результат с помощью другой команды (cat). Между выполнением этих двух команд может пройти произвольный промежуток времени. Таким образом, сервер помещает содержимое файла в канал, а клиент затем запускает команду cat, чтобы считать оттуда данные. Может показаться, что данные каким-то образом хранятся в канале, хотя он не открыт ни одним процессом. На самом деле все не так. После закрытия пpoгрaммнoгo канала или FIFO последним процессом с помощью команды close все данные, в нем находящиеся, теряются. В нашем примере сервер, считав строку запроса от клиента, блокируется при попытке открыть канал клиента, потому что клиент (наша копия интерпретатора) еще не открыл его на чтение (вспомним табл. 4.1). Только после вызова cat некоторое время спустя канал будет открыт на чтение, и тогда сервер разблокируется. Кстати, таким образом осуществляется атака типа «отказ в обслуживании» (denial-of-service attack), которую мы обсудим в следующем разделе.

Использование интерпретатора позволяет провести простейшую проверку способности сервера обрабатывать ошибки. Мы можем отправить серверу строку без идeнтификaтopa процесса или отослать ему такой идентификатор, которому не соответствует никакой канал FIFO в каталоге /tmp. Например, если мы запустим сервер и введем нижеследующие строки:

solaris % cat > /tmp/fifo.serv /no/process/id

999999 /invalid/process/id

то сервер выдаст текст:

solaris % server

bogus request: /no/process/id

cannot open: /tmp/fifo.999999

Атомарность записи в FIFO

Наша простейшая пара клиент-сервер позволяет наглядно показать важность наличия свойства атомарности записи в пpoгрaммныe каналы и FIFO. Предположим, что два клиента посылают серверу запрос приблизительно в один и тот же момент. Первый клиент отправляет следующую строку:

1234 /etc/inet/ntp.conf

второй:

9876 /etc/passwd

Предполагая, что каждый клиент помещает данные в FIFO за один вызов write и кaждая строка имеет размер, не превышающий величины PIPE_BUF (что чаще всего заведомо выполняется, поскольку PIPE_BUF обычно лежит в диапазоне 1024-5120, а длина полного имени обычно oгрaничeнa 1024 байт), мы можем гарантировать, что в FIFO данные будут иметь следующий вид:

1234 /etc/inet/ntp.conf

9876 /etc/passwd

либо

9876 /etc/passwd

1234 /etc/inet/ntp.conf

Данные в канале не могут смешаться в «кашу», наподобие:

1234 /etc/inet9876 /etc/passwd

/ntp.conf

FIFO и NFS

Каналы FIFO представляют собой вид IPC, который может использоваться только в пределах одного узла. Хотя FIFO и обладают именами в файловой системе, они могут применяться только в локальных файловых системах, но не в присоединенных сетевых (NFS).

solaris % mkfifo /nsf/bsdi/usr/rstevens/fifo.temp

mkfifo: I/O error

В этом примере файловая система /nfs/bsdi/usr — это файловая система /usr нa yзлe bsdi.

Некоторые системы (например, BSD/OS) позволяют создавать FIFO в присоединенных файловых системах, но по ним нельзя передавать данные между узлами. В этом случае такой канал может использоваться лишь как «точка рандеву» в файловой системе между клиентами и серверами на одном и том же узле. Процесс, выполняемый на одном узле, нe мoжem послать данные через FIFO процессу, выполняемому на другом узле, даже если оба процесса смогут открыть этот канал, доступный обоим узлам через сетевую файловую систему.

4.9. Последовательные и параллельные серверы

Сервер в нашем простом примере из предыдущего раздела являлся последовательным сервером (iterative server). Он последовательно обрабатывал запросы клиентов, переходя к следующему только после полного завершения работы с предыдущим. Например, если два клиента пошлют запрос такому серверу приблизительно одновременно, причем один из них запросит 10-мегабайтный файл, отправка которого займет, например, 10 секунд, а второй — 10-байтный файл, то второму придется ждать по меньшей мере 10 секунд, пока не будет обслужен первый клиент.

Альтернативой является параллельный сервер (concurrent server). Наиболее часто встречаемый в Unix вид такого сервера называется one-child-per-client (каждому клиенту — один дочерний процесс). Сервер вызывает fork для создания нового процесса каждый раз, когда появляется новый клиент. Дочерний процесс полностью обрабатывает запрос клиента, а поддержка многозадачности в Unix обеспечивает параллельность выполнения всех этих процессов. Однако существуют и другие методы решения задачи, подробно описанные в главе 27 [24]:

■ создание пула дочерних процессов и передача нового клиента свободному дочернему процессу;

■ создание одного пpoгрaммнoгo потока для каждого клиента;

■ создание пула потоков и передача нового клиента свободному потоку.

Хотя в [24] обсуждаются проблемы создания сетевых серверов, те же методы применимы и к серверам межпроцессного взаимодействия (IPC server), клиенты которых находятся на одном узле.

Атака типа «отказ в обслуживании»

Один из недостатков последовательных серверов был уже отмечен выше — некоторым клиентам приходится ждать дольше чем нужно, потому что их запросы приходят после запросов других клиентов, запрашивающих большие файлы. Существует и другая проблема. Вспомним наш пример с интерпретатором команд, приведенный после листинга 4.11, и относящееся к нему обсуждение того, что сервер блокируется при вызове open для FIFO клиента, если клиент еще не открыл этот канал (чего не происходит до выполнения cat). Это дает возможность злоумышленнику «подвесить» сервер, послав ему запрос, не открывая канала. Этот тип атаки называется «отказ в обслуживании» (Denial of Service — DoS). Чтобы исключить возможность такой атаки, нужно быть аккуратным при написании последовательной части любого сервера, учитывая возможность и потенциальную продолжительность его блокирования. Одним из методов решения проблемы является установка максимального времени ожидания для некоторых операций, однако обычно проще сделать сервер параллельным, а не последовательным, поскольку в данном случае атака будет действовать лишь на один из дочерних процессов, а не на весь сервер. Однако даже параллельный сервер не защищен от атаки полностью: злоумышленник все еще может послать множество запросов, что приведет к превышению предела количества порожденных сервером процессов и невозможности выполнения последующих вызовов fork.

4.10. Потоки и сообщения

Приведенные примеры пpoгрaммныx каналов и каналов FIFO использовали потоковую модель ввода-вывода, что естественно для Unix. При этом отсутствуют грaницы записей — данные при операциях чтения и записи не проверяются вовсе. Процесс, считывающий 100 байт из FIFO, не может определить, записал ли другой процесс в FIFO все 100 байт за 1 раз, или за 5 раз по 20 байт, или в любой другой комбинации общим объемом 100 байт. Возможно, один процесс записал в FIFO 55 байт, а потом другой — 45. Данные представляют собой просто поток байтов, никак не интерпретируемых системой. Если же требуется какая-либо интерпретация данных, пишущий и читающий процессы должны заранее «договориться» о ее правилах и выполнять всю работу самостоятельно.

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

1. Специальная внутриполосная завершающая последовательность: множество приложений под Unix используют в качестве разделителя сообщений символ перевода строки. Пишущий процесс добавляет к каждому сообщению этот символ, а считывающий процесс производит построчное считывание. Так работают клиент и сервер из листингов 4.10 и 4.11, чтобы разделить запросы клиентов. Этот метод требует исключения символа-разделителя из самих передаваемых данных (в случае необходимости его передать он должен предваряться другим специальным символом).

2. Явное указание длины: каждой записи предшествует информация об ее длине. Мы вскоре воспользуемся этим методом. Он также применяется в Sun RPC при использовании совместно с TCP. Одним из преимуществ этого метода является отсутствие необходимости исключать разделитель из передаваемых данных, поскольку получатель не проверяет все данные, а переходит сразу к концу очередной записи, чтобы узнать длину следующей.

3. Одна запись за подключение: приложение закрывает подключение к партнеру (подключение TCP для сетевых приложений либо просто подключение IPC), обозначая конец записи. Это требует повторного подключения для передачи следующей записи, однако используется в стандарте HTTP 1.0.

Стандартная библиотека ввода-вывода также может использоваться для считывания и записи данных в пpoгрaммный канал или FIFO. Поскольку канал может быть открыт только функцией piре, возвращающей открытый дескриптор, для создания нового стандартного потока, связанного с этим дескриптором, можно использовать стандартную функцию fdopen. Канал FIFO обладает именем, поэтому он может быть открыт с помощью функции fopen.

Можно создавать и более структурированные сообщения — эта возможность предоставляется очередями сообщений и в Posix, и в System V. Мы вскоре узнаем, что каждое сообщение обладает длиной и приоритетом (типом в System V). Длина и приоритет указываются отправителем и возвращаются получателю после считывания сообщения. Каждое сообщение представляет собой запись, аналогично дeйтaгрaммaм UDP ([24]).

Мы можем структурировать данные, передаваемые по программному каналу или FIFO, самостоятельно. Определим сообщение в нашем заголовочном файле mesg.h, как показано в листинге 4.12.

Листинг 4.12. Структура mymesg и сопутствующие определения

//pipemesg/mesg.h

1  #include "unpipc.h"

2  /* Наши собственные "сообщения", которые могут использоваться с каналами, FIFO и очередями сообщений */

3  /* Мы хотим, чтобы sizeof(struct mymesg) <= PIPE_BUF */

4  #define MAXMESGDATA (PIPE_BUF – 2*sizeof(long))

5  /* Длина mesg_len и mesg_type */

6  #define MESGHDRSIZE (sizeof(struct mymesg) – MAXMESGDATA)

7  struct mymesg {

8   long mesg_len; //количество байтов в mesg_data, может быть О

9   long mesg_type;//тип сообщения, должен быть > 0

10  char mesg_data[MAXMESGDATA];

11 };

12 ssize_t mesg_send(int, struct mymesg *);

13 void Mesg_send(int, struct mymesg *);

14 ssize_t mesg_recv(int, struct mymesg *);

15 ssize_t Mesg_recv(int, struct mymesg *);

Каждое сообщение содержит в себе информацию о своем типе (mesg_type), причем значение этой переменной должно быть больше нуля. Пока мы будем игнорировать это поле в записи, но вернемся к нему в главе 6, где описываются очереди сообщений System V. Каждое сообщение также обладает длиной, кoтopая может быть и нулевой. Структура mymesg позволяет предварить каждое сообщение информацией о его типе и длине вместо использования символа перевода строки для сигнализации конца сообщения. Ранее мы отметили два преимущества этого подхода: получатель не должен сканировать все принятые байты в поисках конца сообщения и отсутствует необходимость исключать появление разделителя в самих данных.

На рис. 4.13 изображен вид структуры mymesg и ее использование с каналами, FIFO и очередями сообщений System V. 

UNIX: взаимодействие процессов

Рис. 4.13. Структура mymesg


Мы определяем две функции для отправки и приема сообщений. В листинге 4.13 приведен текст функции mesg_send, а в листинге 4.14 — функции mesg_recv.

Листинг 4.13. Функция mesg_send

//pipemesg/mesg_send.c

1 #include "mesg.h"

2 ssize_t

3 mesg_send(int fd, struct mymesg *mptr)

4 {

5  return(write(fd, mptr, MESGHDRSIZE + mptr->mesg_len));

6 }

Листинг 4.14. Функция mesg_recv

//pipemesg/mesg_recv.c

1  #include "mesg.h"

2  ssize_t

3  mesg_recv(int fd, struct mymesg *mptr)

4  {

5   size_t len;

6   ssize_t n;

8   /* считывание заголовка сообщения для определения его длины */

9   if ((n = Read(fd, mptr, MESGHDRSIZE)) == 0)

10   return(0); /* end of file */

11  else if (n != MESGHDRSIZE)

12   err_quit("message header: expected %d, got %d". MESGHDRSIZE, n);

13  if ((len = mptr->mesg_len) > 0)

14   if ((n = Read(fd, mptr->mesg_data, len)) != len)

15    err_quit("message data: expected %d, got %d", len, n);

16  return(len);

17 }

Теперь для каждого сообщения функция read вызывается дважды: один раз для считывания длины, а другой — для считывания самого сообщения (если его длина больше 0).

ПРИМЕЧАНИЕ

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

Изменим теперь функции client и server, чтобы воспользоваться новыми функциями mesg_send и mesg_recv. В листинге 4.15 приведен текст функции-клиента.

Листинг 4.15. Функция client с использованием сообщений

//pipemesg/client.c

1  #include "mesg.h"

2  void

3  client(int readfd, int writefd)

4  {

5   size_t len;

6   ssize_t n;

7   struct mymesg mesg;

8   /* считывание полного имени */

9   Fgets(mesg.mesg_data, MAXMESGDATA, stdin);

10  len = strlen(mesg.mesg_data);

11  if (mesg.mesg_data[len-1] == '\n')

12   len--; /* удаление перевода строки из fgets() */

13  mesg.mesg_len = len;

14  mesg.mesg_type = 1;

15  /* запись полного имени в канал IPC */

16  Mesg_send(writefd, &mesg);

17  /* считывание из канала IPC. запись в stdout */

18  while ( (n = Mesg_recv(readfd, &mesg)) > 0)

19   Write(STDOUT_FILENO, mesg.mesg_data, n);

20 }

Считывание имени файла и отправка его серверу

8-16 Полное имя считывается из стандартного потока ввода и затем отправляется на сервер с помощью функции mesg_send.

Считывание содержимого файла или сообщения об ошибке от сервера

17-19 Клиент вызывает функцию mesg_recv в цикле, считывая все приходящие от сервера сообщения. По соглашению, когда mesg_recv возвращает нулевую длину сообщения, это означает конец передаваемых сервером данных. Мы увидим, что сервер добавляет символ перевода строки к каждому сообщению, отправляемому клиенту, поэтому пустая строка будет иметь длину сообщения 1. В листинге 4.16 приведен текст функции-сервера.

Листинг 4.16. Функция server, использующая сообщения

//pipemesg/server.c

1  #include "mesg.h"

2  void

3  server(int readfd, int writefd)

4  {

5   FILE *fp;

6   ssize_t n;

7   struct mymesg mesg;

8   /* считывание полного имени из канала */

9   mesg.mesg_type = 1;

10  if ((n = Mesg_recv(readfd, &mesg)) == 0)

11   err_quit("pathname missing");

12  mesg.mesg_data[n] = '\0'; /* полное имя, завершающееся 0 */

13  if ((fp = fopen(mesg.mesg_data, "r")) == NULL) {

14   /* ошибка, нужно сообщить клиенту */

15   snprintf(mesg.mesg_data + n, sizeof(mesg.mesg_data) – n,

16    ": can't open, %s\n", strerror(errno));

17   mesg.mesg_len = strlen(mesg.mesg_data);

18   Mesg_send(writefd, &mesg);

19  } else {

20   /* файл успешно открыт, передача данных */

21   while (Fgets(mesg.mesg_data, MAXMESGDATA, fp) != NULL) {

22    mesg.mesg_len = strlen(mesg.mesg_data);

23    Mesg_send(writefd, &mesg);

24   }

25   Fclose(fp);

26  }

27  /* отправка сообщения нулевой длины для обозначения конца связи */

28  mesg.mesg_len = 0;

29  Mesg_send(writefd, &mesg);

30 }

Считывание имени файла из канала IPC, открытие файла

8-18 Сервер принимает от клиента имя файла. Хотя значение mesg_type, равное 1, нигде не используется (оно затирается функцией mesg_recv из листинга 4.14), мы будем использовать ту же функцию при работе с очередями сообщений System V (листинг 6.8), а в данном случае в этом значении уже возникает потребность (см., например, листинг 6.11). Стандартная функция ввода-вывода fopen открывает файл, что отличается от листинга 4.3, где вызывалась функция open для получения дескриптора файла. Причина, по которой мы воспользовались fopen, заключается в том, что в этой пpoгрaммe мы пользуемся библиотечной функцией fgets для считывания содержимого файла построчно и затем отправляем клиенту строку за строкой.

Отправка файла клиенту

19-26 Если вызов fopen оказывается успешным, содержимое файла считывается с помощью функции fgets и затем отправляется клиенту построчно. Сообщение с нулевой длиной означает конец файла.

При использовании пpoгрaммныx каналов или FIFO мы могли бы также закрыть канал IPC, чтобы дать клиенту знать о том, что передача файла завершена. Однако мы используем передачу сообщения нулевой длины, потому что другие типы IPC не поддерживают концепцию конца файла.

Функции main, вызывающие новые функции client и server, вообще не претерпели никаких изменений. Мы можем использовать либо версию для работы с каналами (листинг 4.1), либо версию для работы с FIFO (листинг 4.6).

4.11. Ограничения программных каналов и FIFO

На программные каналы и каналы FIFO системой накладываются всего два ограничения:

■ OPEN_MAX — максимальное количество дескрипторов, которые могут быть одновременно открыты некоторым процессом (Posix устанавливает для этой величины ограничение снизу — 16);

■ PIPE_BUF — максимальное количество данных, для которого гарантируется атомарность операции записи (описано в разделе 4.7; Posix требует по меньшей мере 512 байт).

Значение OPEN_MAX можно узнать, вызвав функцию sysconf, как мы вскоре покажем. Обычно его можно изменить из интерпретатора команд с помощью команды ulimit (в Bourne shell и KornShell, как мы вскоре покажем) или с помощью команды limit (в С shell). Оно может быть изменено и самим процессом с помощью вызова функции setrlimit (подробно описана в разделе 7.11 [21]).

Значение PIPE_BUF обычно определено в заголовочном файле <limits.h>, но с точки зрения стандарта Posix оно представляет собой переменную, зависимую от полного имени файла. Это означает, что ее значение может меняться в зависимости от указываемого имени файла (для FIFO, поскольку каналы имен не имеют), поскольку разные имена могут относиться к разным файловым системам и эти файловые системы могут иметь различные характеристики. Это значение можно получить в момент выполнения пpoгрaммы, вызвав либо pathconf, либо fpathconf. В листинге 4.17 приведен пример, выводящий текущее значение этих двух oгрaничeний.

Листинг 4.17. Определение значений PIPE_BUF и OPEN_MAX во время выполнения

//pipe/pipeconf.c

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   if (argc != 2)

6   err_quit("usage: pipeconf <pathname>");

7   printf("PIPE_BUF = %ld. OPEN_MAX = %ld\n",

8   Pathconf(argv[1], _PC_PIPE_BUF), Sysconf(_SC_OPEN_MAX));

9   exit(0);

10 }

Вот несколько примеров, в которых указываются имена файлов, относящиеся к различным файловым системам:

solaris % pipeconf / значения по умолчанию в Solaris 2.6

PIPE_BUF = 5120, OPEN_MAX = 64

solaris % pipeconf /home

PIPE_BUF = 5120, OPEN_MAX = 64

solaris % pipeconf /tmp

PIPE_BUF = 5120, OPEN_MAX = 64

alpha % pipeconf /   значения по умолчанию в Digital Unix 4.0B

PIPE_BUF = 4096, OPEN_MAX = 4096

alpha % pipeconf /usr

PIPE_BUF = 4096, OPEN_MAX = 4096

Покажем теперь, как изменить значение OPEN_MAX в Solaris, используя интерпретатор KornShell:

solaris % ulimit –nS     отображение максимального количества дескрипторов, мягкоео граничение

64

solaris % ulimit –Nh     отображение максимального количества дескрипторов, жесткое ограничение

1024

solaris % ulimit –nS 512 установка мягкого ограничения в 512

solaris % pipeconf /     проверка внесенных изменений

PIPE_BUF = 5120, OPEN_MAX = 512 

ПРИМЕЧАНИЕ

Хотя значение PIPE_BUF для FIFO, в принципе, может меняться в зависимости от файловой системы, к которой относится файл, на самом деле это очень редкий случай.

В главе 2 [21] описаны функции fpathconf, pathconf и sysconf, которые предоставляют информацию о некоторых ограничениях ядра во время выполнения программы. Стандарт Posix.1 определяет 12 констант, начинающихся с _РС_, и 52, начинающихся с _SC_. Системы Digital Unix 4.0B и Solaris 2.6 расширяют последнее ограничение, определяя около 100 констант, значения которых могут быть получены в момент выполнения программы с помощью sysconf. 

Команда getconf определяется стандартом Posix.2 и выводит значения большинства этих ограничений. Например:

alpha % getconf OPEN_MAX

4096

alpha % getconf PIPE_BUF /

4096

4.12. Резюме

Именованные и неименованные каналы представляют собой базовые строительные блоки для множества приложений. Программные каналы (неименованные) обычно используются в интерпретаторе команд, а также внутри программ — часто для передачи информации от дочернего процесса к родительскому. Можно исключить часть кода, относящегося к использованию каналов (piре, fork, close, exec и waitpid), используя функции popen и pclose, которые берут на себя все тонкости и запускают интерпретатор команд.

Каналы FIFO похожи на программные каналы, но создаются вызовом mkfifo и затем могут быть открыты с помощью функции open. При открытии FIFO следует быть аккуратным, поскольку процесс может быть заблокирован, а зависит это от множества условий (см. табл. 4.1).

Используя программные каналы и FIFO, мы создали несколько вариантов приложении типа клиент-сервер: один сервер с несколькими клиентами, последовательный и параллельный серверы. Последовательный сервер единовременно обрабатывает запрос только от одного клиента; такие серверы обычно уязвимы для атак типа «отказ в обслуживании». Параллельный сервер запускает отдельный процесс или поток для обработки запроса нового клиента.

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

Упражнения

1. При переходе от рис. 4.3 к рис. 4.4: что могло бы произойти, если бы дочерний процесс не закрывал дескриптор (close(fd[1]))? 

2. Описывая mkfifo в разделе 4.6, мы сказали, что для открытия существующего FIFO или создания нового, если его не существует, следует вызвать mkfifо, проверить, не возвращается ли ошибка EEXIST, и вызвать open, если это происходит. Что если изменить логику и вызвать сначала open, а затем mkfifо, если FIFO не существует?

3. Что происходит при вызове popen в листинге 4.5, если в интерпретаторе возникает ошибка?

4. Удалите вызов open для FIFO сервера в листинге 4.10 и проверьте, приведет ли это к завершению работы сервера после отключения последнего клиента.

5. К листингу 4.10: мы отметили, что при запуске сервера его работа блокируется при вызове первой функции open, пока FIFO не будет открыт на запись первым клиентом. Как можно обойти это таким образом, чтобы обе функции open завершали работу немедленно, а блокирование происходило при первом вызове readline?

6. Что произойдет с клиентом в листинге 4.11, если поменять порядок вызовов open?

7. Почему сигнал отправляется процессу, в котором канал FIFO открыт на запись, после отключения последнего читающего клиента, а не читающему клиенту после отключения последнего пишущего?

8. Напишите небольшую тестирующую программу для определения того, возвращает ли fstat количество байтов в FIFO в качестве поля st_size структуры stat.

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

ГЛАВА 5

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

5.1. Введение

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

Процесс может записать в очередь какие-то сообщения, после чего они могут быть получены другим процессом в любое время, даже если первый завершит свою работу. Мы говорим, что очереди сообщений обладают живучестью ядра (kernel persistence, раздел 1.3). Это также отличает их от программных каналов и FIFO. В главе 4 говорится о том, что данные, остающиеся в именованном или неименованном канале, сбрасываются, после того как все процессы закроют его.

В этой главе рассматриваются очереди сообщений стандарта Posix, а в главе 6 — стандарта System V. Функции для работы с ними во многом схожи, а главные отличия заключаются в следующем:

■ операция считывания из очереди сообщений Posix всегда возвращает самое старое сообщение с наивысшим приоритетом, тогда как из очереди System V можно считать сообщение с произвольно указанным приоритетом;

■ очереди сообщений Posix позволяют отправить сигнал или запустить программный поток при помещении сообщения в пустую очередь, тогда как для очередей System V ничего подобного не предусматривается.

Каждое сообщение в очереди состоит из следующих частей:

■ приоритет (беззнаковое целое, Posix) либо тип сообщения (целое типа long, System V);

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

■ собственно данные (если длина сообщения отлична от 0).

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

UNIX: взаимодействие процессов

Рис. 5.1. Очередь сообщений Posix, содержащая три сообщения


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

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

5.2. Функции mq_open, mq_close, mq_unlink

Функция mq_open создает новую очередь сообщений либо открывает существующую:

#include <mqueue.h>

mqd_t mq_open(const char *name, int oflag, …

/* mode_t mode, struct mq_attr *attr*/ );

/* Возвращает дескриптор очереди в случае успешного завершения;

 –1 – в противном случае. */

Требования к аргументу пате описаны в разделе 2.2.

Аргумент oflag может принимать одно из следующих значений: O_RDONLY, O_WRONLY, O_RDWR в сочетании (логическое сложение) с O_CREAT, O_EXCL, O_NONBLOCK. Все эти флаги описаны в разделе 2.3.

При создании новой очереди (указан флаг O_CREAT и очередь сообщений еще не существует) требуется указание аргументов mode и attr. Возможные значения аргумента mode приведены в табл. 2.3. Аргумент attr позволяет задать некоторые атрибуты очереди. Если в качестве этого аргумента задать нулевой указатель, очередь будет создана с атрибутами по умолчанию. Эти атрибуты описаны в разделе 5.3.

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

ПРИМЕЧАНИЕ

В системе Solaris 2.6 тип mqd_t определен как void*, а в Digital Unix 4.0B — как int. В нашем примере в разделе 5.8 эти дескрипторы трактуются как указатели на структуру. Название «дескриптор» было дано им по ошибке. 

Открытая очередь сообщений закрывается функцией mq_close:

#include <mqueue.h>

int mq_close(mqd_t mqdes);

/*Возвращает 0 в случае успешного завершения. –1 в случае ошибки */

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

Для удаления из системы имени (пате), которое использовалось в качестве аргумента при вызове mq_open, нужно использовать функцию mq_unlink:

#include <mqueue.h>

int mq_unlink(const char *name);

/* Возвращает 0 в случае успешного завершения. –1 в случае ошибки */

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

Очереди сообщений Posix обладают по меньшей мере живучестью ядра (раздел 1.3), то есть они продолжают существовать, храня все имеющиеся в них сообщения, даже если нет процессов, в которых они были бы открыты. Очередь существует, пока она не будет удалена явно с помощью mq_unlink.

ПРИМЕЧАНИЕ

Мы увидим, что если очередь сообщений реализована через отображаемые в память файлы (раздел 12.2), она может обладать живучестью файловой системы, но это не является обязательным и рассчитывать на это нельзя.

Пример: программа mqcreate1

Поскольку очереди сообщений Posix обладают по крайней мере живучестью ядра, можно написать набор небольших программ для работы с ними — с этими программами будет проще экспериментировать. Программа из листинга 5.1[1] создает очередь сообщений, имя которой принимается в качестве аргумента командной строки.

Листинг 5.1. Создание очереди сообщений (указан флаг O_EXCL)

//pxmsg/mqcreate1.с

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   int с flags:

6   mqd_t mqd;

7   flags = O_RDWR | O_CREAT;

8   while ((c = Getopt(argc, argv, "e")) != –1) {

9    switch (c) {

10   case 'e':

11    flags |= O_EXCL;

12    break;

13   }

14  }

15  if (optind != argc – 1)

16   err_quit("usage: mqcreate [ –e ] <name>");

17  mqd = Mq_open(argv[optind], flags, FILE_MODE, NULL);

18  Mq_close(mqd);

19  exit(0);

20 }

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

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

Ниже приведен результат работы программы в Solaris 2.6:

solaris % mqcreate1 /temp.1234    очередь успешно создается

solaris % ls -l /tmp/.*1234

-rw-rw-rw– 1 rstevens other1 132632 Oct 23 17:08 /tmp/.MQDtemp.1234

-rw-rw-rw– 1 rstevens other1      0 Oct 23 17:08 /tmp/.MQLtemp.1234

-rw-r--r-- 1 rstevens other1      0 Oct 23 17:08 /tmp/.MQPDtemp.1234

solaris % mqcreate1 –e /temp.1234 очередь уже создана

mq_open error for /temp.1234: File exists

Мы назвали эту версию программы mqcreate1, поскольку она будет улучшена в листинге 5.4, после того как мы обсудим использование атрибутов очереди. Разрешения на доступ к третьему файлу определяются константой FILE_MODE (чтение и запись для пользователя, только чтение для группы и прочих пользователей), но у двух первых файлов разрешения отличаются. Можно предположить, что в файле с буквой D в имени хранятся данные; файл с буквой L представляет собой какую-то блокировку, а в файле с буквой Р хранятся разрешения.

В Digital Unix 4.0B мы указываем действительное имя создаваемого файла:

alpha % mqcreate1 /tmp/myq.1234    очередь успешно создается

alpha % ls –l /tmp/myq.1234

-rw-r--r-- 1 rstevens system 11976 Oct 23 17:04 /tmp/myq.1234

alpha % mqcreate1 –e /tmp/myq.1234 очередь уже создана

mq_open error for /tmp/myq.1234: File exists

Пример: программа mqunlink

В листинге 5.2 приведена программа mqunlink, удаляющая из системы очередь сообщений.

Листинг 5.2. Удаление очереди из системы: mqunlink

//pxmsg/mqunlink.c

1 #include "unpipc.h"

2 int

3 main(int argc, char **argv)

4 {

5  if (argc != 2)

6   err_quit("usage: mqunlink <name>");

7  Mq_unlink(argv[1]);

8  exit(0);

9 }

С помощью этой программы мы можем удалить очередь сообщений, созданную программой mqcreate1:

solaris % mqunlink /temp.1234

При этом будут удалены все три файла из каталога /tmp, которые относятся к этой очереди.

5.3. Функции mq_getattr и mq_setattr

У каждой очереди сообщений имеются четыре атрибута, которые могут быть получены функцией mq_getattr и установлены (по отдельности) функцией mq_setattr:

#include <mqueue.h>

int mq_getattr(mqd_t mqdes, struct mq_attr *attr);

int mq_setattr(mqd_t mqdes, const struct mq_attr *attr, struct mq_attr *oattr);

/* Обе функции возвращают 0 в случае успешного завершения; –1 – в случае возникновения ошибок */

Структура mq_attr хранит в себе эти четыре атрибута:

struct mq_attr {

 long mq_flags;   /* флаг очереди: 0, O_NONBLOCK */

 long mq_maxmsg;  /* максимальное количество сообщений в очереди */

 long mq_msgsize; /* максимальный размер сообщения (в байтах) */

 long mq_curmsgs; // текущее количество сообщений в очереди

}

Указатель на такую структуру может быть передан в качестве четвертого аргумента mq_open, что дает возможность установить параметры mq_maxmsg и mq_msgsize в момент создания очереди. Другие два поля структуры функцией mq_open игнорируются.

Функция mq_getattr присваивает полям структуры, на которую указывает attr, текущие значения атрибутов очереди.

Функция mq_setattr устанавливает атрибуты очереди, но фактически используется только поле mqflags той структуры, на которую указывает attr, что дает возможность сбрасывать или устанавливать флаг запрета блокировки. Другие три поля структуры игнорируются: максимальное количество сообщений в очереди и максимальный размер сообщения могут быть установлены только в момент создания очереди, а количество сообщений в очереди можно только считать, но не изменить.

Кроме того, если указатель oattr ненулевой, возвращаются предыдущие значения атрибутов очереди (mq_flags, mq_maxmsg, mq_msgsize) и текущий статус очереди (mq_curmsgs).

Пример: программа mqgetattr

Программа из листинга 5.3 открывает указанную очередь сообщений и выводит значения ее атрибутов.

Листинг 5.3. Получение и вывод значений атрибутов очереди сообщений

//pxmsg/mqgetattr.c

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   mqd_t mqd;

6   struct mq_attr attr;

7   if (argc != 2)

8    err_quit("usage: mqgetattr <name>");

9   mqd = Mq_open(argv[1], O_RDONLY);

10  Mq_getattr(mqd, &attr);

11  printf ("max #msgs = %ld, max #bytes/msg = %ld, "

12   "#currently on queue = %ld\n",

13   attr.mq_maxmsg, attr.mq_msgsize, attr.mq_curmsgs);

14  Mq_close(mqd);

15  exit(0);

16 }

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

solaris % mqcreate1 /hello.world

solaris % mqgetattr /hello.world

max #msgs = 128, max #bytes/msg = 1024, #currently on queue = 0

Вспомним размер одного из файлов очереди, созданной с использованием устанавливаемых по умолчанию значений атрибутов. Он был выведен командой ls в примере после листинга 5.1. Это значение можно получить как 128×1024+1560 = 132632.

Добавочные 1560 байт представляют собой, скорее всего, дополнительную информацию: 8 байт на сообщение плюс добавочные 536 байт.

Пример: программа mqcreate

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

Листинг 5.4. Усовершенствованная программа mqcreate

//pxmsg/mqcreate.c

1  #include "unpipc.h"

2  struct mq_attr attr; /* mq_maxmsg и mq_msgsize инициализируются О */

3  int

4  main(int argc, char **argv)

5  {

6   int с flags;

7   mqd_t mqd;

8   flags = O_RDWR | O_CREAT;

9   while ((c = Getopt(argc, argv, "em:z:")) != –1) {

10   switch (c) {

11   case 'e':

12    flags |= O_EXCL;

13    break;

14   case 'm':

15    attr.mq_maxmsg = atol(optarg);

16    break;

17   case 'z':

18    attr.mq_msgsize = atol(optarg);

19    break;

20   }

21  }

22  if (optind != argc – 1)

23   err_quit("usage: mqcreate [ –е ] [ –m maxmsg –z msgsize ] <name>");

24  if ((attr.mq_maxmsg != 0 && attr.mq_msgsize ==0) ||

25   (attr.mq_maxmsg == 0 && attr.mq_msgsize != 0))

26   err_quit("must specify both –m maxmsg and –z msgsize");

27  mqd = Mq_open(argv[optind], flags, FILE_MODE,

28   (attr.mq_maxmsg != 0) ? &attr : NULL);

29  Mq_close(mqd);

30  exit(0);

31 }

Параметр командной строки, требующий аргумента, указывается с помощью двоеточия (после параметров m и z в вызове getopt). В момент обработки символа параметр optarg указывает на аргумент.

ПРИМЕЧАНИЕ

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

solaris %mqcreate –z

mqcreate: option requires an argument – z

solaris %mqcreate –q

mqcreate: illegal option – q

В следующем примере ошибка (не указан необходимый аргумент — имя очереди) обнаруживается самой программой:

solaris %mqcreate

usage: mqcreate [ –e ] [ –m maxmsg –z msgsize ] <name>

Если не указан ни один из двух новых параметров, мы должны передать функции mq_open пустой указатель в качестве последнего аргумента. В противном случае мы передаем указатель на нашу структуру attr.

Запустим теперь новую версию нашей программы в системе Solaris 2.6, указав максимальное количество сообщений 1024 и максимальный размер сообщения 8192 байт:

solaris % mqcreate –e –m 1024 -z 8192 /foobar

solaris % ls –al /tmp/.*foobar

-rw-rw-rw– 1 rstevens other1 8397336 Oct 25 11:29 /tmp/.MQDfoobar

–rw-rw-rw– 1 rstevens other1       0 Oct 25 11:29 /tmp/.MQLfoobar

–rw-r--r-- 1 rstevens other1       0 Oct 25 11:29 /tmp/.MQPfoobar

Размер файла, содержащего данные этой очереди, соответствует максимальному количеству сообщений в очереди и максимальному размеру сообщения (1024×8192 = 8388608), а оставшиеся 8728 байт предусматривают 8 байт информации на каждое сообщение (8×1024) плюс дополнительные 536 байт. 

При выполнении той же программы в Digital Unix 4.0B получим:

alpha % mqcreate –m 256 -z 2048 /tmp/bigq

alpha % ls-l/tmp/bigq

-rw-r--r-- 1 rstevens system 537288 Oct 25 15:38 /tmp/bigq

В этой реализации размер очереди соответствует максимальному количеству сообщений и максимальному размеру сообщения (256×2048 = 524288), а оставшиеся 13000 байт дают возможность хранить 48 байт добавочной информации для каждого сообщения (48×256) и еще 712 байт.

5.4. Функции mqsend и mqreceive

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

ПРИМЕЧАНИЕ

В Solaris 2.6 значение MQ_PRIO_MAX равняется именно 32, но в Digital Unix 4.0B этот предел равен уже 256. В листинге 5.7 мы покажем, как получить эти значения.

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

ПРИМЕЧАНИЕ

Действие mq_receive отличается от действия msgrcv в System V (раздел 6.4). Сообщения System V имеют поле type, аналогичное по смыслу приоритету, но для функции msgrcv можно указать три различных алгоритма возвращения сообщений: старейшее сообщение в очереди, старейшее сообщение с указанным типом или старейшее сообщение с типом, не превышающим указанного значения.

#include <mqueue.h>

int mq_send(mqd_t mqdes, const char *ptr, size_t len, unsigned int prio);

/* Возвращает 0 в случае успешного завершения, –1 – в случае возникновения ошибок */

ssize_t mq_receive(mqd_t mqdes, char *ptr, size_t len, unsigned int *priop);

/* Возвращает количество байтов в сообщении в случае успешного завершения. –1 – в случае ошибки */

Первые три аргумента обеих функций аналогичны первым трем аргументам функций write и read соответственно.

ПРИМЕЧАНИЕ

Объявление указателя на буфер как char* кажется ошибкой — тип void* больше соответствовал бы по духу прочим функциям Posix.1. 

Значение аргумента len функции mq_receive должно быть по крайней мере не меньше максимального размера сообщения, которое может быть помещено в очередь, то есть значения поля mq_msgsize структуры mq_attr для этой очереди. Если len оказывается меньше этой величины, немедленно возвращается ошибка EMSGSIZE. 

ПРИМЕЧАНИЕ

Это означает, что большинству приложений, использующих очереди сообщений Posix, придется вызывать mq_getattr после открытия очереди для определения максимального размера сообщения, а затем выделять память под один или несколько буферов чтения этого размера. Требование, чтобы буфер был больше по размеру, чем максимально возможное сообщение, позволяет функции mq_receive не возвращать уведомление о том, что размер письма превышает объем буфера. Сравните это, например, с флагом MSG_NOERROR и ошибкой E2BIG для очередей сообщений System V (раздел 6.4) и флагом MSG_TRUNC для функции recvmsg, используемой с дейтаграммами UDP (раздел 13.5 [24]). 

Аргумент prio устанавливает приоритет сообщения для mq_send, его значение должно быть меньше MQ_PRIO_MAX. Если при вызове mq_receive priop является ненулевым указателем, в нем сохраняется приоритет возвращаемого сообщения. Если приложению не требуется использование различных приоритетов сообщений, можно указывать его равным нулю для mq_send и передавать mq_receive нулевой указатель в качестве последнего аргумента.

ПРИМЕЧАНИЕ

Разрешена передача сообщений нулевой длины. Это тот случай, когда важно не то, о чем говорится в стандарте (Posix.1), а то, о чем в нем не говорится: нигде не запрещена передача сообщений нулевой длины. Функция mq_receive возвращает количество байтов в сообщении (в случае успешного завершения работы) или –1 в случае возникновения ошибок, так что 0 обозначает сообщение нулевой длины. 

Очередям сообщений Posix и System V не хватает полезной функции: получатель не может определить отправителя сообщения. Эта информация могла бы пригодиться многим приложениям. К сожалению, большинство механизмов передачи сообщений IPC не позволяют определить отправителя сообщений. В разделе 15.5 мы расскажем, как эта возможность обеспечивается для дверей. В разделе 14.8 [24] описано, как эта возможность обеспечивается в BSD/OS для доменных сокетов Unix. В разделе 15.3.1 [21] описано, как SVR4 передает информацию об отправителе по каналу при передаче по нему дескриптора. В настоящее время методы BSD/OS широко используются, и хотя реализация SVR4 является частью стандарта Unix 98, она требует передачи дескриптора по каналу, что обычно является более дорогостоящей операцией, чем просто передача данных. Мы не можем предоставить отправителю возможность передать информацию о себе (например, эффективный идентификатор пользователя) в самом сообщении, поскольку мы не можем быть уверены, что эта информация окажется истинной. Хотя разрешения доступа к очереди сообщений определяют, имеет ли право отправитель помещать в нее сообщения, это все равно не дает однозначности. Существует возможность создавать одну очередь для каждого отправителя (о которой рассказывается в связи с очередями System V в разделе 6.8), но это плохо подходит для больших приложений. Наконец, если функции для работы с очередями сообщений реализованы как пользовательские функции (как мы показываем в разделе 5.8), а не как часть ядра, мы не можем доверять никакой информации об отправителе, передаваемой с сообщением, так как ее легко подделать. 

Пример: программа mqsend

В листинге 5.5 приведен текст программы, помещающей сообщение в очередь.

Листинг 5.5. Программа mqsend

//pxmsg/mqsend.c

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   mqd_t mqd;

6   void *ptr;

7   size_t len;

8   uint_t prio;

9   if (argc != 4)

10   err_quit("usage: mqsend <name> <#bytes> <priority>");

11  len = atoi(argv[2]);

12  prio = atoi(argv[3]);

13  mqd = Mq_open(argv[1], O_WRONLY);

14  ptr = Calloc(len, sizeof (char));

15  Mq_send(mqd, ptr, len, prio);

16  exit(0);

17 }

И размер сообщения, и его приоритет являются обязательными аргументами командной строки. Буфер под сообщение выделяется функцией callос, которая инициализирует его нулем.

Пример: программа mqreceive

Программа в листинге 5.6 считывает сообщение из очереди.

Листинг 5.6. Программа mqreceive

//pxmsg/mqreceive.с

1  #include "unpipc.h"

2  int

3  main(int argc, char **argv)

4  {

5   int с flags;

6   mqd_t mqd;

7   ssize_t n;

8   uint_t prio;

9   void *buff;

10  struct mq_attr attr;

11  flags = O_RDONLY;

12  while ((c = Getopt(argc, argv, "n")) != –1) {

13   switch (c) {

14   case 'n':

15    flags |= O_NONBLOCK;

16    break;

17   }

18  }

19  if (optind != argc – 1)

20   err_quit("usage: mqreceive [ –n ] <name>");

21  mqd = Mq_open(argv[optind], flags);

22  Mq_getattr(mqd, &attr);

23  buff = Malloc(attr.mqjnsgsize);

24  n = Mq_receive(raqd, buff, attr.mq_msgsize, &prio);

25  printf("read %ld bytes, priority = %u\n", (long) n, prio);

26  exit(0);

27 }

Параметр -n запрещает блокировку

14-17 Параметр командной строки –n отключает блокировку. При этом программа возвращает сообщение об ошибке, если в очереди нет сообщений.

Открытие очереди и получение атрибутов

21-25 Мы открываем очередь и получаем ее атрибуты, вызвав mq_getattr. Нам обязательно нужно определить максимальный размер сообщения, потому что мы должны выделить буфер подходящего размера, чтобы вызвать mq_receive. Программа выводит размер считываемого сообщения и его приоритет.

ПРИМЕЧАНИЕ

Поскольку n имеет тип size_t и мы не знаем, int это или long, мы преобразуем эту величину к типу long и используем строку формата %ld. В 64-разрядной реализации int будет 32-разрядным целым, a long и size_t будут 64-разрядными целыми.

Воспользуемся обеими программами, чтобы проиллюстрировать использование поля приоритета.

solaris % mqcreate /test1

solaris % mqgetattr /test1        создаем очередь и смотрим на ее атрибуты

max #msgs = 128, max #bytes/msg = 1024, #currently on queue = 0

solaris % mqsend /test1 100 99999 отправка с некорректным значением приоритета

mq_send error: Invalid argument

solaris % mqsend /test1 100 6     100 байт, приоритет 6

solaris % mqsend /test1 50 18     50 байт, приоритет 18

solaris % mqsend /test1 33 18     33 байт, приоритет 18

solaris % mqreceive /test1

read 50 bytes, priority = 18         возвращается старейшее сообщение с

solaris % mqreceive /test1        наивысшим приоритетом

read 33 bytes, priority = 18

Solaris % mqreceive /test1

read 100 bytes, priority = 6

Solaris % mqreceive –n /test1     отключаем блокировку и убеждаемся, что очередь пуста

mq_receive error: Resource temporarily unavailable

Мы видим, что mq_receive действительно возвращает старейшее сообщение с наивысшим приоритетом. 

5.5. Ограничения очередей сообщений

Мы уже сталкивались с двумя ограничениями, устанавливаемыми для любой очереди в момент ее создания:

■ mq_maxmsg — максимальное количество сообщений в очереди;

■ mq_msgsize — максимальный размер сообщения.

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

Другие два ограничения определяются реализацией:

■ MQ_OPEN_MAX — максимальное количество очередей сообщений, которые могут быть одновременно открыты каким-либо процессом (Posix требует, чтобы эта величина была не меньше 8);

■ MQ_PRIO_MAX — максимальное значение приоритета плюс один (Posix требует, чтобы эта величина была не меньше 32).

Эти две константы часто определяются в заголовочном файле <unistd.h> и могут быть получены во время выполнения программы вызовом функции sysconf, как мы покажем далее.

Пример: программа mqsysconf

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

Листинг 5.7. Получение ограничений очередей с помощью sysconf

//pxmsg/mqsysconf.с

1 #include "unpipc.h"

2 int

3 main(int argc, char **argv)

4 {

5  printf("MQ_OPEN_MAX = %ld, MQ_PRIO_MAX = %ld\n",

6  Sysconf(_SC_MQ_OPEN_MAX), Sysconf(_SC_MQ_PRIO_MAX));

7  exit(0);

8 }

Запустив эту программу в наших двух операционных системах, получим:

solaris % mqsysconf

MQ_OPEN_MAX = 32, MQ_PRIO_MAX = 32

alpha % mqsysconf

MQ_OPEN_MAX = 64, MQ_PRIO_MAX = 256

5.6. Функция mq_notify

Один из недостатков очередей сообщений System V, как мы увидим в главе 6, заключается в невозможности уведомить процесс о том, что в очередь было помещено сообщение. Мы можем заблокировать процесс при вызове msgrcv, но тогда мы не сможем выполнять другие действия во время ожидания сообщения. Если мы укажем флаг отключения блокировки при вызове msgrcv (IPC_NOWAIT), процесс не будет заблокирован, но нам придется регулярно вызывать эту функцию, чтобы получить сообщение, когда оно будет отправлено. Мы уже говорили, что такая процедура называется опросом и на нее тратится лишнее время. Нужно, чтобы система сама уведомляла процесс о том, что в пустую очередь было помещено новое сообщение.

ПРИМЕЧАНИЕ

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

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

Мы включаем режим уведомления с помощью функции mq_notify:

#include <mqueue.h>

int mq_notify(mqd_t mqdes, const struct sigevent *notification);

/* Возвращает 0 в случае успешного выполнения, –1 – в случае ошибки */

Эта функция включает и выключает асинхронное уведомление о событии для указанной очереди. Структура sigevent впервые появилась в стандарте Posix.1 для сигналов реального времени, о которых более подробно рассказано в следующем разделе. Эта структура и все новые константы, относящиеся к сигналам, определены в заголовочном файле <signal.h>:

union sigval {

 int sival_int; /* целое значение */

 void *sival_ptr; /* указатель */

};


struct sigevent {

 int sigev_notify; /* SIGEV_{NONE,SIGNAL,THREAD} */

 int sigev_signo; /* номер сигнала, если SIGEV_SIGNAL */

 union sigval sigev_value; /* передается обработчику сигнала или потоку */

/* Следующие два поля определены для SIGEV_THREAD */

void (*sigev_notify_function) (union sigval);

pthread_attr_t *sigev_notify_attributes;

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

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

2. Если аргумент notification представляет собой нулевой указатель и процесс уже зарегистрирован на уведомление для данной очереди, то уведомление для него отключается.

3. Только один процесс может быть зарегистрирован на уведомление для любой данной очереди в любой момент.

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

5. При отправке уведомления зарегистрированному процессу регистрация снимается. Процесс должен зарегистрироваться снова (если в этом есть необходимость), вызвав mq_notify еще раз.

ПРИМЕЧАНИЕ

С сигналами в Unix всегда была связана одна проблема: действие сигнала сбрасывалось на установленное по умолчанию каждый раз при отправке сигнала (раздел 10.4 [21]). Обычно первой функцией, вызываемой обработчиком сигнала, была signal, переопределявшая обработчик. Это создавало небольшой временной промежуток между отправкой сигнала и переопределением обработчика, в который процесс мог быть завершен при повторном появлении того же сигнала. На первый взгляд может показаться, что та же проблема должна возникать и при использовании mq_notify, поскольку процесс должен перерегистрироваться каждый раз после появления уведомления. Однако очереди сообщений отличаются по своим свойствам от сигналов, поскольку необходимость отправки уведомления не может возникнуть, пока очередь не будет пуста. Следовательно, необходимо аккуратно перерегистрироваться на получение уведомления до считывания пришедшего сообщения из очереди.

Пример: простая программа с уведомлением

Прежде чем углубляться в тонкости сигналов реального времени и потоков Posix, мы напишем простейшую программу, включающую отправку сигнала SI6USR1 при помещении сообщения в пустую очередь. Эта программа приведена в листинге 5.8, и мы отметим, что она содержит ошибку, о которой мы вскоре поговорим подробно.

Листинг 5.8. Отправка sigusr1 при помещении сообщения в пустую очередь (неправильная версия программы)

//pxmsg/mqnotifysigl.c

1  #include "unpipc.h"

2  mqd_t mqd;

3  void *buff;

4  struct mq_attr attr;

5  struct sigevent sigev;

6  static void sig_usrl(int);


7  int

8  main(int argc, char **argv)

9  {

10  if (argc != 2)

11   err_quit("usage: mqnotifysig1 <name>");

12  /* открываем очередь, получаем атрибуты, выделяем буфер */

13  mqd = Mq_open(argv[1], O_RDONLY);

14  Mq_getattr(mqd, &attr);

15  buff = Malloc(attr.mq_msgsize);

16  /* устанавливаем обработчик, включаем уведомление */

17  Signal(SIGUSR1, sig_usr1);

18  sigev.sigev_notify = SIGEV_SIGNAL;

19  sigev.sigev_signo = SIGUSR1;

20  Mq_notify(mqd, &sigev);

21  for (;;)

22   pause(); /* все делает обработчик */

23  exit(0);

24 }


25 static void

26 sig_usr1(int signo)

27 {

28  ssize_t n;

29  Mq_notify(mqd, &sigev); /* сначала перерегистрируемся */

30  n = Mq_receive(mqd, buff, attr.mq_msgsize, NULL);

31  printf("SIGUSR1 received, read %ld bytes\n", (long) n);

32  return;

33 }

Объявление глобальных переменных

2-6 Мы объявляем несколько глобальных переменных, используемых совместно функцией main и нашим обработчиком сигнала (sig_usr1).

Открытие очереди, получение атрибутов, выделение буфера чтения

12-15 Мы открываем очередь сообщений, получаем ее атрибуты и выделяем буфер считывания соответствующего размера.

Установка обработчика сигнала, включение уведомления

16-20 Сначала мы устанавливаем свой обработчик для сигнала SIGUSR1. Мы присваиваем полю sigev_notify структуры sigevent значение SIGEV_SIGNAL, что говорит системе о необходимости отправки сигнала, когда очередь из пустой становится непустой. Полю sigev_signo присваивается значение, соответствующее тому сигналу, который мы хотим получить. Затем вызывается функция mq_notify.

Бесконечный цикл

Функция main после этого зацикливается, и процесс приостанавливается при вызове pause, возвращающей –1 при получении сигнала.

Получение сигнала, считывание сообщения

Обработчик сигнала вызывает mq_notify для перерегистрации, считывает сообщение и выводит его длину. В этой программе мы игнорируем приоритет полученного сообщения. 

ПРИМЕЧАНИЕ

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

Запустим теперь эту программу в одном из окон

solaris % mqcreate /test1

solaris % mqnotifysig1 /test1

и затем выполним следующую команду в другом окне

solaris % mqsend /test1 50 16

Как и ожидалось, программа mqnotifysig1 выведет сообщение: SIGUSR1 received, read 50 bytes.

Мы можем проверить, что только один процесс может быть зарегистрирован на получение уведомления в любой момент, запустив копию пpoгрaммы в другом окне:

solaris % mqnotifysig1 /test1

mq_notify error: Device busy

Это сообщение соответствует коду ошибки EBUSY.

Сигналы Posix: функции типа Async-Signal-Safe

Недостаток пpoгрaммы из листинга 5.8 в том, что она вызывает mq_notify, mq_receive и printf из обработчика сигнала. Ни одну из этих функций вызывать оттуда не следует.

Функции, которые могут быть вызваны из обработчика сигнала, относятся к группе, называемой, согласно Posix, async-signal-safe functions (функции, обеспечивающие безопасную обработку асинхронных сигналов). В табл. 5.1 приведены эти функции по стандарту Posix вместе с некоторыми дополнительными, появившимися только в Unix 98.

Функции, которых нет в этом списке, не должны вызываться из обработчика сигнала. Обратите внимание, что в списке отсутствуют стандартные функции библиотеки ввода-вывода и функции pthread_XXX для работы с потоками. Из всех функций IPC, рассматриваемых в этой книге, в список попали только sem_post, read и write (подразумевается, что последние две используются с программными каналами и FIFO).

ПРИМЕЧАНИЕ

Стандарт ANSI С указывает четыре функции, которые могут быть вызваны из обработчика сигналов: abort, exit, longjmp, signal. Первые три отсутствуют в списке функций async-signal-safe стандарта Unix 98. 


Таблица 5.1. Функции, относящиеся к группе async-signal-safe

access        fpathconf rename      sysconf

aio_return    fstat     rmdir       tcdrain

aio_suspend   fsync     sem_post    tcflow 

alarm         getegid   setgid      tcflush

cfgetispeed   geteuid   setpgid     tcgetattr

cfgetospeed   getgid    setsid      tcgetgrp

cfsetispeed   getgroups setuid      tcsendbreak

cfsetospeed   getpgrp   sigaction   tcsetattr

chdir         getpid    sigaddset   tcsetpgrp

chmod         getppid   sigdelset   time

chown         getuid    sigemptyset timer_getoverrun

clock_gettime kill      sigfillset  timer_gettime

close         link      sigismember timer_settime

creat         lseek     signal      times

dup           mkdir     sigpause    umask

dup2          mkfifo    sigpending  uname

execle        open      sigprocmask unlink

execve        pathconf  sigqueue    utime

_exit         pause     sigset      wait

fcntl         pipe      sigsuspend  waitpid

fdatasync     raise     sleep       write

fork          read      stat

Пример: уведомление сигналом

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

Глобальная переменная

2 Поскольку единственное действие, выполняемое обработчиком сигнала, заключается в присваивании ненулевого значения флагу mqflag, глобальным переменным из листинга 5.8 уже не нужно являться таковыми. Уменьшение количества глобальных переменных — это всегда благо, особенно при использовании программных потоков.

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

15-18 Мы открываем очередь сообщений, получаем ее атрибуты и выделяем буфер считывания.

Инициализация наборов сигналов

19-22 Мы инициализируем три набора сигналов и устанавливаем бит для сигнала SIGUSR1 в наборе newmask.

Установка обработчика сигнала, включение уведомления

23-27 Мы устанавливаем обработчик сигнала для SIGUSR1, присваиваем значения полям структуры sigevent и вызываем mq_notify. 

Листинг 5.9. Обработчик сигнала устанавливает флаг для главного потока (неправильная версия)

//pxmsg/mqnotifysig2.c

1  #include "unpipc.h"

2  volatile sig_atomic_t mqflag; /* ненулевое значение устанавливается обработчиком */

3  static void sig_usrl(int);


4  int

5  main(int argc, char **argv)

6  {

7   mqd_t mqd;

8   void *buff;

9   ssize_t n;

10  sigset_t zeromask, newmask, oldmask;

11  struct mq_attr attr;

12  struct sigevent sigev;

13  if (argc != 2)

14   err_quit("usage: mqnotifysig2 <name>");

15  /* открытие очереди, получение атрибутов, выделение буфера */

16  mqd = Mq_open(argv[1], O_RDONLY);

17  Mq_getattr(mqd, &attr);

18  buff = Malloc(attr.mq_msgsize);

19  Sigemptyset(&zeromask); /* сигналы не блокируются */

20  Sigemptyset(&newmask);

21  Sigemptyset(&oldmask);

22  Sigaddset(&newmask, SIGUSR1);

23  /* установка обработчика, включение уведомления */

24  Signal(SIGUSR1, sig_usr1);

25  sigev.sigev_notify = SIGEV_SIGNAL;

26  sigev.sigev_signo = SIGUSR1;

27  Mq_notify(mqd, &sigev);

28  for (;;) {

29   Sigprocmask(SIG_BLOCK, &newmask, &oldmask); /* блокируем SIGUSR1 */

30   while (mqflag == 0)

31    sigsuspend(&zeromask);

32   mqflag = 0; /* сброс флага */

33   Mq_notify(mqd, &sigev); /* перерегистрируемся */

34   n = Mq_receive(mqd, buff, attr.mq_msgsize, NULL);

35   printf("read %ld bytes\n", (long) n);

36   Sigprocmask(SIG_UNBLOCK, &newmask, NULL); /* разблокируем SIGUSR1 */

37  }

38  exit(0);

39 }


40 static void

41 sig_usr1(int signo)

42 {

43  mqflag = 1;

44  return;

45 } 

Ожидание установки флага обработчиком

28-32 Мы вызываем sigprocmask, чтобы заблокировать SIGUSR1, сохраняя текущую маску сигналов в oldmask. Затем мы в цикле проверяем значение глобального флага mqflag, ожидая, когда обработчик сигнала установит его в ненулевое значение. Пока значение этого флага равно нулю, мы вызываем sigsuspend, что автоматически приостанавливает вызывающий поток и устанавливает его маску в zeromask (сигналы не блокируются). Раздел 10.16 [21] рассказывает о функции sigsuspend более подробно. Также там объясняются причины, по которым мы должны проверять значение переменной mqflag только при заблокированном сигнале SIGUSR1. Каждый раз при выходе из sigsuspend сигнал SIGUSR1 блокируется.

Перерегистрация и считывание сообщения

33-36 Когда флаг mqflag принимает ненулевое значение, мы регистрируемся на получение уведомления заново и считываем сообщение из очереди. Затем мы разблокируем сигнал SIGUSR1 и возвращаемся к началу цикла.

Мы уже говорили, что в этой версии программы также присутствует ошибка. Посмотрим, что произойдет, если в очередь попадут два сообщения, прежде чем будет считано первое из них. Мы можем имитировать это, добавив sleep перед вызовом mq_notify. Проблема тут в том, что уведомление отсылается только в том случае, когда сообщение помещается в пустую очередь. Если в очередь поступают два сообщения, прежде чем первое будет считано, то отсылается только одно уведомление. Тогда мы считываем первое сообщение и вызываем sigsuspend, ожидая поступления еще одного. А в это время в очереди уже имеется сообщение, которое мы должны прочитать, но которое мы никогда не прочтем.

Пример: уведомление сигналом с отключением блокировки

Исправить описанную выше ошибку можно, отключив блокировку операции считывания сообщений. Листинг 5.10 содержит измененную версию программы из листинга 5.9. Новая программа считывает сообщения в неблокируемом режиме.

Листинг 5.10. Использование уведомления с помощью сигнала для считывания сообщения из очереди сообщений Posix

//pxmsg/mqnotifysig3.с

1  #include "unpipc.h"

2  volatile sig_atomic_t mqflag; /* ненулевое значение устанавливается обработчиком сигнала */

3  static void sig_usr1(int);


4  int

5  main(int argc, char **argv)

6  {

7   mqd_t mqd;

8   void *buff;

9   ssize_t n;

10  sigset_t zeromask, newmask, oldmask;

11  struct mq_attr attr;

12  struct sigevent sigev;

13  if (argc != 2)

14   err_quit("usage: mqnotifysig3 <name>");

15  /* открытие очереди, получение атрибутов, выделение буфера */

16  mqd = Mq_open(argv[1], O_RDONLY | O_NONBLOCK);

17  Mq_getattr(mqd, &attr);

18  buff = Malloc(attr.mq_msgsize);

19  Sigemptyset(&zeromask); /* сигналы не блокируются */

20  Sigemptyset(&newmask);

21  Sigemptyset(&oldmask);

22  Sigaddset(&newmask, SIGUSR1);

23  /* установка обработчика, включение уведомления */

24  Signal(SIGUSR1, sig_usr1);

25  sigev.sigev_notify = SIGEV_SIGNAL;

26  sigev.sigev_signo = SIGUSR1;

27  Mq_notify(mqd, &sigev);

28  for (;;) {

29   Sigprocmask(SIG_BLOCK, &newmask, &oldmask); /* блокируем SIGUSR1 */

30   while (mqflag == 0)

31    sigsuspend(&zeromask);

32   mqflag = 0; /* сброс флага */

33   Mq_notify(mqd, &sigev); /* перерегистрируемся */

34   while ((n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {

35    printf("read $ld bytes\n", (long) n);

36   }

37   if (errno != EAGAIN)

38    err_sys("mq_receive error");

39   Sigprocmask(SIG_UNBLOCK, &newmask, NULL); /* разблокируем SIGUSR1 */

40  }

41  exit(0);

42 }


43 static void

44 sig_usr1(int signo)

45 {

46  mqflag = 1;

47  return;

48 }

Открытие очереди сообщений в режиме отключенной блокировки

15-18 Первое изменение в программе: при открытии очереди сообщений указывается флаг O_NONBLOCK.

Считывание всех сообщений из очереди

34-38 Другое изменение: mq_receive вызывается в цикле, считывая все сообщения в очереди, пока не будет возвращена ошибка с кодом EAGAIN, означающая отсутствие сообщений в очереди. 

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

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

#include <signal.h>

int sigwait(const sigset_t *set, int *sig);

/* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */

Перед вызовом sigwait мы блокируем некоторые сигналы. Набор блокируемых сигналов указывается в качестве аргумента set. Функция sigwait блокируется, пока не придет по крайней мере один из этих сигналов. Когда он будет получен, функция возвратит его. Значение этого сигнала сохраняется в указателе sig, а функция возвращает значение 0. Это называется синхронным ожиданием асинхронного события: мы используем сигнал, но не пользуемся асинхронным обработчиком сигнала.

В листинге 5.11 приведен текст программы, использующей mq_notifу и sigwait.

Листинг 5.11. Использование mq_notify совместно с sigwait

//pxmsg/mqnotifysig4.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int signo;

6   mqd_t mqd;

7   void *buff;

8   ssize_t n;

9   sigset_t newmask;

10  struct mq_attr attr;

11  struct sigevent sigev;

12  if (argc != 2)

13   err_quit("usage: mqnotifysig4 <name>");

14  /* открытие очереди, получение атрибутов, выделение буфера */

15  mqd = Mq_open(argv[1], O_RDONLY | O_NONBLOCK);

16  Mq_getattr(mqd, &attr);

17  buff = Malloc(attr.mq_msgsize);

18  Sigemptyset(&newmask);

19  Sigaddset(&newmask, SIGUSR1);

20  Sigprocmask(SIG_BLOCK, &newmask, NULL); /* блокируем SIGUSR1 */

21  /* установка обработчика, включение уведомления */

22  sigev.sigev_notify = SIGEV_SIGNAL;

23  sigev.sigev_signo = SIGUSR1;

24  Mq_notify(mqd, &sigev);

25  for (;;) {

26   Sigwait(&newmask, &signo);

27   if (signo == SIGUSR1) {

28    Mq_notify(mqd, &sigev); /* перерегистрируемся */

29    while ((n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {

30     printf("read %ld bytes\n", (long) n);

31    }

32    if (errno != EAGAIN)

33     err_sys("mq_receive error");

34   }

35  }

36  exit(0);

37 }

Инициализация набора сигналов и блокировка SIGUSR1

18-20 Инициализируется один набор сигналов, содержащий только SIGUSR1, а затем этот сигнал блокируется sigprocmask.

Ожидание сигнала

26-34 Мы блокируем выполнение программы и ждем прихода сигнала, вызвав sigwait. При получении сигнала SIGUSR1 мы перерегистрируемся на уведомление и считываем все доступные сообщения.

ПРИМЕЧАНИЕ

Функция sigwait часто используется в многопоточных процессах. Действительно, глядя на прототип функции, мы можем заметить, что возвращаемое значение будет 0 или одной из ошибок Еххх, что весьма похоже на функции Pthread. Однако в многопоточном процессе нельзя пользоваться sigprocmask — вместо нее следует вызывать pthread_ sigmask, которая изменяет маску сигналов только для вызвавшего ее потока. Аргументы pthread_sigmask совпадают с аргументами sigprocmask.

Существуют два варианта функции sigwait: sigwaitinfo возвращает структуру siginfo_t (которая будет определена в следующем разделе) и предназначена для использования с надежными сигналами; функция sigtimedwait также возвращает структуру siginfo_t и позволяет вызывающему процессу установить ограничение по времени на ожидание.

Большая часть книг о многопоточном программировании, таких как [3], рекомендуют пользоваться sigwait для обработки всех сигналов в многопоточном процессе и не использовать асинхронные обработчики. 

Пример: очереди сообщений Posix и функция select

Дескриптор очереди сообщений (переменная типа mqd_t) не является «обычным» дескриптором и не может использоваться с функциями select и poll (глава 6 [24]). Тем не менее их можно использовать вместе с каналом и функцией mq_notify. (Аналогичный метод применен в разделе 6.9 для очередей System V, где создается дочерний процесс и канал связи.) Прежде всего обратите внимание, что, согласно табл. 5.1, функция write принадлежит к группе async-signal-safe, поэтому она может вызываться из обработчика сигналов. Программа приведена в листинге 5.12.

Листинг 5.12. Использование уведомления с помощью сигнала и канала

//pxmsg/mqnotifysig5.c

1  #include "unpipc.h"

2  int pipefd[2];

3  static void sig_usr1(int);


4  int

5  main(int argc, char **argv)

6  {

7   int nfds;

8   char c;

9   fd_set rset;

10  mqd_t mqd;

11  void *buff;

12  ssize_t n;

13  struct mq_attr attr;

14  struct sigevent sigev;

15  if (argc != 2)

16   err_quit("usage: mqnotifysig5 <name>");

17  /* открытие очереди, получение атрибутов, выделение буфера */

18  mqd = Mq_open(argv[1], O_RDONLY | O_NONBLOCK);

19  Mq_getattr(mqd, &attr);

20  buff = Malloc(attr.mq_msgsize);

21  Pipe(pipefd);

22  /* установка обработчика, включение уведомления */

23  Signal(SIGUSR1, sig_usr1);

24  sigev.sigev_notify = SIGEV_SIGNAL;

25  sigev.sigev_signo = SIGUSR1;

26  Mq_notify(mqd, &sigev);

27  FD_ZERO(&rset);

28  for (;;) {

29   FD_SET(pipefd[0], &rset);

30   nfds = Select(pipefd[0] + 1, &rset, NULL, NULL, NULL);

31   if (FD_ISSET(pipefd[0], &rset)) {

32    Read(pipefd[0], &c, 1);

33    Mq_notify(mqd, &sigev); /* перерегистрируемся */

34    while ((n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {

35     printf("read %ld bytes\n", (long) n);

36    }

37    if (errno != EAGAIN)

38     err_sys("mq_receive error");

39   }

40  }

41  exit(0);

42 }


43 static void

44 sig_usr1(int signo)

45 {

46  Write(pipefd[1], "", 1); /* один байт – 0 */

47  return;

48 }

Создание канала

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

Вызов select

27-40 Мы инициализируем набор дескрипторов rset и при каждом проходе цикла включаем бит, соответствующий дескриптору pipefd[0] (открытый на считывание конец канала). Затем мы вызываем функцию select, ожидая получения единственного дескриптора, хотя в типичном приложении именно здесь осуществлялось бы размножение дескрипторов одного из концов канала. Когда появляется возможность читать из канала, мы перерегистрируемся на уведомление и считываем все доступные сообщения.

Обработчик сигнала

43-48 Единственное, что делает обработчик сигнала, — записывает в канал 1 байт. Как мы уже отмечали, эта операция относится к разрешенным для асинхронных обработчиков.

Пример: запуск нового потока

Альтернативой снятию блокировки сигналом является присваивание sigev_notify значения SIGEV_THREAD, что приводит к созданию нового потока. Функция, указанная в sigev_notify_function, вызывается с параметром sigev_value. Атрибуты нового канала указываются переменной sigev_notify_attributes, которая может быть и нулевым указателем, если нас устраивают устанавливаемые по умолчанию атрибуты. Текст программы приведен в листинге 5.13.

Листинг 5.13. Функция mq_notify, запускающая новый программный поток

//pxmsg/mqnotifythread1.с

1  #include "unpipc.h"

2  mqd_t mqd;

3  struct mq_attr attr;

4  struct sigevent sigev;

5  static void notify_thread(union sigval); /* наш поток */


6  int

7  main(int argc, char **argv)

8  {

9   if (argc != 2)

10   err_quit("usage: mqnotifythread1 <name>");

11  mqd = Mq_open(argv[1], O_RDONLY | O_NONBLOCK);

12  Mq_getattr(mqd, &attr);

13  sigev.sigev_notify = SIGEV_THREAD;

14  sigev.sigev_value.sival_ptr = NULL;

15  sigev.sigev_notify_function = notify_thread;

16  sigev.sigev_notify_attributes = NULL;

17  Mq_notify(mqd, &sigev);

18  for (;;)

19   pause(); /* новый поток делает все */

20  exit(0);

21 }


22 static void

23 notify_thread(union sigval arg)

24 {

25  ssize_t n;

26  void *buff;

27  printf("notify_thread started\n");

28  buff = Malloc(attr.mq_msgsize);

29  Mq_notify(mqd, &sigev); /* перерегистрируемся */

30  while ((n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {

31   printf("read %ld bytes\n", (long) n);

32  }

33  if (errno != EAGAIN)

34   err_sys("mq_receive error");

35  free(buff);

36  pthread_exit(NULL);

37 }

Мы задаем нулевой указатель в качестве аргумента нового потока (sigev_value), поэтому функции start нового потока ничего не передается. Мы могли бы передать указатель на дескриптор, вместо того чтобы декларировать его как глобальный, но новому потоку все равно нужно получать атрибуты очереди сообщений и структуру sigev (для перерегистрации). Мы также указываем нулевой указатель в качестве атрибутов нового потока, поэтому используются установки по умолчанию. Новые потоки создаются как неприсоединенные (detached threads).

ПРИМЕЧАНИЕ

К сожалению, ни одна из использовавшихся для проверки примеров систем (Solaris 2.6 и Digital Unix 4.0B) не поддерживает SIGEV_THREAD. Обе они допускают только два значения sigev_notify: SIGEV_NONE и SIGEV_SIGNAL.

5.7. Сигналы реального времени Posix

За прошедшие годы сигналы в Unix много раз претерпевали революционные изменения.

1. Модель сигналов, предлагавшаяся в Unix Version 7 (1978), была ненадежной. Сигналы могли быть потеряны, и процессу было трудно отключить отдельные сигналы при выполнении отдельных участков кода.

2. В версии 4.3BSD (1986) надежные сигналы были добавлены.

3. Версия System V Release 3.0 (1986) также добавила надежные сигналы, хотя и иначе, чем BSD.

4. Стандарт Posix.1 (1990) увековечил модель надежных сигналов BSD, и эта модель подробно описана в главе 10 [21].

5. Posix.1 (1996) добавил к модели Posix сигналы реального времени. Это произросло из расширений реального времени Posix.1b (которые были названы Posix.4).

Почти все системы Unix в настоящее время поддерживают надежные сигналы, а новейшие системы предоставляют также и сигналы реального времени стандарта Posix. (Следует различать надежные сигналы и сигналы реального времени.) О сигналах реального времени следует поговорить подробнее, поскольку мы уже столкнулись с некоторыми структурами, определяемыми этим расширением стандарта, в предыдущем разделе (структуры sigval и sigevent).

Сигналы могут быть отнесены к двум группам:

1. Сигналы реального времени, которые могут принимать значения между SIGRTMIN и SIGRTMAX включительно. Posix требует, чтобы предоставлялось по крайней мере RTSIG_MAX сигналов, и минимальное значение этой константы равно 8.

2. Все прочие сигналы: SIGALRM, SIGINT, SIGKILL и пр.

ПРИМЕЧАНИЕ

В Solaris 2.6 обычные сигналы Unix нумеруются с 1 по 37, а 8 сигналов реального времени имеют номера с 38 по 45. В Digital Unix 4.0B обычные сигналы нумеруются с 1 по 32, а 16 сигналов реального времени имеют номера с 33 по 48. Обе реализации определяют SIGRTMIN и SIGRTMAX как макросы, вызывающие sysconf, что позволяет изменять их значения.

Далее все зависит от того, установлен ли процессом, получившим сигнал, флаг SA_SIGINFO при вызове sigaction. В итоге получаются четыре возможных сценария, приведенных в табл. 5.2.


Таблица 5.2. Поведение сигналов Posix в реальном времени в зависимости от SA_SIGINFO 

Сигнал Флаг SA_SIGINFO указан Флаг SA_SIGINFO не указан
От SIGRTMIN до SIGRTMAX Гарантируются характеристики реального времени Характеристики реального времени не обязательны
Все прочие сигналы Характеристики реального времени не обязательны Характеристики реального времени не обязательны

Смысл фразы «характеристики реального времени не обязательны» следующий: некоторые реализации могут обрабатывать эти сигналы как сигналы реального времени, но это не обязательно. Если мы хотим, чтобы сигналы обрабатывались как сигналы реального времени, мы должны использовать сигналы с номерами от SIGRTMIN до SIGRTMAX и должны указать флаг SA_SIGINFO при вызове sigaction при установке обработчика сигнала.

Термин «характеристики реального времени» подразумевает следующее:

■ Сигналы помещаются в очередь. Если сигнал будет порожден трижды, он будет трижды получен адресатом. Более того, повторения одного и того же сигнала доставляются в порядке очереди (FIFO). Мы вскоре покажем пример очереди сигналов. Если же сигналы в очередь не помещаются, трижды порожденный сигнал будет получен лишь один раз.

■ Когда в очередь помещается множество неблокируемых сигналов в диапазоне SIGRTMIN—SIGRTMAX, сигналы с меньшими номерами доставляются раньше сигналов с большими номерами. То есть сигнал с номером SIGRTMIN имеет «больший приоритет», чем сигнал с номером SIGRTMIN+1, и т.д.

■ При отправке сигнала, не обрабатываемого как сигнал реального времени, единственным аргументом обработчика является номер сигнала. Сигналы реального времени несут больше информации, чем прочие сигналы. Обработчик для сигнала реального времени, устанавливаемый с флагом SA_SIGINFO, объявляется как

void func(int signo, siginfo_t *info, void *context);

где signo— номер сигнала, a siginfo_t — структура, определяемая как

typedef struct {

 int si_signo; /* то же, что и signo */

 int si_code; /* SI_{USER,QUEUE,TIMER,ASYNCIO,MESGQ} */

 union sigval si_value; /* целое или указатель от отправителя */

} siginfo_t;

На что указывает context — зависит от реализации.

ПРИМЕЧАНИЕ

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

Тип siginfo_t является единственной структурой Posix, определяемой оператором typedef с именем, оканчивающимся на _t. В листинге 5.14 мы объявляем указатели на эти структуры как siginfo_t * без слова struct. 

■ Для работы с сигналами реального времени добавлено несколько новых функций. Например, для отправки сигнала какому-либо процессу используется функция sigqueue вместо kill. Новая функция позволяет отправить вместе с сигналом структуру sigval.

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

■ SI_ASYNCIO — сигнал был порожден по завершении асинхронного запроса на ввод или вывод одной из функций Posix aio_XXX, которые мы не рассматриваем;

■ SI_MESGQ — сигнал был порожден при помещении сообщения в пустую очередь сообщений (как в разделе 5.6); 

■ SI_QUEUE — сигнал был отправлен функцией sigqueue. Пример будет вскоре приведен;

■ SI_TIMER — сигнал был порожден по истечении установленного функцией timer_settime времени. Эту функцию мы не описываем;

■ SI_USER — сигнал был отправлен функцией kill.

Если сигнал был порожден каким-либо другим событием, si_code будет иметь значение, отличающееся от приведенных выше. Значение поля si_value структуры siginfo_t актуально только в том случае, если si_code имеет одно из следующих значений: SI_ASYNCIO, SI_MESGQ, SI_QUEUE и SI_TIMER.

Пример

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

Листинг 5.14. Тестовая программа, иллюстрирующая работу с сигналами реального времени

//rtsignals/test1.c

1  #include "unpipc.h"

2  static void sig_rt(int, siginfo_t *, void *);


3  int

4  main(int argc, char **argv)

5  {

6   int i, j;

7   pid_t pid;

8   sigset_t newset;

9   union sigval val;

10  printf("SIGRTMIN = %d, SIGRTMAX = %d\n", (int) SIGRTMIN, (int) SIGRTMAX);

11  if ((pid = Fork()) == 0) {

12   /* дочерний процесс блокирует 3 сигнала */

13   Sigemptyset(&newset);

14   Sigaddset(&newset, SIGRTMAX);

15   Sigaddset(&newset, SIGRTMAX – 1);

16   Sigaddset(&newset, SIGRTMAX – 2);

17   Sigprocmask(SIG_BLOCK, &newset, NULL);

18   /* установка обработчика с флагом SA_SIGINFO */

19   Signal_rt(SIGRTMAX, sig_rt);

20   Signal_rt(SIGRTMAX – 1, sig_rt);

21   Signal_rt(SIGRTMAX – 2, sig_rt);

22   sleep(6); /* родительский процесс посылает все сигналы */

23   Sigprocmask(SIG UNBLOCK, &newset, NULL); /* разблокируемся */

24   sleep(3); /* получаем сигналы */

25   exit(O);

26  }

27  /* родительский процесс отправляет сигналы */

28  sleep(3); /* дочерний процесс блокирует сигналы */

29  for (i = SIGRTMAX; i >= SIGRTMAX – 2; i--) {

30   for (j = 0; j <= 2; j++) {

31    val.sival_int = j;

32    Sigqueue(pid, i, val);

33    printf("sent signal %d, val = %d\n", i, j);

34   }

35  }

36  exit(0);

37 }


38 static void

39 sig_rt(int signo, siginfo_t *info, void *context)

40 {

41  printf(received signal #%d, code = %d, ival = %d\n",

42   signo.info->si_code, info->si_value.sival_int);

43 }

Вывод номеров сигналов реального времени

10 Мы печатаем наибольший и наименьший номера сигналов реального времени, чтобы узнать, сколько их предоставляется в данной реализации. Мы преобразуем обе константы к типу integer, поскольку в некоторых реализациях они определяются как макросы, требующие вызова sysconf, например:

#define SIGRTMAX (sysconf(_SC_RTSIG_MAX))

и функция sysconf возвращает целое типа long (см. упражнение 5.4).

Вызов fork и блокирование трех сигналов реального времени

11-17 Запускается дочерний процесс, который вызывает sigprocmask для блокировки трех используемых сигналов реального времени: SIGRTMAX, SIGRTMAX-1 и SIGRTMAX-2.

Установка обработчика сигнала

18-21 Мы вызываем функцию signal_rt (приведенную в листинге 5.15) для установки функции sig_rt в качестве обработчика трех указанных выше сигналов реального времени. Функция устанавливает флаг SA_SIGINFO, и поскольку эти три сигнала являются сигналами реального времени, мы можем ожидать, что они будут обрабатываться соответствующим образом. Эта функция также устанавливает маску сигналов, блокируемых на время выполнения обработчика.

Ожидание порождения сигналов родительским процессом, разблокирование сигналов

22-25 Дочерний процесс ждет 6 секунд, пока родительский породит девять сигналов. Затем вызывается sigprocmask для разблокирования трех сигналов реального времени. Это позволяет всем помещенным в очередь сигналам достичь адресата. Затем делается пауза еще на три секунды, чтобы обработчик успел вызвать printf девять раз, после чего дочерний процесс завершает свою работу.

Родительский процесс отправляет девять сигналов

27-36 Родительский процесс ждет три секунды, пока дочерний не заблокирует все требуемые сигналы. Затем родительский процесс порождает три экземпляра каждого из трех сигналов реального времени: i принимает 3 значения, a j принимает значения 0, 1 и 2 для каждого из значений i. Мы преднамеренно порождаем сигналы начиная с наибольшего номера, поскольку ожидаем, что они будут получены начиная с наименьшего. Мы также отсылаем с каждым из сигналов новое значение sigval_int, чтобы проверить, что копии одного и того же сигнала доставляются в том же порядке, в каком они были отправлены, то есть очередь действительно является очередью.

Обработчик сигнала

38-43 Обработчик сигнала просто выводит информацию о полученном сигнале.

ПРИМЕЧАНИЕ

Из табл. 5.1 следует, что функция printf не относится к функциям типа async-signal-safe и не должна вызываться из обработчика сигналов. Здесь мы используем ее исключительно в качестве проверочного средства в маленькой тестовой программе. 

Запустим эту программу в Solaris 2.6. Результат будет не тем, которого мы ожидали:

solaris % test1

SIGRTMIN = 38, SIGRTMAX = 45 8 сигналов реального времени

                             трехсекундная пауза

sent signal 45, val = 0

sent signal 45, val = 1

sent signal 45, val = 2

sent signal 44, val = 0

sent signal 44, val = 1

sent signal 44, val = 2

sent signal 43, val = 0

sent signal 43, val = 1

sent signal 43, val = 2

solaris % родительский процесс завершил работу, пауза 3 секунды,

          пока дочерний процесс не разблокирует сигналы

received signal #45, code = –2, ival = 2 дочерний процесс получает сигналы

received signal #45, code = –2, ival = 1

received signal #45, code = –2, ival = 0

received signal #44, code = –2, ival = 2

received signal #44, code = –2, ival = 1

received signal #44, code = –2, ival = 0

received signal #43, code = –2, ival = 2

received signal #43, code = –2, ival = 1

received signal #43, code = –2, ival = 0

В очередь помещаются девять сигналов, но первыми принимаются сигналы с большими номерами (а мы ожидали получить сигналы с меньшими номерами). 

Кроме того, сигналы с одинаковым номером приходят в порядке LIFO, а не FIFO. Код si_code = –2 соответствует SI_QUEUE.

Запустив программу в Digital Unix 4.0B, мы получим именно тот результат, которого ожидали:

alpha % test1

SIGRTMIN = 33, SIGRTMAX = 48 16 сигналов реального времени

                             трех секундная пауза

sent signal 48, val = 0

sent signal 48, val = 1

sent signal 48, val = 2

sent signal 47, val = 0

sent signal 47, val = 1

sent signal 47, val = 2

sent signal 46, val = 0

sent signal 46, val = 1

sent signal 46, val = 2

alpha % родительский процесс завершил работу, пауза 3 секунды.

        пока дочерний процесс не разблокируетсигналы

received signal #46, code – –1, ival = 0 дочерний процесс получает сигналы

received signal #46, code = –1, ival = 1

received signal #46, code = –1, ival = 2

received signal #47, code – –1, ival = 0

received signal #47, code = –1, ival = 1

received signal #47, code = –1, ival = 2

received signal #48, code = –1, ival = 0

received signal #48, code = –1, ival = 1

received signal #48, code = –1, ival = 2

Девять сигналов помещаются в очередь и получаются адресатом в ожидаемом порядке: первым приходит сигнал с меньшим номером, а копии сигнала приходят в порядке FIFO.

ПРИМЕЧАНИЕ

Похоже, что в реализации Solaris 2.6 есть ошибка.

Функция signal_rt

В книге [24, с. 120] мы привели пример собственной функции signal, вызывавшей функцию sigaction стандарта Posix для установки обработчика сигнала, обеспечивающего надежную семантику Posix. Изменим эту функцию, чтобы обеспечить поддержку реального времени. Новую функцию мы назовем signal_rt; ее текст приведен в листинге 5.15.

Листинг 5.15. Функция signal_rt с поддержкой реального времени

//lib/signal_rt.c

1  #include "unpipc.h"

2  Sigfunc_rt *


3  signal_rt(int signo, Sigfunc_rt *func)

4  {

5   struct sigaction act, oact;

6   act.sa_sigaction = func; /* сохраняем адрес функции */

7   sigemptyset(&act.sa_mask);

8   act.sa_flags = SA_SIGINFO; /* гарантирует режим реального времени */

9   if (signo == SIGALRM) {

10 #ifdef SA_INTERRUPT

11   act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */

12 #endif

13  } else {

14 #ifdef SA_RESTART

15   act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */

16 #endif

17  }

18  if (sigaction(signo, &act, &oact) < 0)

19   return((Sigfunc_rt *) SIG_ERR);

20  return(oact.sa_sigaction);

21 }

Упрощение прототипа функции с использованием typedef

1-3 В нашем заголовочном файле unpiрс.h (листинг В.1) мы определяем Sigfunc_rt как

typedef void Sigfunc_rt(int, siginfo_t*, void*);

Ранее в этом разделе мы говорили о том, что это прототип функции для обработчика сигнала, устанавливаемого с флагом SA_SIGINFO.

Указание функции-обработчика

Структура sigaction претерпела изменения с добавлением поддержки сигна-5-7 лов реального времени: к ней было добавлено новое поле sa_sigaction:

struct sigaction {

 void (*sa_handler)(); /* SIG_DFL, SIG_IGN или адрес обработчика сигнала */

 sigset_t sa_mask; /* дополнительные блокируемые сигналы */

 int sa_flags; /* параметры сигналов: SA_XXX */

 void (*sa_sigaction)(int, siginfo_t, void *);

};

Правила действуют следующие:

■ Если в поле sa_flags установлен флаг SA_SIGINFO, поле sa_sigaction указывает адрес функции-обработчика сигнала.

■ Если флаг SA_SIGINFO не установлен, поле sa_handler указывает адрес функции-обработчика сигнала.

■ Чтобы сопоставить сигналу действие по умолчанию или игнорировать его, следует установить sa_handler равным либо SIG_DFL, либо SIG_IGN и не устанавливать флаг SA_SIGINFO.

Установка SA_SIGINFO

8-17 Мы всегда устанавливаем флаг SA_SIGINFO и указываем флаг SA_RESTART, если перехвачен какой-либо другой сигнал, кроме SIGALRM.

5.8. Реализация с использованием отображения в память

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

ПРИМЕЧАНИЕ

Взаимные исключения и условные переменные описаны в главе 7, а ввод-вывод с отображением в память — в главах 12 и 13. Вы можете отложить данный раздел до ознакомления с этими главами.

На рис. 5.2 приведена схема структур данных, которыми мы пользуемся для реализации очереди сообщений Posix. Изображенная очередь может содержать до четырех сообщений по 7 байт каждое.

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

Тип mqd_t

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

Листинг 5.16. Заголовочный файл mqueue.h

//my_pxmsg_mmap/mqueue.h

1  typedef struct mymq_info *mymqd_t;


2  struct mymq_attr {

3   long mq_flags; /* флаг очереди : O_NONBLOCK */

4   long mq_maxmsg; /* максимальное количество сообщений в очереди */

5   long mq_msgsize; /* максимальный размер сообщения в байтах */

6   long mq_curmsgs; /* количество сообщений в очереди */

7  };


8  /* одна структура mymq_hdr{} на очередь, в начале отображаемого файла */

9  struct mymq_hdr {

10  struct mymq_attr mqh_attr; /* атрибуты очереди */

11  long mqh_head; /* индекс первого сообщения*/

12  long mqh_free; /* индекс первого пустого сообщения */

13  long mqh_nwait; /* количество заблокированных mq_receive() потоков */

14  pid_t mqh_pid; /* ненулевой PID. если включено уведомление */

15  struct sigevent mqh_event; /* для mq_notify() */

16  pthread_mutex_t mqh_lock; /* блокировка: mutex*/

17  pthread_cond_t mqh_wait; /* и условная переменная */

18 };


19 /* один mymsg_hdr{} в начале каждого сообщения */

20 struct mymsg_hdr {

21  long msg_next; /* индекс следующего сообщения в списке */

22                 /* msg_next должно быть первым полем в структуре */

23  ssize_t msg_len; /* реальная длина */

24  unsigned int msg_prio; /* приоритет */

25 };


26 /* одна mymq_info{} выделяется при каждом mq_open() */

27 struct mymq_info {

28  struct mymq_hdr *mqi_hdr; /* начало отображаемого файла */

29  long mqi_magic; /* магическое значение после инициализации */

30  int mqi_flags; /* флаги для данного процесса */

31 };

32 #define MQI_MAGIC 0x98765432

33 /* размер сообщения округляется для подгонки */

34 #define MSGSIZE(i) ((((i) + sizeof(long)-1) / sizeof(long)) * sizeof(long)) 

UNIX: взаимодействие процессов

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


Структура mq_hdr

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

Обратите внимание, что все переменные, называемые нами индексными (поля этой структуры mqh_head и mqh_free, а также поле msg_next следующей структуры), содержат индексы байтов относительно начала отображаемого в память файла. Например, размер структуры mq_hdr в системе Solaris 2.6 — 96 байт, поэтому индекс первого сообщения, располагающегося сразу за заголовком, имеет значение 96. Каждое сообщение на рис. 5.2 занимает 20 байт (12 байт на структуру msg_hdr и 8 байт на данные), поэтому индексы следующих трех сообщений имеют значения 116, 136 и 156, а размер отображаемого в память файла — 176 байт. Индексы используются для обработки двух связных списков, хранящихся в этом файле: в одном из списков (mqh_head) хранятся все сообщения, имеющиеся в данный момент в очереди, а в другом (mqh_free) — все незаполненные сообщения. Мы не можем использовать настоящие указатели на области памяти (адреса) при работе со списком, поскольку отображаемый файл может находиться в произвольной области памяти для каждого из процессов, работающих с ним (как показано в листинге 13.5).

Структура msg_hdr

19-25 Эта структура располагается в начале каждого сообщения в отображаемом файле. Любое сообщение может принадлежать либо к списку заполненных, либо к списку свободных сообщений, и поле msg_next содержит индекс следующего сообщения в этом списке (или 0, если сообщение является в этом списке последним). Переменная msg_len хранит реальную длину данных в сообщении, которая в нашем примере с рис. 5.2 может иметь значение от 0 до 7 байт включительно. В переменную msg_prio отправителем помещается значение приоритета сообщения.

Структура mq_info

26-32 Экземпляр такой структуры динамически создается функцией mq_open при открытии очереди и удаляется mq_close. Поле mqi_hdr указывает на отображаемый файл (адрес начала файла возвращается mmap). Указатель на эту структуру имеет основной в нашей реализации тип mqd_t, он принимает значение, возвращаемое mq_open.

Поле mqi_magiс принимает значение MQI_MAGIC в момент инициализации структуры. Это значение проверяется всеми функциями, которым передается указатель типа mqd_t, что дает им возможность удостовериться, что указатель действительно указывает на структуру типа mq_infо. mqi_flags содержит флаг отключения блокировки для открывшего очередь процесса.

Макрос MSGSIZE

33-34 В целях выравнивания содержимого файла (alignment) мы располагаем начало каждого сообщения так, чтобы его индекс был кратен размеру длинного целого. Следовательно, если максимальный размер сообщения не допускает такого выравнивания, мы добавляем к нему от 1 до 3 байт, как показано на рис. 5.2. При этом предполагается, что размер длинного целого — 4 байт (что верно для Solaris 2.6). Если размер длинного целого 8 байт (в Digital Unix 4.0B), нам придется добавлять к каждому сообщению от 1 до 7 байт.

Функция mq_open

В листинге 5.17 приведен текст первой части функции mq_open, создающей новую очередь сообщений или открывающей существующую.

Листинг 5.17. Функция mq_open: первая часть

//my_pxmsg._mmap/mq_open. с

1  #include "unpipc.h"

2  #include "mqueue.h"

3  #include <stdarg.h>

4  #define MAX_TRIES 10

5  struct mymq_attr defattr =

6   { 0, 128, 1024, 0 };


7  mymqd_t

8  mymq_open(const char *pathname, int oflag, …)

9  {

10  int i, fd, nonblock, created, save_errno;

11  long msgsize, filesize, index;

12  va_list ap;

13  mode_t mode;

14  int8_t *mptr;

15  struct stat statbuff;

16  struct mymq_hdr *mqhdr;

17  struct mymsg_hdr *msghdr;

18  struct mymq_attr *attr;

19  struct mymq_info *mqinfo;

20  pthread_mutexattr_t mattr;

21  pthread_condattr_t cattr;

22  created = 0;

23  nonblock = oflag & O_NONBLOCK;

24  oflag &= ~O_NONBLOCK;

25  mptr = (int8_t *) MAP_FAILED;

26  mqinfo = NULL;

27 again:

28  if (oflag & O_CREAT) {

29   va_start(ap, oflag); /* ар инициализируется последним аргументом */

30   mode = va_arg(ap, va_mode_t) & ~S_IXUSR;

31   attr = va_arg(ap, struct mymq_attr *);

32   va_end(ap);

33   /* открытие с установкой бита user-execute */

34   fd = open (pathname, oflag | O_EXCL | O_RDWR, mode | S_IXUSR);

35   if (fd < 0) {

36    if (errno == EEXIST && (oflag & O_EXCL) == 0)

37     goto exists; /* уже существует, OK */

38    else

39     return((mymqd_t) –1);

40   }

41   created = 1;

42   /* при создании файла он инициализируется */

43   if (attr == NULL)

44    attr = &defattr;

45   else {

46    if (attr->mq_maxmsg <– 0 || attr->mq_msgsize <= 0) {

47     errno = EINVAL;

48     goto err;

49    }

50   }

Обработка списка аргументов переменного размера

29-32 Функция может быть вызвана либо с двумя, либо с четырьмя аргументами в зависимости от того, указан ли флаг O_CREAT. Если флаг указан, третий аргумент имеет тип mode_t, а это простой системный тип, являющийся одним из целых типов. При этом мы столкнемся с проблемой в BSD/OS, где этот тип данных определен как unsigned short (16 бит). Поскольку целое в этой реализации занимает 32 бита, компилятор С увеличивает аргумент этого типа с 16 до 32 бит, потому что все короткие целые в списке аргументов увеличиваются до обычных целых. Но если мы укажем mode_t при вызове va_arg, он пропустит 16 бит аргумента в стеке, если этот аргумент был увеличен до 32 бит. Следовательно, мы должны определить свой собственный тип данных, va_mode_t, который будет целым в BSD/OS и типом mode_t в других системах. Эту проблему с переносимостью решают приведенные ниже строки нашего заголовка unpipc.h (листинг В.1):

#ifdef __bsdi__

#define va_mode_t int

#else

#define va_mode_t mode_t

#endif

30 Мы сбрасываем бит user-execute (S_IXUSR) в переменной mode по причинам, которые будут вскоре раскрыты.

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

33-34 Создается обычный файл с именем, указанным при вызове функции, и устанавливается бит user-execute. 

Обработка потенциальной ситуации гонок

35-40 Если бы при указании флага O_CREAT мы просто открыли файл, отобразили его содержимое в память и проинициализировали отображенный файл (как будет описано ниже), у нас возникла бы ситуация гонок. Очередь сообщений инициализируется mq_open только в том случае, если вызывающий процесс указывает флаг O_CREAT и очередь сообщений еще не существует. Это означает, что нам нужно каким-то образом определять, существует она или нет. Для этого при открытии файла для последующего отображения в память мы всегда указываем флаг O_EXCL. Возвращение ошибки EEXIST функцией open является ошибкой для mq_open только в том случае, если при вызове был указан флаг O_EXCL. В противном случае при возвращении функцией open ошибки EEXIST мы делаем вывод, что файл уже существует, и переходим к листингу 5.19, как если бы флаг O_CREAT вовсе не был указан.

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

Мы используем бит user-execute для указания того, был ли проинициализирован файл с очередью сообщений. Этот бит устанавливается только тем потоком, который создает этот файл (флаг O_EXCL позволяет определить этот поток); этот поток инициализирует файл с очередью сообщений, а затем сбрасывает бит user-execute.

Аналогичная ситуация может возникнуть в листингах 10.28 и 10.37.

Проверка атрибутов

42-50 Если при вызове в качестве последнего аргумента передан нулевой указатель, очередь сообщений инициализируется со значениями атрибутов по умолчанию: 128 сообщений в очереди и 1024 байта на сообщение. Если атрибуты указаны явно, мы проверяем, что mq_maxmsg и mq_msgsize имеют положительные значения.

Вторая часть функции mq_open приведена в листинге 5.18. Она завершает инициализацию новой очереди сообщений.

Листинг 5.18. Вторая часть функции mq_open: инициализация новой очереди

//my_pxmsg_mmap/mq_open.с

51    /* вычисление и установка размера файла */

52    msgsize = MSGSIZE(attr->mq_msgsize);

53    filesize = sizeof(struct mymq_hdr) + (attr->mq_maxmsg *

54     (sizeof(struct mymsg_hdr) + msgsize));

55    if (lseek(fd, filesize – 1, SEEK_SET) == –1)

56     goto err;

57    if (write(fd, "", 1) == –1)

58     goto err;

59    /* отображение файла в память */

60    mptr = mmap(NULL, filesize, PROT_READ | PROT_WRITE,

61     MAP_SHARED, fd, 0);

62    if (mptr == MAP_FAILED)

63     goto err;

64    /* выделение структуры mymq_info{} для очереди */

65    if ((mqinfo = mallос (sizeof (struct mymq_info))) == NULL)

66     goto err;

67    mqinfo->mqi_hdr = mqhdr = (struct mymq_hdr *) mptr;

68    mqinfo->mqi_magic = MQI_MAGIC;

69    mqinfo->mqi_flags = nonblock;

70    /* инициализация заголовка в начале файла */

71    /* создание списка пустых сообщений */

72    mqhdr->mqh_attr.mq_flags = 0;

73    mqhdr->mqh_attr.mq_maxmsg = attr->mq_maxmsg;

74    mqhdr->mqh_attr.mq_msgsize = attr->mq_msgsize;

75    mqhdr->mqh_attr.mq_curmsgs = 0;

76    mqhdr->mqh_nwait = 0;

77    mqhdr->mqh_pid = 0;

78    mqhdr->mqh_head = 0;

79    index = sizeof(struct mymq_hdr);

80    mqhdr->mqh_free = index;

81    for (i = 0; i < attr->mq_maxmsg – 1; i++) {

82     msghdr = (struct mymsg_hdr *) &mptr[index];

83     index += sizeof(struct mymsg_hdr) + msgsize;

84     msghdr->msg_next = index;

85    }

86    msghdr = (struct mymsg_hdr *) &mptr[index];

87    msghdr->msg_next = 0; /* конец списка пустых сообщений */

88    /* инициализация взаимного исключения и условной переменной */

89    if ((i = pthread_mutexattr_init(&mattr)) != 0)

90     goto pthreaderr;

91    pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);

92    i = pthread_mutex_init(&mqhdr->mqh_lock, &mattr);

93    pthread_mutexattr_destroy(&mattr); /* обязательно нужно удалить */

94    if (i != 0)

95     goto pthreaderr:

96    if ((i = pthread_condattr_init(&cattr)) != 0)

97     goto pthreaderr;

98    pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);

99    i = pthread_cond_init(&mqhdr->mqh_wait, &cattr);

100   pthread_condattr_destroy(&cattr); /* обязательно нужно удалить */

101   if (i != 0)

102    goto pthreaderr;

103   /* инициализация завершена, снимаем бит user-execute */

104   if (fchmod(fd, mode) == –1)

105    goto err;

106   close(fd);

107   return((mymqd_t) mqinfo);

108  }

Установка размера файла

51-58 Вычисляется размер сообщения, который затем округляется до кратного размеру длинного целого. Также в файле отводится место для структуры mq_hdr в начале файла и msghdr в начале каждого сообщения (рис. 5.2). Размер вновь созданного файла устанавливается функцией lseek и записью одного байта со значением 0. Проще было бы вызвать ftruncate (раздел 13.3), но у нас нет гарантий, что это сработало бы для увеличения размера файла.

Отображение файла в память

59-63 Файл отображается в память функцией mmap.

Выделение памяти под структуру mq_info

64-66 При каждом вызове mq_open создается отдельный экземпляр mq_infо. Эта структура после создания инициализируется.

Инициализация структуры mq_hdr

67-87 Инициализируется структура mq_hdr. Заголовок связного списка сообщений (mqh_head) инициализируется нулем, а все сообщения в очереди добавляются к списку свободных (mqh_frее).

Инициализация взаимного исключения и условной переменной

88-102 Поскольку очереди сообщений Posix могут использоваться совместно произвольным количеством процессов, которые знают имя очереди и имеют соответствующие разрешения, нам нужно инициализировать взаимное исключение и условную переменную с атрибутом PTHREAD_PROCESS_SHARED. Для этого мы сначала инициализируем атрибуты вызовом pthread_mutexattr_init, а затем устанавливаем значение атрибута совместного использования процессами, вызвав pthread_mutexattr_setpshared. После этого взаимное исключение инициализируется вызовом pthread_mutex_init. Аналогичные действия выполняются для условной переменной. Мы должны аккуратно удалить взаимное исключение и условную переменную даже при возникновении ошибки, поскольку вызовы pthread_ mutexattr_init и pthread_condattr_init выделяют под них память (упражнение 7.3).

Сброс бита user-execute

103-107 После инициализации очереди сообщений мы сбрасываем бит user-execute. Это говорит другим процессам о том, что очередь была проинициализирована. Мы также закрываем файл вызовом close, поскольку он был успешно отображен в память и держать его открытым больше нет необходимости.

В листинге 5.19 приведен конец функции mq_open, в котором осуществляется открытие существующей очереди сообщений.

Листинг 5.19. Третья часть функции mq_open: открытие существующей очереди сообщений

//my_pxmsg_mmap/mq_open.с

109 exists:

110  /* открытие файла и отображение его в память */

111  if ((fd = open(pathname, O_RDWR)) < 0) {

112   if (errno == ENOENT && (oflag & O_CREAT))

113    goto again;

114  goto err;

115  }

116  /* проверяем, что инициализация завершена */

117  for (i = 0; i < MAX TRIES; i++) {

118   if (stat(pathname, &statbuff) == –1) {

119    if (errno == ENOENT && (oflag & O_CREAT)) {

120     close(fd);

121     goto again;

122    }

123    goto err;

124   }

125   if ((statbuff.st_mode & S_IXUSR) == 0)

126   break;

127   sleep(1);

128  }

129  if (i == MAX_TRIES) {

130   errno = ETIMEDOUT;

131   goto err;

132  }

133  filesize = statbuff.st_size;

134  mptr = mmap(NULL, filesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

135  if (mptr == MAP_FAILED)

136   goto err;

137  close(fd);

138  /* выделяем одну mymq_info{} для каждого вызова open */

139  if ((mqinfo = malloc(sizeof(struct mymq_info))) == NULL)

140   goto err;

141  rnqinfo->mqi_hdr = (struct mymq_hdr *) mptr;

142  mqinfo->mqi_magic = MQI_MAGIC;

143  mqinfo->mqi_flags = nonblock;

144  return((mymqd_t) mqinfo);

145 pthreaderr:

146  errno = i;

147 err:

148  /* не даем следующим вызовам изменить errno */

149  save_errno = errno;

150  if (created)

151   unlink(pathname);

152  if (mptr != MAP_FAILED)

153   munmap(mptr, filesize);

154  if (mqinfo != NULL)

155   free(mqinfo);

156  close(fd);

157  errno = save_errno;

158  return((mymqd_t) –1);

159 }

Открытие существующей очереди сообщений

109-115 Здесь мы завершаем работу, если флаг O_CREAT не был указан или если он был указан, но очередь уже существовала. В любом случае, мы открываем существующую очередь сообщений. Для этого мы открываем для чтения и записи файл, в котором она содержится, функцией open и отображаем его содержимое в адресное пpocтрaнcтвo процесса (mmap).

ПРИМЕЧАНИЕ

Наша реализация сильно упрощена в том, что касается режима открытия файла. Даже если вызвавший процесс указывает флаг O_RDONLY, мы должны дать возможность доступа для чтения и записи при открытии файла командой open и при отображении его в память командой mmap, поскольку невозможно считать сообщение из очереди, не изменив содержимое файла. Аналогично невозможно записать сообщение в очередь, не имея доступа на чтение. Обойти эту проблему можно, сохранив режим открытия (O_RDONLY, O_WRONLY, O_RDWR) в структуре mq_info и проверяя этот режим в каждой из функций. Например, mq_receive должна возвращать ошибку, если в mq_info хранится значение O_WRONLY.

Проверка готовности очереди

116-132 Нам необходимо дождаться, когда очередь будет проинициализирована (в случае, если несколько потоков сделают попытку открыть ее приблизительно одновременно). Для этого мы вызываем stat и проверяем разрешения доступа к файлу (поле st_mode структуры stat). Если бит user-execute сброшен, очередь уже проинициализирована.

Этот участок кода обрабатывает другую возможную ситуацию гонок. Предположим, что два потока разных процессов попытаются открыть очередь приблизительно одновременно. Первый поток создает файл и блокируется при вызове lseek в листинге 5.18. Второй поток обнаруживает, что файл уже существует, и переходит к метке exists, где он вновь открывает файл функцией open и при этом блокируется. Затем продолжается выполнение первого потока, но его вызов mmap в листинге 5.18 не срабатывает (возможно, он превысил лимит использования памяти), поэтому он переходит на метку err и удаляет созданный файл вызовом unlink. Продолжается выполнение второго потока, но если бы мы вызывали fstat вместо stat, он бы вышел по тайм-ауту в цикле for, ожидая инициализации файла. Вместо этого мы вызываем stat, которая возвращает ошибку, если файл не существует, и, если флаг O_CREAT был указан при вызове mq_open, мы переходим на метку again (листинг 5.17) для повторного создания файла. Эта ситуация гонок заставляет нас также проверять, не возвращается ли при вызове open ошибка ENOENT.

Отображение файла в память; создание и инициализация структуры mq_info

133-144 Файл отображается в память, после чего его дескриптор может быть закрыт. Затем мы выделяем место под структуру mq_infо и инициализируем ее. Возвращаемое значение представляет собой указатель на эту структуру.

Обработка ошибок

145-148 При возникновении ошибок происходит переход к метке err, а переменной errno присваивается значение, которое должно быть возвращено функцией mq_open. Мы аккуратно вызываем функции для очистки памяти от выделенных объектов, чтобы переменная errno не изменила свое значение в случае возникновения ошибки в этих функциях.

Функция mq_close

В листинге 5.20 приведен текст нашей функции mq_close.

Листинг 5.20. Функция mq_close

//my_pxmsg_mmap/mq_close.с

1  #include "unpipc.h"

2  #include "mqueue.h"


3  int

4  mymq_close(mymqd_t mqd)

5  {

6   long msgsize, filesize:

7   struct mymq_hdr *mqhdr;

8   struct mymq_attr *attr;

9   struct mymq_info *mqinfo;

10  mqinfo = mqd;

11  if (mqinfo->mqi_magic != MQI_MAGIC) {

12   errno = EBADF;

13   return(-1);

14  }

15  mqhdr = mqinfo->mqi_hdr;

16  attr = &mqhdr->mqh_attr;

17  if (mymq_notify(mqd, NULL) != 0) /* снятие вызвавшего процесса с регистрации */

18   return(-1);

19  msgsize = MSGSIZE(attr->mq_msgsize);

20  filesize = sizeof(struct mymq_hdr) + (attr->mq_maxmsg *

21   (sizeof(struct mymsg_hdr) + msgsize));

22  if (munmap(mqinfo->mqi_hdr, filesize) == –1)

23   return(-1);

24  mqinfo->mqi_magic = 0; /* на всякий случай */

25  free(mqinfo);

26  return(0);

27 }

Получение указателей на структуры

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

Сброс регистрации вызвавшего процесса

17-18 Для сброса регистрации на уведомление вызвавшего процесса мы вызываем mq_notify. Если процесс был зарегистрирован, он будет снят с уведомления, но если нет — ошибка не возвращается.

Отключение отображения файла и освобождение памяти

19-25 Мы вычисляем размер файла для вызова munmap и освобождаем память, используемую структурой mqinfo. На случай, если вызвавший процесс будет продолжать использовать дескриптор очереди сообщений, до того как область памяти будет вновь задействована вызовом malloc, мы устанавливаем значение mq_magiс в ноль, чтобы наши функции для работы с очередью сообщений обнаруживали ошибку.

Обратите внимание, что если процесс завершает работу без вызова mq_close, эти же операции выполняются автоматически: отключается отображение в память, а память освобождается.

Функция mq_unlink

Текст функции mqunlink приведен в листинге 5.21. Она удаляет файл, связанный с очередью сообщений, вызывая функцию unlink.

Листинг 5.21. Функция mq_unlink

//my_pxmsg_mmap/mq_unlink.с

1 #include "unpipc.h"

2 #include "mqueue.h"


3 int

4 mymq_unlink(const char *pathname)

5 {

6  if (unlink(pathname) == –1)

7   return(-1);

8  return(0);

9 }

Функция mq_getattr

В листинге 5.22 приведен текст функции mq_getattr, которая возвращает текущее значение атрибутов очереди.

Листинг 5.22. Функция mq_getattr

//my_pxmsg_mmap/mq_getattr.с

1  #include "unpipc.h"

2  #include "mqueue.h"


3  int

4  mymq_getattr(mymqd_t mqd, struct mymq_attr *mqstat)

5  {

6   int n;

7   struct mymq_hdr *mqhdr;

8   struct mymq_attr *attr;

9   struct mymq_info *mqinfo;

10  mqinfo = mqd;

11  if (mqinfo->mqi_magic != MQI_MAGIC) {

12   errno = EBADF;

13   return(-1);

14  }

15  mqhdr = mqinfo->mqi_hdr;

16  attr = &mqhdr->mqh_attr;

17  if ((n = pthread_mutex_lock(&mqhdr->mqh_lock)) != 0) {

18   errno = n;

19   return (-1);

20  }

21  mqstat->mq_flags = mqinfo->mqi_flags; /* для каждого open */

22  mqstat->mq_maxmsg = attr->mq_maxmsg; /* оставшиеся три – для очереди */

23  mqstat->mq_msgsize = attr->mq_msgsize;

24  mqstat->mq_curmsgs = attr->mq_curmsgs;

25  pthread_mutex_unlock(&mqhdr->mqh_lock);

26  return(0);

27 }

Блокирование взаимного исключения

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

Функция mq_setattr

В листинге 5.23 приведен текст функции mq_setattr, которая устанавливает значение атрибутов очереди.

Считывание текущих атрибутов

22-27 Если третий аргумент представляет собой ненулевой указатель, мы возвращаем предыдущее значение атрибутов перед внесением каких-либо изменений.

Изменение mq_flags

28-31 Единственный атрибут, который можно менять с помощью нашей функции, — mq_flags, хранящийся в структуре mq_infо.

Листинг 5.23. Функция mq_setattr

//my_pxmsg_mniap/mq_setattr.с

1  #include "unpipc.h"

2  #include "mqueue.h"


3  int

4  mymq_setattr(mymqd_t mqd. const struct mymq_attr *mqstat,

5   struct mymq attr *omqstat)

6  {

7   int n;

8   struct mymq_hdr *mqhdr;

9   struct mymq_attr *attr;

10  struct mymq_info *mqinfo;

11  mqinfo = mqd;

12  if (mqinfo->mqi_magic != MQI_MAGIC) {

13   errno = EBADF;

14   return(-1);

15  }

16  mqhdr = mqinfo->mqi_hdr;

17  attr = &mqhdr->mqh_attr;

18  if ((n = pthread_mutex_lock(&mqhdr->mqh_lock)) ! = 0) {

19   errno = n;

20   return(-1);

21  }

22  if (omqstat != NULL) {

23   omqstat->mq_flags = mqinfo->mqi_flags; /* исходные атрибуты */

24   omqstat->mq_maxmsg = attr->mq_maxmsg;

25   omqstat->mq_msgsize = attr->mq_msgsize;

26   omqstat->mq_curmsgs = attr->mq_curmsgs; /* текущий статус */

27  }

28  if (mqstat->mq_flags & O_NONBLOCK)

29   mqinfo->mqi flags |= O_NONBLOCK;

30  else

31   mqinfo->ntqi_flags &= ~O_NONBLOCK;

32  pthread_mutex_unlock(&mqhdr->mqh_lock);

33  return(0);

34 }

Функция mq_notify

Функция mq_notify, текст которой приведен в листинге 5.24, позволяет регистрировать процесс на уведомление для текущей очереди и снимать его с регистрации. Информация о зарегистрированных процессах (их идентификаторы) хранится в поле mqh_pid структуры mq_hdr. Только один процесс может быть зарегистрирован на уведомление в любой момент времени. При регистрации процесса мы сохраняем его структуру sigevent в структуре mqh_event.

Листинг 5.24. Функция mq_notify

//my_pxmsg_mmap/mq_notify.с

1  #include "unpipc.h"

2  #include "mqueue.h"


3  int

4  mymq_notify(mymqd_t mqd, const struct sigevent *notification)

5  {

6   int n;

7   pid_t pid;

8   struct mymq_hdr *mqhdr;

9   struct mymq_info *mqinfo;

10  mqinfo = mqd;

11  if (mqinfo->mqi magic != MQI_MAGIC) {

12   errno = EBADF;

13   return(-1);

14  }

15  mqhdr = mqinfo->mqi_hdr;

16  if ((n = pthread_mutex_lock(&mqhdr->mqh_lock)) != 0) {

17   errno = n;

18   return(-1);

19  }

20  pid = getpid();

21  if (notification == NULL) {

22   if (mqhdr->mqh_pid == pid) {

23    mqhdr->mqh_pid = 0; /* снятие вызвавшего процесса с регистрации */

24   } /* если вызвавший процесс не зарегистрирован – 61К */

25  } else {

26   if (mqhdr->mqh_pid != 0) {

27    if (kill(mqhdr->mqh_pid, 0) != –1 || errno != ESRCH) {

28     errno = EBUSY;

29     goto err;

30    }

31   }

32   mqhdr->mqh_pid = pid;

33   mqhdr->mqh_event = *notification;

34  }

35  pthread_mutex_unlock(&mqhdr->mqh_lock);

36  return(0);

37 err:

38  pthread_mutex_unlock(&mqhdr->mqh_lock);

39  return(-1);

40 }

Снятие процесса с регистрации

20-24 Если второй аргумент представляет собой нулевой указатель, вызвавший процесс снимается с регистрации. Если он не зарегистрирован, никакой ошибки не возвращается.

Регистрация вызвавшего процесса

25-34 Если какой-либо процесс уже зарегистрирован, мы проверяем, существует ли он, отправкой ему сигнала с кодом 0 (называемого нулевым сигналом — null signal). Это обычная проверка на возможность ошибки, на самом деле при этом никакого сигнала процессу не отправляется, но при его отсутствии возвращается ошибка с кодом ESRCH. Если какой-либо процесс уже зарегистрирован на уведомление, функция возвращает ошибку EBUSY. В противном случае сохраняется идентификатор процесса вместе с его структурой sigevent.

ПРИМЕЧАНИЕ

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

Функция mq_send

В листинге 5.25 приведен текст первой половины нашей функции mqsend.

Инициализация

14-29 Мы получаем указатели на используемые структуры и блокируем взаимное исключение для данной очереди. Проверяем, не превышает ли размер сообщения максимально допустимый для данной очереди.

Проверка очереди на пустоту и отправка уведомления

30-38 Если мы помещаем первое сообщение в пустую очередь, нужно проверить, не зарегистрирован ли какой-нибудь процесс на уведомление об этом событии и нет ли потоков, заблокированных в вызове mq_receive. Для проверки второго условия мы воспользуемся сохраняемым функцией mq_receive счетчиком mqh_nwait, содержащим количество потоков, заблокированных в вызове mq_receive. Если этот счетчик имеет ненулевое значение, мы не отправляем уведомление зарегистрированному процессу. Для отправки сигнала SIGEV_SIGNAL используется функция sigqueue. Затем процесс снимается с регистрации.

ПРИМЕЧАНИЕ

Вызов sigqueue для отправки сигнала приводит к передаче сигнала SI_QUEUE обработчику сигнала в структуре типа siginfo_t (раздел 5.7), что неправильно. Отправка правильного значения si_code (а именно SI_MESGQ) из пользовательского процесса осуществляется в зависимости от реализации. На с. 433 стандарта IEEE 1996 [8] отмечается, что для отправки этого сигнала из пользовательской библиотеки необходимо воспользоваться скрытым интерфейсом механизма отправки сигналов. 

Проверка заполненности очереди

39-48 Если очередь переполнена и установлен флаг O_NONBLOCK, мы возвращаем ошибку с кодом EAGAIN. В противном случае мы ожидаем сигнала по условной переменной mqh_wait, который, как мы увидим, отправляется функцией mq_receive при считывании сообщения из переполненной очереди.

ПРИМЕЧАНИЕ

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

В листинге 5.26 приведена вторая половина функции mq_send. К моменту ее выполнения мы уже знаем о наличии в очереди свободного места для нашего сообщения.

Листинг 5.25. Функция mq_send: первая половина

//my_pxmsg_mmap/mq_send.с

1  #include "unpipc.h"

2  #include "mqueue.h"


3  int

4  mymq_send(mymqd_t mqd, const char *ptr, size_t len, unsigned int prio)

5  {

6   int n;

7   long index, freeindex;

8   int8_t *mptr;

9   struct sigevent *sigev;

10  struct mymq_hdr *mqhdr;

11  struct mymq_attr *attr;

12  struct mymsg_hdr *msghdr, *nmsghdr, *pmsghdr;

13  struct mymq_info *mqinfo;

14  mqinfo = mqd;

15  if (mqinfo->mqi_magic != MQI_MAGIC) {

16   errno = EBADF;

17   return(-1);

18  }

19  mqhdr = mqinfo->mqi_hdr; /* указатель типа struct */

20  mptr = (int8_t *) mqhdr; /* указатель на байт */

21  attr = &mqhdr->mqh_attr;

22  if ((n = pthread_mutex_lock(&mqhdr->mqh_lock)) != 0) {

23   errno = n;

24   return(-1);

25  }

26  if (len > attr->mq_msgsize) {

27   errno = EMSGSIZE;

28   goto err;

29  }

30  if (attr->mq_curmsgs == 0) {

31   if (mqhdr->mqh_pid != 0 && mqhdr->mqh_nwait == 0) {

32    sigev = &mqhdr->mqh_event;

33    if (sigev->sigev_notify == SIGEV_SIGNAL) {

34     sigqueue(mqhdr->mqh_pid, sigev->sigev_signo,

35      sigev->sigev_value);

36    }

37    mqhdr->mqh_pid = 0; /* снятие с регистрации */

38   }

39  } else if (attr->mq_curmsgs >= attr->mq_maxmsg) {

40   /* 4queue is full */

41   if (mqinfo->mqi_flags & O_NONBLOCK) {

32    errno = EAGAIN;

43    goto err;

44   }

45   /* ожидание освобождения места в очереди */

46   while (attr->mq_curmsgs >= attr->mq_maxmsg)

47    pthread_cond_wait(&mqhdr->mqh_wait, &mqhdr->mqh_lock);

48  }

Листинг 5.25. Функция mq_send: вторая половина

//my_pxmsg_mmap/mq_send.с

49  /* nmsghdr будет указывать на новое сообщение*/

50  if ((freeindex = mqhdr->mqh_free) == 0)

51   err_dump("mymq_send: curmsgs = %ld; free = 0", attr->mq_curmsgs);

52  nmsghdr = (struct mymsg_hdr *) &mptr[freeindex];

53  nmsghdr->msg_prio = prio;

54  nmsghdr->msg_len = len;

55  memcpy(nmsghdr + 1, ptr, len); /* копирование сообщения в очередь */

56  mqhdr->mqh_free = nmsghdr->msg_next; /* новое начало списка пустых сообщений */

57  /* поиск места в списке для нового сообщения */

58  index = mqhdr->mqh_head;

59  pmsghdr = (struct mymsg_hdr *) &(mqhdr->mqh_head);

60  while (index != 0) {

61   msghdr = (struct mymsg_hdr *) &mptr[index];

62   if (prio > msghdr->msg_prio) {

63    nmsghdr->msg_next = index;

64    pmsghdr->msg_next = freeindex;

65    break;

66   }

67   index = msghdr->msg_next;

68   pmsghdr = msghdr;

69  }

70  if (index == 0) {

71   /* очередь была пуста или новое письмо добавлено к концу списка */

72   pmsghdr->msg_next = freeindex;

73   nmsghdr->msg_next = 0;

74  }

75  /* запускаем любой из процессов, заблокированных в mq_receive */

76  if (attr->mq_curmsgs == 0)

77   pthread_cond_signal(&mqhdr->mqh_wait);

78  attr->mq_curmsgs++;

79  pthread_mutex_unlock(&mqhdr->mqh_lock);

80  return(0);

81 err:

82  pthread_mutex_unlock(&mqhdr->mqh lock);

83  return(-1);

84 }

Получение индекса свободного блока

50-52 Поскольку количество свободных сообщений при создании очереди равно mq_maxmsg, ситуация, в которой mq_curmsgs будет меньше mq_maxmsg для пустого списка свободных сообщений, возникнуть не может.

Копирование сообщения

53-56 Указатель nmsghdr хранит адрес области памяти, в которую помещается сообщение. Приоритет и длина сообщения сохраняются в структуре msg_hdr, а затем в память копируется содержимое сообщения, переданного вызвавшим процессом.

Помещение нового сообщения в соответствующее место связного списка

57-74 Порядок сообщений в нашем списке зависит от их приоритета: они расположены в порядке его убывания. При добавлении нового сообщения мы проверяем, существуют ли сообщения с тем же приоритетом; в этом случае сообщение добавляется после последнего из них. Используя такой метод упорядочения, мы гарантируем, что mq_receive всегда будет возвращать старейшее сообщение с наивысшим приоритетом. По мере продвижения по списку мы сохраняем в pmsghdr адрес предыдущего сообщения, поскольку именно это сообщение будет хранить индекс нового сообщения в поле msg_next.

ПРИМЕЧАНИЕ

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

Пробуждение любого процесса, заблокированного в вызове mq_receive

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

78 Увеличиваем на единицу количество сообщений в очереди mq_curmsgs.

Функция mq_receive

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

Проверка полноты очереди

30-40 Если очередь пуста и установлен флаг O_NONBLOCK, возвращается ошибка с кодом EAGAIN. В противном случае увеличивается значение счетчика mqh_nwait, который проверяется функцией mq_send (листинг 5.25) в случае, если очередь пуста и есть процессы, ожидающие уведомления. Затем мы ожидаем сигнала по условной переменной, который будет передан функцией mq_send (листинг 5.26).

ПРИМЕЧАНИЕ

Наша реализация mq_receive, как и реализация mq_send, упрощает ситуацию с ошибкой EINTR, возвращаемой при прерывании ожидания сигналом, перехватываемым вызвавшим процессом.

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

Листинг 5.27.Функция mq_receive: первая половина

//my_pxmsg_mmap/mq_receive.с

1  #include "unpipc.h"

2  #include "mqueue.h"


3  ssize_t

4  mymq_receive(mymqd_t mqd, char *ptr, size_t maxlen, unsigned int *priop)

5  {

6   int n;

7   long index;

8   int8_t *mptr;

9   ssize_t len;

10  struct mymq_hdr *mqhdr;

11  struct mymq_attr *attr;

12  struct mymsg_hdr *msghdr;

13  struct mymq_info *mqinfo;

14  mqinfo = mqd;

15  if (mqinfo->mqi_magic != MQI_MAGIC) {

16   errno = EBADF;

17   return(-1);

18  }

19  mqhdr = mqinfo->mqi_hdr; /* указатель struct */

20  mptr = (int8_t *) mqhdr; /* указатель на байт */

21  attr = &mqhdr->mqh_attr;

22  if ((n = pthread_mutex_lock(&mqhdr->mqh_lock)) != 0) {

23   errno = n;

24   return(-1);

25  }

26  if (maxlen < attr->mq_msgsize) {

27   errno = EMSGSIZE;

28   goto err;

29  }

30  if (attr->mq_curmsgs = 0) { /* очередь пуста */

31   if (mqinfo->mqi_flags & O_NONBLOCK) {

32    errno = EAGAIN;

33    goto err;

34   }

35   /* ожидаем помещения сообщения в очередь */

36   mqhdr->mqh_nwait++;

37   while (attr->mq_curmsgs == 0)

38    pthread_cond_wait(&mqhdr->mqh_wait, &mqhdr->mqh_lock);

39   mqhdr->mqh_nwait--;

40  }

Листинг 5.28. Функция mq_receive: вторая половина

//my_pxmsg_mmap/mq_receive.c

41  if ((index = mqhdr->mqh_head) == 0)

42   err_dump("mymq_receive: curmsgs = %ld; head = 0", attr->mq_curmsgs);

43  msghdr = (struct mymsg_hdr *) &mptr[index];

44  mqhdr->mqh_head = msghdr->msg_next; /* новое начало списка */

45  len = msghdr->msg_len;

46  memcpy(ptr, msghdr + 1, len); /* копирование самого сообщения */

47  if (priop != NULL)

48   *priop = msghdr->msg_prio;

49  /* только что считанное сообщение становится первым в списке пустых */

50  msghdr->msg_next = mqhdr->mqr_free;

51  mqhdr->mqh_free = index;

52  /* запуск любого процесса, заблокированного в вызове mq_send */

53  if (attr->mq_curmsgs == attr->mq_maxmsg)

54   pthread_cond_signal(&mqhdr->mqh_wait);

55  attr->mq_curmsgs--;

56  pthread_mutex_unlock(&mqhdr->mqh_lock);

57  return(len);

58 err:

59  pthread_mutex_unlock(&mqhdr->mqh_lock);

60  return(-1);

61 }

Возвращение сообщения вызвавшему процессу

43-51 msghdr указывает на msg_hdr первого сообщения в очереди, которое мы и возвратим. Освободившееся сообщение становится первым в списке свободных. 

Разблокирование процесса, заблокированного в вызове mq_send

52-54 Если очередь была полной в момент считывания сообщения, мы вызываем pthread_cond_signal для отправки сообщения любому из процессов, заблокированных в вызове mq_send.

5.9. Резюме

Очереди сообщений Posix просты в использовании: новая очередь создается (или существующая открывается) функцией mq_open; закрываются очереди вызовом mq_close, а удаляются mq_unlink. Поместить сообщение в очередь можно функцией mq_send, а считать его оттуда можно с помощью mq_receive. Атрибуты очереди можно считать и установить с помощью функций mq_getattr и mq_setattr, а функция mq_notify позволяет зарегистрировать процесс на уведомление о помещении нового сообщения в пустую очередь. Каждое сообщение в очереди обладает приоритетом (небольшое целое число), и функция mq_receive всегда возвращает старейшее сообщение с наивысшим приоритетом.

Изучая mq_notify, мы познакомились с сигналами реального времени стандарта Posix, которые обладают номерами от SIGMIN до SIGMAX. При установке обработчика для этих сигналов с флагом SA_SIGINFO они будут помещаться в очередь, доставляться в порядке очереди и сопровождаться двумя дополнительными аргументами (при вызове обработчика).

Наконец, мы реализовали большую часть возможностей очереди сообщений Posix в приблизительно 500 строках кода на языке С, используя отображаемые в память файлы, взаимные исключения и условные переменные Posix. Эта реализация иллюстрирует обработку ситуации гонок при создании новой очереди; еще раз нам придется столкнуться с такой ситуацией в главе 10 при реализации семафоров Posix.

Упражнения

1.  Говоря о листинге 5.4, мы отметили, что атрибут attr функции mq_open при создании новой очереди является ненулевым; следует указать оба поля: mq_maxmsg и mq_msgsize. Как можно было бы указать только одно из них, не указывая второе, для которого использовать значения атрибутов по умолчанию?

2. Измените листинг 5.8 так, чтобы при получении сигнала не вызывалась функция mq_notify. Затем поместите в очередь два сообщения и убедитесь, что для второго из них сигнал порожден не будет. Почему?

3. Измените листинг 5.8 так, чтобы сообщение из очереди при получении сигнала не считывалось. Вместо этого просто вызовите mq_notify и напечатайте сообщение о получении сигнала. Затем отправьте два сообщения и убедитесь, что для второго из них сигнал не порождается. Почему?

4. Что произойдет, если мы уберем преобразование двух констант к целому типу в первом вызове printf в листинге 5.14? 

5. Измените листинг 5.4 следующим образом: перед вызовом mq_open напечатайте сообщение и подождите 30 секунд (sleep). После возвращения из mq_open выведите еще одно сообщение и подождите еще 30 секунд, а затем вызовите mq_close. Откомпилируйте программу и запустите ее, указав большое количество сообщений (несколько сотен тысяч) и максимальный размер сообщения, скажем, в 10 байт. Задача заключается в том, чтобы создать большую очередь и проверить, используются ли в реализации отображаемые в память файлы. В течение 30-секундной паузы запустите программу типа ps и посмотрите на занимаемый программой объем памяти. Сделайте это еще раз после возвращения из mq_open. Можете ли вы объяснить происходящее?

6. Что произойдет при вызове memcpy в листинге 5.26, если вызвавший процесс укажет нулевую длину сообщения?

7. Сравните очередь сообщений с двусторонними каналами, описанными в разделе 4.4. Сколько очередей нужно для двусторонней связи между родительским и дочерним процессами?

8. Почему мы не удаляем взаимное исключение и условную переменную в листинге 5.20?

9. Стандарт Posix утверждает, что дескриптор очереди сообщений не может иметь тип массива. Почему? 

10. В каком состоянии проводит большую часть времени функция main из листинга 5.12? Что происходит каждый раз при получении сигнала? Как мы обрабатываем эту ситуацию?

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

12. Расширьте реализацию очередей сообщений Posix из раздела 5.8 так, чтобы она поддерживала SIGEV_THREAD. 

ГЛАВА 6

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

6.1. Введениеы

Каждой очереди сообщений System V сопоставляется свой идентификатор очереди сообщений. Любой процесс с соответствующими привилегиями (раздел 3.5) может поместить сообщение в очередь, и любой процесс с другими соответствующими привилегиями может сообщение из очереди считать. Как и для очередей сообщений Posix, для помещения сообщения в очередь System V не требуется наличия подключенного к ней на считывание процесса.

Ядро хранит информацию о каждой очереди сообщений в виде структуры, определенной в заголовочном файле <sys/msg.h>:

struct msqid_ds {

 struct ipc_perm msg_perm; /* Разрешения чтения и записи: раздел 3.3 */

 struct msg *msg_first; /* указатель на первое сообщение в очереди */

 struct msg *msg_last; /* указатель на последнее сообщение в очереди */

 msglen_t msg_cbytes; /* размер очереди в байтах */

 msgqnum_t msg_qnum;  /* количество сообщений в очереди */

 msglen_t msg_qbytes; /* максимальный размер очереди в байтах */

 pid_t msg_lspid;  /* идентификатор (pid) последнего процесса, вызвавшего msgsnd(); */

 pid_t msg_lrpid;  /* pid последнего msgrcv(); */

 time_t msg_stime; /* время отправки последнего сообщения */

 time_t msg_rtime; /* время последнего считывания сообщения */

 time_t msg_ctime; /* время последнего вызова msgctl(), изменившего одно из полей структуры */

};

ПРИМЕЧАНИЕ

Unix 98 не требует наличия полей msg_first, msg_last и msg_cbytes. Тем не менее они имеются в большинстве существующих реализаций, производных от System V. Естественно, ничто не заставляет реализовывать очередь сообщений через связный список, который неявно предполагается при наличии полей msg_first и msg_last. Эти два указателя обычно указывают на участки памяти, принадлежащие ядру, и практически бесполезны для приложения.

Мы можем изобразить конкретную очередь сообщений, хранимую ядром как связный список, — рис. 6.1. В этой очереди три сообщения длиной 1, 2 и 3 байта с типами 100, 200 и 300 соответственно.

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

UNIX: взаимодействие процессов

Рис. 6.1. Структура очереди system V в ядре

6.2. Функция msgget

Создать новую очередь сообщений или получить доступ к существующей можно с помощью функции msgget:

#include <sys/msg.h>

int msgget(key_t key, int oflag);

/* Возвращает неотрицательный идентификатор в случае успешного завершения, –1 в случае ошибки */

Возвращаемое значение представляет собой целочисленный идентификатор, используемый тремя другими функциями msg для обращения к данной очереди. Идентификатор вычисляется на основе указанного ключа, который может быть получен с помощью функции ftok или может представлять собой константу IPC_PRIVATE, как показано на рис. 3.1.

Флаг oflag представляет собой комбинацию разрешений чтения-записи, показанную в табл. 3.3. К разрешениям можно добавить флаги IPC_CREAT или IPC_CREAT | IPC_EXCL с помощью логического сложения, как уже говорилось в связи с рис. 3.2.

При создании новой очереди сообщений инициализируются следующие поля структуры msqid_ds:

■ полям uid и cuid структуры msg_perm присваивается значение действующего идентификатора пользователя вызвавшего процесса, а полям gid и cgid — действующего идентификатора группы;

■ разрешения чтения-записи, указанные в oflag, помещаются в msg_perm.mode;

■ значения msg_qnum, msg_lspid, msg_lrpid, msg_stime и msg_rtime устанавливаются в 0;

■ в msg_ctime записывается текущее время;

■ в msg_qbytes помещается системное ограничение на размер очереди.

6.3. Функция msgsnd

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

#include <sys/msg.h>

int msgsnd(int msqid, const void *ptr, size_t length, int flag); 

/* Возвращает 0 в случае успешного завершения; –1 – в случае ошибки */

Здесь msqid представляет собой идентификатор очереди, возвращаемый msgget. Указатель ptr указывает на структуру следующего шаблона, определенного в <sys/ msg.h>:

struct msgbuf {

 long mtype; /* тип сообщения, должен быть > 0 */

 char mtext[1]; /* данные */

};

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

Название mtext в структуре msgbuf употреблено не вполне правильно; данные в сообщении совсем не обязательно должны быть текстом. Разрешена передача любых типов данных как в двоичном, так и в текстовом формате. Ядро никак не интерпретирует содержимое сообщения.

Для описания структуры мы используем термин «шаблон», поскольку ptr указывает на целое типа long, представляющее собой тип сообщения, за которым непосредственно следует само сообщение (если его длина больше 0 байт). Большинство приложений не пользуются этим определением структуры msgbuf, поскольку установленного в ней количества данных (1 байт) обычно недостаточно для прикладных задач. На количество данных в сообщении никаких ограничений при компиляции не накладывается (как правило, оно может быть изменено системным администратором), поэтому вместо объявления структуры с большим объемом данных (большим, чем поддерживается текущей реализацией) определяется этот шаблон. Большинство приложений затем определяют собственную структуру сообщений, в которой передаваемые данные зависят от нужд этих приложений.

Например, если приложению нужно передавать сообщения, состоящие из 16-разрядного целого, за которым следует 8-байтовый массив символов, оно может определить свою собственную структуру так:

#define MY_DATA 8

typedef struct my_msgbuf {

 long mtype; /* тип сообщения */

 int16_t mshort; /* начало данных */

 char mchar[MY_DATA];

} Message;

Аргумент length функции msgsnd указывает длину сообщения в байтах. Это длина пользовательских данных, следующих за типом сообщения (целое типа long). Длина может быть и 0. В указанном выше примере длина может быть вычислена как sizeof(Message) – sizeof(long).

Аргумент flag может быть либо 0, либо IPC_NOWAIT. В последнем случае он отключает блокировку для msgsnd: если для нового сообщения недостаточно места в очереди, возврат из функции происходит немедленно. Это может произойти, если:

■ в данной очереди уже имеется слишком много данных (значение msg_qbytes в структуре msqid_ds);

■ во всей системе имеется слишком много сообщений.

Если верно одно из этих условий и установлен флаг IPC_NOWAIT, функция msgsnd возвращает ошибку с кодом EAGAIN. Если флаг IPC_NOWAIT не указан, а одно из этих условий выполняется, поток приостанавливается до тех пор, пока не произойдет одно из следующего:

■ для сообщения освободится достаточно места;

■ очередь с идентификатором msqid будет удалена из системы (в этом случае возвращается ошибка с кодом EIDRM);

■ вызвавший функцию поток будет прерван перехватываемым сигналом (в этом случае возвращается ошибка с кодом EINTR).

6.4. Функция msgrcv

Сообщение может быть считано из очереди с помощью функции msgrcv.

#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *ptr, size_t length, long type, int flag);

/* Возвращает количество данных в сообщении, –1 – в случае ошибки */

Аргумент ptr указывает, куда следует помещать принимаемые данные. Как и для msgsnd, он указывает на поле данных типа long (рис. 4.13), которое непосредственно предшествует полезным данным.

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

Аргумент type определяет тип сообщения, которое нужно считать из очереди:

■ если значение type равно 0, возвращается первое сообщение в очереди (то есть при указании типа 0 возвращается старейшее сообщение);

■ если тип больше 0, возвращается первое сообщение, тип которого равен указанному;

■ если тип меньше нуля, возвращается первое сообщение с наименьшим типом, значение которого меньше либо равно модулю аргумента type.

Рассмотрим пример очереди сообщений, изображенный на рис. 6.1. В этой очереди имеются три сообщения:

■ первое сообщение имеет тип 100 и длину 1;

■ второе сообщение имеет тип 200 и длину 2;

■ третье сообщение имеет тип 300 и длину 3.

Таблица 6.1 показывает, какое сообщение будет возвращено при различных значениях аргумента type.


Таблица 6.1. Возвращаемое сообщение в зависимости от аргумента type 

type Тип возвращаемого сообщения
0 100
100 100
200 200
300 300
-100 100
-200 100
-300 100 

Аргумент flag указывает, что делать, если в очереди нет сообщения с запрошенным типом. Если установлен бит IPC_NOWAIT, происходит немедленный возврат из функции msgrcv с кодом ошибки ENOMSG. В противном случае вызвавший процесс блокируется до тех пор, пока не произойдет одно из следующего:

■ появится сообщение с запрошенным типом;

■ очередь с идентификатором msqid будет удалена из системы (в этом случае будет возвращена ошибка с кодом EIDRM);

■ вызвавший поток будет прерван перехватываемым сигналом (в этом случае возвращается ошибка EINTR).

В аргументе flag можно указать дополнительный бит MSG_NOERROR. При установке этого бита данные, превышающие объем буфера (аргумент length), будут просто обрезаться до его размера без возвращения кода ошибки. Если этот флаг не указать, при превышении объемом сообщения аргумента length будет возвращена ошибка E2BIG.

В случае успешного завершения работы msgrcv возвращает количество байтов в принятом сообщении. Оно не включает байты, нужные для хранения типа сообщения (long), который также возвращается через указатель ptr.

6.5. Функция msgctl

Функция msgctl позволяет управлять очередями сообщений:

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buff);

/* Возвращает 0 в случае успешного завершения, –1 в случае ошибки */

Команд (аргумент cmd) может быть три:

■ IPC_RMID — удаление очереди с идентификатором msqidиз системы. Все сообщения, имеющиеся в этой очереди, будут утеряны. Мы уже видели пример действия этой функции в листинге 3.2. Для этой команды третий аргумент функции игнорируется.

■ IPC_SET — установка значений четырех полей структуры msqid_ds данной очереди равными значениям соответствующих полей структуры, на которую указывает аргумент buff: msg_perm.uid, msg_perm.gid, msg_perm.mode, msg_qbytes.

■ IPC_STAT — возвращает вызвавшему процессу (через аргумент buff) текущее содержимое структуры msqid_ds для указанной очереди.

Пример

Программа в листинге 6.1 создает очередь сообщений, помещает в нее сообщение с 1 байтом информации, вызывает функцию msgctl с командой IPC_STAT, выполняет команду ipcs, используя функцию system, а затем удаляет очередь, вызвав функцию msgctl с командой IPC_RMID.

Листинг 6.1.[1] Пример использования функции msgctl с командой IPC_STAT

//svmsg/ctl.с

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int msqid;

6   struct msqid_ds info;

7   struct msgbuf buf;

8   msqid = Msgget(IPC_PRIVATE, SVMSG_MODE | IPC_CREAT);

9   buf.mtype = 1;

10  buf.mtext[0] = 1;

11  Msgsnd(msqid, &buf, 1, 0);

12  Msgctl(msqid, IPC_STAT, &info);

13  printf("read-write: *03o, cbytes = %lu, qnum = %lu, qbytes = %lu\n",

14   info.msg_perm.mode & 0777, (ulong_t) info.msg_cbytes,

15   (ulong_t) info.msg_qnum, (ulong_t) info.msg_qbytes);

16  system("ipcs –q");

17  Msgctl(msqid, IPC_RMID, NULL);

18  exit(0);

19 }

Мы собираемся отправить сообщение размером 1 байт, поэтому можно просто воспользоваться стандартным определением структуры msgbuf из <sys/msg.h>. Выполнение этой программы приведет к следующему результату:

solaris %ctl

read-write: 664, cbytes = 1, qnum = 1, qbytes = 4096

IPC status from <running system> as of MOn Oct 20 15:36:49 1997

T ID   Key      MODE       OWNER    GROUP

Message Queues:

q 1150 00000000 –rw-rw-r-- rstevens other1

Выведенные значения соответствуют ожидаемым. Нулевое значение ключа обычно соответствует IPC_PRIVATE, как мы отмечали в разделе 3.2. В этой системе на очередь сообщений накладывается ограничение по объему в 4096 байт. Поскольку мы записали сообщение с 1 байтом данных и msg_cbytes имеет значение 1, это ограничение накладывается на объем полезных данных и не включает тип сообщения (long), указываемый для каждого сообщения.

6.6. Простые примеры

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

Программа msgcreate

В листинге 6.2 приведена программа msgcreate, создающая очередь сообщений.

9-12 Параметр командной строки –e позволяет указать флаг IPC_EXCL.

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

Листинг 6.2. Создание очереди сообщений System V

//svmsg/msgcreate.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int c, oflag, mqid;

6   oflag = SVMSG_MODE | IPC_CREAT;

7   while ((c = Getopt(argc, argv, "e")) != –1) {

8    switch (c) {

9    case 'e':

10    oflag |= IPC_EXCL;

11    break;

12   }

13  }

14  if (optind != argc – 1)

15   err_quit("usage: msgcreate [ –e ] <pathname>");

16  mqid = Msgget(Ftok(argv[optind], 0), oflag);

17  exit(0);

18 }

Программа msgsnd

Программа msgsnd приведена в листинге 6.3. Она помещает в очередь одно сообщение заданной длины и типа.

Мы создаем указатель на структуру msgbuf общего вида, а затем выделяем место под реальную структуру (буфер записи) соответствующего размера, вызвав calloc. Эта функция инициализирует буфер нулем.

Листинг 6.3. Помещение сообщения в очередь System V

//svmsg/msgsnd.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int mqid;

6   size_t len;

7   long type;

8   struct msgbuf *ptr;

9   if (argc != 4)

10   err_quit("usage: msgsnd <pathname> <#bytes> <type>");

11  len = atoi(argv[2]);

12  type = atoi(argv[3]);

13  mqid = Msgget(Ftok(argv[1], 0), MSG_W);

14  ptr = Calloc(sizeof(long) + len, sizeof(char));

15  ptr->mtype = type;

16  Msgsnd(mqid, ptr, len, 0);

17  exit(0);

18 }

Программа msgrcv

В листинге 6.4 приведен текст программы msgrcv, считывающей сообщение из очереди. В командной строке может быть указан параметр –n, отключающий блокировку, а параметр –t может быть использован для указания типа сообщения в функции msgrcv.

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

Листинг 6.4. Считывание сообщения из очереди System V

//svmsg/msgrcv.c

1  #include "unpipc.h"

2  #define MAXMSG (8192 + sizeof(long))


3  int

4  main(int argc, char **argv)

5  {

6   int c, flag, mqid;

7   long type;

8   ssize_t n;

9   struct msgbuf *buff;

10  type = flag = 0;

11  while ((c = Getopt(argc, argv, "nt:")) != –1) {

12   switch (c) {

13   case 'n':

14    flag |= IPC_NOWAIT;

15    break;

16   case 't':

17    type = atol(optarg);

18    break;

19   }

20  }

21  if (optind != argc – 1)

22   err_quit("usage: msgrcv [ –n ] [ –t type ] <pathname>");

23  mqid = Msgget(Ftok(argv[optind], 0), MSG_R);

24  buff = Malloc(MAXMSG);

25  n = Msgrcv(mqid, buff, MAXMSG, type, flag);

26  printf("read %d bytes, type = %ld\n", n, buff->mtype);

27  exit(0);

28 }

Программа msgrmid

Для удаления очереди сообщений мы вызываем функцию msgctl с командой IPC_RMID, как показано в листинге 6.5.

Листинг 6.5. Удаление очереди сообщений System V

//svmsg/msgrmid.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int mqid;

6   if (argc != 2)

7    err_quit("usage: msgrmid <pathname>");

8   mqid = Msgget(Ftok(argv[1], 0), 0);

9   Msgctl(mqid, IPC_RMID, NULL);

10  exit(0);

11 }

Примеры

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

solaris % msgcreate /tmp/no/such/file

ftok error for pathname "tmp/no/such/file" and id 0: No such file or directory

solaris % touch /trap/test1

solaris % msgcreate /tmp/test1

solaris % msgsnd /tmp/test1 1 100

solaris % msgsnd /tmp/test1 2 200

solaris % msgsnd /tmp/test1 3 300

solaris % ipcs –qo

IPC status from <running system> as of Sat Jan 10 11:25:45 1998

T ID  KEY        MODE       OWNER    GROUP  CBYTES QNUM

Message Queues:

q 100 0х0000113e –rw-r--r-- rstevens other1 6      3

Сначала мы пытаемся создать очередь, используя имя несуществующего файла. Пример показывает, что файл, указываемый в качестве аргумента ftok, обязательно должен существовать. Затем мы создаем файл /tmp/test1 и используем его имя при создании очереди сообщений. После этого в очередь помещаются три сообщения длиной 1, 2 и 3 байта со значениями типа 100, 200 и 300 (вспомните рис. 6.1). Программа ipcs показывает, что в очереди находятся 3 сообщения общим объемом 6 байт.

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

solaris % msgrcv –t 200 /tmp/test1

read 2 bytes, type = 200

solaris % msgrcv –t -300 /tmp/test1

read 1 bytes, type = 100

solaris % msgrcv /tmp/test1

read 3 bytes, type = 300

solaris % msgrcv –n /tmp/test1

msgrcv error: No message of desired type

В первом примере запрашивается сообщение с типом 200, во втором примере — сообщение с наименьшим значением типа, не превышающим 300, а в третьем — первое сообщение в очереди. Последний запуск msgrcv иллюстрирует действие флага IPC_NOWAIT.

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

solaris % ipcs –qo

IPC status from <running system> as of Sat Jan 10 11:37:01 1998

T ID  KEY        MODE       OWNER    GROUP  CBYTES QNUM

Message Queues:

q 100 0x0000113e –rw-r--r-- rstevens other1 0      0

solaris % msgsnd /tmp/test1 1 100

solaris % msgrcv –t 999 /temp/test1

^? нажали клавишу прерывания выполнения программы

solaris % msgrcv –n –t999/tmp/test1

msgrcv error: No message of desired type

solaris % grep desired /usr/include/sys/errno.h

#define ENOMSG 35 /* No message of desired type */

solaris % msgrmid /tmp/test1

Сначала мы вызываем ipcs, чтобы убедиться, что очередь пуста, а затем помещаем в нее сообщение длиной 1 байт с типом 100. Затем мы запрашиваем сообщение с типом 999, и программа блокируется (при вызове msgrcv), ожидая помещения в очередь сообщения с указанным типом. Мы прерываем ожидание нажатием клавиши. Затем мы запускаем программу с флагом –n, предотвращающим блокировку, и видим, что в этом случае возвращается ошибка с кодом ENOMSG. После этого мы удаляем очередь с помощью программы msgrmid. Мы могли бы удалить очередь и с помощью системной команды

solaris % ipcrm –q 100

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

solaris % ipcrm –Q 0x113e

где указывается ключ очереди сообщений.

Программа msgrcvid

Покажем теперь, что для получения доступа к очереди сообщений System V не обязательно вызывать msgget: все, что нужно, — это знать идентификатор очереди сообщений, который легко получить с помощью ipcs, и считать разрешения доступа для очереди. В листинге 6.6 приведен упрощенный вариант программы msgrcv из листинга 6.4.

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

Листинг 6.6. Считывание из очереди сообщений System V с известным идентификатором

//svmsg/msgrcvid.c

1  #include "unpipc.h"

2  #define MAXMSG (8192 + sizeof(long))


3  int

4  main(int argc, char **argv)

5  {

6   int mqid;

7   ssize_t n;

8   struct msgbuf *buff;

9   if (argc != 2)

10   err_quit("usage: msgrcvid <mqid>");

11  mqid = atoi(argv[1]);

12  buff = Maloc(MAXMSG);

13  n = Msgrcv(mqid, buff, MAXMSG, 0, 0);

14  printf("read %d bytes, type = %ld\n", n, buff->mtype);

15  exit(0);

16 }

Вот пример использования этой программы:

solaris % touch /tmp/testid

solaris % msgcreate /tmp/testid

solaris % msgsnd /tmp/testid4 400

solaris % ipcs –qo

IPC status from <running system> as of Wed Mar 25 09:48:28 1998

T ID  KEY        MODE       OWNER    GROUP  CBYTES QNUM

Message Queues:

q 150 0x0000118a –rw-r--r-- rstevens other1 4      1

solaris % msgrcvid 150

read 4 bytes, type = 400

Идентификатор очереди (150) мы узнали с помощью ipcs, его мы и предоставляем программе msgrcvid в качестве аргумента командной строки.

Этот же метод можно использовать для семафоров System V (упражнение 11.1) и разделяемой памяти System V (упражнение 14.1).

6.7. Пример программы клиент-сервер

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

Заголовочный файл svmsg.h приведен в листинге 6.7. Мы подключаем наш стандартный заголовочный файл и определяем ключи для каждой из очередей сообщений.

Листинг 6.7. Заголовочный файл svmsg.h для программы клиент-сервер, использующей очереди сообщений

//svmsgcliserv/svmsg.h

1 #include "unpipc.h"

2 #define MQ_KEY1 1234L

3 #define MQ_KEY2 2345L

Функция main для сервера приведена в листинге 6.8. Программа создает обе очереди сообщений, и не беда, если какая-нибудь из них уже существует, потому что мы не указываем флаг IPC_EXCL. Функция server дана в листинге 4.16. Она вызывает наши собственные функции mesgsend и mesgrecv, новые версии которых будут приведены ниже.

Листинг 6.8. Функция main программы-сервера, использующей очереди сообщений

//svmsgcliserv/server_main.с

1  #include "svmsg.h"


2  void server(int, int);

3  int

4  main(int argc, char **argv)

5  {

6   int readid, writeid;

7   readid = Msgget(MQ_KEY1, SVMSG_MODE | IPC_CREAT);

8   writeid = Msgget(MQ_KEY2, SVMSG_MODE | IPC_CREAT);

9   server(readid, writeid);

10  exit(0);

11 }

Листинг 6.9. Функция main программы-клиента, использующей очереди сообщений

//svmsgcliserv/client_main.с

1  #include "svmsg.h"

2  void client(int, int);


3  int

4  main(int argc, char **argv)

5  {

6   int readid, writeid;

7   /* assumes server has created the queues */

8   writeid = Msgget(MQ_KEY1, 0);

9   readid = Msgget(MQ_KEY2, 0);

10  client(readid, writeid);

11  /* now we can delete the queues */

12  Msgctl(readid, IPC_RMID. NULL);

13  Msgctl(writeid, IPC_RMID, NULL);

14  exit(0);

15 }

В листинге 6.9 приведен текст функции main программы-клиента. Программа открывает две очереди сообщений и вызывает функцию client из листинга 4.15. Эта функция использует две другие: mesg_send и mesg_recv, которые будут приведены ниже.

И функция client, и функция server используют формат сообщений, изображенный в листинге 4.12. Для передачи и приема сообщений они используют функции mesg_send и mesg_recv. Старые версии этих функций, приведенные в листингах 4.13 и 4.14, вызывали write и read и работали с программными каналами и FIFO, так что нам придется переписать их для использования очередей сообщений. В листингах 6.10 и 6.11 приведены новые версии этих функций. Обратите внимание, что аргументы функций не изменились, поскольку первый целочисленный аргумент может содержать как целочисленный дескриптор программного канала или FIFO, так и целочисленный дескриптор очереди сообщений.

Листинг 6.10. Функция mesg_send, работающая с очередью сообщений System V

//svmsgcliserv/mesg_send.с

1 #include "mesg.h"


2 ssize_t

3 mesg_send(int id, struct mymesg *mptr)

4 {

5  return(msgsnd(id, &(mptr->mesg_type), mptr->mesg_len, 0));

6 }

Листинг 6.11. Функция mesg_recv, работающая с очередью сообщений System V

//svmsgcliserv/mesg_recv.с

1 #include "mesg.h"


2 ssize_t

3 mesg_recv(int id, struct mymesg *mptr)

4 {

5  ssize_t n;

6  n = msgrcv(id, &(mptr->mesg_type), MAXMESGDATA, mptr->mesg_type, 0);

7  mptr->mesg_len = n; /* количество возвращаемых данных */

8  return(n); /* –1 в случае ошибки, 0 – конец файла, иначе – >0 */

9 }

6.8. Мультиплексирование сообщений

Наличие поля type у каждого сообщения в очереди предоставляет две интересные возможности:

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

2. Поле type может использоваться для установки приоритета сообщений. Это позволяет получателю считывать сообщения в порядке, отличном от обычного для очередей (FIFO). В программных каналах и FIFO данные могли приниматься только в том порядке, в котором они были отправлены. Очереди System V позволяют считывать сообщения в произвольном порядке в зависимости от значений типа сообщений. Более того, можно вызывать msgrcv с флагом IPC_NOWAIT для считывания сообщений с конкретным типом и немедленного возвращения управления процессу в случае отсутствия таких сообщений.

Пример: одна очередь на приложение

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

UNIX: взаимодействие процессов

Рис. 6.2. Мультиплексирование сообщений между несколькими клиентами и одним сервером


Рассмотрим усложненный вариант: один сервер и несколько клиентов. В этом случае можно использовать значение типа 1, например, для обозначения сообщений от любого клиента серверу. Если клиент передаст серверу свой идентификатор процесса в качестве части сообщения, сервер сможет отсылать клиенту сообщения, используя его идентификатор в качестве значения типа сообщения. Каждый клиент будет использовать свой PID в качестве аргумента type при вызове msgrcv. На рис. 6.2 приведен пример использования очереди для мультиплексирования этих сообщений между несколькими клиентами и одним сервером.

ПРИМЕЧАНИЕ

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

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

В листинге 6.12 приведен текст функции main сервера. Заголовочный файл svmsg.h был приведен в листинге 6.7. Создается единственная очередь сообщений (если она существует, ошибки не возникнет). Идентификатор этой очереди сообщений используется в качестве обоих аргументов при вызове функции server.

Листинг 6.12. Функция main сервера

//svmsgmpx1q/server_main.с

1  #include "svmsg.h"

2  void server(int, int);


3  int

4  main(int argc, char **argv)

5  {

6   int msqid;

7   msqid = Msgget(MQ_KEY1, SVMSG_MODE | IPC_CREAT);

8   server(msqid, msqid); /* одна очередь в обе стороны */

9   exit(0);

10 }

Функция server обеспечивает работу сервера. Ее текст приведен в листинге 6.13. Эта функция представляет собой комбинацию листинга 4.10 — нашего сервера FIFO, считывавшего команды, состоявшие из идентификатора процесса и полного имени файла, — и листинга 4.16, в котором использовались функции mesg_send и mesg_recv. Обратите внимание, что идентификатор процесса, отправляемый клиентом, используется в качестве типа для всех сообщений, отправляемых сервером этому клиенту. Эта функция представляет собой бесконечный цикл, в котором считываются запросы клиентов и отсылаются запрошенные файлы. Этот сервер является последовательным (см. раздел 4.9).

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

Функция client, текст которой дан в листинге 6.15, обеспечивает всю обработку со стороны клиента. Эта функция представляет собой комбинацию программ из листингов 4.11 и 4.15. В первой программе клиент отсылал свой идентификатор и полное имя файла, а во второй программе использовались функции mesg_send и mesg_recv. Обратите внимание, что тип сообщений, запрашиваемых функцией mesg_recv, совпадает с идентификатором процесса клиента.

Функции client и server используют функции mesg_send и mesg_recv из листингов 6.9 и 6.11.

Листинг 6.13. Функция server

//svmsgmpx1q/server.c

1  #include "mesg.h"


2  void

3  server(int readfd, int writefd)

4  {

5   FILE *fp;

6   char *ptr;

7   pid_t pid;

8   ssize_t n;

9   struct mymesg mesg;

10  for (;;) {

11   /* считывание полного имени из канала IPC */

12   mesg.mesg_type = 1:

13   if ((n = Mesg_recv(readfd, &mesg)) == 0) {

14    err_msg("pathname missing");

15    continue;

16   }

17   mesg.mesg_data[n] = '\0'; /* полное имя */

18   if ((ptr = strchr(mesg.mesg_data, ' ')) == NULL) {

19    err_msg("bogus request: %s", mesg.mesg_data);

20    continue;

21   }

22   *ptr++ =0; /* ptr = полное имя */

23   pid = atol(mesg.mesg_data);

24   mesg.mesg_type = pid; /* для обратных сообщений */

25   if ((fp = fopen(ptr, "r")) == NULL) {

26    /* 4error: must tell client */

27    snprintf(mesg.mesg_data + n, sizeof(mesg.mesg_data) – n,

28     ": can't open. %s\n", strerror(errno));

29    mesg.mesg_len – strlen(ptr);

30    memmove(mesg.mesg_data, ptr, mesg.mesg_len);

31    Mesg_send(writefd, &mesg);

32   } else {

33    /* файл открыт, копируем клиенту */

34    while (Fgets(mesg.mesg_data, MAXMESGDATA, fp) != NULL) {

35     mesg.mesg_len = strlen(mesg.mesg_data);

36     Mesg_send(writefd, &mesg);

37    }

38    Fclose(fp);

39   }

40   /* сообщение нулевой длины заканчивает связь */

41   mesg.mesg_len = 0;

42   Mesg_send(writefd, &mesg);

43  }

44 }

Листинг 6.14. Функция main клиента

//svmsgmpx1q/client_main.c

1  #include "svmsg.h"

2  void client(int, int);


3  int

4  main(int argc, char **argv)

5  {

6   int msqid;

7   /* сервер должен был создать очередь */

8   msqid = Msgget(MQ_KEY1, 0);

9   client(msqid, msqid); /* одна очередь в обе стороны */

10  exit(0);

11 }

Листинг 6.15. Функция client

//svmsgmpx1q/client.с

1  #include "mesg.h"


2  void

3  client(int readfd, int writefd)

4  {

5   size_t len;

6   ssize_t n;

7   char *ptr;

8   struct mymesg mesg;

9   /* инициализируем буфер идентификатором процесса и пробелом */

10  snprintf(mesg.mesg_data, MAXMESGDATA. "%ld ", (long) getpid());

11  len = strlen(mesg.mesg_data);

12  ptr = mesg.mesg_data + len;

13  /* считываем полное имя файла */

14  Fgets(ptr, MAXMESGDATA – len, stdin);

15  len = strlen(mesg.mesg_data);

16  if (mesg.mesg_data[len-1] == '\n')

17   len--; /* удаляем перевод строки fgets() */

18  mesg.mesg_len = len;

19  mesg.mesg_type = 1;

20  /* записываем PID и имя файла в канал IPC */

21  Mesg_send(writefd, &mesg);

22  /* считываем из канала IPC, записываем в stdout */

23  mesg.mesg_type = getpid();

24  while ((n = Mesg_recv(readfd, &mesg)) > 0)

25   Write(STDOUT_FILENO, mesg.mesg_data, n);

26 } 

Пример: одна очередь для каждого клиента

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

UNIX: взаимодействие процессов

Рис. 6.3. Одна очередь для сервера и по одной для каждого клиента


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

ПРИМЕЧАНИЕ

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

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

■ mesg.h (листинг 4.12);

■ svmsg.h (листинг 6.7);

■ функция main сервера (листинг 6.12);

■ функция mesg_send (листинг 4.13).

Функция main клиента приведена в листинге 6.16; она слегка изменилась по сравнению с листингом 6.14. Мы открываем очередь сервера с известным ключом (MQ_KEY1) и создаем нашу собственную очередь с ключом IPC_PRIVATE. Два идентификатора этих очередей становятся аргументами функции client (листинг 6.17). После завершения работы клиента его персональная очередь удаляется.

Листинг 6.16. Функция main клиента

//svmsgmpxnq/client_main.с

1  #include "svmsg.h"


2  void client(int, int);

3  int

4  main(int argc, char **argv)

5  {

6   int readid, writeid;

7   /* сервер должен создать свою очередь */

8   writeid = Msgget(MQ_KEY1, 0);

9   /* мы создаем свою собственную очередь */

10  readid = Msgget(IPC_PRIVATE, SVMSG_MODE | IPC_CREAT);

11  client(readid, writeid);

12  /* и удаляем нашу собственную очередь */

13  Msgctl(readid, IPC_RMID, NULL);

14  exit(0);

15 }

Листинг 6.17. Функция client

//svmsgmpxnq/client.с

1  #include "mesg.h"


2  void

3  client(int readid, int writeid)

4  {

5   size_t len;

6   ssize_t n;

7   char *ptr;

8   struct mymesg mesg;

9   /* инициализируем буфер идентификатором очереди и пробелом */

10  snprintf(mesg.mesg_data, MAXMESGDATA, "%d ", readid);

11  len = strlen(mesg.mesg_data);

12  ptr = mesg.mesg_data + len;

13  /* считываем имя файла */

14  Fgets(ptr, MAXMESGDATA – len, stdin);

15  len = strlen(mesg.mesg_data);

16  if (mesg.mesg_data[len-1] == '\n')

17   len--; /* удаляем перевод строки fgets() */

18  mesg.mesg_len = len;

19  mesg.mesg_type = 1;

20  /* отправляем идентификатор очереди и имя файла серверу */

21  Mesg_send(writeid, &mesg);

22  /* считываем ответ из нашей очереди и записываем его в stdout */

23  while ((n = Mesg_recv(readid, &mesg)) > 0)

24   Write(STDOUT_FILENO, mesg.mesg_data, n);

25 }

В листинге 6.17 приведен текст функции client. Эта функция практически идентична функции из листинга 6.15, но вместо передачи идентификатора процесса клиента на сервер направляется идентификатор очереди клиента. Тип сообщения в структуре mesg остается равным 1, поскольку это значение устанавливается для сообщений, передаваемых в обе стороны.

В листинге 6.19 приведена функция server. Главное отличие от листинга 6.13 в том, что эта функция представляет собой бесконечный цикл, в котором для каждого нового клиента вызывается fork.

Установка обработчика сигнала для SIGCHLD

10 Поскольку для каждого клиента порождается отдельный процесс, нужно позаботиться о процессах-зомби. В разделах 5.9 и 5.10 [24] об этом говорится подробно. Здесь мы просто установим обработчик для сигнала SIGCHLD, и наша функция sig_chld (листинг 6.18) будет вызываться при завершении работы дочернего процесса.

12-18 Породивший процесс сервера блокируется в вызове mesg_recv, ожидая появления сообщения от очередного клиента.

25-45 Вызовом fork порождается новый процесс, который производит попытку открыть запрошенный файл и отправляет клиенту либо сообщение об ошибке, либо содержимое файла. Мы преднамеренно поместили вызов fopen в дочерний процесс, а не в родительский, поскольку если файл находится в удаленной файловой системе, его открытие может занять довольно много времени в случае наличия проблем с сетью.

Функция-обработчик для SIGCHLD приведена в листинге 6.18. Она скопирована с листинга 5.11 [24].

Листинг 6.18. Обработчик сигнала SIGCHLD, вызывающий waitpid

//svmsgmpxnq/sigchldwaitpid.с

1 #include "unpipc.h"


2 void

3 sig_chld(int signo)

4 {

5  pid_t pid;

6  int stat;

7  while ((pid = waitpid(-1, &stat, WNOHANG)) > 0);

8  return;

9 }

Каждый раз при вызове обработчика происходит циклический вызов waitpid для получения статуса завершения работы всех дочерних процессов, которые могли завершить работу. Затем происходит возврат из обработчика сигнала. При этом может возникнуть проблема, поскольку родительский процесс проводит большую часть времени в заблокированном состоянии (при вызове mesg_recv, листинг 6.9). При возвращении из обработчика этот вызов msgrcv прерывается. Функция возвращает ошибку с кодом EINTR, как рассказывается в разделе 5.9 [24]. 

Нам нужно обработать такой возврат из вызванной функции, поэтому мы пишем новую функцию-обертку Mesg_recv, приведенную в листинге 6.20. Эта программа допускает возвращение ошибки с кодом EINTR функцией mesg_recv (которая просто вызывает msgrcv), и, если это происходит, мы просто еще раз вызываем mesg_recv.

Листинг 6.19. Функция server

//svmsgmpxnq/server.c

1  #include "mesg.h"


2  void

3  server(int readid, int writeid)

4  {

5   FILE *fp;

6   char *ptr;

7   ssize_t n;

8   struct mymesg mesg;

9   void sig_chld(int);

10  Signal(SIGCHLD, sig_chld);

11  for (;;) {

12   /* считывание имени файла из очереди */

13   mesg.mesg_type = 1;

14   if ((n = Mesg_recv(readid, &mesg)) == 0) {

15    err_msg("pathname missing");

16    continue;

17   }

18   mesg.mesg_data[n] = 40'; /* имя файла */

19   if ((ptr = strchr(mesg.mesg_data, ' ')) = NULL) {

20    err_msg("bogus request: %s", mesg.mesg_data);

21    continue;

22   }

23   *ptr++ = 0; /* ptr = имя файла */

24   writeid = atoi(mesg.mesg_data);

25   if (Fork() == 0) { /* дочерний процесс */

26    if ((fp = fopen(ptr, "r")) == NULL) {

27     /* ошибка: нужно сообщить клиенту */

28     snprintf(mesg.mesg_data + n, sizeof(mesg.mesg_data) – n,

29      ": can't open, %s\n", strerror(errno));

30     mesg.mesg_len = strlen(ptr);

31     memmove(mesg.mesg_data, ptr, mesg.mesg_len);

32     Mesg_send(writeid, &mesg);

33    } else {

34     /* файл открыт, копируем клиенту */

35     while (Fgets(mesg.mesg_data, MAXMESGDATA, fp) != NULL) {

36      mesg.mesg_len = strlen(mesg.mesg_data);

37      Mesg_send(writeid, &mesg);

38     }

39     Fclose(fp);

40    }

41    /* отправка сообщения нулевой длины, указывающего конец файла */

42    mesg.mesg_len = 0;

43    Mesg_send(writeid, &mesg);

44    exit(0); /* завершение дочернего процесса */

45   }

46   /* родительский процесс просто зациклен */

47  }

48 }

Листинг 6.20. Функция-обертка Mesg_recv, обрабатывающая прерванный системный вызов

//svmsgmpxnq/mesg_recv.с

10 ssize_t

11 Mesg_recv(int id, struct mymesg *mptr)

12 {

13  ssize_t n;

14  do {

15   n = mesg_recv(id, mptr);

16  } while (n == –1 && errno == EINTR);

17  if (n == –1)

18   err_sys("mesg_recv error");

19  return(n);

20 }

6.9. Использование select и poll с очередями сообщений

Одним из недостатков очередей сообщений System V является то, что они идентифицируются не дескрипторами, а идентификаторами. Поэтому с ними нельзя использовать функции select и poll (глава 6 [24]).

ПРИМЕЧАНИЕ

На самом деле одна из версий Unix, а именно AIX (созданная IBM), позволяет использовать select с очередями сообщений System V, а не только с дескрипторами. Но эта возможность имеется только в AIX.

Этот недостаток часто всплывает, когда возникает необходимость написать сервер, работающий одновременно с сетевыми соединениями и с IPC. Сетевые соединения с использованием интерфейса сокетов или XTI ([24]) используют дескрипторы, что позволяет вызывать select или poll. Программные каналы и FIFO также идентифицируются дескрипторами, поэтому для них тоже допустимо использование этих функций.

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

ПРИМЕЧАНИЕ

В листинге 5.12 мы привели решение с использованием очередей сообщений Posix, которое не требовало вызова fork. Для очередей сообщений Posix можно было обойтись одним процессом, поскольку они предусматривают уведомление о появлении нового сообщения с помощью сигнала. Для очередей System V такая возможность не предусмотрена, поэтому приходится порождать процесс, который будет блокироваться при вызове msgrcv.

Другим недостатком очередей сообщений System V по сравнению с сетевым интерфейсом является невозможность считывания сообщений из оперативной памяти (возможность, предоставляемая флагом MSG_PEEK для функций recv, recvfrom, recvmsg [24, с. 356]). Если бы такая возможность имелась, в предложенной только что схеме клиент-сервер (для обхода проблемы с select) можно было бы сделать работу более эффективной, указав флаг peek при вызове msgrcv дочерним процессом и записав 1 байт в канал при приходе сообщения, а родительский процесс тогда просто считывал бы сообщение из очереди.

6.10. Ограничения, накладываемые на очереди сообщений

Как отмечалось в разделе 3.8, на очереди сообщений часто накладываются системные oгрaничeния. В табл. 6.2 приведены значения этих oгрaничeний для двух конкретных реализаций. Первая колонка представляет собой традиционное имя System V для переменной ядра, хранящей это ограничение.


Таблица 6.2. Характерные значения ограничений для очередей сообщений

Имя Описание DUnix 4.0B Solaris 2.6
msgmax Максимальное количество байтов в сообщении 8192 2048
msgmnb Максимальное количество байтов в очереди сообщений 16384 4096
msgmni Максимальное количество очередей сообщений в системе 64 50
msgtlq Максимальное количество сообщений в системе 40 40 

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

Пример

В листинге 6.21 приведен текст программы, которая определяет четыре ограничения, показанные в табл. 6.2.

Листинг 6.21. Определение системных ограничений для очередей сообщений System V

//svmsg/limits.c

1  #include "unpipc.h"

2  #define MAX_DATA 64*1024

3  #define MAX_NMESG 4096

4  #define MAX_NIDS 4096

5  int max_mesg;


6  struct mymesg {

7   long type;

8   char data[MAX_DATA];

9  } mesg;


10 int

11 main(int argc, char **argv)

12 {

13  int i, j, msqid, qid[MAX_NIDS];

14  /* определение максимального размера сообщения */

15  msqid = Msgget(IPC_PRIVATE, SVMSG_MODE | IPC_CREAT);

16  mesg.type = 1;

17  for (i = MAX_DATA; i > 0; i –= 128) {

18   if (msgsnd(msqid, &mesg, i, 0) == 0) {

19    printf("maximum amount of data per message = %d\n", i);

20    max_mesg = i;

21    break;

22   }

23   if (errno != EINVAL)

24    err_sys("msgsnd error for length %d", i);

25  }

26  if (i == 0)

27   err_quit("i == 0");

28  Msgct(lmsqid, IPC_RMID, NULL);

29  /* количество сообщений в очереди */

30  mesg.type = 1;

31  for (i = 8; i <= max_mesg; i *= 2) {

32   msqid = Msgget(IPC_PRIVATE, SVMSG_MODE | IPC_CREAT);

33   for (j = 0; j < MAX_NMESG; j++) {

34    if (msgsnd(msqid, &mesg, i, IPC_NOWAIT) != 0) {

35     if (errno == EAGAIN)

36      break;

37     err_sys("msgsnd error, i = %d, j = %d", i, j);

38     break;

39    }

40   }

41   printf("%d %d-byte messages were placed onto queue,", j, i);

42   printf(" %d bytes total\n". i*j);

43   Msgctl(msqid, IPC_RMID, NULL);

44  }

45  /* максимальное количество идентификаторов */

46  mesg.type = 1;

47  for (i = 0; i <= MAX_NIDS; i++) {

48   if ((qid[i] = msgget(IPC_PRIVATE, SVMSG_MODE | IPC_CREAT)) == –1) {

49    printf("%d identifiers open at once\n", i);

50    break;

51   }

52  }

53  for (j = 0; j < i; j++)

54   Msgctl(qid[j], IPC_RMID, NULL);

55  exit(0);

56 }

Определение максимального размера сообщения

14-28 Для определения максимально возможного размера сообщения мы пытаемся послать сообщение, в котором будет 65 536 байт данных, и если эта попытка оказывается неудачной, уменьшаем этот объем до 65 408, и т.д., пока вызов msgsnd не окажется успешным.

Сколько сообщений различного размера может быть помещено в очередь?

29-44 Теперь мы начинаем с 8-байтовых сообщений и смотрим, сколько их поместится в очередь. После определения этого ограничения мы удаляем очередь (сбрасывая все эти сообщения) и повторяем процедуру с 16-байтовыми сообщениями. Мы повторяем это до тех пор, пока не будет достигнут максимальный размер сообщения из первого пункта. Ожидается, что небольшие сообщения будут превышать ограничение по количеству сообщений в очереди, а большие — ограничение по количеству байтов.

Сколько идентификаторов может быть открыто одновременно?

45-54 Обычно есть системное ограничение на количество одновременно открытых идентификаторов. Оно определяется непосредственно созданием очередей до тех пор, пока не произойдет ошибка при вызове msgget.

Запустим эту программу сначала в Solaris 2.6, а затем в Digital Unix 4.0B, и результаты подтвердят приведенные в табл. 6.2 величины:

solaris % limits

maximum amount of data per message = 2048

40 8-byte messages were placed on queue, 320 bytes total

40 16-byte messages were placed on queue, 640 bytes total

40 32-byte messages were placed on queue, 1280 bytes total 

40 64-byte messages were placed on queue, 2560 bytes total

32 128-byte messages were placed on queue, 4096 bytes total

16 256-byte messages were placed on queue, 4096 bytes total

8 512-byte messages were placed on queue, 4096 bytes total

4 1024-byte messages were placed on queue, 4096 bytes total

2 2048-byte messages were placed on queue, 4096 bytes total

50 identifiers open at once


alpha % limits

maximum amount of data per message = 8192

40 8-byte messages were placed on queue, 320 bytes total

40 16-byte messages were placed on queue, 640 bytes total

40 32-byte messages were placed on queue, 1280 bytes total

40 64-byte messages were placed on queue, 2560 bytes total

40 128-byte messages were placed on queue, 5120 bytes total

40 256-byte messages were placed on queue, 10240 bytes total

32 512-byte messages were placed on queue, 16384 bytes total

16 1024-byte messages were placed on queue, 16384 bytes total

8 2048-byte messages were placed on queue, 16384 bytes total

4 4096-byte messages were placed on queue, 16384 bytes total

2 8192-byte messages were placed on queue, 16384 bytes total

63 identifiers at once

Причина, по которой в Digital Unix 4.0В получился результат 63 идентификатора, а не 64, как в табл. 6.2, заключается в том, что один идентификатор всегда используется системным демоном.

6.11.Резюме

Очереди сообщений System V аналогичны очередям сообщений Posix. При создании новых приложений следует рассмотреть возможность использования очередей сообщений Posix, но большое количество существующих программ использует очереди сообщений System V. Тем не менее переписать их для использования очередей Posix вместо очередей System V не должно быть слишком сложно. Главный недостаток очередей Posix заключается в невозможности считывания сообщений с произвольным приоритетом. Ни один из типов очередей не использует обычные дескрипторы, что делает сложным применение функций select и poll для очередей сообщений.

Упражнения

1. Почему на рис. 6.2 для сообщений, передаваемых серверу, используется тип 1?

2. Что произойдет с программой с рис. 6.2, если злоумышленник отправит на сервер множество сообщений, но не будет считывать ответы? Что в такой же ситуации произойдет с программой с рис. 6.3?

3. Переделайте реализацию очередей сообщений Posix из раздела 5.8 для использования очередей сообщений System V вместо отображения в память. 

ЧАСТЬ 3

СИНХРОНИЗАЦИЯ

ГЛАВА 7

Взаимные исключения и условные переменные

7.1. Введение

Эта глава начинается с обсуждения синхронизации — принципов синхронизации действий нескольких программных потоков или процессов. Обычно это требуется для предоставления нескольким потокам или процессам совместного доступа к данным. Взаимные исключения (mutual exclusion — mutex) и условные переменные (conditional variables) являются основными средствами синхронизации.

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

ПРИМЕЧАНИЕ

Эта возможность является дополнительной согласно Posix, но обязательной в Unix 98 (см. табл. 1.3).

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

7.2. Взаимные исключения: установка и снятие блокировки

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

блокировать_mutex(…);

критическая область

разблокировать_mutex(…);

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

Взаимные исключения по стандарту Posix объявлены как переменные с типом pthread_mutex_t. Если переменная-исключение выделяется статически, ее можно инициализировать константой PTHREAD_MUTEX_INITIALIZER:

static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;

При динамическом выделении памяти под взаимное исключение (например, вызовом mallос) или при помещении его в разделяемую память мы должны инициализировать эту переменную во время выполнения, вызвав функцию pthread_ mutex_init, как показано в разделе 7.7.

ПРИМЕЧАНИЕ

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

Следующие три функции используются для установки и снятия блокировки взаимного исключения:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mptr);

int pthread_mutex_trylock(pthread_mutex_t *mptr);

int pthread_mutex_unlock(pthread_mutex_t *mptr);

/* Все три возвращают 0 в случае успешного завершения, положительное значение Еххх – случае ошибки */

При попытке заблокировать взаимное исключение, которое уже заблокировано другим потоком, функция pthread_mutex_lock будет ожидать его разблокирования, a pthread_mutex_trylock (неблокируемая функция) вернет ошибку с кодом EBUSY.

ПРИМЕЧАНИЕ

Если несколько процессов ожидают освобождения взаимного исключения, какой из них начнет выполняться первым? Одна из возможностей, добавленных стандартом 1003.1b-1993, заключается в установке приоритета потоков. Мы не будем углубляться в эту область, отметим лишь, что разным потокам могут быть присвоены разные значения приоритета и функции синхронизации (работающие с взаимными исключениями, блокировками чтения-записи и семафорами) будут передавать управление заблокированному потоку с наивысшим значением приоритета. Раздел 5.5 книги [3] описывает возможности планирования выполнения в Posix.1 более подробно.

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

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

7.3. Схема производитель-потребитель

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

С этой задачей мы регулярно сталкиваемся при использовании каналов Unix. Команда интерпретатора, использующая канал

grep pattern chapters.* | wc -l

является примером такой задачи. Программа grep выступает как производитель (единственный), a wc — как потребитель (тоже единственный). Канал используется как форма IPC. Требуемая синхронизация между производителем и потребителем обеспечивается ядром, обрабатывающим команды write производителя и read покупателя. Если производитель опережает потребителя (канал переполняется), ядро приостанавливает производителя при вызове write, пока в канале не появится место. Если потребитель опережает производителя (канал опустошается), ядро приостанавливает потребителя при вызове read, пока в канале не появятся данные.

Такой тип синхронизации называется неявным; производитель и потребитель не знают о том, что синхронизация вообще осуществляется. Если бы мы использовали очередь сообщений Posix или System V в качестве средства IPC между производителем и потребителем, ядро снова взяло бы на себя обеспечение синхронизации.

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

В одном процессе у нас имеется несколько потоков-производителей и один поток-потребитель. Целочисленный массив buff содержит производимые и потребляемые данные (данные совместного пользования). Для простоты производители просто устанавливают значение buff[0] в 0, buff [1] в 1 и т.д. Потребитель перебирает элементы массива, проверяя правильность записей.

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

UNIX: взаимодействие процессов

Рис. 7.1. Производители и потребитель


Листинг 7.1.[1] Функция main

//mutex/prodcons2.с

1  #include "unpipc.h"

2  #define MAXNITEMS 1000000

3  #define MAXNTHREADS 100

4  int nitems; /* только для чтения потребителем и производителем */


5  struct {

6   pthread_mutex_t mutex;

7   int buff[MAXNITEMS];

8   int nput;

9   int nval;

10 } shared = {

11  PTHREAD_MUTEX_INITIALIZER

12 };


13 void *produce(void *), *consume(void *);


14 int

15 main(int argc, char **argv)

16 {

17  int i, nthreads, count[MAXNTHREADS];

18  pthread_t tid_produce[MAXNTHREADS], tid_consume;

19  if (argc != 3)

20   err_quit("usage: prodcons2 <#items> <#threads>");

21  nitems = min(atoi(argv[1]), MAXNITEMS);

22  nthreads = min(atoi(argv[2]), MAXNTHREADS);

23  Set_concurrency(nthreads);

24  /* запуск всех потоков-производителей */

25  for (i = 0; i < nthreads; i++) {

26   count[i] = 0;

27   Pthread_create(&tid_produce[i], NULL, produce, &count[i]);

28  }

29  /* ожидание завершения всех производителей */

30  for (i = 0; i < nthreads; i++) {

31   Pthread_join(tid_produce[i], NULL);

32   printf("count[%d] = %d\n", i, count[i]);

33  }

34  /* запуск и ожидание завершения потока-потребителя */

35  Pthread_create(&tid_consume, NULL, consume, NULL);

36  Pthread_join(tid_consume, NULL);

37  exit(0);

38 }

Совместное использование глобальных переменных потоками

4-12 Эти переменные совместно используются потоками. Мы объединяем их в структуру с именем shared вместе с взаимным исключением, чтобы подчеркнуть, что доступ к ним можно получить только вместе с ним. Переменная nput хранит индекс следующего элемента массива buff, подлежащего обработке, a nval содержит следующее значение, которое должно быть в него помещено (0, 1, 2 и т.д.). Мы выделяем память под эту структуру и инициализируем взаимное исключение, используемое для синхронизации потоков-производителей.

ПРИМЕЧАНИЕ

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

Аргументы командной строки

19-22 Первый аргумент командной строки указывает количество элементов, которые будут произведены производителями, а второй — количество запускаемых потоков-производителей.

Установка уровня параллельности

23 Функция set_concurrency (наша собственная) указывает подсистеме потоков количество одновременно выполняемых потоков. В Solaris 2.6 она просто вызывает thr_setconcurrency, причем ее запуск необходим, если мы хотим, чтобы у нескольких процессов-производителей была возможность начать выполняться. Если мы не сделаем этого вызова в системе Solaris, будет запущен только первый поток. В Digital Unix 4.0B наша функция set_concurrency не делает ничего, поскольку в этой системе по умолчанию все потоки процесса имеют равные права на вычислительные ресурсы.

ПРИМЕЧАНИЕ

Unix 98 требует наличия функции pthread_setconcurrency, выполняющей это же действие. Эта функция требуется для тех реализаций, которые мультиплексируют пользовательские потоки (создаваемые функцией pthread_create) на небольшое множество выполняемых потоков ядра. Такие реализации часто называются «многие-к-немногим» (many-to-few), «двухуровневые» (two-level) или «М-на-N» (M-to-N). В разделе 5.6 книги [3] отношения между пользовательскими потоками и потоками ядра рассматриваются более подробно.

Создание процессов-производителей

24-28 Создаются потоки-производители, каждый из которых вызывает функцию produce. Идентификаторы потоков хранятся в массиве tid_produce. Аргументом каждого потока-производителя является указатель на элемент массива count. Счетчики инициализируются значением 0, и каждый поток увеличивает значение своего счетчика на 1 при помещении очередного элемента в буфер. Содержимое массива счетчиков затем выводится на экран, так что мы можем узнать, сколько элементов было помещено в буфер каждым из потоков.

Ожидание завершения работы производителей, запуск потребителя

29-36 Мы ожидаем завершения работы всех потоков-производителей, выводя содержимое счетчика для каждого потока, а затем запускаем единственный процесс-потребитель. Таким образом (на данный момент) мы исключаем необходимость синхронизации между потребителем и производителями. Мы ждем завершения работы потребителя, а затем завершаем работу процесса. В листинге 7.2 приведен текст функций produce и consume.

Листинг 7.2. Функции produce и consume

//mutex/prodcons2.с

39 void *

40 produce(void *arg)

41 {

42  for (;;) {

43   Pthread_mutex_lock(&shared.mutex);

44   if (shared.nput >= nitems) {

45    Pthread_mutex_unlock(&shared.mutex);

46    return(NULL); /* массив полный, готово */

47   }

48   shared.buff[shared.nput] = shared.nval;

49   shared.nput++;

50   shared.nval++;

51   Pthread_mutex_unlock(&shared.mutex);

52   *((int *) arg) += 1;

53  }

54 }


55 void *

56 consume(void *arg)

57 {

58  int i;

59  for (i = 0; i < nitems; i++) {

60   if (shared.buff[i] != i)

61    printf("buff[%d] = %d\n", i, shared.buff[i]);

62  }

63  return(NULL);

64 }

Формирование данных

42-53 Критическая область кода производителя состоит из проверки на достижение конца массива (завершение работы)

if (shared.nput >= nitems)

и трех строк, помещающих очередное значение в массив:

shared.buff[shared.nput] = shared.nval;

shared.nput++;

shared.nval++;

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

Потребитель проверяет содержимое массива

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

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

solaris % prodcons2 1000000 5

count[0] = 167165

count[1] = 249891

count[2] = 194221

count[3] = 191815

count[4] = 196908

Как мы отмечали ранее, если убрать вызов set_concurrency, в системе Solaris 2.6 значение count[0] будет 1000000, а все остальные счетчики будут нулевыми.

Если убрать из этого примера блокировку с помощью взаимного исключения, он перестанет работать, как и предполагается. Потребитель обнаружит множество элементов buff[i], значения которых будут отличны от i. Также мы можем убедиться, что удаление блокировки ничего не изменит, если будет выполняться только один поток.

7.4. Блокировка и ожидание

Продемонстрируем теперь, что взаимные исключения предназначены для блокирования, но не для ожидания. Изменим наш пример из предыдущего раздела таким образом, чтобы потребитель запускался сразу же после запуска всех производителей. Это даст возможность потребителю обрабатывать данные по мере их формирования производителями в отличие от пpoгрaммы в листинге 7.1, в которой потребитель не запускался до тех пор, пока все производители не завершали свою работу. Теперь нам придется синхронизовать потребителя с производителями, чтобы первый обрабатывал только данные, уже сформированные последними.

В листинге 7.3 приведен текст функции main. Начало кода (до объявления функции main) не претерпело никаких изменений по сравнению с листингом 7.1.

Листинг 7.3. Функция main: запуск потребителя сразу после запуска производителей

//mutex/prodcons3.c

14 int

15 main(int argc, char **argv)

16 {

17  int i, nthreads, count[MAXNTHREADS];

18  pthread_t tid_produce[MAXNTHREADS], tid_consume;

19  if (argc != 3)

20   err_quit("usage: prodcons3 <#items> <#threads>");

21  nitems = min(atoi(argv[1]), MAXNITEMS);

22  nthreads = min(atoi(argv[2]), MAXNTHREADS);

23  /* создание всех производителей и одного потребителя */

24  Set_concurrency(nthreads + 1);

25  for (i = 0; i < nthreads; i++) {

26   count[i] = 0;

27   Pthread_create(&tid_produce[i], NULL, produce, &count[i]);

28  }

29  Pthread_create(&tid_consume, NULL, consume, NULL);

30  /* ожидание завершения производителей и потребителя */

31  for (i = 0; i < nthreads; i++) {

32   Pthread_join(tid_produce[i], NULL);

33   printf("count[%d] = %d\n", i, count[i]);

34  }

35  Pthread_join(tid_consume, NULL);

36  exit(0);

37 }

24 Мы увеличиваем уровень параллельного выполнения на единицу, чтобы учесть поток-потребитель, выполняемый параллельно с производителями.

25-29 Поток-потребитель создается сразу же после создания потоков-производителей.

Функция produce по сравнению с листингом 7.2 не изменяется. В листинге 7.4 приведен текст функции consume, вызывающей новую функцию consume_wait. 

Листинг 7.4. Функции consume и consume_wait

//mutex/prodcons3.с

54 void

55 consume wait(int i)

56 {

57  for (;;) {

58   Pthread_mutex_lock(&shared.mutex);

59   if (i < shared.nput) {

60    Pthread_mutex_unlock(&shared.mutex);

61    return; /* элемент готов */

62   }

63   Pthread_mutex_unlock(&shared.mutex);

64  }

65 }


66 void *

67 consume(void *arg)

68 {

69  int i;

70  for (i = 0; i < nitems; i++) {

71   consume_wait(i);

72   if (shared.buff[i] != i)

73    printf("buff[%d] = %d\n", i, shared.buff[i]);

74  }

75  return(NULL);

76 }

Потребитель должен ждать

71 Единственное изменение в функции consume заключается в добавлении вызова consume_wait перед обработкой следующего элемента массива.

Ожидание производителей

57-64 Наша функция consume_wait должна ждать, пока производители не создадут i-й элемент. Для проверки этого условия производится блокировка взаимного исключения и значение i сравнивается с индексом производителя nput. Блокировка необходима, поскольку значение nput может быть изменено одним из производителей в момент его проверки.

Главная проблема — что делать, если нужный элемент еще не готов. Все, что нам остается и что мы делаем в листинге 7.4, — это повторять операции в цикле, устанавливая и снимая блокировку и проверяя значение индекса. Это называется опросом (spinning или polling) и является лишней тратой времени процессора.

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

7.5. Условные переменные: ожидание и сигнализация

Взаимное исключение используется для блокирования, а условная переменная — для ожидания. Это два различных средства синхронизации, и оба они нужны. Условная переменная представляет собой переменную типа pthread_cond_t. Для работы с такими переменными предназначены две функции:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cptr, pthread_m_tex_t *mptr);

int pthread_cond_signal(pthread_cond_t *cptr);

/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

Слово signal в имени второй функции не имеет никакого отношения к сигналам Unix SIGxxx.

Мы определяем условие, уведомления о выполнении которого будем ожидать.

Взаимное исключение всегда связывается с условной переменной. При вызове pthread_cond_wait для ожидания выполнения какого-либо условия мы указываем адрес условной переменной и адрес связанного с ней взаимного исключения.

Мы проиллюстрируем использование условных переменных, переписав пример из предыдущего раздела. В листинге 7.5 объявляются глобальные переменные.

Переменные производителя и взаимное исключение объединяются в структуру

7-13 Две переменные nput и rival ассоциируются с mutex, и мы объединяем их в структуру с именем put. Эта структура используется производителями.

14-20 Другая структура, nready, содержит счетчик, условную переменную и взаимное исключение. Мы инициализируем условную переменную с помощью PTHREAD_ COND_INITIALIZER.

Функция main по сравнению с листингом 7.3 не изменяется.

Листинг 7.5. Глобальные переменные: использование условной переменной

//mutex/prodcons6.c

1  #include "unpipc.h"

2  #define MAXNITEMS 1000000

3  #define MAXNTHREADS 100


4  /* глобальные переменные для всех потоков */

5  int nitems; /* только для чтения потребителем и производителем */

6  int buff[MAXNITEMS];


7  struct {

8   pthread_mutex_t mutex;

9   int nput; /* следующий сохраняемый элемент */

10  int nval; /* следующее сохраняемое значение */

11 } put = {

12  PTHREAD_MUTEX_INITIALIZER

13 };


14 struct {

15  pthread_mutex_t mutex:

16  pthread_cond_t cond;

17  int nready; /* количество готовых для потребителя */

18 } nready = {

19  PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER

20 };

Функции produce и consume претерпевают некоторые изменения. Их текст дан в листинге 7.6.

Листинг 7.6. Функции produce и consume

//mutex/prodcons6.c

46 void *

47 produce(void *arg)

48 {

49  for (;;) {

50   Pthread_mutex_lock(&put.mutex);

51   if (put.nput >= nitems) {

52    Pthread_mutex_unlock(&put.mutex);

53    return(NULL); /* массив заполнен, готово */

54   }

55   buff[put.nput] = put.nval;

56   put.nput++;

57   put.nval++;

58   Pthread_mutex_unlock(&put.mutex);

59   Pthread_mutex_lock(&nready.mutex):

60   if (nready.nready == 0)

61    Pthread_cond_signal(&nready.cond);

62   nready.nready++;

63   Pthread_mutex_unlock(&nready.mutex);

64   *((int *) arg) += 1;

65  }

66 }


67 void*

68 consume(void *arg)

69 {

70  int i;

71  for (i = 0; i < nitems; i++) {

72   Pthread_mutex_lock(&nready.mutex);

73   while (nready.nready == 0)

74    Pthread_cond_wait(&nready.cond, &nready.mutex);

75   nready.nready--;

76   Pthread_mutex_unlock(&nready.mutex);

77   if (buff[i] != i)

78    printf("buff[%d] = *d\n", i, buff[i]);

79  }

80  return(NULL);

81 }

Помещение очередного элемента в массив

50-58 Для блокирования критической области в потоке-производителе теперь используется исключение put.mutex. 

Уведомление потребителя

59-64 Мы увеличиваем счетчик nready.nready, в котором хранится количество элементов, готовых для обработки потребителем. Перед его увеличением мы проверяем, не было ли значение счетчика нулевым, и если да, то вызывается функция pthread_cond_signal, позволяющая возобновить выполнение всех потоков (в данном случае потребителя), ожидающих установки ненулевого значения этой переменной. Теперь мы видим, как взаимодействуют взаимное исключение и связанная с ним условная переменная. Счетчик используется совместно потребителем и производителями, поэтому доступ к нему осуществляется с блокировкой соответствующего взаимного исключения (nready.mutex). Условная переменная используется для ожидания и передачи сигнала.

Потребитель ждет, пока значение nready.nready не станет отличным от нуля

72-76 Потребитель просто ждет, пока значение счетчика nready. nready не станет отличным от нуля. Поскольку этот счетчик используется совместно с производителями, его значение можно проверять только при блокировке соответствующего взаимного исключения. Если при проверке значение оказывается нулевым, мы вызываем pthread_cond_wait для приостановки процесса. При этом выполняются два атомарных действия:

1. Разблокируется nready.mutex.

2. Выполнение потока приостанавливается, пока какой-нибудь другой поток не вызовет pthread_cond_signal для этой условной переменной.

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

Код, передающий сигнал условной переменной, выглядит следующим образом:

struct {

 pthread_mutex_t mutex;

 pthread_cond_t cond;

 переменные, для которых устанавливается условие

} var = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, … };


Pthread_mutex_lock(&var.mutex);

установка истинного значения условия

Pthread_cond_signal(&var.cond);

Pthread_mutex_unlock(&var.mutex);

В нашем примере переменная, для которой устанавливалось условие, представляла собой целочисленный счетчик, а установка условия означала просто увеличение счетчика. Мы оптимизировали программу, посылая сигнал только при изменении значения счетчика с 0 на 1.

Код, проверяющий условие и приостанавливающий процесс, если оно не выполняется, обычно выглядит следующим образом:

Pthread_mutex_lock(&var.mutex);

while (условие ложно)

 Pthread_cond_wait(&var.cond, &var.mutex);

изменение условия

Pthread_mutex_unlock(&var.mutex);

Исключение конфликтов блокировок

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

int dosignal;

Pthread_mutex_lock(nready.mutex);

dosignal = (nready.nready == 0);

nready.nready++;

Pthread_mutex_unlock(&nready.mutex);

if (dosignal)

 Pthread_cond_signal(&nready.cond);

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

7.6. Условные переменные: время ожидания и широковещательная передача

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

ПРИМЕЧАНИЕ

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

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

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cptr);

int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime);

/* Функции возвращают 0 в случае успешного завершения, положительный код Еххх — в случае ошибки */

Функция pthread_cond_timedwait позволяет установить ограничение на время блокирования процесса. Аргумент abstime представляет собой структуру timespec:

struct timespec {

 time_t tv_sec; /* секунды */

 long tv_nsec; /* наносекунды */

};

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

Эта величина представляет собой абсолютное значение времени, а не промежуток. Аргумент abstime задает таким образом количество секунд и наносекунд с 1 января 1970 UTC до того момента времени, в который должен произойти возврат из функции. Это отличает функцию от select, pselect и poll (глава 6 [24]), которые в качестве аргумента принимают некоторое количество долей секунды, спустя которое должен произойти возврат. (Функция select принимает количество микросекунд, pselect — наносекунд, a poll — миллисекунд.) Преимущество использования абсолютного времени заключается в том, что если функция возвратится до ожидаемого момента (например, при перехвате сигнала), ее можно будет вызвать еще раз, не изменяя содержимого структуры timespec.

7.7. Атрибуты взаимных исключений и условных переменных

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

Прежде всего инициализировать и удалять взаимное исключение и условную переменную можно с помощью функций

#include <pthread.h>

int pthread_mutex_imt(pthread_mutex_t *mptr, const pthread_mutexattr_t *attr);

int pthread_mutex_destroy(pthread_mutex_t *mptr);

int pthread_cond_init(pthread_cond_t *cрtr, const pthread_condattr_t *attr);

int pthread_cond_destroy(pthread_cond_t *cptr);

/* Все четыре функции возвращают 0 в случае успешного завершения работы, положительное значение Еххх – в случае ошибки */

Рассмотрим, например, взаимное исключение. Аргумент mptr должен указывать на переменную типа pthread_mutex_t, для которой должна быть уже выделена память, и тогда функция pthread_mutex_init инициализирует это взаимное исключение. Значение типа pthread_mutexattr_t, на которое указывает второй аргумент функции pthread_mutex_init(attr ), задает атрибуты этого исключения. Если этот аргумент представляет собой нулевой указатель, используются значения атрибутов по умолчанию.

Атрибуты взаимного исключения имеют тип pthread_mutexattr_t, а условной переменной — pthread_condattr_t, и инициализируются и уничтожаются с помощью следующих функций:

#include <pthread.h>

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

int pthread_condattr_init(pthread_condattr_t *attr);

int pthread_condattr_destroy(pthread_condattr_t *attr);

/* Все четыре функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

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

#include <pthread.h>

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int value);

int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *valptr);

int pthread_condattr_setpshared(pthread_condattr_t *attr, int value);

/* Все четыре функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

Две функции get возвращают текущее значение атрибута через целое, на которое указывает valptr, а две функции set устанавливают значение атрибута равным значению value. Значение value может быть либо PTHREAD_PROCESS_PRIVATE, либо PTHREAD_PROCESS_SHARED. Последнее также называется атрибутом совместного использования процессами. 

ПРИМЕЧАНИЕ

Эта возможность поддерживается только в том случае, если константа _POSIX_THREAD_PROCESS_SHARED определена в заголовочном файле <unistd.h>. Она является дополнительной согласно Posix.1 и обязательной по Unix 98 (табл. 1.3). 

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

pthread_mutex_t *mptr; /* указатель на взаимное исключение, находящееся в разделяемой памяти */

pthread_mutexattr_t mattr; /* атрибуты взаимного исключения */

mptr = /* некоторое значение, указывающее на разделяемую память */

Pthread_mutexattr_init(&mattr);

#ifdef _POSIX_THREAD_PROCESS_SHARED

 Pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);

#else

 # error Эта реализация не поддерживает _POSIX_THREAD_PROCESS_SHARED

#endif

Pthread_mutex_init(mptr, &mattr);

Мы объявляем переменную mattr типа pthread_mutexattr_t, инициализируем ее значениями атрибутов по умолчанию, а затем устанавливаем атрибут PTHREAD_PROCESS_SHARED, позволяющий совместно использовать взаимное исключение нескольким процессам. Затем pthread_mutex_init инициализирует само исключение с соответствующими атрибутами. Количество разделяемой памяти, которое следует выделить под взаимное исключение, равно sizeof(pthread_mutex_t).

Практически такая же последовательность команд (с заменой mutex на cond) позволяет установить атрибут PTHREAD_PROCESS_SHARED для условной переменной, хранящейся в разделяемой несколькими процессами памяти.

Пример совместно используемых несколькими процессами взаимных исключений и условных переменных был приведен в листинге 5.18.

Завершение процесса, заблокировавшего ресурс

Когда взаимное исключение используется совместно несколькими процессами, всегда существует возможность, что процесс будет завершен (возможно, принудительно) во время работы с заблокированным им ресурсом. Не существует способа заставить систему автоматически снимать блокировку во время завершения процесса. Мы увидим, что это свойственно и блокировкам чтения-записи, и семафорам Posix. Единственный тип блокировок, автоматически снимаемых системой при завершении процесса, — блокировки записей fcntl (глава 9). При использовании семафоров System V можно специально указать ядру, следует ли автоматически снимать блокировки при завершении работы процесса (функция SEM_UNDO, о которой будет говориться в разделе 11.3).

Поток также может быть завершен в момент работы с заблокированным ресурсом, если его выполнение отменит (cancel) другой поток или он сам вызовет pthread_exit. Последнему варианту не следует уделять много внимания, поскольку поток должен сам знать, блокирует ли он взаимное исключение в данный момент или нет, и в зависимости от этого вызывать pthread_exit. На случай отмены другим потоком можно предусмотреть обработчик сигнала, вызываемый при отмене потока, что продемонстрировано в разделе 8.5. Если же для потока возникают фатальные условия, это обычно приводит к завершению работы всего процесса. Например, если поток делает некорректную операцию с указателем, что приводит к отправке сигнала SIGSEGV, это приводит к остановке всего процесса, если сигнал не перехватывается, и мы возвращаемся к предыдущей ситуации с гибелью процесса, заблокировавшего ресурс.

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

В некоторых случаях автоматическое снятие блокировки (или счетчика — для семафора) при завершении процесса не вызывает проблем. Например, сервер может использовать семафор System V (с функцией SEM_UNDO) для подсчета количества одновременно обслуживаемых клиентов. Каждый раз при порождении процесса вызовом fork он увеличивает значение семафора на единицу, уменьшая его при завершении работы дочернего процесса. Если дочерний процесс завершит работу досрочно, ядро само уменьшит значение семафора. Пример, в котором автоматическое снятие блокировки ядром (а не уменьшение счетчика, как в вышеописанной ситуации) также не вызывает проблем, приведен в разделе 9.7. Демон блокирует один из файлов данных при записи в него и не снимает эту блокировку до завершения работы. Если кто-то попробует запустить копию демона, она завершит работу досрочно, когда обнаружит наличие блокировки на запись. Это гарантирует работу единственного экземпляра демона. Если же демон досрочно завершит работу, ядро само снимет блокировку, что позволит запустить копию демона.

7.8. Резюме

Взаимные исключения (mutual exclusion — mutex) используются для защиты критических областей кода, запрещая его одновременное выполнение несколькими потоками. В некоторых случаях потоку, заблокировавшему взаимное исключение, требуется дождаться выполнения какого-либо условия для выполнения последующих действий. В этом случае используется ожидание сигнала по условной переменной. Условная переменная всегда связывается с каким-либо взаимным исключением. Функция pthread_cond_wait, приостанавливающая работу процесса, разблокирует взаимное исключение перед остановкой работы и заново блокирует его при возобновлении работы процесса спустя некоторое время. Сигнал по условной переменной передается каким-либо другим потоком, и этот поток может разбудить либо только один произвольный поток из множества ожидающих (pthread_cond_signal), либо все их одновременно (pthread_cond_broadcast).

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

Упражнения

1. Удалите взаимное исключение из листинга 7.2 и убедитесь, что программа работает неправильно, если одновременно запущено более одного производителя.

2. Что произойдет с листингом 7.1, если убрать вызов Pthread_join для потока-потребителя?

3. Напишите пpoгрaммy, вызывающую pthread_mutexatt_init и pthread_condattr_init в бесконечном цикле. Следите за используемой этим процессом памятью с помощью какой-нибудь программы, например ps. Что происходит? Теперь добавьте вызовы pthread_mutexattr_destroy и pthread_condattr_destroy и убедитесь, что утечки памяти нет.

4. В программе из листинга 7.6 производитель вызывает pthread_cond_signal только при изменении nready.nready с 0 на 1. Чтобы убедиться в эффективности этой оптимизации, вызывайте pthread_cond_signal каждый раз, когда nready.nready увеличивается на 1, и выведите его значение в главном потоке после завершения работы потребителя. 

ГЛАВА 8

Блокировки чтения-записи

8.1. Введение

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

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

1. Любое количество потоков могут заблокировать ресурс для считывания, если ни один процесс не заблокировал его на запись.

2. Блокировка чтения-записи может быть установлена на запись, только если ни один поток не заблокировал ресурс для чтения или для записи.

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

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

Такой вид совместного доступа к ресурсу также носит название совместно-исключающей блокировки (shared-exclusive), поскольку тип используемой блокировки на чтение называется совместной блокировкой (shared lock), а тип используемой блокировки на запись называется исключающей блокировкой (exclusive lock). Существует также специальное название для данной задачи (несколько считывающих процессов и один записывающий): задача читателей и писателей (readers and writers problem), и говорят также о блокировке читателей и писателя (readers-writer lock). В последнем случае слово «читатель» специально употреблено во множественном числе, а «писатель» — в единственном, чтобы подчеркнуть сущность задачи.

ПРИМЕЧАНИЕ

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

Функции, описываемые в этой главе, определены стандартом Unix 98, поскольку блокировки чтения-записи не были частью стандарта Posix.1 1996 года. Эти функции были разработаны группой производителей Unix, известной под названием Aspen Group, в 1995 году вместе с другими расширениями, которые еще не были определены Posix.1. Рабочая группа Posix (1003.1j) в настоящее время разрабатывает набор расширений Pthreads, включающий блокировки чтения-записи, который, хочется верить, совпадет с описываемым в этой главе.

8.2. Получение и сброс блокировки чтения-записи

Блокировка чтения-записи имеет тип pthread_rwlock_t. Если переменная этого типа является статической, она может быть проинициализирована присваиванием значения константы PTHREAD_RWLOCK_INITIALIZER.

Функция pthread_rwlock_rdlock позволяет заблокировать ресурс для чтения, причем вызвавший процесс будет заблокирован, если блокировка чтения-записи уже установлена записывающим процессом. Функция pthread_rwlock_wrlock позволяет заблокировать ресурс для записи, причем вызвавший процесс будет заблокирован, если блокировка чтения-записи уже установлена каким-либо другим процессом (считывающим или записывающим). Функция pthread_rwlock_unlock снимает блокировку любого типа (чтения или записи):

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);

int pthread_rwlock_unlock(pthread_rwlock_t *rwptr );

/* Все функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

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

#include <pthread.h>

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

8.3. Атрибуты блокировки чтения-записи

Мы уже отмечали, что статическая блокировка может быть проинициализирована присваиванием ей значения PTHREAD_RWLOCK_INITIALIZER. Эти переменные могут быть проинициализированы и динамически путем вызова функции pthread_rwlock_init. 

Когда поток перестает нуждаться в блокировке, он может вызвать pthread_rwlock_ destroy:

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);

int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);

/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

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

#include <pthread.h>

int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

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

#include <pthread.h>

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr);

int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int value );

/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

Первая функция возвращает текущее значение в целом, на которое указывает аргумент valptr. Вторая функция устанавливает значение этого атрибута равным value, которое может быть либо PTHREAD_PROCESS_PRIVATE, либо PTHREAD_ PROCESS_SHARED.

8.4. Реализация с использованием взаимных исключений и условных переменных

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

ПРИМЕЧАНИЕ

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

Другие реализации блокировок чтения записи заслуживают отдельного изучения. В разделе 7.1.2 книги [3] представлена реализация, в которой приоритет имеют ожидающие записи потоки и предусмотрена обработка отмены выполнения потока (о которой мы вскоре будем говорить подробнее). В разделе В.18.2.3.1 стандарта IEEE 1996 [8] представлена другая реализация, в которой предпочтение имеют ожидающие записи потоки и в которой также предусмотрена обработка отмены. В главе 14 книги [12] также приводится возможная реализация, в которой приоритет имеют ожидающие записи процессы. Реализация, приведенная в этом разделе, взята из пакета АСЕ (http://www.cs.wustl.edu/~schmidt/ACE.html), автором которого является Дуг Шмидт (Doug Schmidt). Аббревиатура АСЕ означает Adaptive Communications Environment. Во всех четырех реализациях используются взаимные исключения и условные переменные. 

Тип данных pthread_rwlock_t

В листинге 8.1[1] приведен текст заголовочного файла pthread_rwlock .h, в котором определен основной тип pthread_rwlock_t и прототипы функций, работающих с блокировками чтения и записи. Обычно все это находится в заголовочном файле <pthread.h>.

Листинг 8.1. Определение типа данных pthread_rwlock_t

//my_rwlock/pthread_rwlock.h

1  #ifndef __pthread_rwlock_h

2  #define __pthread_rwlock_h


3  typedef struct {

4   pthread_mutex_t rw_mutex; /* блокировка для структуры */

5   pthread_cond_t rw_condreaders; /* для ждущих читающих потоков */

6   pthread_cond_t rw_condwriters; /* для ждущих пишущих потоков */

7   int rw_magic; /* для проверки ошибок */

8   int rw_nwaitreaders;/* число ожидающих */

9   int rw_nwaitwriters;/* число ожидающих */

10  int rw_refcount;

11  /* –1, если блокировка на запись, иначе – количество блокировок на чтение */

12 } pthread_rwlock_t;


13 #define RW_MAGIC 0x19283746

14 /* порядок должен быть такой же, как у элементов структуры */

15 #define PTHREAD_RWLOCK_INITIALIZER { PTHREAD_MUTEX_INITIALIZER, \

16  PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER, \

17  RW_MAGIC, 0, 0, 0 }


18 typedef int pthread_rwlockattr_t; /* не поддерживается */


19 /* прототипы функций */

20 int pthread_rwlock_destroy(pthread_rwlock_t *);

21 int pthread_rwlock_init(pthread_rwlock_t *, pthread_rwlockattr_t *);

22 int pthread_rwlock_rdlock(pthread_rwlock_t *);

23 int pthread_rwlock_tryrdlock(pthread_rwlock_t *);

24 int pthread_rwlock_trywrlock(pthread_rwlock_t *);

25 int pthread_rwlock_unlock(pthread_rwlock_t *);

26 int pthread_rwlock_wrlock(pthread_rwlock_t *);


27 /* и наши функции-обертки */

28 void pthread_rwlock_destroy(pthread_rwlock_t *);

29 void pthread_rwlock_init(pthread_rwlock_t*, pthread_rwlockattr_t *);

30 void Pthread_rwlock_rdlock(pthread_rwlock_t *);

31 int Pthread_rwlock_tryrdlock(pthread_rwlock_t *);

32 int pthread_rwlock_trywrlock(pthread_rwlock_t *);

33 void pthread_rwlock_unlock(pthread_rwlock_t *);

34 void pthread_rwlock_wrlock(pthread_rwlock_t *);


35 #endif __pthread_rwlock_h

3-13 Наш тип pthread_rwlock_t содержит одно взаимное исключение, две условные переменные, один флаг и три счетчика. Мы увидим, для чего все это нужно, когда будем разбираться с работой функций нашей программы. При просмотре или изменении содержимого этой структуры мы должны устанавливать блокировку rw_mutex. После успешной инициализации структуры полю rw_magic присваивается значение RW_MAGIC. Значение этого поля проверяется всеми функциями — таким образом гарантируется, что вызвавший поток передал указатель на проинициализированную блокировку. Оно устанавливается в 0 после уничтожения блокировки.

Обратите внимание, что в счетчике rw_refcount всегда хранится текущий статус блокировки чтения-записи: –1 обозначает блокировку записи (и только одна такая блокировка может существовать в любой момент времени), 0 обозначает, что блокировка доступна и может быть установлена, а любое положительное значение соответствует количеству установленных блокировок на чтение.

14-17 Мы также определяем константу для статической инициализации нашей структуры.

Функция pthread_rwlock_init

Первая функция, pthread_rwlock_init, динамически инициализирует блокировку чтения-записи. Ее текст приведен в листинге 8.2.

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

9-19 Мы инициализируем взаимное исключение и две условные переменные, которые содержатся в нашей структуре. Все три счетчика устанавливаются в 0, а полю rw_magiс присваивается значение, указывающее на то, что структура была проинициализирована.

20-25 Если при инициализации взаимного исключения или условной переменной возникает ошибка, мы аккуратно уничтожаем проинициализированные объекты и возвращаем код ошибки.

Листинг 8.2. Функция pthread_rwlock_init: инициализация блокировки чтения-записи

//my_rwlock/pthread_rwlock_init.с

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  int

4  pthread_rwlock_init(pthread_rwlock_t *rw, pthread_rwlockattr_t *attr)

5  {

6   int result;

7   if (attr != NULL)

8    return(EINVAL); /* not supported */

9   if ((result = pthread_mutex_init(&rw->rw_mutex, NULL)) != 0)

10   goto err1;

11  if ((result = pthread_cond_init(&rw->rw_condreaders, NULL)) != 0)

12   goto err2;

13  if ((result = pthread_cond_init(&rw->rw_condwriters, NULL)) != 0)

14   goto err3;

15  rw->rw_nwaitreaders = 0;

16  rw->rw_nwaitwriters = 0;

17  rw->rw_refcount = 0;

18  rw->rw_magic = RW_MAGIC;

19  return(0);

20 err3:

21  pthread_cond_destroy(&rw->rw_condreaders);

22 err2;

23  pthread_mutex_destroy(&rw->rw_mutex);

24 err1:

25  return(result); /* значение errno */

26 }

Функция pthread_rwlock destroy

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

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

Листинг 8.З. Функция pthread_rwlock_destroy: уничтожение блокировки чтения-записи

//my_rwlock/pthread_rwlock_destroy.с

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  int

4  pthread_rwlock_destroy(pthread_rwlock_t *rw)

5  {

6   if (rw->rw_magic != RW_MAGIC)

7    return(EINVAL);

8   if (rw->rw_refcount != 0 ||

9    rw->rw_nwaitreaders != 0 || rw->rw_nwaitwriters != 0)

10   return(EBUSY);

11  pthread_mutex_destroy(&rw->rw_mutex);

12  pthread_cond_destroy(&rw->rw_condreaders);

13  pthread_cond_destroy(&rw->rw_condwriters);

14  rw->rw_magic = 0;

15  return(0);

16 }

Функция pthread_rwlock_rdlock

Текст функции pthread_rwlock_rdlock приведен в листинге 8.4.

Листинг 8.4. Функция pthread_rwlock_rdlock: получение блокировки на чтение

//my_rwlock/pthread_rwlock_rdlock.с

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  int

4  pthread_rwlock_rdlock(pthread_rwlock_t *rw)

5  {

6   int result;

7   if (rw->rw_magic != RW_MAGIC)

8    return(EINVAL);

9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

10   return(result);

11  /* предпочтение отдается ожидающим разрешения на запись процессам */

12  while (rw->rw_refcount < 0 || rw->rw_nwaitwriters > 0) {

13   rw->rw_nwaitreaders++;

14   result = pthread_cond_wait(&rw->rw_condreaders, &rw->rw_mutex);

15   rw->rw_nwaitreaders--;

16   if (result != 0)

17    break;

18  }

19  if (result == 0)

20   rw->rw_refcount++; /* блокировка на чтение уже кем-то установлена */

21  pthread_mutex_unlock(&rw->rw_mutex);

22  return (result);

23 }

9-10 При работе со структурой pthread_rwl ock_t всегда устанавливается блокировка на rw_mutex, являющееся ее полем.

11-18 Нельзя получить блокировку на чтение, если rw_refcount имеет отрицательное значение (блокировка установлена на запись) или имеются потоки, ожидающие возможности получения блокировки на запись (rw_nwaitwriters больше 0). Если одно из этих условий верно, мы увеличиваем значение rw_nwaitreaders и вызываем pthread_cond_wait для условной переменной rw_condreaders. Вскоре мы увидим, что при разблокировании ресурса прежде всего проверяется наличие процессов, ожидающих возможности установить блокировку на запись, и если таковых не существует, проверяется наличие ожидающих возможности считывания. Если они имеются, для условной переменной rw_condreaders передается широковещательный сигнал.

19-20 При получении блокировки на чтение мы увеличиваем значение rw_refcount. Блокировка взаимного исключения после этого снимается.

ПРИМЕЧАНИЕ

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

Функция pthread_rwlock_tryrdlock

В листинге 8.5 показана наша реализация функции pthread_rwlock_tryrdlock, которая не вызывает приостановления вызвавшего ее потока.

Листинг 8.5. Функция pthread_rwlock_tryrdlock: попытка заблокировать ресурс для чтения

//my_rwlock/pthread_rwlock_tryrdlock.с

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  int

4  pthread_rwlock_tryrdlock(pthread_rwlock_t *rw)

5  {

6   int result;

7   if (rw->rwjnagic != RW_MAGIC)

8    return(EINVAL);

9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

10   return(result);

11  if (rw->rw_refcount < 0 || rw->rw_nwaitwriters > 0)

12   result = EBUSY; /* блокировка установлена пишущим потоком или есть пишущие потоки, ожидающие освобождения ресурса */

13  else

14   rw->rw_refcount++; /* увеличение количества блокировок на чтение */

15  pthread_mutex_unlock(&rw->rw_mutex);

16  return(result);

17 }

11-14 Если блокировка в данный момент установлена на запись или есть процессы, ожидающие возможности установить ее на запись, возвращается ошибка с кодом EBUSY. В противном случае мы устанавливаем блокировку, увеличивая значение счетчика rw_refcount.

Функция pthread_rwlock_wrlock

Текст функции pthread_rwlock_wrlock приведен в листинге 8.6.

11-17 Если ресурс заблокирован на считывание или запись (значение rw_refcount отлично от 0), мы приостанавливаем выполнение потока. Для этого мы увеличиваем rw_nwaitwriters и вызываем pthread_cond_wait с условной переменной rw_condwriters. Для этой переменной посылается сигнал при снятии блокировки чтения-записи, если имеются ожидающие разрешения на запись процессы.

18-19 После получения блокировки на запись мы устанавливаем значение rw_refcount в –1.

Листинг 8.6. Функция pthread_rwlock_wrlock: получение блокировки на запись

//my_rwlock/pthread_rwlock_wrlock.c

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  int

4  pthread_rwlock_wrlock(pthread_rwlock_t *rw)

5  {

6   int result;

7   if (rw->rw_magic != RW_MAGIC)

8    return(EINVAL);

9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

10   return(result);

11  while (rw->rw_refcount != 0) {

12   rw->rw_nwaitwriters++;

13   result = pthread_cond_wait(&rw->rw_condwriters, &rw->rw_mutex);

14   rw->rw_nwaitwriters--;

15   if (result != 0)

16    break;

17  }

18  if (result == 0)

19   rw->rw_refcount = –1;

20  pthread_mutex_unlock(&rw->rw_mutex);

21  return(result);

22 }

Функция pthread_rwlock_trywrlock

Неблокируемая функция pthread_rwlock_trywrlock показана в листинге 8.7.

11-14 Если значение счетчика rw_refcount отлично от нуля, блокировка в данный момент уже установлена считывающим или записывающим процессом (это безразлично) и мы возвращаем ошибку с кодом EBUSY. В противном случае мы устанавливаем блокировку на запись, присвоив переменной rw_refcount значение –1.

Листинг 8.7. Функция pthread_rwlock_trywrlock: попытка получения блокировки на запись

//my_rwlock/pthread_rwlock_trywrlock.c

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  int

4  pthread_rwlock_trywrlock(pthread_rwlock_t *rw)

5  {

6   int result;

7   if (rw->rw_magic != RW_MAGIC)

8    return(EINVAL);

9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

10   return(result);

11  if (rw->rw_refcount != 0)

12   result = EBUSY; /* заблокирован пишущим потоком или ожидающим возможности записи */

13  else

14   rw->rw_refcount = –1; /* доступна */

15  pthread_mutex_unlock(&rw->rw_mutex);

16  return(result);

17 }

Функция pthread_rwlock_unlock

Последняя функция, pthread_rwlock_unlock, приведена в листинге 8.8.

Листинг 8.8. Функция pthread_rwlock_unlock: разблокирование ресурса

//my_rwlock/pthread_rwlock_unlock.c

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  int

4  pthread_rwlock_unlock(pthread_rwlock_t *rw)

5  {

6   int result;

7   if (rw->rw_magic != RW_MAGIC)

8    return(EINVAL);

9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

10   return(result);

11  if (rw->rw_refcount > 0)

12   rw->rw_refcount--; /* снятие блокировки на чтение */

13  else if (rw->rw_refcount == –1)

14   rw->rw_refcount = 0; /* снятие блокировки на запись */

15  else

16   err_dump("rw_refcount = %d", rw->rw_refcount);

17  /* преимущество отдается ожидающим возможности записи потокам */

18  if (rw->rw_nwaitwriters > 0) {

19   if (rw->rw_refcount == 0)

20    result = pthread_cond_signal(&rw->rw_condwriters);

21  } else if (rw->rw_nwaitreaders > 0)

22   result = pthread_cond_broadcast(&rw->rw_condreaders);

23  pthread_mutex_unlock(&rw->rw_mutex);

24  return(result);

25 }

11-16 Если rw_refcount больше 0, считывающий поток снимает блокировку на чтение. Если rw_refcount равно –1, записывающий поток снимает блокировку на запись.

17-22 Если имеются ожидающие разрешения на запись потоки, по условной переменной rw_condwriters передается сигнал (если блокировка свободна, то есть значение счетчика rw_refcount равно 0). Мы знаем, что только один поток может осуществлять запись, поэтому используем функцию pthread_cond_signal. Если нет потоков, ожидающих возможности записи, но есть потоки, ожидающие возможности чтения, мы вызываем pthread_cond_broadcast для переменной rw_condreaders, поскольку возможно одновременное считывание несколькими потоками. Обратите внимание, что мы перестаем устанавливать блокировку для считывающих потоков, если появляются потоки, ожидающие возможности записи. В противном случае постоянно появляющиеся потоки с запросами на чтение могли бы заставить поток, ожидающий возможности записи, ждать целую вечность. По этой причине мы используем два отдельных оператора if и не можем написать просто:

/* предпочтение отдается записывающим процессам */

if (rw->rw_nwaitreaders > 0 && rw->rw_refcount == 0)

 result = pthread_cond_signal(&rw->rw_condwriters);

else if (rw->rw_nwaitreaders > 0)

 result = pthread_cond_broadcast(&rw->rw_condreaders);

Мы могли бы исключить и проверку rw->rw_refcount, но это может привести к вызовам pthread_cond_signal даже при наличии блокировок на чтение, что приведет к потере эффективности.

8.5. Отмена выполнения потоков

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

#include <pthread.h>

int pthread_cancel(pthread_t tid);

/* Возвращает 0 в случае успешного завершения, положительное значение Еххх –в случае ошибки */

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

Для обработки отмены выполнения поток может установить (push) или снять (pop) обработчик-очиститель (cleanup handler):

#include <pthread.h>

void pthread_cleanup_push(void (*function) (void *) void *arg);

void pthread_cleanup_pop(int execute);

Эти обработчики представляют собой обычные функции, которые вызываются:

■ в случае отмены выполнения потока (другим потоком, вызвавшим pthread_ cancel);

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

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

Аргумент function представляет собой адрес вызываемой функции, а arg — ее единственный аргумент. Функция pthread_cleanup_pop всегда удаляет обработчик из верхушки стека и вызывает эту функцию, если значение execute отлично от 0.

ПРИМЕЧАНИЕ

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

Пример

Легче всего продемонстрировать проблему нашей реализации из предыдущего раздела с помощью примера. На рис. 8.1 изображена временная диаграмма выполнения нашей программы, а текст самой программы приведен в листинге 8.9. 

UNIX: взаимодействие процессов

Рис. 8.1. Временная диаграмма выполнения программы из листинга 8.9


Создание двух потоков

10-13 Создаются два потока, первый из которых выполняет функцию thread1, а второй — thread2. После создания первого делается пауза длительностью в одну секунду, чтобы он успел заблокировать ресурс на чтение. 

Ожидание завершения потоков

14-23 Мы ожидаем завершения работы второго потока и проверяем, что его статус имеет значение PTHREAD_CANCEL. Затем мы ждем завершения работы первого потока и проверяем, что его статус представляет собой нулевой указатель. Затем мы выводим значение трех счетчиков в структуре pthread_rwlock_t и уничтожаем блокировку.

Листинг 8.9. Тестовая программа, иллюстрирующая отмену выполнения потока

//my_rwlock_cancel/testcancel.с

1  #include "unpipc.h"

2  #include "pthread_rwlock.h"


3  pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

4  pthread_t tid1, tid2;

5  void *thread1(void *), *thread2(void *);


6  int

7  main(int argc, char **argv)

8  {

9   void *status;

10  Set_concurrency(2);

11  Pthread_create(&tid1, NULL, thread1, NULL);

12  sleep(1); /* даем первому потоку возможность получить блокировку */

13  Pthread_create(&tid2, NULL, thread2, NULL);

14  Pthread_join(tid2, &status);

15  if (status != PTHREAD_CANCELED)

16   printf("thread2 status = %p\n", status);

17  Pthread_join(tid1, &status);

18  if (status != NULL)

19   printf("thread1 status = %p\n", status);

20  printf("rw_refcount = %d, rw_nwaitreaders = %d, rw_nwaitwriters = %d\n",

21   rwlock.rw_refcount, rwlock.rw_nwaitreaders,

22   rwlock.rw_nwaitwriters);

23  Pthread_rwlock_destroy(&rwlock);

24  exit(0);

25 }


26 void *

27 thread1(void *arg)

28 {

29  Pthread_rwlock_rdlock(&rwlock);

30  printf("thread1() got a read lock\n");

31  sleep(3); /* даем второму потоку возможность заблокироваться при вызове pthread_rwlock_wrlock() */

32  pthread_cancel(tid2);

33  sleep(3);

34  Pthread_rwlock_unlock(&rwlock);

35  return(NULL);

36 }


37 void *

38 thread2(void *arg)

39 {

40  printf("thread2() trying to obtain a write lock\n"):

41  Pthread_rwlock_wrlock(&rwlock);

42  printf("thread2() got a write lock\n"); /* не будет выполнено */

43  sleep(1);

44  Pthread_rwlock_unlock(&rwlock);

45  return(NULL);

46 }

Функция thread1

26-36 Поток получает блокировку на чтение и ждет 3 секунды. Эта пауза дает возможность другому потоку вызвать pthread_rwlock_wrlock и заблокироваться при вызове pthread_cond_wait, поскольку блокировка на запись не может быть установлена из-за наличия блокировки на чтение. Затем первый поток вызывает pthread_cancel для отмены выполнения второго потока, ждет 3 секунды, освобождает блокировку на чтение и завершает работу.

Функция thread2

37-46 Второй поток делает попытку получить блокировку на запись (которую он получить не может, поскольку первый поток получил блокировку на чтение). Оставшаяся часть функции никогда не будет выполнена.

При запуске этой программы с использованием функций из предыдущего раздела мы получим следующий результат:

solaris % testcancel

thread1() got a read lock

thread2() trying to obtain a write lock

и мы никогда не вернемся к приглашению интерпретатора. Программа зависнет. Произошло вот что:

1. Второй поток вызвал pthread_rwlock_wrlock (листинг 8.6), которая была заблокирована в вызове pthread_cond_wait.

2. Первый поток вернулся из вызова slеер(3) и вызвал pthread_cancel.

3. Второй поток был отменен и завершил работу. При отмене потока, заблокированного в ожидании сигнала по условной переменной, взаимное исключение блокируется до вызова первого обработчика-очистителя. (Мы не устанавливали обработчик, но взаимное исключение все равно блокируется до завершения потока.) Следовательно, при отмене выполнения второго потока взаимное исключение осталось заблокированным и значение rw_nwaitwriters в листинге 8.6 было увеличено.

4. Первый поток вызывает pthread_rwlock_unlock и блокируется навсегда при вызове pthread_mutex_lock (листинг 8.8), потому что взаимное исключение все еще заблокировано отмененным потоком.

Если мы уберем вызов pthread_rwlock_unlock в функции thread1, функция main выведет вот что:

rw_refcount = 1, rw_nwaitreaders = 0, rw_nwaitwriters = 1

pthread_rwlock_destroy error: Device busy 

Первый счетчик имеет значение 1, поскольку мы удалили вызов pthread_rwlock_ unlock, а последний счетчик имеет значение 1, поскольку он был увеличен вторым потоком до того, как тот был отменен.

Исправить эту проблему просто. Сначала добавим две строки к функции pthread_rwlock_rdlock в листинге 8.4. Строки отмечены знаком +:

  rw->rw_nwaitreaders++;

+ pthread_cleanup_push(rwlock_cancelrdwait, (void *) rw);

  result = pthread_cond_wait(&rw->rw_condreaders, &rw->rw_mutex);

+ pthread_cleanup_pop(0);

  rw->rw_nwaitreaders++;

Первая новая строка устанавливает обработчик-очиститель (функцию rwlock_cancelrdwait), а его единственным аргументом является указатель rw. После возвращения из pthread_cond_wait вторая новая строка удаляет обработчик. Аргумент функции pthread_cleanup_pop означает, что функцию-обработчик при этом вызывать не следует. Если этот аргумент имеет ненулевое значение, обработчик будет сначала вызван, а затем удален.

Если поток будет отменен при вызове pthread_cond_wait, возврата из нее не произойдет. Вместо этого будут запущены обработчики (после блокирования соответствующего взаимного исключения, как мы отметили в пункте 3 чуть выше).

В листинге 8.10 приведен текст функции rwlock_cancelrdwait, являющейся обработчиком-очистителем для phtread_rwlock_rdlock.

Листинг 8.10. Функция rwlock_cancelrdwait: обработчик для блокировки чтения

//my_rwlock_cancel/pthread_rwlock_rdlock.с

3  static void

4  rwlock_cancelrdwait(void *arg)

5  {

6   pthread_rwlock_t *rw;

7   rw = arg;

8   rw->rw_nwaitreaders--;

9   pthread_mutex_unlock(&rw->rw_mutex);

10 }

8-9 Счетчик rw_nwaitreaders уменьшается, а затем разблокируется взаимное исключение. Это состояние, которое должно быть восстановлено при отмене потока.

Аналогично мы исправим текст функции pthread_rwlock_wrlock из листинга 8.6. Сначала добавим две новые строки рядом с вызовом pthread_cond_wait:

  rw->rw_nwaitreaders++;

+ pthread_cleanup_push(rwlock_cancelrwrwait, (void*) rw);

  result = pthread_cond_wait(&rw->rw_condwriters, &rw->rw_mutex);

+ pthread_cleanup_pop(0);

  rw->rw_nwaitreaders--;

В листинге 8.11 приведен текст функции rwlock_cancelwrwait, являющейся обработчиком-очистителем для запроса блокировки на запись.

Листинг 8.11. Функция rwlock_cancelwrwait: обработчик для блокировки записи

//my_rwlock_cancel/pthread_rwlock_wrlock.с

3  static void

4  rwlock_cancelwrwait(void *arg)

5  {

6   pthread_rwlock_t *rw;

7   rw = arg;

8   rw->rw_nwaitwriters––;

9   pthread_mutex_unlock(&rw->rw_mutex);

10 }

8-9 Счетчик rw_nwaitwriters уменьшается, и взаимное исключение разблокируется. При запуске нашей тестовой программы из листинга 8.9 с этими новыми функциями мы получим правильные результаты:

solaris %testcancel

thread1() got a read lock

thread2() trying to obtain a write lock

rw_refcount = 0, rw_nwaitreaders = 0, rw_nwaitwriters = 0

Теперь три счетчика имеют правильные значения, первый поток возвращается из вызова pthread_rwlock_unlock, а функция pthread_rwlock_destroy не возвращает ошибку EBUSY.

ПРИМЕЧАНИЕ

Этот раздел представляет собой обзор вопросов, связанных с отменой выполнения потоков. Для более детального изучения этих проблем можно обратиться, например, к разделу 5.3 книги [3].

8.6. Резюме

Блокировки чтения-записи позволяют лучше распараллелить работу с данными, чем обычные взаимные исключения, если защищаемые данные чаще считываются, чем изменяются. Функции для работы с этими блокировками определены стандартом Unix 98, их мы и описываем в этой главе. Аналогичные или подобные им функции должны появиться в новой версии стандарта Posix. По виду функции аналогичны функциям для работы со взаимными исключениями (глава 7).

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

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

Упражнения

1. Измените реализацию в разделе 8.4 таким образом, чтобы приоритет имели считывающие, а не записывающие потоки.

2. Сравните скорость работы нашей реализации из раздела 8.4 с предоставленной производителем. 

ГЛАВА 9

Блокирование записей

9.1. Введение

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

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

В этой главе мы в первый раз встретимся с нашим примером на увеличение последовательного номера. Рассмотрим следующую ситуацию, с которой столкнулись, например, разработчики спулера печати для Unix (команда lpr в BSD и lp в System V). Процесс, помещающий задания в очередь печати для последующей их обработки другим процессом, должен присваивать каждому из них уникальный последовательный номер. Идентификатор процесса, уникальный во время его выполнения, не может использоваться как последовательный номер, поскольку задание может просуществовать достаточно долго для того, чтобы этот идентификатор был повторно использован другим процессом. Процесс может также отправить на печать несколько заданий, каждому из которых нужно будет присвоить уникальный номер. Метод, используемый спулерами печати, заключается в том, чтобы хранить очередной порядковый номер задания для каждого принтера в отдельном файле. Этот файл содержит всего одну строку с порядковым номером в формате ASCII. Каждый процесс, которому нужно воспользоваться этим номером, должен выполнить следующие три действия:

1. Считать порядковый номер из файла.

2. Использовать этот номер.

3. Увеличить его на единицу и записать обратно в файл.

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

ПРИМЕЧАНИЕ

Описанная выше проблема называется проблемой взаимных исключений. Она может быть решена с использованием взаимных исключений из главы 7 или блокировок чтения-записи из главы 8. Различие состоит в том, что здесь мы предполагаем неродственность процессов, что усложняет использование предложенных выше методов. Мы могли бы использовать разделяемую память (подробно об этом говорится в четвертой части книги), поместив в нее переменную синхронизации одного из этих типов, но для неродственных процессов проще воспользоваться блокировкой fcntl. Другим фактором в данном случае стало то, что проблема со спулерами печати возникла задолго до появления взаимных исключений, условных переменных и блокировок чтения-записи. Блокировка записей была добавлена в Unix в начале 80-х, до того как появились концепции разделяемой памяти и программных потоков.

Таким образом, процессу нужно заблокировать файл, чтобы никакой другой процесс не мог получить к нему доступ, пока первый выполняет свои три действия. В листинге 9.2 приведен текст простой программы, выполняющей соответствующие действия. Функции my_lock и my_unlock обеспечивают блокирование и разблокирование файла в соответствующие моменты. Мы приведем несколько возможных вариантов реализации этих функций.

20 Каждый раз при прохождении цикла мы выводим имя программы (argv[0]) перед порядковым номером, поскольку эта функция main будет использоваться с различными версиями функций блокировки и нам бы хотелось видеть, какая версия программы выводит данную последовательность порядковых номеров.

ПРИМЕЧАНИЕ

Вывод идентификатора процесса требует преобразования переменной типа pid_t к типу long и последующего использования строки формата %ld. Проблема тут в том, что идентификатор процесса принадлежит к одному из целых типов, но мы не знаем, к какому именно, поэтому предполагается наиболее вместительный — long. Если бы мы предположили, что идентификатор имеет тип int и использовали бы строку %d, a pid_t на самом деле являлся бы типом long, код мог бы работать неправильно.

Посмотрим, что будет, если не использовать блокировку. В листинге 9.1[1] приведены версии функций my_lock и my_unlock, которые вообще ничего не делают.

Листинг 9.1. Функции, не осуществляющие блокировку

//lock/locknone.c

1  void

2  my_lock(int fd)

3  {

4   return;

5  }


6  void

7  my_unlock(int fd)

8  {

9   return;

10 }

Листинг 9.2. Функция main для примеров с блокировкой файла

//lock/lockmain.c

1  #include "unpipc.h"

2  #define SEQFILE "seqno" /* имя файла */


3  void my_lock(int), my_unlock(int);

4  int

5  main(int argc, char **argv)

6  {

7   int fd;

8   long i, seqno;

9   pid_t pid;

10  ssize_t n;

11  char line[MAXLINE + 1];

12  pid = getpid();

13  fd = Open(SEQFILE, O_RDWR, FILE_MODE);

14  for (i = 0; i < 20; i++) {

15   my_lock(fd); /* блокируем файл */

16   Lseek(fd, 0L, SEEK_SET); /* переходим к его началу */

17   n = Read(fd, line, MAXLINE);

18   line[n] = '\0'; /* завершающий 0 для sscanf */

19   n = sscanf(line, "%ld\n", &seqno);

20   printf(%s; pid = %ld, seq# = %ld\n", argv[0], (long) pid, seqno);

21   seqno++; /* увеличиваем порядковый номер */

22   snprintf(line, sizeof(line), "%ld\n", seqno);

23   Lseek(fd, 0L, SEEK_SET); /* переходим на начало перед записью */

24   Write(fd, line, strlen(line));

25   my_unlock(fd); /* разблокируем файл */

26  }

27  exit(0);

28 }

Если начальное значение порядкового номера в файле было 1 и был запущен только один экземпляр программы, мы увидим следующий результат:

solaris % locknone

locknone: pid = 15491, seq# = 1

locknone: pid = 15491, seq# = 2

locknone: pid = 15491, seq# = 3

locknone: pid = 15491, seq# = 4

locknone: pid = 15491. seq# = 5

locknone: pid = 15491, seq# = 6

locknone: pid = 15491, seq# = 7

locknone: pid = 15491, seq# – 8

locknone: pid = 15491, seq# = 9

locknone: pid = 15491, seq# = 10

locknone: pid = 15491, seq# = 11

locknone: pid = 15491, seq# = 12

locknone: pid = 15491, seq# = 13

locknone: pid = 15491, seq# = 14

locknone: pid = 15491, seq# = 15

locknone: pid = 15491, seq# = 16

locknone: pid = 15491, seq# = 17

locknone: pid = 15491, seq# = 18

locknone: pid = 15491, seq# = 19

locknone: pid = 15491, seq# = 20

ПРИМЕЧАНИЕ

Обратите внимание, что функция main хранится в файле lockmain.c, но мы компилируем и компонуем эту программу с функциями, не осуществляющими никакой блокировки (листинг 9.1), поэтому мы называем ее locknone. Ниже будут использоваться другие версии функций my_lock и my_unlock, и исполняемый файл будет называться по-другому в соответствии с используемым методом блокировки.

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

solaris % locknone & locknone&

solaris % locknone: pid = 15498, seq# = 1

locknone: pid = 15498, seq# = 2

locknone: pid = 15498, seq# = 3

locknone: pid = 15498, seq# = 4

locknone: pid = 15498, seq# = 5

locknone: pid = 15498, seq# = 6

locknone: pid = 15498, seq# = 7

locknone: pid = 15498, seq# = 8

locknone: pid = 15498, seq# = 9

locknone: pid = 15498, seq# = 10

locknone: pid = 15498, seq# = 11

locknone: pid = 15498, seq# = 12

locknone: pid = 15498, seq# = 13

locknone: pid = 15498, seq# = 14

locknone: pid = 15498, seq# = 15

locknone: pid = 15498, seq# = 16

locknone: pid = 15498, seq# = 17

locknone: pid = 15498, seq# = 18

locknone: pid = 15498, seq# = 19

locknone: pid = 15498, seq# = 20

locknone: pid = 15499, seq# = 1

locknone: pid = 15499, seq# = 2

locknone: pid = 15499, seq# = 3

locknone: pid = 15499, seq# = 4

locknone: pid = 15499, seq# = 5

locknone: pid = 15499, seq# = 6

locknone: pid = 15499, seq# = 7

locknone: pid = 15499, seq# = 8

locknone: pid = 15499, seq# = 9

locknone: pid – 15499, seq# = 10

locknone: pid = 15499, seq# = 11

locknone: pid = 15499, seq# – 12

locknone: pid = 15499, seq# = 13

locknone: pid = 15499, seq# = 14

locknone: pid = 15499, seq# = 15

locknone: pid = 15499, seq# = 16

locknone: pid = 15499, seq# = 17

locknone: pid = 15499, seq# = 18

locknone: pid = 15499, seq# = 19

locknone: pid = 15499, seq# = 20

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

Первые двадцать строк вывода не содержат ошибок. Они были сформированы первым экземпляром программы (с идентификатором 15 498). Проблема возникает в первой строке, выведенной вторым экземпляром (идентификатор 15499): он напечатал порядковый номер 1. Получилось это, скорее всего, так: второй процесс был запущен ядром, считал из файла порядковый номер (1), а затем управление было передано первому процессу, который работал до завершения. Затем второй процесс снова получил управление и продолжил выполняться с тем значением порядкового номера, которое было им уже считано (1). Это не то, что нам нужно. Каждый процесс считывает значение, увеличивает его и записывает обратно 20 раз (на экран выведено ровно 40 строк), поэтому конечное значение номера должно быть 40.

Нам нужно каким-то образом предотвратить изменение файла с порядковым номером на протяжении выполнения трех действий одним из процессов. Эти действия должны выполняться как атомарная операция по отношению к другим процессам. Код между вызовами my_lock и my_unlock представляет собой критическую область (глава 7).

При запуске двух экземпляров программы в фоновом режиме результат на самом деле непредсказуем. Нет никакой гарантии, что при каждом запуске мы будем получать один и тот же результат. Это нормально, если три действия будут выполняться как одна атомарная операция; в этом случае конечное значение порядкового номера все равно будет 40. Однако при неатомарном выполнении конечное значение часто будет отличным от 40, и это нас не устраивает. Например, нам безразлично, будет ли порядковый номер увеличен от 1 до 20 первым процессом и от 21 до 40 вторым или же процессы будут по очереди увеличивать его значение на единицу. Неопределенность не делает результат неправильным, а вот атомарность выполнения операций — делает. Однако неопределенность выполнения усложняет отладку программ.

9.2. Блокирование записей и файлов

Ядро Unix никак не интерпретирует содержимое файла, оставляя всю обработку записей приложениям, работающим с этим файлом. Тем не менее для описания предоставляемых возможностей используется термин «блокировка записей». В действительности приложение указывает диапазон байтов файла для блокирования или разблокирования. Сколько логических записей помещается в этот диапазон — значения не имеет.

Стандарт Posix определяет один специальный диапазон с началом в 0 (начало файла) и длиной 0 байт, который устанавливает блокировку для всего файла целиком. Мы будем говорить о блокировке записей, подразумевая блокировку файла как частный случай.

Термин «степень детализации» (granularity) используется для описания минимального размера блокируемого объекта. Для стандарта Posix эта величина составляет 1 байт. Обычно степень детализации связана с максимальным количеством одновременных обращений к файлу. Пусть, например, с некоторым файлом одновременно работают пять процессов, из которых три считывают данные из файла и два записывают в него. Предположим также, что каждый процесс работает со своим набором записей и каждый запрос требует примерно одинакового времени для обработки (1 секунда). Если блокировка осуществляется на уровне файла (самый низкий уровень детализации), три считывающих процесса смогут работать со своими записями одновременно, а двум записывающим придется ждать окончания их работы. Затем запись будет произведена сначала одним из оставшихся процессов, а потом другим. Полное затраченное время будет порядка 3 секунд (это, разумеется, очень грубая оценка). Если же уровень детализации соответствует размеру записи (наилучший уровень детализации), все пять процессов смогут работать одновременно, поскольку они обрабатывают разные записи. При этом на выполнение будет затрачена только одна секунда.

ПРИМЕЧАНИЕ

Потомки BSD поддерживают лишь блокировку файла целиком с помощью функции flock. Возможность заблокировать диапазон байтов не предусматривается. 

История

За долгие годы было разработано множество методов блокировки файлов и записей. Древние программы вроде UUCP и демонов печати играли на реализации файловой системы (три из них описаны в разделе 9.8). Они работали достаточно медленно и не подходили для баз данных, которые стали появляться в начале 80-х.

Первый раз возможность блокировать файлы и записи появилась в Version 7, куда она была добавлена Джоном Бассом John Bass) в 1980 году в виде нового системного вызова locking. Это блокирование было обязательным (mandatory locking); его унаследовали многие версии System III и Xenix. (Разница между обязательным и рекомендательным блокированием и между блокированием записей и файлов описана далее в этой главе.)

Версия 4.2BSD предоставила возможность блокирования файлов (а не записей) функцией flock в 1983. В 1984 году стандарт /usr/group (один из предшественников Х/Open) определил функцию lockf, которая осуществляла только исключающую блокировку (на запись), но не совместную.

В 1984 году в System V Release 2 была добавлена возможность рекомендательной блокировки записей с помощью fcntl. Функция lockf в этой версии также имелась, но она осуществляла просто вызов fcntl. (Многие нынешние версии также реализуют lockf через вызов fcntl.) В 1986 году в версии System V Release 3 появилась обязательная блокировка записей с помощью fcntl. При этом использовался бит set-group-ID (установка идентификатора группы) — об этом методе рассказано в разделе 9.5.

В 1988 году стандарт Posix.1 включил в себя рекомендательную и обязательную блокировку файлов и записей с помощью функции fcntl, и это именно то, что является предметом обсуждения данной главы. Стандарт X/Open Portability Guide Issue 3 (XPG3, 1988) также указывает на необходимость осуществления блокировки записей через fcntl.

9.3. Блокирование записей с помощью fcntl по стандарту Posix

Согласно стандарту Posix, интерфейсом для блокировки записей является функция fcntl:

#include <fcntl.h>

int fcntl(int fd, int cmd,… /* struct flock *arg */);

/* Возвращает –1 в случае ошибки: результат, возвращаемый в случае успешного завершения, зависит от аргумента cmd */

Для блокировки записей используются три различных значения аргумента cmd. Эти три значения требуют, чтобы третий аргумент, arg, являлся указателем на структуру flock:

struct flock {

 short l_type;   /* F_RDLCK, F_WRLCK, F_UNLCK */

 short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */

 off_t l_start;  /* относительный сдвиг в байтах */

 off_t l_len;    /* количество байтов; 0 означает до конца файла */

 pid_t l_pid;    /* PID, возвращаемый F_GETLK */

};

Вот три возможные команды (значения аргумента cmd ):

■ F_SETLK — получение блокировки (l_type имеет значение либо F_RDLCK, либо F_WRLCK) или сброс блокировки (l_type имеет значение F_UNLCK), свойства которой определяются структурой flock, на которую указывает arg. Если процесс не может получить блокировку, происходит немедленный возврат с ошибкой EACCESS или EAGAIN.

■ F_SETLKW — эта команда идентична предыдущей. Однако при невозможности блокирования ресурса процесс приостанавливается, до тех пор пока блокировка не сможет быть получена (W в конце команды означает «wait»).

■ F_GETLK — проверка состояния блокировки, на которую указывает arg. Если в данный момент блокировка не установлена, поле l_type структуры flock, на которую указывает arg, будет иметь значение F_UNLCK. В противном случае в структуре flock, на которую указывает arg, возвращается информация об установленной блокировке, включая идентификатор процесса, заблокировавшего ресурс. 

Обратите внимание, что последовательный вызов F_GETLK и F_SETLK не является атомарной операцией. Если мы вызвали F_GETLK и она вернула значение F_UNLCK в поле l_type, это не означает, что немедленный вызов F_SETLK будет успешным. Между этими двумя вызовами другой процесс мог уже заблокировать ресурс.

Причина, по которой была введена команда F_GETLK, — необходимость получения информации о блокировке в том случае, когда F_SETLK возвращает ошибку. Мы можем узнать, кто и каким образом заблокировал ресурс (на чтение или на запись). Но и в этом случае мы должны быть готовы к тому, что F_GETLK вернет результат F_UNLCK, поскольку между двумя вызовами другой процесс мог освободить ресурс.

Структура flock описывает тип блокировки (чтение или запись) и блокируемый диапазон. Как и в 1 seek, начальный сдвиг представляет собой сдвиг относительно начала файла, текущего положения или конца файла, и интерпретируется в зависимости от значения поля l_whence (SEEK_SET, SEEK_CUR, SEEK_END).

Поле l_len указывает длину блокируемого диапазона. Значение 0 соответствует блокированию от l_start до конца файла. Существуют, таким образом, два способа заблокировать файл целиком:

1. Указать l_whence = SEEK_SET, l_start = 0 и l_len = 0.

2. Перейти к началу файла с помощью lseek, затем указать l_whence = SEEK_CUR, l_start = 0 и l_len = 0.

Чаще всего используется первый метод, поскольку он предусматривает единственный вызов (fcntl — см. также упражнение 9.10).

Блокировка может быть установлена либо на чтение, либо на запись, и для конкретного байта файла может быть задан только один тип блокировки. Более того, на конкретный байт может быть установлено несколько блокировок на чтение, но только одна блокировка на запись. Это соответствует тому, что говорилось о блокировках чтения-записи в предыдущей главе. Естественно, при попытке установить блокировку на чтение для файла, открытого только на запись, будет возвращена ошибка.

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

ПРИМЕЧАНИЕ

Снятие блокировок при завершении процесса обеспечивается только для блокировок записей fcntl и (в качестве дополнительной возможности) для семафоров System V. Для других средств синхронизации (взаимных исключений, условных переменных, блокировок чтения-записи и семафоров Posix) автоматическое снятие при завершении процесса не предусматривается. Об этом мы говорили в конце раздела 7.7. 

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

Пример

Вернемся к нашему примеру из листинга 9.2 и перепишем функции my_lock и my_unlock из листинга 9.1 так, чтобы воспользоваться блокировкой записей Posix. Текст этих функций приведен в листинге 9.3.

Листинг 9.3. Блокировка записей fcntl по стандарту Posix

//lock/lockfcntl.c

1  #include "unpipc.h"


2  void

3  my_lock(int fd)

4  {

5   struct flock lock;

6   lock.l_type = F_WRLCK;

7   lock.l_whence = SEEK_SET;

8   lock.l_start = 0;

9   lock.l_len = 0; /* блокирование всего файла на запись */

10  Fcntl(fd, F_SETLKW, &lock);

11 }


12 void

13 my_unlock(int fd)

14 {

15  struct flock lock;

16  lock.l_type = F_UNLCK;

17  lock.l_whence = SEEK_SET;

18  lock.l_start = 0;

19  lock.l_len = 0; /* разблокирование всего файла */

20  Fcntl(fd. F_SETLK, &lock);

21 }

Обратите внимание, что мы устанавливаем блокировку на запись, что гарантирует единственность изменяющего данные процесса (см. упражнение 9.4). При получении блокировки мы используем команду F_SETLKW, чтобы приостановить выполнение процесса при невозможности установки блокировки.

ПРИМЕЧАНИЕ

Зная определение структуры flock, приведенное выше, мы могли бы проинициализировать структуру my_lock как

static struct flock lock = { F_WRLCK, SEEK_SET, 0, 0, 0 };

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

Мы не приводим результат работы пpoгрaммы, но она, судя по всему, работает правильно. Выполнение этой программы не дает возможности утверждать, что в ней нет ошибок. Если результат оказывается неправильным, то можно сказать с уверенностью, что что-то не так. Но успешное выполнение программы еще ни о чем не говорит. Ядро могло выполнить сначала одну программу, затем другую, и если они не выполнялись параллельно, мы не имеем возможности увидеть ошибку. Увеличить шансы обнаружения ошибки можно, изменив функцию main таким образом, чтобы последовательный номер увеличивался 10000 раз, и запустив 20 экземпляров программы одновременно. Если начальное значение последовательного номера в файле было 1, мы можем ожидать, что после завершения работы всех этих процессов мы увидим в файле число 200001.

Пример: упрощение с помощью макросов

В листинге 9.3 установка и снятие блокировки занимали шесть строк кода. Мы должны выделить место под структуру, инициализировать ее и затем вызвать fcntl. Программы можно упростить, если определить следующие семь макросов, которые взяты из раздела 12.3 [21]:

#define read_lock(fd, offset, whence, len) \

 lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)

#define readw_lock(fd, offset, whence, len) \

 lock_reg(fd, F_SETLKW, F_RDlCK, offset, whence, len)

#define write_lock(fd, offset, whence, len) \

 lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)

#define writew_lock(fd, offset, whence, len) \

 lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, len)

#define un_lock(fd, offset, whence, len) \

 lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, len)

#define is_read_lockable(fd, offset, whence, len) \

 lock_test(fd, F_RDLCK, offset, whence, len)

#define is_write_lockable(fd, offset, whence, len) \

 lock_test(fd, F_WRLCK, offset, whence, len)

Эти макросы используют наши функции lock_reg и lock_test, текст которых приведен в листингах 9.4 и 9.5. С ними нам уже не нужно заботиться об инициализации структур и вызове функций. Первые три аргумента специально сделаны совпадающими с первыми тремя аргументами функции lseek.

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

С помощью новых макросов мы можем записать функции my_lock и my_unlock из листинга 9.3 как

#define my_lock(fd) (Writew_lock(fd, 0, SEEK_SET, 0))

#define my_unlock(fd) (Un_lock(fd, 0, SEEK_SET, 0))

Листинг 9.4. Вызов fcntl для получения и снятия блокировки

//lib/lock_reg.c

1  #include "unpipc.h"


2  int

3  lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)

4  {

5   struct flock lock;

6   lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */

7   lock.l_start = offset; /* сдвиг по отношению к l_whence */

8   lock.l_whence = whence; /* SEEK SET. SEEK CUR, SEEK END */

9   lock.l_len = len; /* количество байтов (0 – до конца файла) */

10  return(fcnt(fd, cmd, &lock)"); /* –1 в случае ошибки */

11 }

Листинг 9.5. Вызов fcntl для проверки состояния блокировки

//lib/lock_test.c

1  #include "unpipc.h"


2  pid_t

3  lock_test(int fd, int type, off_t offset, int whence, off_t len)

4  {

5   struct flock lock;

6   lock.l_type = type; /* F_RDLCK or F_WRLCK */

7   lock.l_start = offset; /* сдвиг по отношению к l_whence */

8   lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */

9                      lock.l_len = len; /* количество байтов. 0 – до конца файла */

10 if (fcntl(fd, F_GETLK, &lock) == –1)

11 return(-1); /* непредвиденная ошибка */

12 if (lock.l_type == F_UNLCK)

13 return(0); /* false, область не заблокирована другим процессом */

14 return(lock.l_pid); /* true, возвращается положительный PID процесса. заблокировавшего ресурс */

15 }

9.4. Рекомендательная блокировка

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

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

Пример: несотрудничающие процессы

Мы можем проиллюстрировать рекомендательный характер блокировок, запустив два экземпляра нашей программы, один из которых (lockfcntl) использует функции из листинга 9.3 и блокирует файл перед увеличением последовательного номера, а другой (locknone) использует функции из листинга 9.1 и не устанавливает никаких блокировок:

solaris % lockfcntl & locknone &

lockfcntl: pid = 18816, seq# = 1

lockfcntl: pid = 18816, seq# = 2

lockfcntl: pid = 18816, seq# = 3

lockfcntl: pid = 18816, seq# = 4

lockfcntl: pid = 18816, seq# = 5

lockfcntl: pid = 18816, seq# = 6

lockfcntl: pid = 18816, seq# = 7

lockfcntl: pid = 18816, seq# = 8

lockfcntl: pid = 18816, seq# = 9

lockfcntl: pid = 18816, seq# = 10

lockfcntl: pid = 18816, seq# = 11

locknone: pid = 18817, seq# = 11

locknone: pid = 18817, seq# = 12

locknone: pid = 18817, seq# = 13

locknone: pid = 18817, seq# = 14

locknone: pid = 18817, seq# = 15

locknone: pid = 18817, seq# = 16

locknone: pid = 18817, seq# = 17

locknone: pid = 18817, seq# = 18

lockfcntl: pid = 18816, seq# = 12

lockfcntl: pid = 18816, seq# = 13

lockfcntl: pid = 18816, seq# = 14

lockfcntl: pid = 18816, seq# = 15

lockfcntl: pid = 18816, seq# = 16

lockfcntl: pid = 18816, seq# = 17

lockfcntl: pid = 18816, seq# = 18

lockfcntl: pid = 18816, seq# = 19

lockfcntl: pid = 18816, seq# = 20

locknone: pid = 18817, seq# = 19

locknone: pid = 18817, seq# = 20

locknone: pid = 18817, seq# = 21

locknone: pid = 18817, seq# = 22

locknone: pid = 18817, seq# = 23

locknone: pid = 18817, seq# = 24

locknone: pid = 18817, seq# = 25

locknone: pid = 18817, seq# = 26

locknone: pid = 18817, seq# = 27

locknone: pid = 18817, seq# = 28

locknone: pid = 18817, seq# = 29

locknone: pid = 18817, seq# = 30

Программа lockfcntl запускается первой, но в тот момент, когда она выполняет три действия для увеличения порядкового номера с 11 до 12 (в этот момент файл заблокирован), ядро переключается на второй процесс и запускает пpoгрaмму locknone. Этот процесс считывает значение 11 из файла с порядковым номером и использует его. Рекомендательная блокировка, установленная для этого файла пpoгрaммoй lockfcntl, никак не влияет на работу программы locknone.

9.5. Обязательная блокировка

Некоторые системы предоставляют возможность установки блокировки другого типа — обязательной (mandatory locking). В этом случае ядро проверяет все вызовы read и write, блокируя их при необходимости. Если для дескриптора установлен флаг O_NONBLOCK, вызов read или write, конфликтующий с установленной блокировкой, вернет ошибку EAGAIN. Если флаг O_NONBLOCK не установлен, выполнение процесса в такой ситуации будет отложено до тех пор, пока ресурс не освободится.

ПРИМЕЧАНИЕ

Стандарты Posix.1 и Unix 98 определяют только рекомендательную блокировку. Во многих реализациях, производных от System V, имеется возможность установки как рекомендательной, так и обязательной блокировки. Обязательная блокировка записей впервые появилась в System V Release 3.

Для установки обязательной блокировки какого-либо файла требуется выполнение двух условий:

■ бит group-execute должен быть снят;

■ бит set-group–ID должен быть установлен.

Обратите внимание, что установка бита set-user– ID без установки user-execute смысла не имеет; аналогично и с битами set-group-ID и group-execute. Таким образом, добавление возможности обязательной блокировки никак не повлияло на работу используемого программного обеспечения. Не потребовалось и добавлять новые системные вызовы.

В системах, поддерживающих обязательную блокировку записей, команда ls просматривает файлы на наличие указанной специальной комбинации битов и выводит буквы l или L, указывающие на то, что для данного файла включена обязательная блокировка. Аналогично команда chmod принимает аргумент l, позволяющий включить для указанного файла обязательную блокировку.

Пример

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

Чтобы использовать в нашем примере обязательную блокировку, изменим биты разрешений файла seqno. Кроме того, мы будем использовать новую версию функции main, которая принимает количество проходов цикла for в качестве аргумента командной строки (вместо использования константы 20) и не вызывает printf при каждом проходе цикла:

solaris % cat > seqno    инициализируем файл единицей

1

^D                      конец файла

solaris % ls –l seqno

-rw-r--r-- 1 rstevens other1 2 Oct 7 11:24 seqno

solaris % chmod +l seqno включение обязательной блокировки

solaris % ls -l seqno

-rq-r-lr-- 1 rstevens other1 2 Oct 7 11:24 seqno

Теперь запустим две программы в качестве фоновых процессов: loopfcntl использует блокировку записей fcntl, а loopnone не использует блокировку вовсе. 

Укажем в командной строке аргумента 10000 — количество последовательных увеличений порядкового номера.

solaris % loopfcntl 10000 & loopnone 10000 & запуск фоновых процессов

solaris % wait                                   ожидание их завершения

solaris % cat seqno                             вывод последовательного номера

14378                                            ошибка, должно быть 20001

UNIX: взаимодействие процессов

Рис. 9.1. Временная диаграмма работы программ loopfcntl и loopnone


Каждый раз при выполнении этих программ результат будет между 14000 и 16000. Если бы блокировка работала так как надо, он всегда был бы равен 20001. Чтобы понять, где же возникает ошибка, нарисуем временную диaгрaммy выполнения процессов, изображенную на рис. 9.1. 

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

Дальше ядро передает управление программе loopnone, которая выполняет шаги 17-23. Вызовы read и write разрешены, поскольку файл был разблокирован на шаге 15. Проблема возникает в тот момент, когда программа считывает значение 5 на шаге 23, а ядро в этот момент передает управление другому процессу. Он устанавливает блокировку и также считывает значение 5. Затем он дважды увеличивает это значение (получается 7), и управление передается loopnone на шаге 36. Однако эта программа записывает в файл значение 6. Так и возникает ошибка.

На этом примере мы видим, что обязательная блокировка предотвращает доступ к заблокированному файлу (шаг 11), но это не решает проблему. Проблема заключается в том, что левый процесс (на рисунке) может обновить содержимое файла (шаги 25-34) в тот момент, когда процесс справа также находится в состоянии обновления данных (шаги 23, 36 и 37). Если файл обновляется несколькими процессами, все они должны сотрудничать, используя некоторую форму блокировки. Один неподчиняющийся процесс может все разрушить.

9.6. Приоритет чтения и записи

В нашей реализации блокировок чтения-записи в разделе 8.4 приоритет предоставлялся ожидающим записи процессам. Теперь мы изучим детали возможного решения задачи читателей и писателей с помощью блокировки записей fcntl. Хочется узнать, как обрабатываются накапливающиеся запросы на блокировку, когда ресурс уже заблокирован. Стандарт Posix этого никак не оговаривает.

Пример: блокировка на чтение при наличии в очереди блокировки на запись

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

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

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

UNIX: взаимодействие процессов

Рис. 9.2. Определение возможности установки блокировки на чтение при наличиивочереди блокировки на запись


Листинг 9.6. Определение возможности установки блокировки на чтение при наличии в очереди блокировки на запись

//lock/test2.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int fd;

6   fd = Open("test1.data", O_RDWR | O_CREAT, FILE_MODE);

7   Read_lock(fd, 0, SEEK_SET, 0); /* родительский процесс блокирует весь файл на чтение */

8   printf("%s: parent has read lock\n", Gf_time());

9   if (Fork() == 0) {

10   /* первый дочерний процесс */

11   sleep(1);

12   printf("%s: first child tries to obtain write lock\n", Gf_time());

13   Writew_lock(fd, 0, SEEK_SET, 0); /* здесь он будет заблокирован */

14   printf("%s: first child obtains write lock\n", Gf_time());

15   sleep(2);

16   Un_lock(fd, 0, SEEK_SET, 0);

17   printf("ls: first child releases write lock\n", Gf_time());

18   exit(0);

19  }

20  if (Fork() == 0) {

21   /* второй дочерний процесс */

22   sleep(3);

23   printf("%s: second child tries to obtain read lock\n", Gf_time());

24   Readw_lock(fd, 0, SEEK_SET, 0);

25   printf("%s: second child obtains read lock\n", Gf_time());

26   sleep(4);

27   Un_lock(fd, 0, SEEK_SET, 0);

28   printf("%s: second child releases read lock\n", Gf_time());

29   exit(0);

30  }

31  /* родительский процесс */

32  sleep(5);

33  Un_lock(fd, 0, SEEK_SET, 0);

34  printf("%s: parent releases read lock\n", Gf_time());

35  exit(0);

36 }

Родительский процесс открывает файл и получает блокировку на чтение

6-8 Родительский процесс открывает файл и устанавливает блокировку на чтение для всего файла целиком. Обратите внимание, что мы вызываем read_lock (которая возвращает ошибку в случае недоступности ресурса), а не readw_lock (которая ждет его освобождения), потому что мы ожидаем, что эта блокировка будет установлена немедленно. Мы также выводим значение текущего времени функцией gf_time [24, с. 404], когда получаем блокировку.

Первый дочерний процесс

9-19 Порождается первый процесс, который ждет 1 секунду и блокируется в ожидании получения блокировки на запись для всего файла. Затем он устанавливает эту блокировку, ждет 2 секунды, снимает ее и завершает работу.

Второй дочерний процесс

20-30 Порождается второй процесс, который ждет 3 секунды, давая возможность первому попытаться установить блокировку на запись, а затем пытается получить блокировку на чтение для всего файла. По моменту возвращения из функции readw_lock мы можем узнать, был ли ресурс предоставлен немедленно или второму процессу пришлось ждать первого. Блокировка снимается через четыре секунды.

Родительский процесс блокирует ресурс 5 секунд

31-35 Родительский процесс ждет пять секунд, снимает блокировку и завершает работу.

На рис. 9.2 приведена временная диаграмма выполнения программы в Solaris 2.6, Digital Unix 4.0B и BSD/OS 3.1. Как видно, блокировка чтения предоставляется второму дочернему процессу немедленно, несмотря на наличие в очереди запроса на блокировку записи. Существует вероятность, что запрос на запись так и не будет выполнен, если будут постоянно поступать новые запросы на чтение. Ниже приведен результат выполнения программы, в который были добавлены пустые строки для улучшения читаемости:

alpha % test2

16:32:29.674453: parent has read lock


16:32:30.709197: first child tries to obtain write lock


16:32:32.725810: second child tries to obtain read lock

16:32:32.728739: second child obtains read lock


16:32:34.722282: parent releases read lock


16:32:36.729738: second child releases read lock

16:32:36.735597: first child obtains write lock


16:32:38.736938: first child releases write lock

Пример: имеют ли приоритет запросы на запись перед запросами на чтение?

Следующий вопрос, на который мы попытаемся дать ответ, таков: есть ли приоритет у запросов на блокировку записи перед запросами на блокировку чтения, если все они находятся в очереди? Некоторые решения задачи читателей и писателей предусматривают это.

В листинге 9.7 приведен текст нашей тестовой программы, а на рис. 9.3 — временная диаграмма ее выполнения.

Листинг 9.7. Есть ли у писателей приоритет перед читателями

//lock/test3.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int fd;

6   fd = Open("test1.data", O_RDWR | O_CREAT, FILE_MODE);

7   Write_lock(fd, 0, SEEK_SET, 0); /* родительский процесс блокирует весь файл на запись */

8   printf("ls: parent has write lock\n", Gf_time());

9   if (Fork() == 0) {

10   /* первый дочерний процесс */

11   sleep(1);

12   printf("ls: first child tries to obtain write lock\n", Gf_time());

13   Writew_lock(fd, 0, SEEK_SET, 0); /* здесь процесс будет заблокирован */

14   printf("%s: first child obtains write lock\n", Gf_time());

15   sleep(2);

16   Un_lock(fd, 0, SEEK_LET, 0);

17   printf("ls: first child releases write lock\n", Gf_time());

18   exit(0);

19  }

20  if (Fork() == 0) {

21   /* второй дочерний процесс */

22   sleep(3);

23   printf("ls: second child tries to obtain read lock\n", Gf_time());

24   Readw_lock(fd, 0, SEEK_SET, 0);

25   printf(%s: second child obtains read lock\n", Gf_time());

26   sleep(4);

27   Un_lock(fd, 0, SEEK_SET, 0);

28   printf("ls: second child releases read lock\n", Gf_time());

29   exit(0);

30  }

31  /* родительский процесс */

32  sleep(5);

33  Un_lock(fd, 0, SEEK_SET, 0);

34  printf("ls: parent releases write lock\n", Gf_time());

35  exit(0);

36 }

Родительский процесс создает файл и устанавливает блокировку на запись

6-8 Родительский процесс создает файл и блокирует его целиком на запись.

Первый дочерний процесс

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

Второй дочерний процесс

20-30 Порождается второй процесс, который ждет три секунды, а затем запрашивает блокировку на чтение на весь файл. Этот запрос будет также помещен в очередь.

И в Solaris 2.6, и в Digital Unix 4.0B мы видим, что блокировка на запись предоставляется первому процессу, как изображено на рис. 9.3. Но это еще не означает, что у запросов на запись есть приоритет перед запросами на чтение, поскольку, возможно, ядро предоставляет блокировку в порядке очереди вне зависимости от того, на чтение она или на запись. Чтобы проверить это, мы создаем еще одну тестовую программу, практически идентичную приведенной в листинге 9.7, но в ней блокировка на чтение запрашивается через одну секунду, а блокировка на запись — через три секунды. Эти две программы иллюстрируют, что Solaris и Digital Unix обрабатывают запросы в порядке очереди вне зависимости от типа запроса. Однако в BSD/OS 3.1 приоритет имеют запросы на чтение. 

UNIX: взаимодействие процессов

Рис. 9.3. Есть ли у писателей приоритет перед читателями


Вот вывод программы из листинга 9.7, на основании которого была составлена временная диaгрaммa на рис. 9.3:

alpha % test3

16:34:02.810285: parent has write lock

16:34:03.848166: first child tries to obtain write lock

16:34:05.861082: second child tries to obtain read lock

16:34:07.858393: parent releases write lock

16:34:07.865222: first child obtains write lock

16:34:09.865987: first child releases write lock

16:34:09.872823: second child obtains read lock

16:34:13.873822: second child releases read lock

9.7. Запуск единственного экземпляра демона

Часто блокировки записей используются для обеспечения работы какой-либо пpoгрaммы (например, демона) в единственном экземпляре. Фрагмент кода, приведенный в листинге 9.8, должен выполняться при запуске демона.

Листинг 9.8. Гарантия выполнения единственного экземпляра программы

//lock/onedaemon.c

1  #include "unpipc.h"

2  #define PATH_PIDFILE "pidfile"


3  int

4  main(int argc, char **argv)

5  {

6   int pidfd;

7   char line[MAXLINE];

8   /* открытие или создание файла с идентификатором процесса */

9   pidfd = Open(PATH_PIDFILE, O_RDWR | O_CREAT, FILE_MODE);

10  /* попытка блокирования всего файла на запись */

11  if (write_lock(pidfd, 0, SEEK_SET, 0) < 0) {

12   if (errno == EACCES || errno == EAGAIN)

13    err_quit("unable to lock %s, is %s already running?",

14     PATH_PIDFILE, argv[0]);

15   else

16    err_sys("unable to lock %s", PATH_PIDFILE):

17  }

18  /* запись идентификатора: файл остается открытым, чтобы он был заблокирован */

19  snprintf(line, sizeof(line), "%ld\n", (long) getpid());

20  Ftruncate(pidfd, 0);

21  Write(pidfd, line, strlen(line));

22  /* основной текст программы демона… */

23  pause();

24 }

Открытие и блокирование файла

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

ПРИМЕЧАНИЕ

Во многих версиях Unix демоны записывают свои идентификаторы в файл. Solaris 2.6 хранит подобные файлы в каталоге /etc, a Digital Unix 4.0B и BSD/OS — в каталоге /var/run.

Запись идентификатора процесса в файл

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

Вот результат работы программы из листинга 9.8:

solaris % onedaemon&   запускаем первый экземпляр

[1] 22388

solaris % cat pidfile проверяем идентификатор

22388

solaris % onedaemon   пытаемся запустить второй экземпляр

unable to lock pidfile, is onedaemon already running?

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

9.8. Блокирование файлов

Стандарт Posix.1 гарантирует, что если функция open вызывается с флагами O_CREAT (создать файл, если он еще не существует) и O_EXCL (исключающее открытие), функция возвращает ошибку, если файл уже существует. Более того, проверка существования файла и его создание (если он еще не существует) должны представлять собой атомарную по отношению к другим процессам операцию. Следовательно, мы можем использовать создаваемый таким методом файл как блокировку. Можно быть уверенным, что только один процесс сможет создать файл (то есть получить блокировку), а для снятия этой блокировки файл можно удалить командой unlink.

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

У этого метода есть три недостатка.

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

В такой ситуации лучше пользоваться блокировкой fcntl, которая автоматически снимается по завершении процесса.

2. Если файл открыт каким-либо другим процессом, мы должны еще раз вызвать open, повторяя эти вызовы в бесконечном цикле. Это называется опросом и является напрасной тратой времени процессора. Альтернативным методом является вызов sleep на 1 секунду, а затем повторный вызов open (этапроблема обсуждалась в связи с листингом 7.4).

Эта проблема также исчезает при использовании блокировки fcntl, если использовать команду F_SETLKW. Ядро автоматически приостанавливает выполнение процесса до тех пор, пока ресурс не станет доступен.

3. Создание и удаление файла вызовом open и unlink приводит к обращению к файловой системе, что обычно занимает существенно больше времени, чем вызов fcntl (обращение производится дважды: один раз для получения блокировки, а второй — для снятия). При использовании fcntl программа выполнила 10000 повторов цикла с увеличением порядкового номера в 75 раз быстрее, чем программа, вызывавшая open и unlink.

Листинг 9.9. Функции блокировки с использованием open с флагами O_CREAT и O_EXCL

//lock/lockopen.c

1  #include "unpipc.h"

2  #define LOCKFILE "/tmp/seqno.lock"


3  void

4  my_lock(int fd)

5  {


6   int tempfd;

7   while ((tempfd = open(LOCKFILE, O_RDWR|O_CREAT|O_EXCL, FILE_MODE)) < 0) {

8    if (errno != EEXIST)

9     err_sys("open error for lock file");

10   /* блокировка установлена кем-то другим, повторяем попытку */

11  }

12  Close(tempfd); /* открыли файл, блокировка установлена */

13 }


14 void

15 my_unlock(int fd)

16 {

17  Unlink(LOCKFILE); /* снимаем блокировку удалением файла */

18 }

Есть еще две особенности файловой системы Unix, которые использовались для реализации блокировок. Первая заключается в том, что функция link возвращает ошибку, если имя новой ссылки уже существует. Для получения блокировки создается уникальный временный файл, полное имя которого содержит в себе его идентификатор процесса (или комбинацию идентификаторов процесса и потока, если требуется осуществлять блокировку между отдельными потоками). Затем вызывается функция link для создания ссылки на этот файл с каким-либо определенным заранее именем. После успешного создания сам файл может быть удален вызовом unlink. После осуществления работы с блокировкой файл с известным именем удаляется командой unlink. Если link возвращает ошибку EEXIST, поток должен попытаться создать ссылку еще раз (аналогично листингу 9.9). Одно из требований к этому методу — необходимо, чтобы и временный файл, и ссылка находились в одной файловой системе, поскольку большинство версий Unix не допускают создания жестких ссылок (результат вызова link) в разных файловых системах.

Вторая особенность заключается в том, что функция open возвращает ошибку в случае существования файла, если указан флаг O_TRUNC и запрещен доступ на запись. Для получения блокировки мы вызываем open, указывая флаги O_CREAT | O_WRONLY | O_TRUNC и аргумент mode со значением 0 (то есть разрешения на доступ к файлу установлены в 0). Если вызов оказывается успешным, блокировка установлена и мы просто удаляем файл вызовом unlink после завершения работы. Если вызов open возвращает ошибку EACESS, поток должен сделать еще одну попытку (аналогично листингу 9.9). Этот трюк не срабатывает, если поток обладает правами привилегированного пользователя.

Урок, который можно извлечь из этих примеров, прост: нужно пользоваться блокировкой fcntl. Тем не менее вы можете столкнуться с программой, в которой используются старые методы блокировки, и особенно часто это будет встречаться в программах, написанных до широкого распространения реализации с блокировкой fcntl.

9.9. Блокирование в NFS

Аббревиатура NFS расшифровывается как Network File System (сетевая файловая система); эта система подробно обсуждается в главе 29 [22]. Блокировка записей fcntl представляет собой расширение NFS, поддерживаемое большинством ее реализаций. Обслуживается эта блокировка двумя дополнительными демонами: lockd и statd. При вызове fcntl для получения блокировки ядро обнаруживает, что файл находится в файловой системе NFS. Тогда локальный демон lockd посылает демону lockd сервера запрос на получение блокировки. Демон statd хранит информацию о клиентах, установивших блокировку, и взаимодействует с lockd для обеспечения снятия блокировок в случае завершения процессов.

Установка блокировки записи в NFS должна занимать в среднем больше времени, чем для локального файла, поскольку для установки и снятия блокировки требуется передача информации по сети. Для проверки работы блокировки NFS нужно всего лишь изменить имя файла, определяемое константой SEQFILE в листинге 9.2. Если измерить время, требуемое для выполнения 10000 операций по увеличению порядкового номера новой версией программы, оно окажется примерно в 80 раз больше, чем для локального файла. Однако нужно понимать, что в этом случае происходит передача информации по сети и при операциях чтения и записи (для изменения порядкового номера).

ПРИМЕЧАНИЕ

Блокировка записей в NFS была связана с проблемами в течение многих лет, и большинство проблем были следствием плохой реализации. Несмотря на тот факт, что большинство производителей Unix все-таки доделали эту реализацию, использование блокировки fcntl через NFS все еще далеко от совершенства. Не будем делать безответственных утверждений: блокировка fcntl должна работать и в NFS, но будет ли — зависит от реализации демона и сервера. 

9.10. Резюме

Блокирование записей с помощью fcntl предоставляет возможность установки рекомендательной или обязательной блокировки для файла, указываемого с помощью открытого дескриптора. Эти блокировки предназначены для сотрудничества процессов, но не отдельных потоков одного процесса. Термин «запись» используется не вполне корректно, поскольку ядро не различает отдельные записи в файле. Лучше использовать термин «блокировка диапазона», поскольку при установке блокировки или ее снятии указывается именно диапазон байтов в файле. Практически во всех случаях применения этой блокировки она является рекомендательной и используется при совместной работе сотрудничающих процессов, поскольку даже обязательная блокировка не может исключить повреждения данных.

При использовании fcntl не гарантируется, что читающие или пишущие процессы имеют приоритет при ожидании (в отличие от того, что мы реализовали в главе 8 с блокировками чтения-записи). Если это важно для приложения, придется реализовать блокировки самостоятельно (как в разделе 8.4) с тем приоритетом, который требуется.

Упражнения

1. Создайте программу locknone из листингов 9.2 и 9.1 и выполните ее много раз. Убедитесь, что программа не работает и результат непредсказуем.

2. Измените листинг 9.2 так, чтобы стандартный поток вывода не буферизовался. Как это повлияет на работу программы?

3. Продолжайте изменять программу, вызывая putchar для каждого выводимого символа (вместо printf). Как изменится результат?

4. Измените блокировку в функции my_lock из листинга 9.3 так, чтобы устанавливалась блокировка на чтение, а не на запись. Что произойдет?

5. Измените вызов open в программе loopmain.c, указав также флаг O_NONBLOCK. Создайте программу loopfcntlnonb и запустите два экземпляра. Что произойдет? 

6. Продолжите предыдущий пример, используя неблокируемую версию loopmain.с для создания программы loopnonenonb (используя файл locknone.c). Включите обязательную блокировку для файла seqno. Запустите один экземпляр этой программы и один экземпляр программы loopfcntlnonb из предыдущего примера одновременно. Что произойдет?

7. Создайте программу loopfcntl и запустите ее 10 раз в фоновом режиме из сценария интерпретатора команд. Каждому из 10 экземпляров следует указать аргумент 10000. Измерьте скорость работы сценария при использовании обязательной и рекомендательной блокировок. Как влияет обязательная блокировка на производительность?

8. Почему мы вызывали fork в листингах 9.6 и 9.7 для порождения процессов, вместо того чтобы воспользоваться pthread_create для создания потоков?

9. В листинге 9.9 мы вызываем ftruncate для установки размера файла в 0 байт. Почему бы нам просто не указать флаг O_TRUNC при открытии файла?

10. Какой из констант — SEEK_SET, SEEK_CUR или SEEK_END — следует пользоваться при указании блокируемого диапазона при написании многопоточного приложения и почему? 

ГЛАВА 10

Семафоры Posix

10.1.Введение

Семафор представляет собой простейшее средство синхронизации процессов и потоков. Мы рассматриваем три типа семафоров:

■ именованные семафоры Posix, идентифицируемые именами, соответствующими стандарту Posix для IPC (см. раздел 2.2);

■ размещаемые в разделяемой памяти семафоры Posix;

■ семафоры System V (глава 11), обслуживаемые ядром.

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

UNIX: взаимодействие процессов

Рис. 10.1. Два процесса взаимодействуют с помощью бинарного семафора


На этом рисунке изображен бинарный семафор, хранящийся в ядре (семафор System V).

Семафоры Posix не обязательно должны обрабатываться ядром. Их особенностью является наличие имен, которые могут соответствовать именам реальных файлов в файловой системе. На рис. 10.2 изображена схема, лучше иллюстрирующая предмет обсуждения данной главы — именованный семафор Posix.

UNIX: взаимодействие процессов

Рис. 10.2. Два процесса, использующие бинарный именованный семафор Posix


ПРИМЕЧАНИЕ

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

На рис. 10.1 и 10.2 мы указали три операции, которые могут быть применены к семафорам:

1. Создание семафора. При этом вызвавший процесс должен указать начальное значение (часто 1, но может быть и 0).

2. Ожидание изменения значения семафора (wait). При этом производится проверка его значения и процесс блокируется, если значение оказывается меньшим либо равным 0, а при превышении 0 значение уменьшается на 1. Это может быть записано на псевдокоде как

while (semaphore_value <= 0); /* wait: т.е. поток или процесс блокируется */

semaphore_value--; /* семафор разрешает выполнение операций */

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

У этой операции есть несколько общеупотребительных имен. Изначально она называлась Р, от голландского proben (проверка, попытка), — это название было введено Эдсгером Дейкстрой. Используются также и термины down (поскольку значение семафора уменьшается) и lock, но мы будем следовать стандарту Posix и говорить об ожидании (wait). 

3. Установка значения семафора (post). Значение семафора увеличивается одной командой, которая может быть записана на псевдокоде как

semaphore_value++;

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

Для этой операции также имеется несколько общеупотребительных терминов. Изначально она называлась V, от голландского verhogen (увеличивать). Называют ее up (значение семафора увеличивается), unlock и signal. Мы, следуя стандарту Posix, называем эту операцию post.

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

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

ПРИМЕЧАНИЕ

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

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

Листинг 10.1. Сравнение бинарных семафоров и взаимных исключений

инициализация взаимного исключения;  инициализация семафора единицей;

pthread_mutex_lock(&mutex);          sem_wait(&sem);

критическая область                  критическая область

pthread_mutex_unlock(&mutex);        sem_post(&sem);

Мы инициализируем семафор значением 1. Вызвав sem_wait, мы ожидаем, когда значение семафора окажется больше 0, а затем уменьшаем его на 1. Вызов sem_post увеличивает значение с 0 до 1 и возобновляет выполнение всех потоков, заблокированных в вызове sem_wait для данного семафора.

Хотя семафоры и могут использоваться в качестве взаимных исключений, они обладают некоторыми особенностями: взаимное исключение должно быть разблокировано именно тем потоком, который его заблокировал, в то время как увеличение значения семафора может быть выполнено другим потоком. Можно привести пример использования этой особенности для решения упрощенной версии задачи потребителей и производителей из главы 7 с двумя бинарными семафорами. На рис. 10.3 приведена схема с одним производителем, помещающим объект в общий буфер, и одним потребителем, изымающим его оттуда. Для простоты предположим, что в буфер помещается ровно один объект. 

UNIX: взаимодействие процессов

Рис. 10.3. Задача производителя и потребителя с общим буфером


В листинге 10.2 приведен текст соответствующей программы на псевдокоде.

Листинг 10.2. Псевдокод для задачи производителя и потребителя

Producer                                Consumer

инициализация семафора get значением 0;

инициализация семафора put значением 1;

for (;;) {                              for (;;) {

 sem_wait(&put);                         sem_wait(&get);

 помещение данных в буфер;               обработка данных в буфере;

 sem_post(&get);                         sem_post(&put);

}                                       }

Семафор put oгрaничивaeт возможность помещения объекта в общий буфер, а семафор get управляет потребителем при считывании объекта из буфера. Работает эта пpoгрaммa в такой последовательности:

1. Производитель инициализирует буфер и два семафора.

2. Пусть после этого запускается потребитель. Он блокируется при вызове sem_wait, поскольку семафор get имеет значение 0.

3. После этого запускается производитель. При вызове sem_wait значение put уменьшается с 1 до 0, после чего производитель помещает объект в буфер. Вызовом sem_post значение семафора get увеличивается с 0 до 1. Поскольку имеется поток, заблокированный в ожидании изменения значения этого семафора, этот поток помечается как готовый к выполнению. Предположим, тем не менее, что производитель продолжает выполняться. В этом случае он блокируется при вызове sem_wait в начале цикла for, поскольку значение семафора put — 0. Производитель должен подождать, пока потребитель не извлечет данные из буфера.

4. Потребитель возвращается из sem_wait, уменьшая значение семафора get с 0 до 1. Затем он обрабатывает данные в буфере и вызывает sem_post, увеличивая значение put с 0 до 1. Заблокированный в ожидании изменения значения этого семафора поток-производитель помечается как готовый к выполнению. Предположим опять, что выполнение потребителя продолжается. Тогда он блокируется при вызове sem_wait в начале цикла for, поскольку семафор get имеет значение 0.

5. Производитель возвращается из sem_wait, помещает данные в буфер, и все повторяется.

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

Перечислим три главных отличия семафоров и взаимных исключений в паре с условными переменными:

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

2. Взаимное исключение может быть либо заблокировано, либо разблокировано (пара состояний, аналогично бинарному семафору).

3. Поскольку состояние семафора хранится в определенной переменной, изменение его значения оказывает влияние на процессы, которые вызовут функцию wait уже после этого изменения, тогда как при отправке сигнала по условной переменной в отсутствие ожидающих его потоков сигнал будет утерян. Взгляните на листинг 10.2 и представьте, что при первом проходе цикла производителем потребитель еще не вызвал sem_wait. Производитель сможет поместить объект в буфер, вызвать sem_post для семафора get (увеличивая его значение с 0 до 1), а затем он заблокируется в вызове sem_wait для семафора put. Через некоторое время потребитель дойдет до цикла for и вызовет sem_wait для переменной get, что уменьшит значение этого семафора с 1 до 0, а затем потребитель приступит к обработке содержимого буфера.

ПРИМЕЧАНИЕ

В Обосновании Posix.1 (Rationale) содержится следующий комментарий по поводу добавления семафоров помимо взаимных исключений и условных переменных: «Семафоры включены в стандарт в первую очередь с целью предоставить средства синхронизации выполнения процессов; эти процессы могут и не использовать общий сегмент памяти. Взаимные исключения и условные переменные описаны как средства синхронизации потоков, у которых всегда есть некоторое количество общей памяти. Оба метода широко используются уже много лет. Каждое из этих простейших средств имеет свой предпочтительный круг задач». В разделе 10.15 мы увидим, что для реализации семафоров-счетчиков с живучестью ядра требуется написать около 300 строк кода на С, использующего взаимные исключения и условные переменные. Несмотря на предпочтительность применения семафоров для синхронизации между процессами и взаимных исключений для синхронизации между потоками, и те и другие могут использоваться в обоих случаях. Следует пользоваться тем набором средств, который удобен в данном приложении.

Выше мы отмечали, что стандартом Posix описано два типа семафоров: именованные (named) и размещаемые в памяти (memory-based или unnamed). На рис. 10.4 сравниваются функции, используемые обоими типами семафоров.

Именованный семафор Posix был изображен на рис. 10.2. Неименованный, или размещаемый в памяти, семафор, используемый для синхронизации потоков одного процесса, изображен на рис. 10.5.

UNIX: взаимодействие процессов

Рис. 10.4. Вызовы для семафоров Posix


UNIX: взаимодействие процессов

Рис. 10.5. Семафор, размещенный в общей памяти двух потоков


На рис. 10.6 изображен размещенный в разделяемой памяти семафор (часть 4), используемый двумя процессами. Общий сегмент памяти принадлежит адресному пространству обоих процессов.

UNIX: взаимодействие процессов

Рис. 10.6. Семафор, размещенный в разделяемой двумя процессами памяти


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

Мы рассмотрим три реализации именованных семафоров Posix: с использованием каналов FIFO, отображаемых в память файлов и семафоров System V. 

10.2. Функции sem_open, sem_close и sem_unlink

Функция sem_open создает новый именованный семафор или открывает существующий. Именованный семафор может использоваться для синхронизации выполнения потоков и процессов:

#include <semaphore.h>

sem_t *sem_open(const char *name, int oflag, …

 /* mode_t mode, unsigned int value */);

/* Возвращает указатель на семафор в случае успешного завершения, SEM_FAILED — в случае ошибки */

Требования к аргументу пате приведены в разделе 2.2.

Аргумент oflag может принимать значения 0, O_CREAT, O_CREAT | O_EXCL, как описано в разделе 2.3. Если указано значение O_CREAT, третий и четвертый аргументы функции являются обязательными. Аргумент mode указывает биты разрешений доступа (табл. 2.3), a value указывает начальное значение семафора. Это значение не может превышать константу SEM_VALUE_MAX, которая, согласно Posix, должна быть не менее 32767. Бинарные семафоры обычно устанавливаются в 1, тогда как семафоры-счетчики чаще инициализируются большими величинами.

При указании флага O_CREAT (без O_EXCL) семафор инициализируется только в том случае, если он еще не существует. Если семафор существует, ошибки не возникнет. Ошибка будет возвращена только в том случае, если указаны флаги O_CREAT | O_EXCL.

Возвращаемое значение представляет собой указатель на тип sem_t. Этот указатель впоследствии передается в качестве аргумента функциям sem_close, sem_wait, sem_trywait, sem_post и sem_getvalue.

ПРИМЕЧАНИЕ

Кажется странным возвращать SEM_FAILED в случае ошибки — нулевой указатель был бы более уместен. В ранних версиях стандарта Posix указывалось возвращаемое значение –1, и во многих реализациях константа SEM_FAILED определена как

#define SEM_FAILED ((sem_t *)(-1))

В Posix.1 мало говорится о битах разрешений, связываемых с семафором при его создании и открытии. Вспомните, мы говорили в связи с табл. 2.2 о том, что для именованных семафоров не нужно даже указывать флаги O_RDONLY, O_WRONLY и O_RDWR. В системах, на которых мы тестируем все программы этой книги (Digital Unix 4.0B и Solaris 2.6), для работы с семафором (его открытия) необходимо иметь к нему доступ как на чтение, так и на запись. Причина, скорее всего, в том, что обе операции, выполняемые с семафором (post и wait), состоят из считывания текущего значения и последующего его изменения. Отсутствие доступа на чтение или запись в этих реализациях приводит к возвращению функцией sem_open ошибки EACCESS ("Permission denied"). 

Открыв семафор с помощью sem_open, можно потом закрыть его, вызвав sem_close:

#include <semaphore.h>

int sem_close(sem_t *sem);

/* Возвращает 0 в случае успешного завершения. –1 – в случае ошибки */ 

Операция закрытия выполняется автоматически при завершении процесса для всех семафоров, которые были им открыты. Автоматическое закрытие осуществляется как при добровольном завершении работы (вызове exit или _exit), так и при принудительном (с помощью сигнала).

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

Именованный семафор удаляется из системы вызовом sem_unlink:

#include <semaphore.h>

int sem_unlink(const char *name);

/* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */

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

10.3. Функции sem_wait и sem_trywait

Функция sem_wait проверяет значение заданного семафора на положительность, уменьшает его на единицу и немедленно возвращает управление процессу. Если значение семафора при вызове функции равно нулю, процесс приостанавливается, до тех пор пока оно снова не станет больше нуля, после чего значение семафора будет уменьшено на единицу и произойдет возврат из функции. Ранее мы отметили, что операция «проверка и уменьшение» должна быть атомарной по отношению к другим потокам, работающим с этим семафором:

#include <semaphore.h>

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

/* Обе функции возвращают 0 в случае успешного завершения. –1 – в случае ошибки */

Разница между sem_wait и sem_trywait заключается в том, что последняя не приостанавливает выполнение процесса, если значение семафора равно нулю, а просто немедленно возвращает ошибку EAGAIN.

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

10.4. Функции sem_post и sem_getvalue

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

#include <semaphore.h>

int sem_post(sem_t *sem);

int sem_getvalue(sem_t *sem, int *valp);

/* Обе функции возвращают 0 в случае успешного завершения. –1 – в случае ошибки */ 

Функция sem_getvalue возвращает текущее значение семафора, помещая его в целочисленную переменную, на которую указывает valp. Если семафор заблокирован, возвращается либо 0, либо отрицательное число, модуль которого соответствует количеству потоков, ожидающих разблокирования семафора.

Теперь мы ясно видим отличия семафоров от взаимных исключений и условных переменных. Прежде всего взаимное исключение может быть разблокировано только заблокировавшим его потоком. Для семафоров такого ограничения нет: один из потоков может ожидать изменения значения семафора, чтобы потом уменьшить его с 1 до 0 (действие аналогично блокированию семафора), а другой поток может изменить значение семафора с 0 до 1, что аналогично разблокированию семафора.

Далее, поскольку любой семафор имеет некоторое значение, увеличиваемое операцией post и уменьшаемое операцией wait, поток может изменить его значение (например, увеличить с 0 до 1), даже если нет потоков, ожидающих его изменения. Если же поток вызывает pthread_cond_signal в отсутствие заблокированных при вызове pthread_cond_wait потоков, сигнал просто теряется.

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

ПРИМЕЧАНИЕ

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

10.5. Простые примеры

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

Программа semcreate

В листинге 10.3 приведен текст программы, создающей именованный семафор. При вызове программы можно указать параметр –е, обеспечивающий исключающее создание (если семафор уже существует, будет выведено сообщение об ошибке), а параметр –i с числовым аргументом позволяет задать начальное значение семафора, отличное от 1. 

Листинг 10.3.[1] Создание именованного семафора

//pxsem/semcreate.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int с, flags;

6   sem_t *sem;

7   unsigned int value;

8   flags = O_RDWR | O_CREAT;

9   value = 1;

10  while ((c = Getopt(argc, argv, "ei:")) != –1) {

11   switch (c) {

12   case 'e':

13    flags |= O_EXCL;

14    break;

15   case 'i':

16    value = atoi(optarg);

17    break;

18   }

19  }

20  if (optind != argc – 1)

21   err_quit("usage: semcreate [ –e ] [ –i initialvalue ] <name>");

22  sem = Sem_open(argv[optind], flags, FILE_MODE, value);

23  Sem_close(sem);

24  exit(0);

25 }

Создание семафора

22 Поскольку мы всегда указываем флаг O_CREAT, нам приходится вызывать sem_open с четырьмя аргументами. Последние два используются только в том случае, если семафор еще не существует.

Закрытие семафора

23 Мы вызываем sem_close, хотя, если бы мы не сделали этот вызов, семафор все равно закрылся бы автоматически при завершении процесса и ресурсы системы были бы высвобождены.

Программа semunlink

Программа в листинге 10.4 удаляет именованный семафор.

Листинг 10.4. Удаление именованного семафора

//pxsem/semunlink.c

1 #include "unpipc.h"


2 int

3 main(int argc, char **argv)

4 {

5  if (argc != 2)

6   err_quit("usage: semunlink <name>");

7  Sem_unlink(argv[1]);

8  exit(0);

9 }

Программа semgetvalue

В листинге 10.5 приведен текст простейшей программы, которая открывает указанный именованный семафор, получает его текущее значение и выводит его.

Листинг 10.5. Получение и вывод значения семафора

//pxsem/semgetvalue.с

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   sem_t *sem;

6   int val;

7   if (argc != 2)

8    err_quit("usage: semgetvalue <name>");

9   sem = Sem_open(argv[1], 0);

10  Sem_getvalue(sem, &val);

11  printf("value = %d\n", val);

12  exit(0);

13 }

Открытие семафора

9 Семафор, который мы открываем, должен быть заранее создан другой программой. Вторым аргументом sem_open будет 0: мы не указываем флаг O_CREAT и нам не нужно задавать никаких других параметров открытия 0_ххх.

Программа semwait

Программа в листинге 10.6 открывает именованный семафор, вызывает semwait (которая приостанавливает выполнение процесса, если значение семафора меньше либо равно 0, а при положительном значении семафора уменьшает его на 1), получает и выводит значение семафора, а затем останавливает свою работу навсегда при вызове pause.

Листинг 10.6. Ожидание изменения значения семафора и вывод нового значения

//pxsem/semwait.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   sem_t *sem;

6   int val;

7   if (argc != 2)

8    err_quit("usage: semwait <name>");

9   sem = Sem_open(argv[1], 0);

10  Sem_wait(sem);

11  Sem_getvalue(sem, &val);

12  printf("pid %ld has semaphore, value = %d\n", (long) getpid(), val);

13  pause(); /* блокируется, пока не будет удален */

14  exit(0);

15 }

Программа sempost

В листинге 10.7 приведена программа, которая выполняет операцию post для указанного семафора (то есть увеличивает его значение на 1), а затем получает значение этого семафора и выводит его.

Листинг 10.7. Увеличение значения семафора

//pxsem/sempost.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   sem_t *sem;

6   int val;

7   if (argc != 2)

8    err_quit("usage: sempost <name>");

9   sem = Sem_open(argv[1], 0);

10  Sem_post(sem);

11  Sem_getvalue(sem, &val);

12  printf("value = %d\n", val);

13  exit(0);

14 }

Примеры

Для начала мы создадим именованный семафор в Digital Unix 4.0B и выведем его значение, устанавливаемое по умолчанию при инициализации:

alpha % semcreate /tmp/test1

alpha % ls-l /tmp/test1

-rw-r--r-- 1 rstevens system 264 Nov 13 08:51 /tmp/test1

alpha %semgetvalue /tmp/test1

value = 1

Аналогично очередям сообщений Posix система создает файл семафора с тем именем, которое мы указали при вызове функции.

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

alpha % semwait /tmp/test1

pid 9702 has semaphore, value = 0 значение после возврата из sem_wait

^?                                клавиша прерывания работы в нашей системе

alpha % semgetvalue /tmp/test1

value = 0                         значение остается нулевым

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

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

alpha % semgetvalue /tmp/test1

value = 0                          это значение сохранилось с конца предыдущего примера

alpha % semwait /tmp/test1 &      запуск в фоновом режиме

[1] 9718                           блокируется в ожидании изменения значения семафора

alpha % semgetvalue /tmp/test1

value = –1                         один процесс ожидает изменения семафора

alpha % semwait /tmp/test1 &       запуск еще одного процесса в фоновом режиме

[2] 9727                           он также блокируется

alpha % semgetvalue /tmp/test1

value = –2                         два процесса ожидают изменения семафора

alpha % sempost /tmp/test1

value = –1                         значение после возвращенияиз sem_post

pid 9718 has semaphore, value = –1 вывод программы semwait

alpha % sempost /tmp/test1

value = 0

pid 9727 has semaphore, value = 0  вывод программы semwait

При первом вызове sem_post значение семафора изменилось с –2 на –1 и один из процессов, ожидавших изменения значения семафора, был разблокирован.

Выполним те же действия в Solaris 2.6, обращая внимание на различия в реализации:

solaris % semcreate /test2

solaris % ls –l /tmp/.*test2*

-rw-r--r-- 1 rstevens other1 48 Nov 13 09:11 /tmp/.SEMDtest2

–rw-rw-rw– 1 rstevens other1  0 Nov 13 09:11 /tmp/.SEMLtest2

solaris % semgetvalue /test2

value = 1

Аналогично очередям сообщений Posix файлы создаются в каталоге /tmp, причем указываемое при вызове имя становится суффиксом имен файлов. Разрешения первого файла соответствуют указанным в вызове sem_open, а второй файл, как можно предположить, используется для блокировки доступа.

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

solaris % semwait /test2

pid 4133 has semaphore, value = 0

^? нажимаем клавишу прерывания выполнения

solaris % semgetvalue /test2

value = 0

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

solaris % semgetvalue /test2

value = 0                       значение сохранилось с конца предыдущего примера

solaris % semwait /test2&       запуск в фоновом режиме

[1] 4257                        программа блокируется

solaris % semgetvalue /test2

value = 0                        в этой реализации отрицательные значения не используются

solaris % semwait /test2&        еще один фоновый процесс

[2] 4263

solaris % semgetvalue /test2

value 0                          и для двух ожидающих процессов значение остается нулевым

solaris % sempost /test2          выполняем операцию post

pid 4257 has semaphore, value = 0 вывод программы semwait

value = 0

solaris % sempost /test2

pid 4263 has semaphore, value = 0 вывод программы semwait

value = 0

Можно заметить отличие по сравнению с результатами выполнения той же последовательности команд в Digital Unix 4.0B: после изменения значения семафора управление сразу же передается ожидающему изменения семафора процессу.

10.6. Задача производителей и потребителей

В разделе 7.3 мы описали суть задачи производителей и потребителей и привели несколько возможных ее решений, в которых несколько потоков-производителей заполняли массив, который обрабатывался одним потоком-потребителем.

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

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

Расширим постановку задачи производителей и потребителей, используя общий буфер в качестве циклического: заполнив последнее поле, производитель (buff[NBUFF-1]) возвращается к его началу и заполняет первое поле (buff[0]), и потребитель действует таким же образом. Возникает еще одно требование к синхронизации: потребитель не должен опережать производителя. Мы все еще предполагаем, что производитель и потребитель представляют собой отдельные потоки одного процесса, но они также могут быть и просто отдельными процессами, если мы сможем создать для них общий буфер (например, используя разделяемую память, часть 4).

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

1. Потребитель не должен пытаться извлечь объект из буфера, если буфер пуст.

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

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

Наше решение использует три семафора:

1. Бинарный семафор с именем mutex защищает критические области кода: помещение данных в буфер (для производителя) и изъятие данных из буфера (для потребителя). Бинарный семафор, используемый в качестве взаимного исключения, инициализируется единицей. (Конечно, мы могли бы воспользоваться и обычным взаимным исключением вместо двоичного семафора. См. упражнение 10.10.)

2. Семафор-счетчик с именем nempty подсчитывает количество свободных полей в буфере. Он инициализируется значением, равным объему буфера (NBUFF).

3. Семафор-счетчик с именем nstored подсчитывает количество заполненных полей в буфере. Он инициализируется нулем, поскольку изначально буфер пуст.

UNIX: взаимодействие процессов

Рис. 10.7. Состояние буфера и двух семафоров-счетчиков после инициализации


На рис. 10.7 показано состояние буфера и двух семафоров-счетчиков после завершения инициализации. Неиспользуемые элементы массива выделены темным.

В нашем примере производитель помещает в буфер целые числа от 0 до NLOOP-1 (buff[0] = 0, buff[1] = 1), работая с ним как с циклическим. Потребитель считывает эти числа и проверяет их правильность, выводя сообщения об ошибках в стандартный поток вывода.

На рис. 10.8 изображено состояние буфера и семафоров-счетчиков после помещения в буфер трех элементов, но до изъятия их потребителем.

UNIX: взаимодействие процессов

Рис. 10.8. Буфер и семафоры после помещения в буфер трех элементов


Предположим, что потребитель изъял один элемент из буфера. Новое состояние изображено на рис. 10.9.

UNIX: взаимодействие процессов

Рис. 10.9. Буфер и семафоры после удаления первого элемента из буфера


В листинге 10.8 приведен текст функции main, которая создает три семафора, запускает два потока, ожидает их завершения и удаляет семафоры.

Листинг 10.8. Функция main для решения задачи производителей и потребителей с помощью семафоров

//pxsem/prodcons1.с

1  #include "unpipc.h"

2  #define NBUFF 10

3  #define SEM_MUTEX "mutex" /* аргументы px_ipc_name() */

4  #define SEM_NEMPTY "nempty"

5  #define SEM_NSTORED "nstored"


6  int nitems; /* read-only для производителя и потребителя */

7  struct { /* разделяемые производителем и потребителем данные */

8   int buff[NBUFF];

9   sem_t *mutex, *nempty, *nstored;

10 } shared;

11 void *produce(void *), *consume(void *);


12 int

13 main(int argc, char **argv)

14 {

15  pthread_t tid_produce, tid_consume;

16  if (argc != 2)

17   err_quit("usage: prodcons1 <#items>");

18  nitems = atoi(argv[1]);

19  /* создание трех семафоров */

20  shared.mutex = Sem_open(Px_ipc_name(SEM_MUTEX), O_CREAT | O_EXCL,

21   FILE_MODE, 1);

22  shared.nempty = Sem_open(Px_ipc_name(SEM_NEMPTY), 0_CREAT | O_EXCL,

23   FILE_MODE, NBUFF);

24  shared.nstored = Sem_open(Px_ipc_name(SEM_NSTORED), O_CREAT | O_EXCL,

25   FILE_MODE, 0);

26  /* создание одного потока-производителя и одного потока-потребителя */

27  Set_concurrency(2);

28  Pthread_create(&tid_produce, NULL, produce, NULL);

29  Pthread_create(&tid_consume, NULL, consume, NULL);

30  /* ожидание завершения работы потоков */

31  Pthread_join(tid_produce, NULL);

32  Pthread_join(tid_consume, NULL);

33  /* удаление семафоров */

34  Sem_unlink(Px_ipc_name(SEM_MUTEX));

35  Sem_unlink(Px_ipc_name(SEM_NEMPTY));

36  Sem_unlink(Px_ipc_name(SEM_NSTORED));

37  exit(0);

38 }

Глобальные переменные

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

Создание семафоров

19-25 Мы создаем три семафора, передавая их имена функции px_ipc_name. Флаг O_EXCL мы указываем, для того чтобы гарантировать инициализацию каждого семафора правильным значением. Если после преждевременно завершенного предыдущего запуска программы остались неудаленные семафоры, мы обработаем эту ситуацию, вызвав перед их созданием sem_unlink и игнорируя ошибки. Мы могли бы проверять возвращение ошибки EEXIST при вызове sem_open с флагом O_EXCL, а затем вызывать sem_unlink и еще раз sem_open, но это усложнило бы программу. Если нам нужно проверить, что запущен только один экземпляр программы (что следует сделать перед созданием семафоров), можно обратиться к разделу 9.7, где описаны методы решения этой задачи.

Создание двух потоков

26-29 Создаются два потока, один из которых является производителем, а другой — потребителем. При запуске никакие аргументы им не передаются.

30-36 Главный поток ждет завершения работы производителя и потребителя, а затем удаляет три семафора.

ПРИМЕЧАНИЕ

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

В листинге 10.9 приведен текст функций produce и consume.

Листинг 10.9. Функции produce и consume

//pxsem/prodcons1.c

39 void *

40 produce(void *arg)

41 {

42  int i;

43  for (i = 0; i < nitems; i++) {

44   Sem_wait(shared.nempty); /* ожидаем освобождения поля */

45   Sem_wait(shared.mutex);

46   shared.buff[i % NBUFF] = i; /* помещаем i в циклический буфер */

47   Sem_post(shared.mutex);

48   Sem_post(shared.nstored); /* сохраняем еще 1 элемент */

49  }

50  return(NULL);

51 }


52 void *

53 consume(void *arg)

54 {

55  int i;

56  for (i = 0; i < nitems; i++) {

57   Sem_wait(shared.nstored); /* ожидаем появления объекта в буфере */

58   Sem_wait(shared.mutex);

59   if (shared.buff[i % NBUFF] != i)

60    printf("buff[%d] = %d\n", i, shared.buff[i % NBUFF]);

61   Sem_post(shared.mutex);

62   Sem_post(shared.nempty); /* еще одно пустое поле */

63  }

64  return(NULL);

65 }

Производитель ожидает освобождения места в буфере

44 Производитель вызывает sem_wait для семафора nempty, ожидая появления свободного места. В первый раз при выполнении этой команды значение семафора nempty уменьшится с NBUFF до NBUFF-1.

Производитель помещает элемент в буфер

45-48 Перед помещением нового элемента в буфер производитель должен установить блокировку на семафор mutex. В нашем примере, где производитель просто сохраняет значение в элементе массива с индексом i % NBUFF, для описания состояния буфера не используется никаких разделяемых переменных (то есть мы не используем связный список, который нужно было бы обновлять каждый раз при помещении элемента в буфер). Следовательно, установка и снятие семафора mutex не являются обязательными. Тем не менее мы иллюстрируем эту технику, потому что обычно ее применение является необходимым в задачах такого рода (обновление буфера, разделяемого несколькими потоками).

После помещения элемента в буфер блокировка с семафора mutex снимается (его значение увеличивается с 0 до 1) и увеличивается значение семафора nstored. Первый раз при выполнении этой команды значение nstored изменится с начального значения 0 до 1.

Потребитель ожидает изменения семафора nstored

57-62 Если значение семафора nstored больше 0, в буфере имеются объекты для обработки. Потребитель изымает один элемент из буфера и проверяет правильность его значения, защищая буфер в момент доступа к нему с помощью семафора mutex. Затем потребитель увеличивает значение семафора nempty, указывая производителю на наличие свободных полей.

Зависание

Что произойдет, если мы по ошибке поменяем местами вызовы Sem_wait в функции consumer (листинг 10.9)? Предположим, что первым запускается производитель (как в решении, предложенном для упражнения 10.1). Он помещает в буфер NBUFF элементов, уменьшая значение семафора nempty от NBUFF до 0 и увеличивая значение семафора nstored от 0 до NBUFF. Затем производитель блокируется в вызове Sem_wait(shared. nempty), поскольку буфер полон и помещать элементы больше некуда.

Запускается потребитель и проверяет первые NBUFF элементов буфера. Это уменьшает значение семафора nstored от NBUFF до 0 и увеличивает значение семафора nempty от 0 до NBUFF. Затем потребитель блокируется в вызове Sem_wait(shared, nstored) после вызова Sem_wait(shared, mutex). Производитель мог бы продолжать работу, поскольку значение семафора nempty уже отлично от 0, но он вызвал Sem_wait(shared, mutex) и его выполнение было приостановлено. 

Это называется зависанием программы (deadlock). Производитель ожидает освобождения семафора mutex, а потребитель не снимает с него блокировку, ожидая освобождения семафора nstored. Но производитель не может изменить nstored, пока он не получит семафор mutex. Это одна из проблем, которые часто возникают с семафорами: если в программе сделать ошибку, она будет работать неправильно.

ПРИМЕЧАНИЕ

Стандарт Posix позволяет функции sem_wait обнаруживать зависание и возвращать ошибку EDEADLK, но ни одна из систем, использовавшихся для написания примеров (Digital Unix 4.0B и Solaris 2.6), не обнаружила ошибку в данном случае.

10.7. Блокирование файлов

Вернемся к задаче о порядковом номере из главы 9. Здесь мы напишем новые версии функций my_lock и my_unlосk, использующие именованные семафоры Posix. В листинге 10.10 приведен текст этих функций.

Листинг 10.10. Блокирование файла с помощью именованных семафоров Posix

//lock/lockpxsem.c

1  #include "unpipc.h"

2  #define LOCK_PATH "pxsemlock"

3  sem_t *locksem;

4  int initflag;


5  void

6  my_lock(int fd)

7  {

8   if (initflag == 0) {

9    locksem = Sem_open(Px_ipc_name(LOCK_PATH), O_CREAT, FILE_MODE, 1);

10   initflag = 1;

11  }

12  Sem_wait(locksem);

13 }


14 void

15 my_unlock(int fd)

16 {

17  Sem_post(locksem);

18 }

Один из семафоров используется для рекомендательной блокировки доступа к файлу и инициализируется единицей при первом вызове функции. Для получения блокировки мы вызываем sem_wait, а для ее снятия — sem_post.

10.8. Функции sem_init и sem_destroy

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

#include <semaphore.h>

int sem_init(sem_t *sem, int shared, unsigned int value);

/* Возвращает –1 в случае ошибки */

int sem_destroy(sem_t *sem);

/* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */

Размещаемый в памяти семафор инициализируется вызовом sem_init. Аргумент sem указывает на переменную типа sem_t, место под которую должно быть выделено приложением. Если аргумент shared равен 0, семафор используется потоками одного процесса, в противном случае доступ к нему могут иметь несколько процессов. Если аргумент shared ненулевой, семафор должен быть размещен в одном из видов разделяемой памяти и должен быть доступен всем процессам, использующим его. Как и в вызове sem_open, аргумент value задает начальное значение семафора.

После завершения работы с размещаемым в памяти семафором его можно уничтожить, вызвав sem_destroy.

ПРИМЕЧАНИЕ 1

Функции sem_open не требуется параметр, аналогичный shared; не требуется ей и атрибут, аналогичный PTHREAD_PROCESS_SHARED (упоминавшийся в связи с взаимными исключениями и условными переменными в главе 7), поскольку именованный семафор всегда используется совместно несколькими процессами.

ПРИМЕЧАНИЕ 2

Обратите внимание, что для размещаемого в памяти семафора нет ничего аналогичного флагу O_CREAT: функция sem_init всегда инициализирует значение семафора. Следовательно, нужно быть внимательным, чтобы вызывать sem_init только один раз для каждого семафора. (Упражнение 10.2 иллюстрирует разницу в этом смысле между именованным и размещаемым в памяти семафорами.) При вызове sem_init для уже инициализированного семафора результат непредсказуем.

ПРИМЕЧАНИЕ 3

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

ПРИМЕЧАНИЕ 4

Стандарт Posix.1 предупреждает, что для обращения к размещаемым в памяти семафорам можно использовать только указатель, являющийся аргументом при вызове sem_init. Использование копий этого указателя может привести к неопределенным результатам.

Функция sem_init возвращает –1 в случае ошибки, но она не возвращает 0 в случае успешного завершения. Это действительно странно, и примечание в Обосновании Posix. 1 говорит, что в будущих версиях функция, возможно, начнет возвращать 0 в случае успешного завершения. 

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

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

■ Если размещаемый в памяти семафор совместно используется потоками одного процесса (аргумент shared при вызове sem_init равен 0), семафор обладает живучестью процесса и удаляется при завершении последнего.

■ Если размещаемый в памяти семафор совместно используется несколькими процессами (аргумент shared при вызове seminit равен 1), он должен располагаться в разделяемой памяти, и в этом случае семафор существует столько, сколько существует эта область памяти. Вспомните, что и разделяемая память Posix, и разделяемая память System V обладают живучестью ядра (табл. 1.1). Это значит, что сервер может создать область разделяемой памяти, инициализировать в ней размещаемый в памяти семафор Posix, а затем завершить работу. Некоторое время спустя один или несколько клиентов могут присоединить эту область к своему адресному пространству и получить доступ к хранящемуся в ней семафору.

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

sem_t mysem;

Sem_init(&mysem, 1.0); /* 2-й аргумент 1 –> используется процессами */

if (Fork() == 0) { /* дочерний процесс */

 …

 Sem_post(&mysem);

}

Sem_wait(&mysem); /* родительский процесс: ожидание дочернего */

Проблема тут в том, что семафор не располагается в разделяемой памяти (см. раздел 10.12). Память, как правило, не делится между дочерним и родительским процессами при вызове fork. Дочерний процесс запускается с копией памяти родителя, но это не то же самое, что разделяемая память.

Пример

В качестве иллюстрации перепишем наш пример решения задачи производителей и потребителей из листингов 10.8 и 10.9 для использования размещаемых в памяти семафоров Posix. В листинге 10.11 приведен текст новой программы.

Листинг 10.11. Задача производителей и потребителей с использованием размещаемых в памяти семафоров

//pxsem/prodcons2.c

1  #include "unpipc.h"

2  #define NBUFF 10


3  int nitems; /* только для чтения производителем и потребителем */

4  struct { /* общие данные производителя и потребителя */

5   int buff[NBUFF];

6   sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

7  } shared;

8  void *produce(void *), *consume(void *);


9  int

10 main(int argc, char **argv)

11 {

12  pthread_t tid_produce, tid_consume;

13  if (argc != 2)

14   err_quit("usage: prodcons2 <#items>");

15  nitems = atoi(argv[1]);

16  /* инициализация трех семафоров */

17  Sem_init(&shared.mutex, 0, 1);

18  Sem_init(&shared.nempty, 0, NBUFF);

19  Sem_init(&shared.nstored, 0, 0);

20  Set_concurrency(2);

21  Pthread_create(&tid_produce, NULL, produce, NULL);

22  Pthread_create(&tid_consume, NULL, consume, NULL);

23  Pthread_join(tid_produce, NULL);

24  Pthread_join(tid_consume, NULL):

25  Sem_destroy(&shared.mutex);

26  Sem_destroy(&shared.nempty):

27  Sem_destroy(&shared.nstored);

28  exit(0);

29 }


30 void *

31 produce(void *arg)

32 {

33  int i;

34  for (i = 0; i < nitems; i++) {

35   Sem_wait(&shared.nempty); /* ожидание одного свободного поля */

36   Sem_wait(&shared.mutex);

37   shared.buff[i % NBUFF] = i; /* помещение i в циклический буфер */

38   Sem_post(&shared.mutex);

39   Sem_post(&shared.nstored); /* поместили еще один элемент */

40  }

41  return(NULL);

42 }


43 void *

44 consume(void *arg)

45 {

46  int i;

47  for (i = 0; i < nitems; i++) {

48   Sem_wait(&shared.nstored); /* ожидаем появления хотя бы одного готового для обработки элемента */

49   Sem_wait(&shared.mutex);

50   if (shared.buff[i % NBUFF] != i)

51    printf("buff[*d] = *d\n", i, shared.buff[i % NBUFF]);

52   Sem_post(&shared.mutex);

53   Sem_post(&shared.nempty); /* еще одно пустое поле */

54  }

55  return(NULL);

56 }

Выделение семафоров

6 Мы объявляем три семафора типа sem_t, и теперь это сами семафоры, а не указатели на них.

Вызов sem_init

16-27 Мы вызываем sem_init вместо sem_open* а затем sem_destroy вместо sem_unlink. Вызывать sem_destroy на самом деле не требуется, поскольку программа все равно завершается.

Остальные изменения обеспечивают передачу указателей на три семафора при вызовах sem_wait и sem_post.

10.9. Несколько производителей, один потребитель

Решение в разделе 10.6 относится к классической задаче с одним производителем и одним потребителем. Новая, интересная модификация программы позволит нескольким производителям работать с одним потребителем. Начнем с решения из листинга 10.11, в котором использовались размещаемые в памяти семафоры. В листинге 10.12 приведены объявления глобальных переменных и функция main.

Листинг 10.12. Функция main задачи с несколькими производителями

//pxsem/prodcons3.c

1  #include "unpipc.h"

2  #define NBUFF 10

3  #define MAXNTHREADS 100


4  int nitems, nproducers; /* только для чтения производителем и потребителем */

5  struct { /* общие данные */

6   int buff[NBUFF];

7   int nput;

8   int nputval;

9   sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

10 } shared;


11 void *produce(void *), *consume(void *);


12 int

13 main(int argc, char **argv)

14 {

15  int i, count[MAXNTHREADS];

16  pthread_t tid_produce[MAXNTHREADS], tid_consume;

17  if (argc != 3)

18   err_quit("usage: prodcons3 <#items> <#producers>");

19  nitems = atoi(argv[1]);

20  nproducers = min(atoi(argv[2]), MAXNTHREADS);

21  /* инициализация трех семафоров */

22  Sem_init(&shared.mutex, 0, 1);

23  Sem_init(&shared.nempty, 0, NBUFF);

24  Sem_init(&shared.nstored, 0, 0);

25  /* создание всех производителей и одного потребителя */

26  Set_concurrency(nproducers + 1);

27  for (i = 0; i < nproducers; i++) {

28   count[i] = 0;

29   Pthread_create(&tid_produce[i], NULL, produce, &count[i]);

30  }

31  Pthread_create(&tid_consume, NULL, consume, NULL);

32  /* ожидание завершения всех производителей и потребителя */

33  for (i = 0; i < nproducers; i++) {

34   Pthread_join(tid_produce[i], NULL);

35   printf("count[%d] = %d\n", i, count[i]);

36  }

37  Pthread_join(tid_consume, NULL);

38  Sem_destroy(&shared.mutex);

39  Sem_destroy(&shared.nempty);

40  Sem_destroy(&shared.nstored);

41  exit(0);

42 }

Глобальные переменные

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

Общая структура

5-10 В структуру shared добавляются два новых элемента: nput, обозначающий индекс следующего элемента, куда должен быть помещен объект (по модулю BUFF), и nputval следующее значение, которое будет помещено в буфер. Эти две переменные взяты из нашего решения в листингах 7.1 и 7.2. Они нужны для синхронизации нескольких потоков-производителей.

Новые аргументы командной строки

17-20 Два новых аргумента командной строки указывают полное количество элементов, которые должны быть помещены в буфер, и количество потоков-производителей. 

Запуск всех потоков

21-41 Инициализируем семафоры и запускаем потоки-производители и поток-потребитель. Затем ожидается завершение работы потоков. Эта часть кода практически идентична листингу 7.1.

В листинге 10.13 приведен текст функции produce, которая выполняется каждым потоком-производителем.

Листинг 10.13. Функция, выполняемая всеми потоками-производителями

//pxsem/prodcons3.c

43 void *

44 produce(void *arg)

45 {

46  for (;;) {

47   Sem_wait(&shared.nempty); /* ожидание освобождения поля */

48   Sem_wait(&shared.mutex);

49   if (shared.nput >= nitems) {

50    Sem_post(&shared.nempty);

51    Sem_post(&shared.mutex);

52    return(NULL); /* готово */

53   }

54   shared.buff[shared.nput % NBUFF] = shared.nputval;

55   shared.nput++;

56   shared.nputval++;

57   Sem_post(&shared.mutex);

58   Sem_post(&shared.nstored); /* еще один элемент */

59   *((int *) arg) += 1;

60  }

61 }

Взаимное исключение между потоками-производителями

49-53 Отличие от листинга 10.8 в том, что цикл завершается, когда nitems объектов будет помещено в буфер всеми потоками. Обратите внимание, что потоки-производители могут получить семафор nempty в любой момент, но только один производитель может иметь семафор mutex. Это защищает переменные nput и nval от одновременного изменения несколькими производителями.

Завершение производителей

50-51 Нам нужно аккуратно обработать завершение потоков-производителей. После того как последний объект помещен в буфер, каждый поток выполняет

Sem_wait(&shared.nempty); /* ожидание пустого поля */

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

Функция consume в листинге 10.14 проверяет правильность всех записей в буфере, выводя сообщение при обнаружении ошибки.

Листинг 10.14. Функция, выполняемая потоком-потребителем

//pxsem/prodcons3.с

62 void *

63 consume(void *arg)

64 {

65  int i;

66  for (i = 0; i < nitems; i++) {

67   Sem_wait(&shared.nstored); /* ожидание помещения по крайней мере одного элемента в буфер */

68   Sem_wait(&shared.mutex);

69   if (shared.buff[i % NBUFF] != i)

70    printf("error: buff[%d] = %d\n", i, shared.buff[i % NBUFF]);

71   Sem_post(&shared.mutex);

72   Sem_post(&shared.nempty); /* еще одно пустое поле */

73  }

74  return(NULL);

75 }

Условие завершения единственного потока-потребителя звучит просто: он считает все потребленные объекты и останавливается по достижении nitems.

10.10. Несколько производителей, несколько потребителей

Следующее изменение, которое мы внесем в нашу пpoгрaммy, будет заключаться в добавлении возможности одновременной работы нескольких потребителей вместе с несколькими производителями. Есть ли смысл в наличии нескольких потребителей — зависит от приложения. Автор видел два примера, в которых использовался этот метод.

1. Пpoгрaммa преобразования IP-адресов в имена узлов. Каждый потребитель берет IP-адрес, вызывает gethostbyaddr (раздел 9.6 [24]), затем дописывает имя узла к файлу. Поскольку каждый вызов gethostbyaddr обрабатывается неопределенное время, порядок IP-адресов в буфере будет, скорее всего, отличаться от порядка имен узлов в файле, созданном потоками-потребителями. Преимущество этой схемы в параллельности выполнения вызовов gethostbyaddr (каждый из которых может работать несколько секунд) — по одному на каждый поток-потребитель.

ПРИМЕЧАНИЕ

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

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

В листинге 10.15 приведены глобальные переменные программы.

Листинг 10.15. Глобальные переменные

//pxsem/prodcons4.с

1  #include "unpipc.h"

2  #define NBUFF 10

3  #define MAXNTHREADS 100

4  int nitems, nproducers, nconsumers; /* только для чтения */

5  struct { /* общие данные производителей и потребителей */

6   int buff[NBUFF];

7   int nput; /* номер объекта: 0, 1. 2, … */

8   int nputval; /* сохраняемое в buff[] значение */

9   int nget; /* номер объекта: 0, 1, 2, … */

10  int ngetval; /* получаемое из buff[] значение */

11  sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

12 } shared;

13 void *produce(void *), *consume(void *);

Глобальные переменные и общая структура

4-12 Количество потоков-потребителей является глобальной переменной, устанавливаемой из командной строки. В структуру shared добавилось два новых поля: nget — номер следующего объекта, получаемого одним из потоков-потребителей, и ngetval — соответствующее значение.

Функция main, текст которой приведен в листинге 10.16, запускает несколько потоков-потребителей и потоков-производителей одновременно.

19-23 Новый аргумент командной строки указывает количество потоков-потребителей. Для хранения идентификаторов потоков-потребителей выделяется место под специальный массив (tid_consume), а для подсчета обработанных каждым потоком объектов выделяется массив conscount.

24-50 Создаются несколько потоков-производителей и потребителей, после чего основной поток ждет их завершения.

Листинг 10.16. Функция main для версии с несколькими производителями и потребителями

//pxsem/prodcons4.с

14 int

15 main(int argc, char **argv)

16 {

17  int i, prodcount[MAXNTHREADS], conscount[MAXNTHREADS];

18  pthread_t tid_produce[MAXNTHREADS], tid_consume[MAXNTHREADS];

19  if (argc != 4)

20   err_quit("usage: prodcons4 <#items> <#producers> <#consumers>");

21  nitems = atoi(argv[1]);

22  nproducers = min(atoi(argv[2]), MAXNTHREADS);

23  nconsumers = min(atoi(argv[3]), MAXNTHREADS);

24  /* инициализация трех семафоров */

25  Sem_init(&shared.mutex, 0, 1);

26  Sem_init(&shared.nempty, 0, NBUFF);

27  Sem_init(&shared.nstored, 0, 0);

28  /* создание производителей и потребителей */

29  Set_concurrency(nproducers + nconsumers);

30  for (i = 0; i < nproducers; i++) {

31   prodcount[i] = 0;

32   Pthread_create(&tid_produce[i], NULL, produce, &prodcount[i]);

33  }

34  for (i = 0; i < nconsumers; i++) {

35   conscount[i] = 0;

36   Pthread_create(&tid_consume[i], NULL, consume, &conscount[i]);

37  }

38  /* ожидание завершения всех производителей и потребителей */

39  for (i = 0; i < nproducers: i++) {

40   Pthread_join(tid_produce[i], NULL);

41   printf("producer count[%d] = %d\n", i, prodcount[i]);

42  }

43  for (i = 0; i < nconsumers; i++) {

44   Pthread_join(tid_consume[i], NULL);

45   printf("consumer count[%d] = %d\n", i, conscount[i]);

46  }

47  Sem_destroy(&shared.mutex);

48  Sem_destroy(&shared.nempty);

49  Sem_destroy(&shared.nstored);

50  exit(0);

51 }

Функция produce содержит одну новую строку по сравнению с листингом 10.13. В части кода, относящейся к завершению потока-производителя, появляется строка, отмеченная знаком +:

 if (shared.nput >= nitems) {

+ Sem_post(&shared.nstored); /* даем возможность потребителям завершить работу */

  Sem_post(&shared.nempty);

  Sem_post(&shared.mutex);

  return(NULL); /* готово */

 }

Снова нам нужно быть аккуратными при обработке завершения процессов-производителей и потребителей. После обработки всех объектов в буфере все потребители блокируются в вызове

Sem_wait(&shared.nstored); /* Ожидание помещения объекта в буфер */

Производителям приходится увеличивать семафор nstored для разблокирования потрeбитeлeй, чтобы они узнали, что работа завершена. Функция consume приведена в листинге 10.17. 

Листинг 10.17. Функция, выполняемая всеми потоками-потребителями

//pxsem/prodcons4.c

72 void *

73 consume(void *arg)

74 {

75  int i;

76  for (;;) {

77   Sem_wait(&shared.nstored); /* ожидание помещения объекта в буфер */

78   Sem_wait(&shared.mutex);

79   if (shared.nget >= nitems) {

80    Sem_post(&shared.nstored);

81    Sem_post(&shared.mutex);

82    return(NULL); /* готово */

83   }

84   i = shared.nget % NBUFF;

85   if (shared.buff[i] != shared.ngetval)

86    printf("error: buff[%d] = %d\n", i, shared.buff[i]);

87   shared.nget++;

88   shared.ngetval++;

89   Sem_post(&shared.mutex);

90   Sem_post(&shared.nempty); /* освобождается место для элемента */

91   *((int *) arg) += 1;

92  }

93 }

Завершение потоков-потребителей

79-83 Функция consume сравнивает nget и nitems, чтобы узнать, когда следует остановиться (аналогично функции produce). Обработав последний объект в буфере, потоки-потребители блокируются, ожидая изменения семафора nstored. Когда завершается очередной поток-потребитель, он увеличивает семафор nstored, давая возможность завершить работу другому потоку-потребителю.

10.11. Несколько буферов

Во многих программах, обрабатывающих какие-либо данные, можно встретить цикл вида

while ((n = read(fdin, buff, BUFFSIZE)) > 0) {

 /* обработка данных */

 write(fdout, buff, n);

}

Например, программы, обрабатывающие текстовые файлы, считывают строку из входного файла, выполняют с ней некоторые действия, а затем записывают строку в выходной файл. Для текстовых файлов вызовы read и write часто заменяются на функции стандартной библиотеки ввода-вывода fgets и fputs.

На рис. 10.11 изображена иллюстрация к такой схеме. Здесь функция reader считывает данные из входного файла, а функция writer записывает данные в выходной файл. Используется один буфер.

UNIX: взаимодействие процессов

Рис. 10.10. Процесс считывает данные в буфер, а потом записывает его содержимое в другой файл


UNIX: взаимодействие процессов

Рис. 10.11. Один процесс, считывающий данные в буфер и записывающий их в файл


На рис. 10.10 приведена временная диаграмма работы такой программы. Числа слева проставлены в условных единицах времени. Предполагается, что операция чтения занимает 5 единиц, записи — 7, а обработка данных между считыванием и записью требует 2 единицы времени.

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

UNIX: взаимодействие процессов

Рис. 10.12. Разделение копирования файла между двумя потоками


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

UNIX: взаимодействие процессов

Рис. 10.13. Копирование файла двумя потоками


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

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

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

UNIX: взаимодействие процессов

Рис. 10.14. Копирование файла двумя потоками с двумя буферами


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

На рис. 10.15 изображена временная диаграмма процесса с двойной буферизацией. Считывающий поток помещает данные в буфер № 1, а затем уведомляет записывающий о том, что буфер готов к обработке. Затем считывающий процесс помещает данные в буфер № 2, а записывающий берет их из буфера № 1.

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

Обратите внимание, что операции записи выполняются так быстро, как только возможно. Они разделены промежутками времени всего лишь в 2 единицы, тогда как в предыдущих примерах между ними проходило 9 единиц времени (рис. 10.10 и 10.13). Это может оказаться выгодным при работе с некоторыми устройствами типа накопителей на магнитной ленте, которые функционируют быстрее, если данные записываются с максимально возможной скоростью (это называется потоковым режимом — streaming mode).

UNIX: взаимодействие процессов

Рис. 10.15. Процесс с двойной буферизацией


Интересно, что задача с двойной буферизацией представляет собой лишь частный случай общей задачи производителей и потребителей.

Изменим нашу программу так, чтобы использовать несколько буферов. Начнем с решения из листинга 10.11, в котором использовались размещаемые в памяти семафоры. Мы получим даже не двойную буферизацию, а работу с произвольным числом буферов (задается NBUFF). В листинге 10.18 даны глобальные переменные и функция main.

Листинг 10.18. Глобальные переменные и функция main

//pxsem/mycat2.c

1  #include "unpipc.h"

2  #define NBUFF 8


3  struct { /* общие данные */

4   struct {

5    char data[BUFFSIZE]; /* буфер */

6    ssize_t n; /* объем буфера */

7   } buff[NBUFF]; /* количество буферов */

8   sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

9  } shared;

10 int fd; /* входной файл, копируемый в стандартный поток вывода */

11 void *produce(void *), *consume(void *);


12 int

13 main(int argc, char **argv)

14 {

15  pthread_t tid_produce, tid_consume;

16  if (argc != 2)

17   err_quit("usage: mycat2 <pathname>");

18  fd = Open(argv[1], O_RDONLY);

19  /* инициализация трех семафоров */

20  Sem_init(&shared.mutex, 0, 1);

21  Sem_init(&shared.nempty, 0, NBUFF);

22  Sem_init(&shared.nstored, 0, 0);

23  /* один производитель, один потребитель */

24  Set_concurrency(2);

25  Pthread_create(&tid_produce, NULL, produce, NULL); /* reader thread */

26  Pthread_create(&tid_consume, NULL, consume, NULL); /* writer thread */

27  Pthread_join(tid_produce, NULL);

28  Pthread_join(tid_consume, NULL);

29  Sem_destroy(&shared.mutex);

30  Sem_destroy(&shared.nempty);

31  Sem_destroy(&shared.nstored);

32  exit(0);

33 }

Объявление нескольких буферов

2-9 Структура shared содержит массив структур buff, которые состоят из буфера и его счетчика. Мы создаем NBUFF таких буферов.

Открытие входного файла

18 Аргумент командной строки интерпретируется как имя файла, который копируется в стандартный поток вывода.

В листинге 10.19 приведен текст функций produce и consume.

Листинг 10.19. Функции produce и consume

//pxsem/mycat2.c

34 void *

35 produce(void *arg)

36 {

37  int i;

38  for (i = 0;;) {

39   Sem_wait(&shared.nempty); /* Ожидание освобождения места в буфере */

40   Sem_wait(&shared.mutex);

41   /* критическая область */

42   Sem_post(&shared.mutex);

43   shared.buff[i].n = Read(fd, shared.buff[i].data, BUFFSIZE);

44   if (shared.buff[i].n == 0) {

45    Sem_post(&shared.nstored); /* еще один объект */

46    return(NULL);

47   }

48   if (++i >= NBUFF)

49    i = 0; /* кольцевой буфер */

50   Sem_post(&shared.nstored); /* еще один объект */

51  }

52 }


53 void *

54 consume(void *arg)

55 {

56  int i;

57  for (i = 0;;) {

58   Sem_wait(&shared.nstored); /* ожидание появления объекта для обработки */

59   Sem_wait(&shared.mutex);

60   /* критическая область */

61   Sem_post(&shared.mutex);

62   if (shared.buff[i].n == 0)

63    return(NULL);

64   Write(STDOUT_FILENO, shared.buff[i].data, shared.buff[i].n);

65   if (++i >= NBUFF)

66    i=0; /* кольцевой буфер */

67   Sem_post(&shared.nempty); /* освободилось место для объекта */

68  }

69 }

Пустая критическая область

40-42 Критическая область, защищаемая семафором mutex, в данном примере пуста. Если бы буферы данных представляли собой связный список, здесь мы могли бы удалять буфер из списка, не конфликтуя при этом с производителем. Но в нашем примере, где мы просто переходим к следующему буферу с единственным потоком-производителем, защищать нам просто нечего. Тем не менее мы оставляем операции установки и снятия блокировки, подчеркивая, что они могут потребоваться в новых версиях кода.

Считывание данных и увеличение семафора nstored

43-49 Каждый раз, когда производитель получает пустой буфер, он вызывает функцию read. При возвращении из read увеличивается семафор nstored, уведомляя потребителя о том, что буфер готов. При возвращении функцией read значения 0 (конец файла) семафор увеличивается, а производитель завершает работу.

Поток-потребитель

57-68 Поток-потребитель записывает содержимое буферов в стандартный поток вывода. Буфер, содержащий нулевой объем данных, обозначает конец файла. Как и в потоке-производителе, критическая область, защищенная семафором mutex, пуста.

ПРИМЕЧАНИЕ

В разделе 22.3 книги [24] мы разработали пример с несколькими буферами. В этом примере производителем был обработчик сигнала SIGIO, а потребитель представлял собой основной цикл обработки (функцию dg_echo). Разделяемой переменной был счетчик nqueue. Потребитель блокировал сигнал SIGIO на время проверки или изменения счетчика.

10.12. Использование семафоров несколькими процессами

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

ПРИМЕЧАНИЕ

Эти правила аналогичны требованиям к разделению взаимного исключения, условной переменной или блокировки чтения-записи между процессами: средство синхронизации (переменная типа pthread_mutex_t, pthread_cond_t или pthread_rwlock_t) должно находиться в разделяемой памяти и инициализироваться с атрибутом PTHREAD_PROCESS SHARED.

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

Что произойдет, если мы вызовем функцию sem_open, возвращающую указатель на тип sem_t, а затем вызовем fork? В описании функции fork в стандарте Posix.1 говорится, что «все открытые родительским процессом семафоры будут открыты и в дочернем процессе». Это о