/
Author: Рочкинд М.Дж.
Tags: системное программное обеспечение компьютерные технологии программирование операционная система unix
ISBN: 5-94157-749-4
Year: 2005
Text
Advanced UNIX Programming
Second Edition
Marc J. Rochkind
Addison-Wesley
Программирование для UNIX
Второе издание
Марк Дж. Рочкинд
«Русская Редакция» *БХВ-Петербург»
2005
VJK 004.45
ББК 32.973.26-018.2
P80
РочкиндМ.
Р80 Программирование для UNIX. 2-е изд. перераб. и доп. — Пер. с
англ. — М.: Издательско-торговый дом «Русская Редакция»; СПб.: БХВ-
Петербург, 2005. — 704 стр..- ил.
ISBN 5-94157-749-4 («БХВ-Петербург»)
ISBN 5-7502-0261-5 («Русская Редакция»)
Практическое руководство, написанное одним из пионеров
программирования для UNIX Марком Дж. Рочкиндом, поможет разработчикам решить
свои задачи. Автор подробно, на примерах, описывает самые полезные
системные вызовы UNIX. Также описаны особенности системных вызовов для
различных реализаций UNIX и UNIX-подобных систем, что поможет
создавать универсальные портируемые приложения. Рассматривается
межпроцессное и сетевое взаимодействие, терминальный и файловый ввод-
вывод, управление сигналами, многопоточность, работа в реальном времени
и многое другое.
Перевод книги «Advanced UNIX Programming», Second Edition, ISBN 0131411543, автор Rochkind,
Marc J., изданной на английском языке Pearson Education, Inc, publishing as Addison-Wesley
Professional, осуществлен на основании соответствующего лицензионного договора.
Все права защищены. Никакая часть этой книги не может быть воспроизведена или передана в
любой форме и любыми средствами, электронными или механическими, включая
фотокопирование, запись на носители, а также с помощью устройств хранения и обработки данных, без
разрешения Pearson Education, Inc.
Перевод с английского под общей редакцией В. Г. Вшивцева
Подписано в печать 16.05.05.
Формат 70х100'/|0. Печать офсетная. Усл. печ. л. 56,76.
Тираж 4500 экз. Заказ № 4030
«БХВ-Петербург», 194354, Санкт-Петербург, ул. Есенина, 5Б.
Санитарно-эпидемиологическое заключение на продукцию
№ 77.99.02.953.Д.006421.11.04 от 11.11.2004 г. выдано Федеральной службой
но надзору в сфере защиты прав потребителей и благополучия человека.
Отпечатано с готовых диапозитивов
в ГУП «Типография "Наука"»
199034, Санкт-Петербург, 9 линия, 12
ISBN 013-141154-3 (англ.)
ISBN 5-7502-0261-5
(«Русская редакция»)
ISBN 5-94157-749-4
(«БХВ-Петербург»)
© Оригинальное издание на английском языке,
Pearson Education, Inc, 2004
© Перевод на русский язык, оформление, издательско-
торговый дом «Русская Редакция», 2005
© Издание, ООО «БХВ-Петербург», 2005
ш % «;'ь ШчВМЩ^Щ
ПНЯ
«■га
пинии
Содержание
Эюеаакловие VIII
шжк! 1 Фундаментальные концепции 1
j-'раткий обзор систем UNIX и Linux 1
. Ьгрсии UNIX 15
Использование системных вызовов 19
Обработка ошибок 23
Стандарты UNIX 37
"• ниверсальный заголовочный файл 53
Слта и время 5 5
j коде примеров 66
"слезные ресурсы 67
кша 2 Базовые операции файлового ввода-вывода 69
Введение в операции файлового ввода-вывода 69
Файловые дескрипторы и системная таблица файлов 70
- - Идентификаторы наборов прав доступа 73
Системные вызовы open и creat 74
. : Системный вызов umask 84
Системный вызов unlink 84
Создание временных файлов 86
Текущая позиция в файле и флаг 0_APPEND 88
Системный вызов write 90
- . Системный вызов read 94
- . Системный вызов close 95
. 1 Буферизация ввода-вывода в приложениях 96
- .; Системный вызов lseek 103
- - Системные вызовы pread и pwrite 106
- ; Системные вызовы readv и writev 108
. : Синхронизированный ввод-вывод 111
. " Системные вызовы truncate и ftruncate 117
:-i*a 3 Дополнительные операции файлового ввода-вывода 119
Введение 119
Специальные файлы дисковых устройств и файловые системы 119
Жесткие и символические ссылки 133
Полные имена объектов файловой системы 140
Получение и отображение метаданных файла 142
Каталоги 154
VI Содержание
3.7 Изменение индексного узла 177
3.8 Дополнительные вызовы для работы с файлами 180
3.9 Асинхронный ввод-вывод 184
Глава 4 Терминальный ввод-вывод 197
4.1 Введение 197
4.2 Чтение данных с терминала 198
4.3 Сеансы и группы процессов (задания) 218
4.4 Системный вызов ioctl 226
4.5 Установка атрибутов терминала 226
4.6 Дополнительные системные вызовы, служащие для управления
терминалом 239
4.7 Системные вызовы, служащие для идентификации терминала 241.
4.8 Полноэкранные приложения 243
4.9 Ввод-вывод с использованием STREAMS 248
4.10 Псевдотерминалы 249
Глава 5 Процессы и потоки ............... . 269
5.1 Введение 269
5.2 Среда окружения 269
5.3 Системный вызов exec 276
5.4 Командная оболочка (версия 1) 284
5.5 Системный вызов fork 288
5.6 Командная оболочка (версия 2) 292
5.7 Завершение процесса и системные вызовы exit 294
5.8 Системные вызовы wait, waitpid и waitid 296
5.9 Сигналы, завершение и ожидание 305
5.10 Командная оболочка (версия 3) 306
5.11 Получение идентификаторов пользователя и группы 307
5.12 Изменение идентификаторов пользователя и группы 308
5.13 Получение идентификатора процесса 311
5.14 Системный вызов enroot 311
5.15 Получение и изменение приоритета 312
5.16 Ограничения для процессов 314
5.17 Введение в потоки 321
5.18 Проблема блокирования 342
Глава 6 Базовые механизмы межпроцессного взаимодействия 353
6.1. Введение 353
6.2 Каналы 354
6.3 Системные вызовы dup и dup2 ЗбЗ
6.4 Настоящая командная оболочка 368
6.5 Двунаправленное взаимодействие с использованием
однонаправленных каналов 382
6.6 Двунаправленное взаимодействие с использованием
двунаправленных каналов 391
Содержание
VII
Глава 7 Улучшенные механизмы взаимодействия процессов 397
~.1 Введение 397
~.2 Именованные каналы (FIFO) 398
~ ? Упрощенный интерфейс обмена сообщениями 406
".4 System V IPC .. 420
~.5 Очереди сообщений System V 425
~.6 POSIXIPC 433
Очереди сообщений POSIX 436
"8 О семафорах 448
~ 9 Семафоры System V 450
" 10 Семафоры POSIX 4б0
" 11 Блокировка файлов 4б7
"12 О общей памяти 479
~ 13 Общая память System V 479
" 14 Общая память POSIX 495
" 15 Сравнение производительности 506
Гтава 8 Сетевое взаимодействие и сокеты 509
5 1 Введение в сокеты 510
S 2 Адресация сокетов 524
5 3 Параметры сокетов 534
S 4 Упрощенный интерфейс сокетов 540
5 5 Реализация SMI с сокетами 553
5 6 Сокеты, не требующие создания соединения 557
3" Внеочередные данные 567
? 8 Получение сведений о сетевом окружении 5б8
5 9 Прочие системные вызовы 583
5 10 Вопросы производительности 588
Г хава 9 Сигналы и таймеры 591
-" 1 Основы сигналов 591
г 2 Ожидание сигнала 614
" 3 Прочие системные вызовы для работы с сигналами 624
г 4 Системные вызовы, не рекомендуемые к использованию 625
г S Сигналы реального времени 628
-" 6 Далекие, или глобальные переходы 638
Часы и таймеры 641
Приложение А Атрибуты процессов 659
Приложение Б Ux: оболочка C++ для стандартных функций UNIX ........ 664
Приложение В Jtux: интерфейс Java/Jython для стандартных
функций UNIX 667
Приложение Г Список функций по алфавиту и по категориям .............. 673
сылки 691
Предисловие
Эта книга представляет собой обновление издания 1985 года, написанное для того,
чтобы отразить несколько изменений, произошедших за последние 18 лет. Хотя,
пожалуй, «несколько» — не совсем правильное слово. Равно как и «обновление».
В действительности эта книга почти полностью написана заново. В первом издании
были описаны около 70 системных вызовов, в этом — около 300. Никакие из
стандартов и версий UNIX, обсуждаемых в этой книге, — POSIX, Solaris, Linux, FreeBSD и
Darwin (Mac OS X) — в 1985 году даже не существовали. Однако несколько
предложений из предисловия к изданию 1985 года я могу оставить почти неизменными:
«Эта книга посвящена системным вызовам UNIX — интерфейсу между ядром
UNIX и использующими его возможности пользовательскими программами.
Если вы используете только команд1гую оболочку, текстовые редакторы и другие
приложения, знать системные вызовы необязательно, но программистам эти
знания необходимы. Системные вызовы обеспечивают единственный способ
доступа к механизмам ядра, таким как файловая система, средства реализации
многозадачности и примитивы межпроцессного взаимодействия.
Системные вызовы определяют саму суть UNIX. Все остальное — функции и
команды — создано на их основе. Хотя своей популярностью UNIX во
многом обязана высокоуровневым программам, их с тем же успехом можно
реализовать для любой современной ОС. Когда кто-то говорит о UNIX как об
элегантной, простой, эффективной, надежной и портируемой ОС, он имеет
в виду не команды (которые не всегда соответствуют указанным
характеристикам), а ядро».
Все это верно и сейчас, только интерфейс ядра уже не элегантен и не прост.
Программирование для UNIX за последние два десятилетия разделилось на множество
направлений, a Open Group — главный комитет по стандартизации — охватил почти
все, что можно (в общей сложности 1108 функций), в результате чего этот
интерфейс стал громоздким, несогласованным, избыточным, непонятным и
подверженным ошибкам. Однако он все еще эффективен, надежен и портируем, и именно
поэтому UNIX и юниксоподобные системы пользуются таким успехом. На самом
деле интерфейс системных вызовов UNIX — это единственный широко
распространенный портируемый интерфейс, который у нас есть, и скорее всего, при нашей
жизни ситуация не изменится.
Как справочник Yellow Pages не гарантирует, что вы найдете хороший ресторан или
отель, так и полная документация — это еще не все, что нужно для успешного про-
Предисловие IX
граммирования. Вам нужно руководство, в котором объясняется, что хорошо и что
плохо, а не просто описываются все возможности, имеющиеся в вашем
распоряжении. Это и есть цель данной книги, и именно этим она отличается от
большинства других книг по программированию для UNIX. Я не только объясню, как
использовать системные вызовы, но и расскажу, каких вызовов следует избегать в силу
того, что они являются ненужными, устаревшими, некорректными или просто
неудачно реализованными.
Как я решил, что включить в книгу? Я начал с 1108 функций, определенных в
третьей версии Single UNIX Specification, и отбросил примерно 590 функций
стандартного С и других библиотечных функций, не относящихся к уровню интерфейса
ядра, около 90 функций POSIX Threads (оставив несколько самых важных), около
25 функций учета и регистрации, где-то 50 функций трассировки, около 15
странных и устаревших функций и около 40 функций, служащих для планирования и
решения других редких задач. В итоге осталось ровно 307 функций (см. список в
приложении Г). Не могу сказать, что все эти 307 функций хороши — некоторые из
них бесполезны и даже опасны — но именно о них вам нужно знать.
Мы не будем обсуждать реализацию ядра (кроме некоторых основ), написание
драйверов устройств, программирование на С (по крайней мере,
непосредственно), команды UNIX (командные оболочки, vi, emacs и т. д.) и системное
администрирование.
Эта книга включает девять глав. Советую вам сначала полностью прочитать главу 1,
а остальные материалы вы можете изучать в каком угодно порядке. Чтобы вы не
заблудились, я включил в книгу много перекрестных ссылок.
Как и первое издание, эта новая книга включает тысячи строк кода примеров,
которые большей частью взяты из реалистичных, пусть и упрощенных приложений,
таких как командная оболочка, полноэкранная система меню, Web-сервер и
программа записи и воспроизведения вывода команд в реальном времени. Все
примеры написаны на С, но в приложениях Б и В вы найдете информацию об интерфейсах,
позволяющих программировать на C++, Java и Jython (вариант Python).
Текст книги и код примеров — это просто ресурсы: научиться программировать можно
только программируя. Чтобы вам было чем заняться, каждую главу я дополнил
упражнениями. По сложности они варьируются от вопросов, на которые можно
ответить несколькими предложениями, до простых задач и серьезных проектов.
Для проведения исследований и тестирования примеров я использовал четыре
системы UNIX: Solaris 8, SuSE Linux 8 (ядро 2.4), FreeBSD 4.6 и Darwin (ядро Mac OS
X) 6.8. Исходный код я хранил на FreeBSD, монтируя его на других системах с
использованием NFS или Samba.'
Эти четыре системы работали на моих старых ПК и на Макинтоше, который я купил на eBay
за 200 долларов. Позднее я удалил SuSE Linux и установил на соответствующий компьютер
RedHat 9.
X Предисловие
Я редактировал код на компьютере с ОС Windows, используя TextPad, и обращался
к четырем тестовым системам при помощи Telnet или SSH (PuTTY) или X Window
System (XFree86 и Cygwin). Как оказалось, работать с текстовым редактором и
четырьмя открытыми окнами Telnet/SSH/Xterm, отображаемыми на одном экране,
очень удобно, потому что при этом на написание кода и его проверку на четырех
системах уходит всего лишь несколько минут. Кроме того, обычно я держал
открытыми два окна браузера с загруженными сайтами Single UNIX Specification и Google
и еще одно окно Microsoft Word, в котором я редактировал текст книги. За
исключением Word, который очень плохо подходит для работы с крупными
документами (зависания, смешанные стили, неэффективные средства обработки
перекрестных ссылок и составления документации), все эти инструменты работали прекрасно*.
Для извлечения примеров кода, поддержания БД системных вызовов и решения
некоторых других задач я использовал Perl и Python.
Весь код примеров (открытый и свободно распространяемый), список ошибок и
другую информацию вы можете найти на Web-сайте книги по адресу ivww.base-
path.com/aup.
Я хотел бы поблагодарить людей, выполнивших обзор черновиков или оказавших
мне иную техническую помощь: это Том Каргилл (Tom Cargill), Джефф Клэр (Geoff
Clare), Эндрю Гирт (Andrew Gierth), Эндрю Джози (Andrew Josey), Брайан Керни-
ган (Brian Kernighan), Барри Марголин (Barry Margolin), Крейг Партридж (Craig
Partridge) и Дэвид Шварц (David Schwartz). Особую благодарность выражаю
одному преданному педантичному рецензенту, который пожелал остаться неизвестным.
Разумеется, никто из них не повинен в ошибках, которые вы, возможно, найдете
— все они на моей совести.
Я также хочу поблагодарить редактора Мэри Франц (Mary Franz), которая
предложила мне этот проект примерно год назад. К счастью, она застала меня в тот
момент, когда я хотел поглубже изучить Linux и снова начал ощущать на себе чары
UNIX. Что-то подобное было в 1972 году...
Надеюсь, что эта книга вам понравится. Если вы сочтете что-то неверным, будете
портировать код на какую-то новую систему или просто захотите поделиться
своими мыслями, пишите мне по адресу aup@basepath.com.
Марк Дж. Рочкинд
Боулдер, Колорадо
Апрель 2004 г.
Я мог бы использовать любую систему в качестве базовой. Я выбрал Windows потому, что к
компьютеру с Windows подключен мой большой жидкокристаллический монитор, и
потому, что мне нравится TextPad {ivivwxextpad.com). Информацию о PuTTY можно найти по адресу
wwiv.chiarkgreenend.org.uk/~sgtatham/putty/. (Если эта ссылка не работает, поищите «PuTTY»
в Google).
Кш шш ШШШ ШШШ шШШ I
Фундаментальные
концепции
1.1 Краткий обзор систем UNIX и Linux
В этом разделе мы вкратце рассмотрим возможности, обеспечиваемые ядрами UNIX
и Linux. Я не буду затрагивать пользовательские программы (команды), которые
обычно поставляются с UNIX, такие как is, vi и grep. Внутренние механизмы ядра
(например, реализацию файловой системы) мы также подробно обсуждать не
будем. Все, что я буду говорить о UNIX, относится и к Linux, если только не будет указано
обратное.
Я включил в книгу этот обзор, чтобы вы могли повторить уже известный вам
материал. Таким образом, я полагаю, что вы примерно представляете значение таких
терминов, как процесс, и буду иногда использовать их до определения. Если
многое покажется вам новым, постарайтесь перед чтением этой книги лучше
ознакомиться с UNIX (если вы не знаете, что такое процесс, вам определенно нужна
подготовка!). Есть много хороших книг о UNIX для начинающих, но здесь я назову только
две: это «The UNIX Programming Environment» [Kerl984] и «UNIX for the Impatient»
[Abrl996]* (отличным введением в нашу тему является глава 2).
1.1.1 Файлы
Есть несколько типов файлов UNIX обычные файлы, каталоги, символические связи,
специальные файлы, именованные каналы (FIFO) и сокеты. В этом разделе будут
представлены первые четыре, а о последних двух я расскажу в разделе 1.1.7.
1.1.1.1 Обычные файлы
Обычный файл (regular file) содержит байты данных, организованные в
одномерный массив. Любой байт и любую последовательность байтов можно прочитать и
записать. Чтение и запись начинаются с байта, определяемого смещением в файле
file offset), которому можно присвоить любое значение (даже превышающее
размер файла). Обычные файлы хранятся на диске.
Список литературы приведен в конце книги.
2 ГЛАВА 1 Фундаментальные концепции
Вставить байты в середину файла или удалить их из середины невозможно. По мере
записи байтов в конец файла он становится больше — каждый раз на один байт.
Удаляя байты или добавляя нули, файл можно уменьшить или увеличить до
любого размера.
Несколько процессов могут читать и записывать один и тот же файл
одновременно. Получаемые при этом результаты зависят от очередности отдельных запросов
ввода-вывода и в целом непредсказуемы. Для поддержания порядка используются
механизмы блокировки файлов и семафоры — общесистемные флаги, которые
процессы могут проверять и устанавливать (см. раздел 1.1.7).
Файлы не имеют имен — они имеют номера, называемые индексами (i-numbers).
Индекс относится к массиву индексных узлов (i-nodes), хранимому перед каждой
областью диска с файловой системой UNIX. Каждый индексный узел содержит
важную информацию об одном файле. Интересно, что эта информация не включает
ни имени, ни байтов данных. Она включает следующее: тип файла (обычный,
каталог, сокет и т. д.), число связей (это я объясню чуть ниже), пользовательский и
групповой идентификаторы владельца, три набора значений, определяющих
права доступа к файлу для владельца, группы и других пользователей, размер файла в
байтах, время последнего обращения к файлу, время последнего изменения файла,
время последнего изменения статуса файла (т. е. самого индексного узла) и, конечно,
указатели на блоки диска, в которых хранится содержимое файла.
1.1.1.2 Каталоги и символические связи
Обращаться к файлам по индексным дескрипторам неудобно, поэтому системы UNIX
поддерживают каталоги (directories), позволяющие использовать вместо этого
имена. На практике каталог почти всегда используется для доступа к файлу.
С концептуальной точки зрения каждый каталог является таблицей, включающей
два столбца: в одном столбце хранятся имена, а во втором — соответствующие им
индексы. Пара «имя/индекс» называется связью {link). Когда ядру UNIX
приказывают обратиться к файлу по имени, оно автоматически ищет в каталоге нужный
индекс. Затем оно обращается к соответствующему индексному узлу, содержащему более
подробную информацию о файле (например, права доступа). Если ядру нужны сами
данные, оно изучает индексный узел и узнает, где именно на диске они находятся.
Каталог очень похож на обычный файл: он имеет индексный узел и содержит
данные. Таким образом, индексный узел, соответствующий конкретному имени в
каталоге, может быть индексный узлом другого каталога. Благодаря этому
пользователи UNIX могут организовывать файлы в иерархическую структуру. Если вы
вводите путь — такой как memo/july/smith — ядро получает индексный узел текущего
каталога (current directory) для нахождения его данных, находит в этих данных
имя memo, получает соответствующий ему индекс, обращается к его индексному узлу
с целью получения данных каталога memo, находит среди них j uly, получает
соответствующий индекс, обращается к его индексному узлу для нахождения данных
каталога july, находит имя smith и, наконец, добирается до ассоциированного с ним
индексного узла.
ГЛАВА 1 Фундаментальные концепции 3
Как ядро узнает, с какого элемента нужно начать, если оно имеет дело с
относительным путем (путем, который начинается с текущего каталога)? Оно просто следит за
индексом текущего каталога каждого процесса. Когда процесс изменяет свой
текущий каталог, он должен указать путь к новому каталогу. Этот путь позволяет
определить индекс, который и становится новым индексом текущего каталога.
Абсолютный путь предваряется символом / и начинается с корневого каталога. Ядро
просто резервирует для корневого каталога индекс (скажем, 2) — это выполняется
при создании файловой системы. Существует системный вызов, изменяющий
корневой каталог процесса (на каталог с индексом, отличающимся от 2).
Так как каталоги используются непосредственно ядром (это один из тех редких
случаев, когда ядро интересуется содержимым файлов) и так как некорректный
каталог вполне может привести к краху всей системы UNIX, программе (даже
выполняемой суперпользователем) не дозволяется записывать данные в каталоги как
в обычные файлы. Вместо этого для работы с каталогами предусмотрены
специальные системные вызовы. Что касается записи каталогов, то программа может лишь
добавить связь и удалить ее.
На один индекс могут ссылаться несколько связей, определенных в одном или разных
каталогах — это означает, что файл может иметь более одного имени. При
доступе к файлу с использованием конкретного пути нет никакой неоднозначности, так
как будет найден только один индекс. Этот же индекс может быть обнаружен и при
использовании другого пути, но это не имеет значения. Если бы этим все и
ограничилось, при удалении связи из каталога системе не сразу становилось бы ясно,
можно ли ей удалить индексный узел и связанные с ним данные, поэтому
индексный узел содержит счетчик связей. При удалении связи просто уменьшается
счетчик связей индексного узла; когда счетчик достигает нуля, ядро удаляет файл.
Никакой аспект архитектуры UNIX не запрещает иметь несколько связей,
ссылающихся на один каталог, однако это осложняет программирование команд,
сканирующих всю файловую систему, поэтому большинство ядер это не допускают.
Множественные ссылки на файл с использованием индексов работают только в том
случае, если связи относятся к той же файловой системе, так как индексы уникальны
только в пределах файловой системы. Этот недостаток устраняют символические
связи (symbolic links) — в этом случае путь к целевом)' файлу помещается в раздел
данных действительного файла. Это более сложный и ресурсоемкий подход, чем
создание второй связи в каком-то каталоге, но в то же время и более универсальный. Для
работы с файлами символических ссылок служат специальные системные вызовы.
1.1.1.3 Специальные файлы
Специальным файлом обычно является некоторое устройство (например
дисковод CD-ROM или канал связи).*
' Именованные каналы также иногда считаются специальными файлами, но я решил
выделить их в отдельную категорию.
Специальные файлы устройств делятся на две основных категории: блочные и
символьные. Блочные специальные файлы соответствуют конкретной модели:
устройство содержит массив блоков фиксированного размера (например, 4096 байтов),
а для ускорения ввода-вывода используется пул буферов ядра, играющий роль кэша.
Символьные специальные файлы не определяются вообще никакими правилами. Они
позволяют выполнять ввод-вывод как совсем маленькими блоками (символами), так
и очень большими (дорожками диска); эти файлы слишком разнородны, чтобы в
их случае можно было использовать буферный кэш.
Одному физическому устройству могут соответствовать и блочные, и символьные
специальные файлы — например, обычно это верно для дисков. Код файловой
системы, реализованный в ядре, обращается к файлам и каталогам посредством
блочного специального файла, чтобы извлечь выгоду из использования
буферного кэша. Однако приложениям, к которым предъявляются повышенные требования,
нужен механизм более прямого доступа. Например, менеджер БД может
полностью обойти файловую систему и обратиться к диску (но не к той области,
которая используется файловой системой) при помощи символьного специального
файла. Большинство систем UNIX предоставляют для этой цели символьный
специальный файл, позволяющий передавать данные непосредственно между
адресным пространством процесса и диском с использованием прямого доступа к
памяти (direct memory access, DMA) — быстродействие благодаря этому может
возрастать на порядки. Другим достоинством этого подхода является более надежное
обнаружение ошибок, так как при наличии дополнительного звена — буферного
кэша — реализовать механизм обнаружения ошибок сложнее.
Специальный файл имеет индексный узел, но он не указывает ни на какие данные
на диске. Вместо этого индексный узел содержит номер устройства — индекс в
таблице, которую ядро использует для нахождения набора функций, называемого
драйвером устройства.
При выполнении системного вызова, делающего что-то со специальным файлом,
вызывается соответствующая функция драйвера устройства. Дальше все зависит от
драйвера устройства; так как драйвер работает в режиме ядра, а не
пользовательском режиме, он может обратиться (и, возможно, изменить) к любой части ядра, к
любому пользовательскому процессу и любым регистрам и областям памяти
самого компьютера. Добавить в ядро новые драйверы устройств относительно легко,
благодаря чему вы можете сделать много интересного, не ограничиваясь простым
взаимодействием с новыми типами устройств ввода-вывода. Это самый
популярный способ заставить UNIX выполнить что-то, о чем ее разработчики никогда не
помышляли. Рассматривайте его как одобренный механизм реализации чего-то
необычного.
1.1.2 Программы, процессы и потоки
Программа — это набор команд и данных, который хранится в обычном файле на
диске. В индексном узле этот файл отмечен как исполняемый, а содержимое фай-
ГЛАВА 1 Фундаментальные концепции 5
ла организовано согласно определенным ядром правилам (еще один случай, когда
ядру небезразлично содержимое файла).
Программисты могут создавать исполняемые файлы так, как им
заблагорассудится: если содержимое файла соответствует правилам и файл отмечен как
исполняемый, программу можно выполнить. На практике обычно имеет место следующее:
сначала исходный код программы, написанный на каком-то языке
программирования (скажем, С или C++), сохраняется в обычном файле, который часто
называют текстовым файлом, потому что он содержит строки текста. Затем создается
другой обычный файл, называемый объектным файлам — он содержит машинный
код, полученный в результате преобразования исходной программы. Для выполнения
этого преобразования используется компилятор или ассемблер (которые сами
являются программами). Если в объектном файле имеются все нужные функции, он
отмечается как исполняемый и может быть выполнен как есть. В противном
случае разработчик должен с помощью компоновщика (иногда называемого в мире
UNIX «загрузчиком») связать объектный файл с другими объектными файлами,
которые могут быть организованы в библиотеки. Если компоновщик сможет
обнаружить все, что ему нужно, он создаст исполняемый файл.*
Чтобы запустить программу, ядро сначала должно создать новый процесс — среду,
в которой выполняется программа. Процесс состоит из трех сегментов: сегмента
команд," сегмента пользовательских данных и сегмента системных данных.
Программа используется для инициализации сегментов команд и пользовательских
данных. После этой инициализации процесс начинает все сильнее отличаться от
выполняемой в нем программы — чтобы подтвердить это, достаточно вспомнить,
что программисты изменяют данные и, в редких случаях, сами команды. Кроме того,
процесс может получать в свое распоряжение дополнительную память, открывать
файлы и приобретать другие ресурсы, отсутствующие в программе.
Пока процесс выполняется, ядро следит за его потоками — отдельными путями
выполнения команд, которые потенциально могут осуществлять чтение и запись одних
и тех же данных процесса (однако каждый поток имеет свой стек). Приступив к
написанию программы, вы имеете в своем распоряжении только один поток, пока
не создадите другой с помощью специального системного вызова. Таким образом,
начинающие программисты могут считать, что процесс является однопоточным.'"
' Интерпретируемые языки, такие как Java, Perl, Python и языки командных оболочек,
основаны на другой модели. Для таких языков исполняющей средой является интерпретатор, а
программа, пусть даже она компилируется в тот или иной промежуточный код,
представляет собой данные, обрабатываемые интерпретатором. Ядро UNIX не взаимодействует с
интерпретируемыми программами — клиентом ядра является интерпретатор.
~ На жаргоне UNIX сегмент команд называется «текстовым сегментом», но я буду избегать этого
не совсем адекватного термина.
"~ Не каждая версия UNIX поддерживает множественные потоки. Эта возможность
предусмотрена необязательной спецификацией, называемой POSIX Threads, или «pthreads», и была
представлена в середине 1990-х годов. Более подробно я расскажу о POSIX в разделе 1.5, а о
потоках — r гпяир S
6 ГЛАВА 1 Фундаментальные концепции
Используя в качестве источника одну программу, можно создать и инициализировать
несколько одновременно выполняемых процессов, однако между ними не будет
никакого функционального отношения. Ядро может сэкономить память, сделав сегмент
команд этих процессов общим, но сами процессы не могут узнать об этом. В то же
время потоки одного процесса связаны сильным функциональным отношением.
Системные данные процесса включают такие атрибуты, как текущий каталог,
дескрипторы открытых файлов, использованное время процессора и т. д. Процесс не
может читать или изменять свои системные данные непосредственно, так как они
находятся вне его адресного пространства. Вместо этого для чтения и изменения
атрибутов используются различные системные вызовы.
Выполняемый процесс может поручить ядру создать другой процесс и стать
родителем нового дочернего процесса. Дочерний процесс наследует большинство
атрибутов системных данных родителя. Например, если родительскому процессу
соответствуют какие-то открытые файлы, для дочернего процесса эти файлы
также будут открыты. Преемственность такого рода — фундаментальный принцип UNIX,
и мы много раз в этом убедимся. Это отличается от создания потоком нового
потока. Потоки одного процесса в большинстве отношений равны и ничего не
наследуют друг от друга. Все потоки могут обращаться ко всем данным и ресурсам —
не к копиям.
1.1.3 Сигналы
Ядро может отправить процессу сигнал. Сигнал может быть отправлен процессу
ядром, самим собой, другим процессом или пользователем.
Примером сигнала, исходящего из ядра, может служить сигнал нарушения
сегментации, отправляемый процессу, когда он пытается обратиться к памяти,
расположенной вне его адресного пространства. В качестве примера сигнала,
отправляемого процессом самому себе, можно привести сигнал, отправляемый функцией abort
для завершения процесса с созданием дампа памяти. Примером сигнала,
отправляемого одним процессом другому, является сигнал завершения, отправляемый
одним из нескольких связанных процессов, когда он решает завершить целое
семейство процессов. Наконец, примером пользовательского сигнала может служить
сигнал прерывания, отправляемый всем созданным пользователем процессам при
нажатии Ctrl-c.
Существует примерно 28 типов сигналов (это число немного колеблется в
зависимости от версии UNIX). Все сигналы кроме двух — kill и stop — позволяют
процессу контролировать происходящее при получении сигнала. Процесс может
принять действие по умолчанию, которое обычно приводит к завершению процесса,
он может проигнорировать сигнал или перехватить сигнал и выполнить функцию,
называемую обработчиком сигнала. Тип сигнала (скажем, SIGALRM) передается в
обработчик как аргумент. Однако нет никакого непосредственного способа, позво-
ГЛАВА 1 Фундаментальные концепции 7
ляющего обработчику узнать, кто отправил сигнал.* Когда обработчик сигнала
возвращает управление, выполнение процесса возобновляется с точки прерывания.
Два сигнала ядром не используются. Приложение может использовать их для
собственных целей.
1.1.4 Идентификаторы процессов, группы процессов
и сеансы
Каждому процессу соответствует положительное число, которое называется
идентификатором процесса и всегда является уникальным. У каждого процесса кроме
одного есть родитель.
Системные данные процесса включают также идентификатор его родительского
процесса. Если из-за завершения родителя дочерний процесс становится сиротой,
его идентификатору родительского процесса присваивается значение 1 (или
некоторое другое фиксированное число). Это значение идентификатора процесса
инициализации (init), который создается во время загрузки и является
прародителем всех других процессов. Иначе говоря, процесс инициализации «усыновляет»
всех сирот.
Иногда программисты решают реализовать какую-то подсистему как группу
родственных процессов, а не как один процесс. Ядро UNIX позволяет организовать эти
родственные процессы в группу процессов. Группы процессов далее организуются
в сеансы (sessions).
Одним из членов сеанса является лидер сеанса, а одним из членов каждой группы
процессов — лидер группы процессов. Чтобы каждый процесс мог узнать свое
место в иерархии, UNIX следит за идентификаторами лидера его группы процессов и
лидера его сеанса.
Ядро предоставляет системный вызов, позволяющий отправить сигнал каждому члену
группы процессов. Обычно это используется для завершения всех процессов группы,
однако широковещательно можно отправить любой сигнал.
Как правило, командные оболочки UNIX создают один сеанс на учетную запись. Для
процессов, формирующих конвейер, они обычно создают отдельную группу
процессов; в этом контексте группа процессов также называется заданием (job).
Командным оболочкам нравится работать именно так, но на самом деле ядро
предоставляет более гибкие возможности, так что для одной учетной записи можно
созвать любое число сеансов, а в каждом сеансе группы процессов могут быть
сформированы как угодно, лишь бы поддерживалась иерархия.
Это работает следующим образом: любой процесс может выйти из сеанса и стать
r-цером собственного сеанса (одной группы процессов, содержащей только один
Это верно для 28 базовых сигналов. Необязательная спецификация Realtime Signals
включает некоторые дополнительные сигналы, для которых доступна эта и кое-какая другая
информация. Более подробную информацию об этом можно найти в главе 9.
8 ГЛАВА 1 Фундаментальные концепции
процесс). Затем он может создать дочерние процессы, сформировав новую
полноценную группу процессов. В сеансе можно сформировать дополнительные группы
процессов, а разным группам можно назначить имеющиеся процессы
(распределение процессов по группам также может быть изменено). Следовательно, один
пользователь может выполнять, например, сеанс из 10 процессов, организованных
в три группы процессов.
Сеанс — и соответственно относящиеся к нему процессы — может иметь
управляющий терминал (controlling terminal), являющийся первым терминальным
устройством, которое открывает лидер сеанса; после этого лидер сеанса становится
управляющим процессом. Обычно управляющим терминалом для процессов
пользователя является терминал, с использованием которого пользователь вошел в
систему. При формировании нового сеанса включенные в него процессы не имеют
управляющего терминала, пока он не будет создан. Отсутствие управляющего
терминала типично для так называемых демонов (Web-серверы, утилита сгоп и т. д.),
которые работают сами по себе, с какого бы терминала они ни были запущены.
Можно предоставить доступ к терминалу только одной группе процессов сеанса
— заданию переднего плана — и перемещать группы процессов между передним
планом и фоном. Это называется управлением гаданиями (job control). При этом
группа фоновых процессов, пытающаяся получить доступ к терминалу,
приостанавливается до перемещения на передний план. Это не позволяет ей «красть»
вводимые данные у задания переднего плана или искажать информацию, выводимую
группой процессов переднего плана.
Если управление заданиями не применяется (обычно его используют только
командные оболочки), все процессы сеанса являются процессами переднего плана и
могут свободно обращаться к терминалу, несмотря на то, что это может привести к краху.
Драйвер терминального устройства посылает генерируемые терминалом сигналы
прерывания, выхода и разрыва связи каждому процессу из группы процессов
переднего плана, для которой этот терминал является управляющим. Например, если
не предпринять меры предосторожности, разрыв связи с терминалом (скажем,
закрытие соединения telnet) завершит все пользовательские процессы из этой
группы. Для предотвращения этого процесс можно настроить на игнорирование
сигнала освобождения линии.
Сигнал разрыва связи отправляется всем процессам из группы процессов переднего
плана, когда лидер сеанса (управляющий процесс) завершается по какой бы то ни
было причине. Таким образом, при простом выходе из системы все процессы
пользователя обычно завершаются, если только не были предприняты определенные
действия, переводящие их в фоновый режим или как-то иначе делающие процессы
невосприимчивыми к сигналу разрыва связи.
Короче говоря, с каждым процессом связаны четыре идентификатора процесса.
• Идентификатор процесса: положительное число, однозначно
идентифицирующее процесс.
• Идентификатор родительского процесса.
ГЛАВА 1 Фундаментальные концепции 9
• Идентификатор группы процессов: идентификатор процесса, являющегося
лидером группы процессов. Если он равен идентификатору процесса, данный
процесс является лидером группы.
• Идентификатор лидера сеанса.
1.1.5 Система прав
Идентификатор полъзоватепя (user-ID) — это положительное число,
ассоциированное с регистрационным именем (login name) пользователя в файле паролей
(/etc/passwd). Когда пользователь входит в систему, команда login делает этот
идентификатор идентификатором пользователя первого создаваемого процесса —
регистрационной оболочки. Процессы, дочерние по отношению к оболочке,
наследуют этот идентификатор пользователя.
Пользователи организуются в группы (не путайте их с группами процессов),
которые также имеют идентификаторы, называемые идентификаторами групп.
Регистрационным идентификатором группы пользователя становится идентификатор
группы, связанный с регистрационной оболочкой пользователя.
Группы определяются в файле групп (/etc/group). Войдя в систему, пользователь
может переключиться на другую группу, членом которой он является. При этом
изменяется идентификатор группы, связанный с обрабатывающим запрос процессом
(обычно им является командная оболочка, идентификатор которой изменяется с
помощью команды newgrp), а все производные процессы наследуют этот
идентификатор. Так как пользователь может быть членом нескольких групп, процесс
также имеет ряд дополнительных идентификаторов групп. Как правило, при проверке
прав, связанных с группой процесса, эти идентификаторы также учитываются, так
что пользователям не приходится постоянно изменять группы вручную.
Два этих регистрационных идентификатора пользователя и группы называются
реальным идентификаторам пользователя и реальным идентификатором
группы, потому что они представляют реального пользователя — человека, вошедшего
в систему. Кроме того, с каждым процессом связаны два других идентификатора:
действующий (effective) идентификатор пользователя и действующий
идентификатор группы. Обычно эти идентификаторы равны соответствующим реальным
идентификаторам, но могут и отличаться от них, как мы скоро увидим.
При определении прав всегда используется действующий идентификатор.
Реальный идентификатор используется при учете и при взаимодействии пользователей.
Первый определяет права пользователя, второй — его личность.
Каждый файл (обычный файл, каталог, сокет и т. д.) содержит в своем индексном
узле идентификатор пользователя-владельца (короче говоря, владельца) и
идентификатор группы владельца (или просто группы). Кроме того, индексный узел
содержит три набора, каждый из которых включает три бита, определяющих
права доступа к файлу (всего девять битов). В каждом наборе один бит определяет право
на чтение, второй — право на запись, а третий — право па выполнение. Операция
1 О ГЛАВА 1 Фундаментальные концепции
разрешена, если соответствующий бит равен 1. Три набора битов определяют
права владельца, права группы и права других пользователей (не относящихся к
первым двум категориям). Описание битов прав доступа приведено в табл. 1.1
(нулевой бит является самым правым).
Таблица 1.1. Биты прав доступа к файлам
Бит Описание
8 владелец имеет право читать файл
7 владелец имеет право записывать файл
6 владелец имеет право на выполнение файла
5 члены группы имеют право читать файл
4 члены группы имеют право записывать файл
3 члены группы имеют право на выполнение файла
2 другие пользователи имеют право читать файл
1 другие пользователи имеют право записывать файл
О другие пользователи имеют право на выполнение файла
Биты прав доступа часто задаются с использованием восьмеричного числа.
Например, восьмеричное 775 предоставляет владельцу и членам группы права на чтение,
запись и выполнение файла, а другим пользователям — права на его чтение и
выполнение, но не на запись. Команда is отобразила бы эту комбинацию прав как
rwxrwxr-x, что в двоичной форме представляется как число 111111101, легко
преобразуемое в восьмеричное 775 (в десятичной системе счисления эта комбинация
была бы представлена как число 509, которое бесполезно из-за несоответствия цифр
наборам битов).
Система прав определяет, может ли конкретный процесс прочитать, записать или
выполнить конкретный файл. В случае обычных файлов смысл этих операций
очевиден. Право на чтение каталога также не требует пояснений. Под правом «на
запись» каталога понимают возможность выполнения системного вызова,
изменяющего каталог (добавляющего или удаляющего связь), а под правом «на
выполнение» каталога (которое иногда называют правом «на поиск») — возможность
включить каталог в путь. Права на чтение и запись специальных файлов определяют
возможность выполнения системных вызовов чтения и записи. Что это означает
— зависит от разработчика драйвера устройства. Право на выполнение
специального файла не имеет смысла.
Система управления правами принимает решения о предоставлении прав,
используя следующий алгоритм.
1. Если действующий идентификатор пользователя равен нулю, право
предоставляется сразу (действующий пользователь является суперпользователем).
2. Если используемый процессом действующий идентификатор пользователя
соответствует идентификатору пользователя файла^ для определения
допустимости операции используется набор битов владельца.
ГЛАВА 1 Фундаментальные концепции 11
3. Если используемый процессом действующий идентификатор группы или один
из дополнительных идентификаторов групп соответствует групповому
идентификатору файла, используется набор битов группы.
4. Если ни идентификаторы пользователей, ни идентификаторы групп не
позволяют обратиться к файлу, система приходит к выводу, что процесс выполняется
«другим» пользователем, и использует третий набор битов.
Эти этапы выполняются в указанном порядке, так что если на этапе 3 система
запретит запись файла из-за того, что группа не обладает нужным правом, процесс не
сможет записать файл, даже если другие пользователи обладают таким правом (этап 4).
Ситуация, при которой группа обладает меньшими правами, чем другие
пользователи, может показаться странной, но система прав работает именно так (представьте себе
приглашение на вечеринку, организаторы которой подготовили сюрприз для группы
сотрудников. Такое приглашение могут читать все, за исключением членов группы).
Есть другие действия — наверное, их можно было бы назвать «изменениями
индексного узла» — которые может выполнять только владелец файла или
суперпользователь. В их число входят изменение идентификатора пользователя или
группового идентификатора файла, изменение прав доступа к файлу и изменение
времени доступа к файлу или его модификации. Стоит отметить, что право на запись файла
позволяет сделать временем доступа к файлу и временем его модификации
текущее время..
Иногда требуется временно назначить пользователю права другого пользователя.
Например, при выполнении команды passwd для изменения пароля нам хотелось
бы сделать действующим идентификатором пользователя идентификатор
суперпользователя, потому что только он может записывать данные в файл паролей. Для этого
достаточно сделать пользователя root (регистрационное имя суперпользователя)
владельцем команды passwd (т. е. обычного файла, содержащего программу passwd),
после чего установить в индексном узле команды passwd другой бит, называемый
битом установки идентификатора пользователя. Если этот бит установлен, при
выполнении программы действующий идентификатор пользователя изменяется на
идентификатор владельца файла, содержащего программу. Так как права
определяет действующий идентификатор пользователя, а не реальный, это позволяет
пользователю временно получить права кого-то другого. Бит установки
идентификатора группы используется подобным образом.
Дочерний процесс наследует от родительского оба идентификатора пользователя
(действующий и эффективный), поэтому благодаря возможности установки
идентификатора пользователя вы можете очень долго работать с другим действующим
идентификатором пользователя. Именно это делает команда su.
Такая возможность создает брешь в защите системы. Предположим, что вы
копируете команду sh в собственный каталог (и становитесь владельцем копии), после
чего с помощью команды chinod устанавливаете бит установки идентификатора
пользователя и выполняете команду chown, делая владельцем файла
суперпользователя. Теперь вы можете выполнить свою копию sh и получить права суперпользо-
1 2 ГЛАВА 1 Фундаментальные концепции
вателя! К счастью, эта брешь была устранена давным-давно. Если вы не являетесь
суперпользователем, при изменении владельца файла биты установки
идентификаторов пользователя и группы автоматически очищаются.
1.1.6 Другие атрибуты процесса
Системный сегмент данных процесса включает несколько других интересных
атрибутов.
Каждому открытому процессом файлу (обычному или специальному файлу, соке-
ту или именованному каналу) соответствует один дескриптор открытого файла
(целое число в диапазоне от 0 до 1000 или около того), а каждому созданному
процессом неименованному каналу (см. раздел 1.1.7) — два таких дескриптора.
Дочерний процесс не наследует от родителя дескрипторы открытых файлов, он
наследует их дубликаты. Тем не менее, они являются индексами в той же
общесистемной таблице открытых файлов, а это кроме всего прочего означает, что
родительский и дочерний процессы используют одно и то же смещение в файле (байт,
начиная с которого будут прочитаны или записаны данные при выполнении
следующей операции чтения или записи).
Приоритет процесса используется планировщиком ядра. Любой процесс может
понизить свой приоритет, выполнив системный вызов nice; процесс
суперпользователя может при помощи этого же системного вызова повысить свой приоритет.
С технической точки зрения вызов nice устанавливает атрибут, называемый
показателем вежливости (nice value), который, однако, является далеко не
единственным фактором, учитываемым при определении действительного приоритета.
Соответствующий процессу лимит размера файла может иметь (и обычно имеет)
меньшее значение, чем общесистемный лимит; это предотвращает запись
огромных файлов неопытными или невоспитанными пользователями. Процесс
суперпользователя может увеличить свой лимит.
Существуют многие другие атрибуты процесса, которые мы рассмотрим при
обсуждении соответствующих системных вызовов.
1.1.7 Межпроцессное взаимодействие
В самых старых (выпущенных до 1980 года) системах UNIX процессы могли
взаимодействовать друг с другом, используя общие смещения в файлах, сигналы,
трассировку процесса, файлы и каналы. Позднее в этот список были добавлены
именованные каналы (FIFO), за которыми последовали семафоры, блокировки файлов,
сообщения, общая память и сетевые сокеты. Как мы увидим, ни один из этих
механизмов не лишен недостатков, вот почему их одиннадцать! На самом деле их даже
больше, потому что семафоры, сообщения и общая память имеют по две
разновидности. Есть один очень старый набор механизмов межпроцессного взаимодействия
из System V UNIX компании AT&T, называемый «System V IPC», и гораздо более новый
набор «POSIX IPC», определенный в стандартах технологий реального времени,
ГЛАВА 1 Фундаментальные концепции 13
разработанных в начале 1990-х годов. Почти все версии UNIX (в том числе FreeBSD
и Linux) поддерживают 11 старейших механизмов, а основные коммерческие
версии UNIX (например, Solaris компании Sun или HP/UX компании HP) — все 14.
Общие смещения в файлах (shared file offsets) редко используются для реализации
межпроцессного взаимодействия. Теоретически, один процесс может задать
смещение, указывающее на некоторое место в файле, а второй процесс может узнать,
куда оно указывает. Это смещение (число, попадающее, скажем, в диапазон от 0 до
100) и было бы переданными данными. Чтобы совместно использовать смещение
в файле, процессы должны быть связанными, поэтому они могут с тем же успехом
воспользоваться каналами.
Если одному процессу нужно просто сообщить другому о каком-то событии, можно
использовать сигналы. Например, спулер печати мог бы посылать сигнал процессу,
выполняющему печать, при поступлении файла в очередь печати. Однако сигналы
недостаточно информативны и потому в большинстве случаев бесполезны. Кроме
того, сигнал прерывает работу процесса-получателя, что делает программирование
более сложным в сравнении с подходом, при котором получатель принимает
информацию, когда он готов к этому. Как правило, сигналы используются просто для
завершения процессов или для информирования об очень необычных событиях.
Трассировка процесса (process tracing) позволяет родительскому процессу
управлять выполнением дочернего процесса. Так как при этом родитель может читать и
записывать данные дочернего процесса, оба процесса могут свободно
взаимодействовать. Трассировка процесса используется только отладчиками, так как она
слишком сложна и небезопасна для обычного применения.
Самой популярной формой межпроцессного взаимодействия является
использование файлов. Например, мы можем записать файл при помощи редактора vi и затем
выполнить его в интерпретаторе python. Однако если два процесса выполняются
одновременно, файлы неудобны. Это имеет две причины: во-первых,
процесс-читатель, работающий быстрее процесса-писателя, при досгижении конца файла может
решить, что взаимодействие завершено (эту проблему можно устранить, написав
кое-какой замысловатый код). Во-вторых, по мере взаимодействия процессов файл
становится все больше и больше. Иногда процессы взаимодействуют многие дни
и недели, передавая друг другу миллиарды байтов. Файловая система может не
справиться с такой нагрузкой.
Еще одним традиционным способом межпроцессного взаимодействия в мире UNIX
является использование пустого файла в качестве семафора. Этот подход,
основанный на некоторых тонкостях механизма создания файлов UNIX, более подробно
обсуждается в разделе 2.4.3.
Позволю себе дать вам один совет: чтобы не ломать голову над синхронизацией
файлов, используйте каналы. Канал не является разновидностью обычного файла:
хотя он имеет индексный узел, на него нет ссылок. Операции чтения и записи
канала в целом похожи на чтение и запись файла, но имеют и некоторые важные
отличия: если писатель отстает от читателя, читатель блокируется (приостанавли-
1 4 ГЛАВА 1 Фундаментальные концепции
вается на время) до появления данных. Если писатель намного обгоняет читателя,
он блокируется и ждет читателя, поэтому ядру не приходится иметь дело с очень
большими очередями. Наконец, как только какой-то байт прочитан, он уходит в
небытие, так что при длительном выполнении процессов, соединенных посредством
каналов, файловая система не переполняется.
Каналы хорошо известны пользователям командных оболочек: например, команда
Is | wc
позволяющая узнать число файлов, также использует канал. Однако, как вы
узнаете в главе б, ядро поддерживает гораздо более общие средства работы с каналами,
чем командные оболочки.
Каналы имеют три основных недостатка: во-первых, процессы,
взаимодействующие при помощи канала, должны быть родственными, т. е., как правило, они
должны быть родителем и потомком или двумя потомками одного родителя. Это
слишком жесткое ограничение: например, если один процесс является менеджером БД,
а второй относится к приложению, которому нужно обращаться к БД, этот подход
неприменим. Второй недостаток состоит в том, что при записи блоков, объем
которых превышает локально заданный максимум (скажем, 4096 байтов), атомарность
операций записи не гарантируется. Это не позволяет использовать каналы при
наличии нескольких процессов-писателей: их данные могут перемешаться. Наконец,
быстродействие каналов может оказаться слишком низким, потому что данные
пользовательского процесса-писателя копируются в структуры ядра, после чего
передаются другому пользовательскому процессу — читателю. Никакой дисковый
ввод-вывод при этом не выполняется, но копирование само по себе может оказаться
слишком длительным для некоторых требовательных приложений. Из-за этих
недостатков и были разработаны усовершенствованные методики межпроцессного
взаимодействия.
Именованные каналы, также называемые FIFO — аббревиатура FIFO
расшифровывается как «first-in-first-out (первым пришел — первым обслужен)» — устраняют
первый недостаток каналов. По сути, именованный канал является специальным
файлом, и любой процесс, имеющий такое право, может открыть его для чтения
или записи. Кроме того, именованные каналы легко использовать (см. главу 7).
Что не так с именованными каналами? Они не лишены второго и третьего
недостатков каналов: они не исключают чередование при записи больших блоков
данных и иногда работают слишком медленно. При разработке самых требовательных
приложений можно использовать более новые механизмы межпроцессного
взаимодействия (например, сообщения или общую память), но это гораздо сложнее.
Семафором в компьютерном мире называют счетчик, не позволяющий получить
доступ к одному ресурсу сразу двум или более процессам. Как я уже говорил, в
качестве семафоров можно использовать и файлы, но порой это слишком накладно.
В системах UNIX реализованы две совершенно разных технологии семафоров, одна
из которых является частью спецификации System V IPC, а вторая — частью POSIX
IPC (мы обсудим спецификацию POSIX в разделе 1.5).
ГЛАВА 1 Фундаментальные концепции 15
Блокировка файла — это, по сути, специализированный семафор, не позволяющий
сразу двум или более процессам обратиться к тому же сегменту файла. Обычно она
эффективна только в том случае, если процессы ее проверяют; эта нестрогая
форма блокировки называется рекомендуемой (advisory). Более строгая форма
блокировки — обязательная (mandatory) — эффективна независимо от того, проверяют
ее процессы или нет, но она нестандартна и поддерживается далеко не всеми
системами UNIX.
Сообщение представляет собой небольшой блок данных (скажем, 500 байтов),
который может быть отправлен в очередь сообщений. Есть разные типы сообщений.
Получать сообщения из очереди может любой процесс, имеющий
соответствующие права. Процесс может извлечь из очереди или первое сообщение, или первое
сообщение конкретного типа, или первое сообщение, тип которого относится к
определенной группе. Как и в случае с семафорами, предназначенные для работы
с сообщениями системные вызовы System V IPC полностью отличаются от
системных вызовов POSIX IPC.
Общая память потенциально является самой быстрой технологией
межпроцессного взаимодействия. Этот подход основан на отображении одной области
памяти на адресные пространства двух или более процессов. Данные, записанные в общую
память, сразу же становятся доступны процессам-читателям. Для синхронизации
читателя и писателя используется семафор или сообщение. Методика общей
памяти имеет две версии, и если вы были внимательны, вы сами догадаетесь, что это
за версии.
Самое лучшее я оставил на десерт: сетевое межпроцессное взаимодействие с
использованием сокетов и группы соответствующих системных вызовов (системные
вызовы, относящиеся к этой группе, имеют такие имена, как bind, connect и accept).
В отличие от всех других механизмов, сокеты позволяют вам взаимодействовать
по локальной сети или Интернету с процессом, выполняемым на другом
компьютере, причем этот компьютер может не быть UNIX-системой. Он может работать
под управлением Windows или другой ОС. Он даже может быть сетевым
принтером или радио.
Выбрать механизм межпроцессного взаимодействия для конкретного приложения
нелегко, поэтому в главах 7 и 8 я тщательно сравню возможные варианты.
1.2 Версии UNIX
Кен Томпсон и Деннис Ричи (Ken Thompson and Dennis Ritchie) начали работать
над UNIX в AT&T Bell Laboratories как над исследовательским проектом в 1969 году,
и очень скоро UNIX стала широко использоваться внутри AT&T (например, для
автоматизации центров вызовов специалистов по ремонту телефонов). В то время
компания AT&T не занималась производством компьютеров на продажу или
продажей коммерческого ПО, но в начале 1970-х годов она стала предоставлять UNIX
университетам для образовательных целей с условием, что исходный код можно
было передавать только другим университетам, также обладавшим лицензией. В
1 6 ГЛАВА 1 Фундаментальные концепции
конце 1970-х годов AT&T стала предоставлять лицензии на исходный код и
коммерческим организациям.
Многие лицензиаты изменяли код UNIX с целью ее портирования на новые
компьютеры, для добавления драйверов устройств или просто для настройки.
Наверное, два самых существенных изменения были реализованы в UNIX сотрудниками
Калифорнийского университета из Беркли: сетевые системные вызовы («сокеты»)
и поддержка виртуальной памяти. В результате университеты и исследовательские
лаборатории из всех уголков мира стали использовать систему Беркли (названную
BSD — Berkeley Software Distribution), хотя получили свои лицензии у AT&T.
Некоторые коммерческие производители, среди которых можно выделить Sun
Microsystems, также начали разработку своих систем с BSD UNIX. Билл Джой (Bill Joy),
основатель Sun, был главным разработчиком BSD.
Сотрудники Bell Labs также продолжали разработку UNIX, и к середине 1980-х
годов два варианта UNIX стали сильно различаться (система компании AT&T
получила к тому времени название System V). Обе ОС поддерживали виртуальную
память, но их сетевые системные вызовы были совершенно разными. Механизмы
межпроцессного взаимодействия System V (сообщения, общая память и семафоры)
отличались от механизмов BSD, а разработчики BSD изменили почти все
имевшиеся команды и реализовали целый ряд дополнительных, таких как vi.
Были созданы и многие другие варианты UNIX, в том числе несколько клонов UNIX,
не включавших исходный код AT&T. Однако если опустить детали, можно сказать,
что мир UNIX разделился на лагерь BSD, который составили почти все
академические учреждения и некоторые крупные производители рабочих станций, и
лагерь System V, включивший саму AT&T и некоторых коммерческих
производителей UNIX. Почти все студенты факультетов информатики учились на системах BSD
(разумеется, начиная позднее работать в Bell Labs, они не собирались
отказываться от своих любимых команд и изучать UNIX заново). По иронии судьбы, ни AT&T,
ни Калифорнийский университет не захотели заниматься коммерческим ПО (AT&T
утверждала обратное, но это было не так)!
В середине 1980-х годов Институт инженеров по электротехнике и электронике
(Institute of Electrical and Electronics Engineers, IEEE) приступил к стандартизации
системных вызовов и команд UNIX, продолжив дело, начатое отраслевой группой
/usr/group. Первый стандарт системных вызовов был издан институтом IEEE в 1988
году. Этот стандарт имел официальное название IEEE Std 1003.1-1988, но ради
краткости я буду называть его POSIX1988.' Первый стандарт команд (например, vi и
дгер) появился в 1992 году (IEEE Std 1003.2-I992). После внесения небольших
изменений стандарт POSIX1988 был принят Международной организацией по
стандартизации (International Organization for Standardization, ISO) в качестве
международного стандарта и стал известен как POSIX1990.
' Аббревиатура POSIX расшифровывается как Portable Operating System Interface (интерфейс
переносимой операционной системы).
ГЛАВА 1 Фундаментальные концепции 17
Стандарты определяют только синтаксис и семантику интерфейсов прикладного
программирования (application program interfaces, API), no не лежащую в их
основе реализацию. Таким образом, независимо от исторических корней и внутренней
архитектуры любая ОС — даже Microsoft Windows и Digital (теперь HP) VMS —
обладающая достаточной функциональностью, может претендовать на соответствие
стандарту POSIX1990 или его более поздним версиям. Если приложение
соответствует стандартам API-интерфейсов операционной системы и языка, на котором
оно написано (например, C++), и этим же стандартам соответствует целевая
система, вы сможете портировать исходный код приложения на эту систему, не внося
в него никаких изменений, и он скомпилируется и запустится. На практике все не
так прекрасно: приложения, компиляторы и операционные системы содержат
ошибки, к тому же почти все серьезные программы используют те или иные
нестандартные API, так что при портировании придется проделать кое-какую
работу, но стандарты делают ее гораздо менее объемной.
Принятие стандарта POSIX1990 явилось огромным достижением, но его оказалось
недостаточно для объединения лагерей System V и BSD, потому что он не
определял важные API, такие как сокеты BSD и средства работы с сообщениями,
семафорами и общей памятью System V. Кроме того, за это время появились и более
новые API UNIX, предназначенные, главным образом, для использования в
приложениях реального времени (приложениях, взаимодействующих с реальным миром,
таких как система управления двигателем автомобиля). К счастью, IEEE не оставил
без внимания ни технологии реального времени, ни потоки (в разделе 1.5 мы
подробнее обсудим стандартизацию UNIX после принятия стандарта POSIX1990).
В 1987 году AT&T и Sun, которая использовала систему UNIX, созданную на базе
BSD, решили, что они могут объединить две основных версии UNIX в одну, но это
напугало остальных представителей отрасли, подтолкнув их к формированию Фонда
открытого ПО (Open Software Foundation), который в итоге разработал
собственную версию UNIX (все еще содержащую код, лицензированный у AT&T). Таким
образом, в начале 1990-х годов имелись следующие версии UNIX: BSD, система UNIX
компаний AT&T/Sun, версии UNIX, созданные разными производителями до
формирования OSF, и версия OSF.
Напряжение в мире UNIX нарастало, Microsoft Windows победоносно шествовала
по миру, и все это побудило отрасль поддержать организацию X/Open Company,
которая гораздо агрессивнее, чем IEEE, относилась к стандартизации важных API.
В новейшем стандарте определены 1108 интерфейсов функций!
К середине 1990-х годов суматоха немного улеглась, но тут появилась ОС Linux. Все
началось в 1991 году с объявления, опубликованного студентом Линусом Торвальдсом
в группе новостей Usenet и озаглавленного «Free minix-like* kernel sources for 386-AT»
(свободный исходный код ядра minix-подобной ОС для 386-АТ). Начиналось оно так:
Minix — это небольшая командная ОС, разработанная Эндрю Таненбаумом (Andrew Tanen-
baum) из Открытого амстердамского университета (Free University of Amsterdam) и
увидевшая свет в 1987 году. Код AT&T в ней не использовался.
* 5 ГЛАВА 1 Фундаментальные концепции
-Вы тоскуете по прекрасным временам minix-1.1, когда мужчины были мужчинами
и писали собственные драйверы устройств? У вас нет интересного проекта, и вы
просто умираете от желания вонзить свои зубы в ОС, которую можно было бы
адаптировать к своим потребностям? Вас огорчает то, что под управлением minix все
работает? Вы больше не проводите ночи, с упоением отлаживая замысловатые
программы? Тогда прочитайте это объявление — возможно, оно вас заинтересует :-)
Как я уже писал месяц (?) назад, я работаю над свободной версией minix для
компьютеров АТ-386. Наконец, мне удалось довести свою систему до состояния,
позволяющего ее использовать (хотя это зависит от того, что вам хочется делать), и я
охотно публикую ее исходные коды для их более широкого распространения. Это
всего лишь версия 0.02 [+1 (очень небольшая) заплатка], однако я смог успешно
запустить bash/gcc/gnu-make/gnu-sed/compress и т. д.»."
К концу 1990-х годов благодаря усилиям сотен программистов, работавших над
открытым исходным кодом Linux, она превратилась в полноценную серьезную ОС.
Никого уже не удивляют коммерческие поставщики двоичных дистрибутивов Linux
для ПК с архитектурой Intel (такие как Red Hat, SuSE, Mandrake) и производители
оборудования, предлагающие компьютеры Linux (например Dell, IBM и даже Sun).
Все они предоставляют доступ к исходному коду, потому что этого требует
лицензия Linux — полная противоположность лицензии AT&T!
Между тем, несмотря на внесение значительных изменений в ядро на протяжении
15 лет, программисты из Беркли все еще имели дело с системой, содержащей код
AT&T. Они начали удалять код AT&T, но исчерпали все средства, не доведя работу
до конца. Разработчики решили не влезать в долги и выпустили в начале 1990-х
годов неполную систему, ставшую известной как 4.4BSD-Lite (сделай они это чуть
раньше, Торвальдс, наверное, взял бы за основу ее, а не Minix). Несколько групп сразу
же начали дорабатывать 4.4BSD-Lite, и результаты их усилий сейчас известны как
FreeBSD (система для процессоров Intel и некоторых других), NetBSD/OpenBSD
(портируемая ОС), BSD/OS компании WindRiver (ОС с коммерческой поддержкой)
и Darwin (UNIX внутри Mac OS X). Что касается группы из Беркли, то она, к
сожалению, вышла из игры.
Сегодня можно выделить три основных категории систем UNIX.
• Коммерческие закрытые системы, уходящие корнями к AT&T System V или BSD
(Solaris, HP/UX, AIX и т. д.).
• Системы на базе BSD, из которых наиболее известна FreeBSD; ОС Darwin также
довольно популярна.
• Linux.
Какие из этих систем являются «настоящими UNIX»? Организация, занимавшаяся
разработкой UNIX в AT&T и владевшая торговой маркой UNIX, была продана в 1993
году компании Novell. Novell тут же уступила торговую марку UNIX организации
' Сообщение Линуса и 700 миллионов других сообщений, опубликованных пользователями
Usenet с 1981 года, хранятся в архивах групп Google (uww.google.com/grpbp).
ГЛАВА 1 Фундаментальные концепции 19
Х/Ореп. В 1996 году OSF и Х/Ореп объединились и сформировали Open Group,
которая в настоящее время разрабатывает исчерпывающий стандарт UNIX под
названием Single UNIX Specification [SUS2002]. Теперь торговую марку «UNIX» можно
легально ассоциировать с любой системой, отвечающей требованиям Open Group.
Все главные производители компьютеров предлагают системы с торговой маркой
UNIX — даже мейнфрейм MVS компании IBM (теперь называемый OS/390)
является такой системой. Однако ОС Linux и FreeBSD с открытым исходным кодом не
имеют торговой марки UNIX, хотя и утверждается, что они соответствуют тому или
иному стандарту POSIX. Все эти системы можно назвать «юниксоподобными».
1.3 Использование системных вызовов
В этом разделе я объясню, как выполнять системные вызовы при работе с С и
другими языками, и приведу некоторые принципы их корректного использования.
1.3.1 С и C++
Как при программировании на С или C++ выполнить системный вызов? Как и
вызов любой другой функции. Например, вызов read может быть выполнен так:
amt = read(fd, buf, numbyte);
Реализация функции read зависит от варианта UNIX. Как правило, это небольшая
функция, которая выполняет некоторый специальный код, передающий
управление от пользовательского процесса ядру и затем возвращающий результаты. Реальная
работа выполняется внутри ядра, вот почему это системный вызов, а не просто
библиотечная функция вроде qsort или strlen. Библиотечные функции
выполняют всю свою работу в пользовательском режиме.
Помните, что системный вызов приводит к двум переключениям контекста (из
пользовательского режима в режим ядра и обратно) и потому выполняется
гораздо дольше, чем простой вызов функции в адресном пространстве процесса. Таким
образом, без необходимости системные вызовы использовать не следует. Я подчеркну
это в разделе 2.12, при обсуждении буферизованного ввода-вывода.
Каждый системный вызов определен в заголовочном файле, и вы не сможете
выполнить вызов, если не включите нужные ему заголовочные файлы (иногда нужно
включить несколько файлов). Например, вызову read нужен заголовочный файл unistd.h:
«include <unistd.h>
Как правило, заголовочный файл включает определения многих вызовов (в файле
unistd.h определены около 80 вызовов), так что типичная программа содержит лишь
несколько директив «include. Даже если учесть заголовочные файлы, определяющие
функции стандартного С* (например, string.h), директивы «include не выходят из
" Под стандартным С я понимаю варианты С, соответствующие первоначальному стандарту
1989 года, обновленному стандарту 1995 года или самому новому стандарту, известному как
С99. Если я буду иметь в виду только С99, я буду отмечать это.
2 О ГЛАВА 1 Фундаментальные концепции
под контроля. Если вы включите ненужный заголовочный файл, ничего
страшного не случится, так что вы можете просто указать часто используемые файлы в
главном заголовочном файле и включать в программы его. В этой книге главным
заголовочным файлом является файл defs.h, который я приведу в разделе 1.6. Я не буду
включать его в примерах, но вам следует предполагать, что я делаю это в каждом
файле С.
К сожалению, несоответствия стандартов или недосмотры программистов могут
приводить к конфликтам заголовочных файлов, поэтому иногда нам приходится
жонглировать порядком директив «include или проделывать кое-какие трюки с
препроцессором С. Примеры этого вы можете найти в файле defs.h, но, по крайней
мере, он скрывает все хитрости от кода, в который я его включаю.
Никакой стандарт не требует, чтобы та или иная возможность была реализована
как системный вызов, а не как библиотечная функция, поэтому я не буду
подчеркивать различия между ними. Не воспринимайте также термин «системный вызов»
слишком буквально, когда я его использую, я просто имею в виду, что конкретная
возможность обычно реализуется как системный вызов.
1.3.2 Другие языки
Хотя разработчики стандартов POSIX и SUS — главных стандартов UNIX —
описали в документации интерфейсы С, они понимали, что свет не сошелся клином на
С, и потому определили механизмы выполнения системных вызовов и для других
стандартизированных языков, таких как Fortran и Ada. С несложными функциями
вроде read все просто, если только язык позволяет некоторым образом передать в
функцию целые числа и адреса буферов и получить обратно целое число, но
большинство языков поддерживают эти возможности (с Fortran были проблемы).
Структуры (используемые, например, функцией stat) и связные списки (используемые
функцией getaddrinfo) обрабатывать сложнее, однако опытные программисты обычно
справляются и с этими проблемами.'
Во многих языках, таких как Java, Perl и Python, реализованы стандартные средства,
поддерживающие некоторые возможности POSIX. Например, ниже приведен код
на языке Python, использующий системный вызов fork для создания дочернего
процесса. (Мы обсудим fork в главе 5; пока что вам нужно только знать, что он создает
дочерний процесс и возвращает управление в дочернем и в родительском
процессах. Таким образом, в следующем коде будет выполнен и блок if, и блок else,
только первый будет выполнен в родительском процессе, а второй в дочернем).
import os
pid = os.fork()
if pid == 0:
print 'Parent says, "HELLO!"'
' Например, в пакете Jtux, описанном в приложении В, интенсивно используется Java Native
Interface (J14'!)-
ГЛАВА 1 Фундаментальные концепции 21
else:
print 'Child says, "hello!"'
Результат выполнения этого кода таков:
Nrent says, "HELLO!"
Otild says, "hello!"
Все последующие примеры будут написаны на С или на C++, за исключением
примеров из приложения С, в котором пойдет речь о Java и Jython (версия Python,
выполняемая в среде Java).
1.3.3 Принципы вызова библиотечных функций
Ниже я привел некоторые общие принципы вызова библиотечных функций (из кода
С или C++), относящиеся также и к системным вызовам, и вообще к любым другим
функциям.
• Включите необходимые заголовочные файлы (как было сказано выше).
• Узнайте, как функция сообщает об ошибках, и проверяйте, все ли в порядке,
если только у вас нет достойной причины не делать это (см. раздел 1.4). Если
вы не собираетесь выполнять проверку, задокументируйте свое решение,
приведя возвращаемое функцией значение к типу void:
(void)close(fd);
Мы последовательно нарушаем этот принцип в случае функции printf, но в
программах-примерах я почти всегда его соблюдаю.
• Если приведения типов не являются абсолютно необходимыми, не
используйте их, потому что они могут скрывать ошибки. Вот пример того, что делать
не следует:
int n;
struct xyz »p;
free((void *)p); /* неоправданное приведение типа */
Проблема в том, что если вы по ошибке напишете п вместо р, приведение типа
не позволит компилятору предупредить вас об этом. Если в прототипе функции
указан тип void ♦, приводить тип не нужно. Если вы не уверены, корректен ли
тип, который вы хотите использовать, уберите приведение типа и узнайте, что
по этому поводу думает компилятор. Чтобы лучше уяснить этот принцип,
представьте, что в функцию, принимающую int, вы хотите передать pid_t (тип,
соответствующий идентификатору процесса). Лучше получить предупреждение, чем
затыкать компилятору рот. Если вы получите предупреждение и поймете, что
приведение типа устранит проблему, выполните его.
• Если вы имеете дело с несколькими потоками, узнайте, безопасна ли функция,
которую вы собираетесь вызвать, в многопоточной среде, т. е. корректно ли она
2 2 ГЛАВА 1 Фундаментальные концепции
работает с глобальными переменными, мьютексами (семафорами), сигналами
и т. д. в многопоточной программе. Многие функции такими не являются.
• Старайтесь использовать стандартные интерфейсы, а не интерфейсы своей
конкретной системы. Тогда ваш код будет более портируемым, вы сможете
использовать преимущества тщательно протестированных стандартизированных
возможностей, облегчите жизнь другим программистам, которым придется читать
ваш код, и добьетесь лучшей совместимости своего кода со следующими
версиями ОС. Почаще посещайте Web-сайт Open Group (см. раздел 1.9 и [SUS2002]),
содержащий огромный алфавитный список всех стандартных функций (в том
числе библиотечных функций стандартного С) и заголовочных файлов. Это
гораздо лучше, чем использовать страницы man, входящие в состав вашей
системы UNIX. Конечно, иногда вы должны будете изучать особенности системы и
использовать что-то нестандартное. Однако вам следует сделать это
исключением, а не правилом, и документировать применение всех нестандартных
возможностей с помощью комментариев (о том, что я понимаю под
«стандартными возможностями», я расскажу в разделе 1.5).
1.3.4 Краткие описания функций
Обсуждая системные вызовы или функции, я буду приводить их краткие
формальные описания, указывая необходимые заголовочные файлы, аргументы и способ
информирования об ошибках. Вот, например, краткое описание atexit — функции
стандартного С, которая нам понадобится в разделе 1.4.2:
atexit — регистрирует функцию,
цесса
«include <stdlib.h>
int atexit(
vcid (*fcn)(vcid)
/* Возвращает О в случае
определено) */
/*
успеха
вызываемую при
вызываемая функция
завершении работы
•/>;
и ненулевое значение при
сшибке
(errnc
про-
не
(Если вы не знаете, что такое errno, не волнуйтесь.- я расскажу об этом в
следующем разделе).
Используя atexit, вы объявляете функцию, вызываемую при завершении процесса:
static vcid fcn(vcid)
{
/♦ действия, кстсрые следует выполнить при завершении процесса ♦/
}
а затем регистрируете ее:
if (atexit(fcn) l= 0) {
/* сбрабстка сшибки */
}
ГЛАВА 1 Фундаментальные концепции 23
После этого при завершении процесса автоматически будет вызвана ваша
функция. Вы можете зарегистрировать более одной функции (до 32), и все они будут
вызваны в порядке, обратном порядку регистрации. Обратите внимание, что в
операторе if выполняется проверка на ненулевое значение, так как именно это сказано
в кратком описании — не 1, не больше нуля, не -1, не false или NULL, а именно
ненулевое значение. В данном случае это единственный надежный способ проверки
успешности выполнения функции. Кроме того, это облегчает поиск ошибок и
сравнение кода с документацией. Вы сможете легко сравнить два варианта, не ломая
голову.
1.4 Обработка ошибок
Проверять возвращаемые системными вызовами значения, свидетельствующие об
ошибках, непросто, а правильно обработать обнаруженную ошибку еще сложнее.
В этом разделе я объясню суть проблемы и опишу некоторые практические
способы ее решения.
1.4.1 Обнаружение ошибок
Большинство системных вызовов возвращают значение. Например, если все
нормально, вызов read (см. пример в разделе 1.3-1) возвращает число прочитанных байтов.
Если системный вызов хочет сообщить об ошибке, он обычно возвращает значение,
которое невозможно спутать с корректными данными — как правило, это число -1.
Таким образом, пример с вызовом read следовало бы переписать как-нибудь так:
if ((amt = read(fd, buf, numbyte)) == -1) {
fprintf(stderr, "Read failed!\n");
exit(EXIT_FAILURE);
>
Обратите внимание, что exit также является системным вызовом, но он не может
возвратить ошибку, потому что он не возвращает управление. Символ EXIT_FAILURE
определен в стандартном С.
Из всех системных вызовов, описанных в этой книге, где-то 60% возвращают при
ошибке -1, примерно 20% возвращают что-то другое — например NULL, ноль или
специальный символ, такой как SIG_ERR — ну а оставшиеся 20% вообще не
сообщают об ошибках. Из-за этого разнообразия всем нам приходится обращаться к
документации чаще, чем хотелось бы. Описывая каждый системный вызов, я буду
приводить нужную информацию.
Системный вызов, сообщающий об ошибках, может завершиться неудачей по многим
причинам. В 80% случаев код, определяющий причину ошибки, записывается в
целочисленный символ errno. Для использования errno нужно включить заголовочный
файл errno.h. Вы можете использовать errno как целое число, но это не
обязательно целочисленная переменная. Если вы работаете с несколькими потоками, при
обращении к errno, скорее всего, будет выполнен вызов функции, потому что одна
2 Зак. 4030
24 ГЛАВА 1 Фундаментальные концепции
глобальная переменная не отвечает требованиям такой среды. Таким образом, не
объявляйте errno самостоятельно — включайте вместо этого соответствующий
заголовочный файл (другие директивы «include не показаны):
«include <errno.h>
if ((ant = read(fd, buf, nunbyte)) == -1) {
fprintf(stderr, "Read failed! errno = Xd\n", errno);
exit(EXIT_FAILURE);
}
Например, при некорректном дескрипторе файла было бы выведено следующее:
Read failed! errno = 9
Как правило, использовать значение errno можно только в том случае, если вы до
этого убедились в том, что ошибка произошла; вы не можете просто проверить errno,
чтобы узнать, произошла ли ошибка, потому что значение errno устанавливается
только тогда, когда устанавливающая его функция возвращает ошибку. Таким
образом, следующий код некорректен:
amt = read(fd, buf, nunbyte);
if (errno != 0) { /* неверно! */
fprintf(stderr, "Read failed! errno = Xd\n", errno);
exit(EXIT_FAILURE);
}
Если errno сначала обнулить, все работает:
errno = 0;
ant = read(fd, buf, nunbyte);
if (errno != 0) { /* плохо! */
fprintf(stderr, "Read failed! errno = Xd\n", errno);
exit(EXIT_FAILURE);
}
Но это все еще плохой подход, потому что:
• если вы вставите перед вызовом read другой системный вызов или какую-либо
функцию, выполняющую в итоге системный вызов, вы не сможете узнать,
какой системный вызов установил значение errno;
• не все системные вызовы устанавливают значение errno, и вам следует
выработать привычку проверять ошибки в соответствии со спецификацией функции.
Таким образом, при выполнении системного вызова проверяйте errno только
после того, как вы убедились, что произошла ошибка. Это правило относится почти
ко всем системным вызовам.
Предупредив вас о том, что для обнаружения ошибок не следует использовать только
errno, я должен сказать, что есть несколько исключений (например, функции sysconf
ГЛАВА 1 Фундаментальные концепции 25
и readdir), которые для указания на ошибку изменяют значение errno, но даже они
возвращают специфическое значение, говорящее проверить errno. Следовательно,
правило, согласно которому перед проверкой errno следует проверить
возвращенное значение, распространяется и на большинство исключений.
Сообщение errno = 9, которое вы увидели чуть выше, неинформативно и
нестандартизировано, поэтому лучше использовать функцию perror стандартного С:
if ((ant = read(fd, buf, numbyte)) == -1) {
perror("Read failed!");
exit(EXIT_FAiLURE);
}
Теперь выведенное сообщение было бы таким:
Read failed!: Bad file number
Другая полезная функция стандартного С — st re г го г — просто предоставляет
сообщение в форме строки, но не отображает его, как делает perror.
Сообщение «Bad file number» понятно, но оно тоже нестандартизировано, так что
кое-какая проблема остается: в официальной документации, описывающей
системные вызовы и другие функции, использующие errno, для ссылки на ошибки
используются не текстовые сообщения, а символы вроде EBADF. Например, вот фрагмент
описания вызова read в спецификации SUS:
[EAGAIN]
The 0_N0NBL0CK flag Is set for the file desorlptor and the prooess would be delayed.
[EBADF]
The flldes argument Is not a valid file desorlptor open for reading.
[EBADMSG]
The file Is a STREAM file that Is set to oontrol-nornai node and the message
•aitlng to be read lnoludes a oontrol part.
Увидев EBADF, легко понять, что это означает «Bad file number», но что вы будете делать,
если произойдет более загадочная ошибка? Нам хотелось бы получать вместе с
текстовым сообщением символ ошибки, но ни в стандартном С, ни в SUS нет
такой функции. Что ж, давайте напишем собственную функцию, преобразующую номер
ошибки в символ. Символы, указанные в следующем коде, взяты из файлов errno.h
систем Linux, Solaris и BSD, так как наборы символов специфичны для ОС. Возможно,
юм придется адаптировать этот код к своей системе. Ради экономии места я
сократил листинг, но его полную версию вы можете найти на Web-сайте AUP (раздел
1 8 и [AUP2003]).
«ratio struot {
int oode;
ohar *str;
> erroodes[] =
i
2 6 ГЛАВА 1 Фундаментальные концепции
{ EPERM, "EPERM" },
{ ENOENT, "EN0ENT" },
{ EINPROGRESS, "EINPROGRESS" },
{ ESTALE, "ESTALE" },
«ifndef BSD
{ EOHRNG, "EOHRNG" },
{ EL2NSYN0, "EL2NSYNC" },
{ ESTRPIPE, "ESTRPIPE" },
{ EDOUOT, "EDQUOT" },
«ifndef SOLARIS
{ EDOTDOT, "EDOTDOT" },
{ EUCLEAN, "EUCLEAN" },
{ ENOMEDIUM, "ENOMEDIUM" },
{ EMEDIUMTYPE, "EMEDIUMTYPE" },
«endif
«endlf
{ 0, NULL}
};
ccnst char *errsyrabcl(int errno_arg)
{
int I;
fcr (i = D; errccdes[i].str != NULL; I++)
if (errccdes[i].ccde == errnc_arg)
return errcodes[i].str;
return "[UnkncwnSyrabcI]";
}
Вот наш пример, переписанный с использованием этой новой функции:
if ((amt = read(fd, buf, numbyte)) == -1) {
fprlntf(stderr, "Read failed!: Xs (ermc = Xd; Xs)\n",
strerrcr(errnc), ermc, errsynbcl(ermc));
exit(EXIT.FAILURE);
}
Теперь выводится вся информация:
Read failed!: Bad file descriptor (errnc = 9; EBADF)
Давайте напишем еще одну вспомогательную функцию, форматирующую
информацию об ошибке. Мы будем использовать ее в следующем разделе.
char *syserrrasg(char *buf, slze.t bufjuax, ccnst char «rasg, Int errno_arg)
{
ГЛАВА 1 Фундаментальные концепции 27
char «errmsg;
if (msg == NULL)
msg = "???";
if (errnc_arg == 0)
snprintf(buf, bufjnax, "Xs", msg);
else {
errmsg = strerrcr(errnc_arg);
snprintf(buf, bufjnax, "Xs\n\t\t*" Xs (Xd: \"Xs\") ***", msg,
errsymbcl(errnc_arg), errnc_arg,
errmsg != NULL ? errmsg : "no message string");
>
return buf;
>
Использовать функцию syserrmsg мы будем подобным образом:
if ((amt = read(fd, buf, numbyte)) == -1) {
fprintf(stderr, "Xs\n", syserrmsg(buf, sizecf(buf),
"Call to read function failed", errnc));
exit(EXIT_FAILURE);
}
а результаты будут напоминать следующее:
Call tc read function failed
*** EBADF (9: "Bad file descriptor") ***
Что можно сказать о 20% вызовов, которые сообщают об ошибке, но не
устанавливают errnc? Ну, где-то 20 из них сообщают об ошибке другим способом, обычно
просто возвращая код ошибки (т. е. ненулевое значение, говорящее о том, что
произошла ошибка, и содержащее код ошибки), а остальные вообще не сообщают о
конкретной причине ошибки. Описывая каждую функцию (а всего я рассматриваю
в этой книге около 300 функций), я буду указывать, что она делает в случае
ошибки. Вот одна из функций, возвращающая непосредственно код ошибки (что этот
код означает, сейчас неважно):
struct addrinfc «infcp;
if ((r = getaddrinfc("lccalhcst", "80", NULL, &infcp)) != 0) {
fprintf(stderr, "Get error cede Xd frcm getaddrinfc\n", r);
exit(EXIT_FAILURE);
}
Функция getaddrinfc не устанавливает errno, и вы не можете передать
возвращенный ею код ошибки в функцию strerrcr, потому что strerror работает только со
значениями errnc. Коды ошибок, возвращаемые функциями, которые не
устанавливают errno, вы можете найти в [SUS2002] или в руководстве по своей системе, и
2 8 ГЛАВА 1 Фундаментальные концепции
ничто не мешает вам разработать для этих функций версию уже рассмотренной
нами функции errsymbol. К сожалению, символы одной функции (например,
символ EAI.BADFLAGS функции getaddrinfo) могут иметь те же значения, что и символы
другой функции. Это означает, что вы не можете написать функцию,
определяющую символ только по коду ошибки, как это делает errsymbol. Вместе с кодом ошибки
нужно передать и имя функции (при этом вы можете воспользоваться
преимуществами функции gai.strerror — специальной версией strerror, адаптированной к
функции getaddrinfo).
В этой книге вы встретите десятка два функций, для которых в стандарте [SUS2002]
не определены никакие значения errno или даже сказано, что значение errno
устанавливается, но вы можете устанавливать его в своем коде. В кратких описаниях
таких функций я указываю «errno не определено».
Ну как, голова не болит? Обработка ошибок в UNIX далека от элегантности. Это
плохо, потому что несоответствия затрудняют реализацию механизмов обработки
ошибок и их тестирование. Но в ближайшем будущем ситуация не изменится (она
зафиксирована стандартами), поэтому вам придется с ней смириться. Просто будьте
внимательны!
1.4.2 Удобные макросы обнаружения ошибок
для программистов на С
Заключать каждый системный вызов в оператор if и писать после него код,
отображающий сообщение об ошибке и возвращающий управление, нудно. Если
нужно очистить какие-то ресурсы, все становится еще хуже, например:
If ((p = malloo(sizeof(buf))) == NULL) {
fprlntf(stderr, "Xs\n", syserrnsg(buf, slzeof(buf),
"nalloo failed", errno));
return false;
>
If ((fdln = open(fileln, O.RDONLY)) == -1) {
fprlntf(stderr, "Xs\n", syserrnsg(buf, slzeof(buf),
"open (input) failed", errno));
free(p);
return false;
}
if ((fdout = open(fileout, 0_WR0NLY)) == -1) {
fprintf(stderr, "Xs\n", syserrmsg(buf, sizeof(buf),
"open (output) failed", errno));
(void)olose(fdin);
free(p);
return false;
По мере прогресса код очистки ресурсов становится все больше и больше. Его трудно
писать, трудно читать и трудно сопровождать. Чтобы не повторять код очистки,
ГЛАВА 1 Фундаментальные концепции 29
многие программисты просто используют goto. Обычно операторов goto следует
избегать, но в этой ситуации они кажутся оправданными. Обратите внимание, что
мы должны внимательно инициализировать используемые при очистке
переменные и тщательно проверять значения дескрипторов файлов, чтобы код очистки
корректно выполнился независимо от текущего состояния ресурсов.
ohar «p = NULL;
int fdin = -1, fdout = -1;
if ((p = naiioo(sizeof(buf))) == NULL) {
fprintf(stderr, "Xs\n", sysern»sg(buf, sizeof(buf),
"maiioo failed", errno));
goto oleanup;
>
if ((fdin = open(fiiein, OJTOONLY)) == -1) {
fprintf(stderr, "Xs\n", syserrnsg(buf, sizeof(buf),
"open (input) failed", errno));
goto oieanup;
}
if ((fdout = open(fiieout, O.WRONLY)) == -1) {
fprintf(stderr, "Xs\n", sysern»sg(buf, sizeof(buf),
"open (output) failed", errno));
goto oieanup;
>
return true;
cleanup:
free(p);
if (fdin l= -1)
(void)ciose(fdin);
if (fdout l= -1)
(void)close(fdout);
return false;
Все же кодировать все эти if, fprintf и goto надоедает, а системные вызовы почти
погребены под нагромождениями кода!
Мы можем упростить обнаружение ошибок, вывод информации об ошибках и выход
из функции при помощи кое-каких макросов. Сначала я покажу, как они
используются, а их реализацию мы обсудим потом (в них нет ничего необычного, и они не
имеют особого отношения к системным вызовам и даже к UNIX, но я все равно
привел их код, потому что буду часто использовать их в оставшейся части книги).
Ниже приведен предыдущий пример, переписанный с использованием макросов
обнаружения ошибок («ее» — error-checking). Считайте, что этот код находится
внутри функции с именем fen:
char *p = NULL;
int fdin = -1, fdout = -1;
3 О ГЛАВА 1 Фундаментальные концепции
ec_null( p = mallcc(sizecf(buf)) )
ec_neg1( fdln = cpen(flleln, 0_RD0NLY) )
ec_neg1( fdcut = cpen(fllecut, O.WRONLY) )
return true;
EC_CLEANUP_BGN
free(p);
If (fdln != -1)
(vcld)clcse(fdin);
If (fdcut != -1)
(vcld)clcse(fdcut);
return false;
EC_CLEANUP_ENO
Ниже показан вызов этой функции. Так как она вызывается в функции main, в
случае ошибки имеет смысл выполнить выход.
ec_false( fcn() )
/* другие операции */
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
В этих фрагментах происходит следующее: макросы ec_null, ec_neg1 и ec_false
проверяют свои выражения-аргументы на предмет того, равны ли они соответственно
NULL, -1 и false, сохраняют информацию об ошибке и выполняют переход к метке,
установленной макросом EC_CLEANUP_BGN. Затем выполняется тот же код очистки, что
и раньше. В main проверка возвращенного функцией fen значения также вызывает
переход к той же метке в main, и программа завершается. Функция, установленная при
помощи atexit (см. раздел 1.3-4), выводит всю собранную информацию об ошибке:
ERROR: D: main [/aup/C1/errcrhandling.c:41] fcn()
1: fen [/aup/c1/errcrhandllng.c:15] fdln = cpen(flleln, OxOOOD)
*** ENOENT (2: "Nc such file or directory") •*.
Что мы здесь видим? В последней строке — символ errnc, значение errnc и
описывающий ошибку текст, а в предыдущих строках — информацию о действиях,
которые привели к ошибке: номер уровня, имя провинившейся функции, имя файла,
номер строки и код, возвративший ошибку. Пользователям видеть это не следует,
но во время разработки эта информация придется как нельзя кстати. Позднее вы
сможете изменить эти макросы, чтобы они записывали все сведения в файл
журнала и предоставляли пользователям более понятную им информацию.
ГЛАВА 1 Фундаментальные концепции 31
Мы накапливаем информацию об ошибке, а не выводим ее по ходу дела потому,
что это предоставляет разработчику максимальную свободу, позволяя ему
обрабатывать ошибки там, где он считает нужным. На самом деле библиотечным
функциям не следует просто записывать сообщения об ошибках в поток stderr. Иногда
это не самый лучший вариант, к тому же формулировка сообщений может быть
непонятной конечным пользователям. Конечно, в итоге мы все равно печатаем
сообщение, но при использовании этих макросов в реальных приложениях вы
можете легко изменить это поведение.
Итак, эти макросы обеспечивают нам:
• возможность легкого написания понятного кода обнаружения ошибок;
• автоматический переход на код очистки ресурсов;
• полную информацию об ошибке и действиях, к ней приведших.
Недостаток этих макросов в том, что они имеют несколько странный синтаксис (в
конце отсутствует точка с запятой) и включают скрытую в глубинах кода команду
перехода, что очень не нравится некоторым программистам. Если вы думаете, что
недостатки макросов с лихвой компенсируются их достоинствами, используйте их
(что я и буду делать). Если нет, разработайте собственные макросы (возможно,
используя явный goto вместо скрытого) или вообще откажитесь от них.
Сами макросы обнаружения ошибок реализованы в заголовочном файле ec.h. Вот его
код (объявления функций и некоторые другие второстепенные детали опущены):
extern oonst bool eo_ln_oleanup;
typedef enum {EC_ERRN0, EC_EA1} EC.ERRTYPE;
«define EC_CLEANUP_BGN\
eo_warn();\
eo_oleanup_bgn:\
<\
bool eo_in_oleanup;\
eo_ln_oleanup = true;
«define EC_CLEANUP_END\
}
«define eo_omp(var, errrtn)\
<\
assert(!eo_in_oleanup);\
if ((lntptr_t)(var) == (intptr_t)(errrtn)) {\
eo_push(_funo_, _FILE_, _LINE_, #var, errno, EC_ERRN0);\
goto eo_oleanup_bgn;\
}\
}
32 ГЛАВА 1 Фундаментальные концепции
«define ec_rv(var)\
<\
int errrtn;\
assert(Iec_in_cieanup); \
if ((errrtn = (var)) l= 0) {\
ec_push(_func_, _FILE_, _LINE_, #var, errrtn, E0_ERRN0);\
gctc ec_cieanup_bgn;\
}\
>
«define ec_ai(var)\
<\
int errrtn;\
assert(!ec_in_cieanup); \
if ((errrtn = (var)) != 0) {\
ec_push(_func_, _FILE_, _LINE_, #var, errrtn, E0_EAI);\
gctc ec_cieanup_bgn;\
>\
>
«define ec_neg1(x) ec_cnip(x, -1)
«define ec_nuil(x) ес_сяр(х, NULL)
«define ec_faise(x) ec_cnp(x, false)
«define ec_ecf(x) ec_cnp(x, EOF)
«define ec_nzerc(x)\
(\
if ((x) != 0)\
E0_FAIL\
>
«define E0_FAIL ес_сяр(0, О)
«define E0_0LEANUP gctc ec_cieanup_bgn;
«define E0_FLUSH(str)\
{\
ec_print();\
ec_reinit();\
}
Перед описанием этих макросов я должен пояснить суть проблемы и обсудить ее
решение. Проблема в том, что если вы вызываете один из макросов обнаружения
ошибок (например, ec_neg1) в коде очистки ресурсов и при этом происходит ошибка,
она, скорее всего, приведет к бесконечному циклу, так как макрос выполнит
переход к коду очистки! Вот о чем я беспокоюсь:
E0_0LEANUP_BGN
free(p);
ГЛАВА 1 Фундаментальные концепции 33
if (fdin != -1)
ec_neg1( close(fdin) )
if (fdcut != -1)
ec_neg1( clcse(fdcut) )
return false;
EC_CLEANUP_END
Все выглядит так, как будто программист тщательно проверяет значение,
возвращенное вызовом clcse, но на самом деле это путь к катастрофе. Хуже всего то, что
программа зациклится только в том случае, если при очистке ресурсов, выполняемой
после ошибки, произойдет другая ошибка — ясно, что это редкая ситуация, которая,
пожалуй, не возникнет во время тестирования. Нас это не устраивает: макросы
обнаружения ошибок должны повышать надежность программы, а не снижать ее!
Чтобы решить эту проблему, я при выполнении кода очистки устанавливаю
локальную переменную ec_in_cleanup в true — см. определение макроса EC_CLEANUP_BGN. Эта
переменная проверяется в макросе ес_сшр — если она установлена, это означает,
что выполняется код очистки.
Тип beel и его значения — true и false — появились в спецификации С99 Если ваш
язык их не поддерживает, просто включите в один из своих заголовочных файлов
следующий код:
typedef int bcol;
#define true 1
«define false 0
Чтобы макрос assert не срабатывал при вызове ес_сшр вне кода очистки (т. е. при
нормальном вызове), я использую глобальную переменную с таким же именем ec_in_cleanup,
которая постоянно имеет значение false. Это один из тех редких случаев, когда
скрывать глобальную переменную локальной действительно целесообразно.
Зачем вообще нужна локальная переменная ec_in_cleanup? Почему бы просто не
присваивать глобальной переменной значение true в начале кода очистки и false
в конце? Ну, представьте, что произошло бы, если бы вы вызвали в коде очистки
функцию, использующую макрос ес.сшр. Она нашла бы глобальную переменную,
установленную в t rue, и ошибочно подумала бы, что выполняется ее собственный
код очистки. Таким образом, каждой функции (т. е. каждому уникальному блоку кода
очистки) нужна своя переменная.
А теперь я объясню макросы.
• Макрос EC_CLEANUP_BGN включает метку кода очистки (ec_cleanup_bgn), которой
предшествует вызов функции, выводящей предупреждение о достижении
метки. Это служит для профилактики одной частой оплошности, когда программист
забывает указать перед меткой оператор return, из-за чего код очистки
выполняется даже при отсутствии ошибки (я сам включил вызов функции ec.warn только
после того, как провел целый час, разыскивая ошибку, которой не было). Далее
определяется локальная переменная ec_in_cleanup, про которую я уже рассказал.
34 ГЛАВА 1 Фундаментальные концепции
• Макрос EC_CLEANUP_END просто предоставляет закрывающую фигурную скобку.
Фигурные скобки нужны для создания локального контекста.
• Макрос ес_стр выполняет основную часть работы: проверяет, что мы не
находимся в коде очистки, проверяет, произошла ли ошибка, вызывает функцию
ec_push (до которой я скоро доберусь), помещающую информацию о месте
ошибки ( FILE и т. д.) в стек, и выполняет переход к коду очистки. Тип intptr_t
появился в спецификации С99: это целочисленный тип, который в любой среде
достаточно велик, чтобы хранить указатель. Если ваша версия языка его не
поддерживает, объявите его при помощи директивы typedef как lcng, и все,
вероятно, будет в порядке. Для пущей предосторожности выполните где-нибудь в
своем коде проверку значений sizecf(void *) и sizecf(lcng) на равенство (если вам
незнакома нотация #var, почитайте документацию по С — оператор «var
преобразует результат расширения параметра var в строку).
• Макрос ec_rv похож на ес_сшр, но предназначен для функций, которые
возвращают ненулевой код ошибки и не используют еггпс. Однако возвращаемые им
коды являются значениями errno, поэтому они могут быть переданы в макрос
ec_push.
• Макрос ec_ai похож на ec_rv, но коды ошибок, с которыми он работает, не
являются значениями errno, поэтому последний аргумент функции ec.push назван
ЕС_ЕА1 (этот подход используется только двумя функциями из главы 8).
• Макросы ec_negl, ec_null, ec_false и ec_ecf вызывают ec_cmp, передавая ему
соответствующие аргументы, а макрос ec_nzerc выполняет проверку сам. Эти
макросы охватывают наиболее частые ситуации, а в остальных случаях мы можем
использовать непосредственно ес_сшр.
• Макрос EC.FAIL используется тогда, когда ошибка обнаруживается в результате
теста, не использующего макросы из предыдущего пункта.
• Макрос EC_CLEANUP предназначен для тех случаев, когда нужно просто перейти к
коду очистки.
• Макрос EC_FLUSH используется в тех ситуациях, когда мы просто хотим вывести
информацию об ошибке, не дожидаясь выхода. Он особенно полезен в
интерактивных программах, которые должны продолжать работу.
Код вызываемых из макросов сервисных функций я приводить не буду, так как они
не имеют особого отношения к системным вызовам UNIX (они просто
используют стандартный С), но вы можете найти его вместе с описанием на Web-сайте AUP
[AUP2003]. Расскажу об этих функциях лишь в двух словах.
• Функция ec_push помещает переданную ей (например, макросом ес_стр)
информацию об ошибке и контексте ошибки в стек.
• Функция atexit регистрирует функцию, вызываемую при выходе из программы
и выводящую информацию, которая содержится в стеке:
static void ec_atexit_fcn(void)
{
ec_print();
>
ГЛАВА 1 Фундаментальные концепции 35
• Функция ec.print просматривает стек и выводит информацию об ошибке и
приведших к ней действиях.
• Функция ec_reinit удаляет содержимое стека, очищая протокол обнаружения
ошибки.
• Функция ec_warn вызывается в макросе EC_CLEANUP_BGN, если мы случайно
«проваливаемся» в него.
Все эти функции безопасны в многопоточной среде, так что их можно использовать
в многопоточных программах. Более подробно мы обсудим эту тему в разделе 517.
1.4.3 Исключения C++
Прежде чем тратить время и энергию на проведение испытаний и улучшение
макросов «ее» из предыдущего раздела, спросите себя, будете ли вы вообще
программировать на С. В наши дни вы, скорее всего, отдадите предпочтение C++. Почти
все, что рассматривается в этой книге, прекрасно работает и в программах C++. Язык
С все еще часто оказывается оптимальным для разработки встроенных систем, ОС
(например Linux), компиляторов и другого относительно низкоуровневого ПО, но
такие системы обычно имеют собственные специализированные механизмы
обработки ошибок.
C++ позволяет использовать для обработки ошибок вместо операторов goto и return
встроенный механизм исключений. Исключения имеют свои недостатки, но при
должном внимании они более просты в использовании и более надежны, чем
макросы «ее», которые не защищают вас, например, от ошибочного применения ec_nuii
вместо ec_neg1.
Библиотеки, содержащие оболочки системных вызовов, обычно создаются для С и
не генерируют исключения. Таким образом, для использования исключений
нужно создать оболочки более высокого уровня на C++. В случае системного вызова
close код может быть таким:
class syscaii_ex {
public:
int se_errnc;
syscaii_ex(int n)
: se_errnc(n)
{ }
vcid print(vcid)
{
fprintf(stderr, "ERROR: Xs\n", strerrcr(se_errnc));
}
>;
class syscall {
public:
static int ciose(int fd)
36 ГЛАВА 1 Фундаментальные концепции
{
int r;
if ((г = ::cicse(fd)) == -1)
thrcw(syscaii_ex(errnc));
return r;
}
};
После этого вы можете вместо cicse вызывать функцию syscaii: :cicse, которая при
ошибке будет генерировать исключение. Создавать оболочки для 1100 или около
того функций UNIX вам, наверное, не захочется — выберите из них те, которые
нужны вашему приложению.
Если вы хотите, чтобы информация об исключении включала название файла, номер
строки или другие данные об источнике ошибки, вам нужно определить еще одну
оболочку — на этот раз макрос — получающую данные препроцессора (например,
при помощи макроса LINE ).* Тогда код становится еще более замысловатым:
class syscaii.ex {
public:
int se.errnc;
ccnst char *se_fiie;
int se_iine;
ccnst char *se_func;
syscaii_ex(int n, ccnst char *fiie, int iine, ccnst char *func)
: se_errnc(n), se_fiie(fiie), se_iine(iine), se_func(func)
{ }
vcid print(vcid)
{
fprintf(stderr, "ERROR: Xs [Xs:Xd Xs()]\n",
strerrcr(se_errnc), se.fiie, se.iine, se.func);
}
};
ciass syscaii {
public:
static int cicsednt fd, ccnst char *fiie, int iine, const char «func)
{
int r;
if ((r = ::ciose(fd)) == -1)
throw(syscaii_ex(errno, file, iine, func));
return r;
Макрос нужен потому, что если вы просто сделаете __line__ и другие макросы
непосредственными аргументами конструктора класса syscall_ex, они будут ссылаться на место в
определении класса syscaii, что неверно.
ГЛАВА 1 Фундаментальные концепции 37
«define Close(fd) (syscall::close(fd, FILE , _LINE_, _func_))
Теперь вам следует вызывать Close, а не close.
Если хотите, можете реализовать трассировку последовательности вызовов, как мы
сделали для макросов «ее», или что-нибудь еще более интересное.
В приложении Б обсуждается написанная на C++ оболочка Ux, которая охватывает
все системные вызовы, рассматриваемые в данной книге.
1.5 Стандарты UNIX
Реальная ситуация такова, что коммерческие поставщики UNIX сертифицируют свои
системы в соответствии с требованиями Open Group (см. раздел 1.2), а
дистрибуторы открытого исходного кода объявляют только о соответствии стандарту POSIX1990,
хотя за несколькими исключениями они на самом деле не заботятся о выполнении
сертификационных тестов (доступ к этим тестам уже сделан свободным, так что
вероятность выполнения сертификации этими организациями повысилась).
1.5.1 Эволюция стандартов API
Пытаясь перечислить все стандарты, руководства и спецификации POSIX и Open
Group, я бы не избежал путаницы. К счастью, исчерпывающее понимание
эволюции стандартов обычно не требуется, поэтому мы можем упрощенно представить
ее как развитие стандартов, определяющих API-интерфейсы UNIX. Для нас важны
только восемь стандартов, указанных в табл. 1.2.
Таблица 1.2. Стандарты API-интерфейсов POSIX и Open Group
Название Стандарт Комментарии
POSIX1988 IEEE Std 1003.1-1988 (198808L*)
POSIX1990 IEEE Std 10031-1990/ ISO 9945-
1:1990 (I99009L)
POSIX1993 IEEE Std 1003.1b-1993 (199309L)
POSIX1996 IEEE Std I003.1-1996ASO 9945-
1:1996 (199506L)
XPG3 X/Open Portability Guide
SUS1 Single UNIX Specification, Version 1
Первый стандарт
Небольшое обновление стандарта
POSIX 1988
POSIX1990 + стандарты API реального
времени
POSIX 1993 + стандарты API для
работы с потоками + исправления
стандартов API реального времени
Первое широко распространенное
руководство Х/Ореп
POSIX1990 + все часто используемые
API-интерфейсы BSD, AT&T System V
и OSF; эта спецификация также
известна как Spec 1170";
сертифицированные системы обозначаются как UNIX 95
(см. след. стр.)
38 ГЛАВА 1 Фундаментальные концепции
Таблица 1.2 (окончание)
Название Стандарт Комментарии
SUS2 Single UNIX Specification, Version 2 Спецификация SUS1, обновленная до
стандарта POSIX 1996 + стандарты 64-
битной архитектуры, крупных
файлов, улучшенных средств работы
с многобайтными символами и
стандарты, имеющие отношение к
проблеме Y2K; обозначение — UNIX 98
SUS3 Single UNIX Specification, Обновление SUS2; раздел API иденти-
Version 3 (200112L) чен стандарту IEEE Std 1003.1-2001
(стандарты POSIX и Open Group были
полностью объединены);
обозначение - UNIX 03
Эти числа составлены из года и месяца утверждения стандарта институтом IEEE. Они
используются при определении поддерживаемых системой возможностей (см. следующий раздел).
Название спецификации 1170 отражает общее число API-интерфейсов, заголовочных
файлов и команд.
На самом деле еще существует стандарт POSIX2001 (IEEE Std POSIX. 1-2001), но так
как он содержится в SUS3, я его не указывал.
Даже этот упрощенный список позволяет сделать вывод, что высказывание «ОС
соответствует POSIX» не имеет смысла. Кто бы такое ни сказал, он либо не
разбирается в стандартах POSIX (или SUS), либо предполагает, что в них не разбирается
слушатель. Какому именно стандарту POSIX соответствует система? Какие
возможности она поддерживает (ведь более новые возможности вроде технологий
реального времени и многопоточности необязательны)? В общем, вот как
интерпретировать то, что вы слышите.
• Если упоминается только POSIX, скорее всего, имеется в виду POSIX 1990,
который стал первым и последним стандартом ОС, известным большинству
разработчиков.
• Если вы не владеете информацией о том, что ОС прошла сертификационные
тесты, определенные комитетами по стандартизации, вы можете быть уверены
только в том, что разработчики ОС некоторым образом приняли во внимание
стандарт POSIX.
• Поддержка необязательных возможностей требует отдельного исследования
(более подробно я расскажу об этом чуть позднее).
1.5.2 Как сказать системе, что вам нужно
В этом и следующем разделах я расскажу, что, согласно стандартам,
рекомендуется делать, если нужно проверить соответствие программы стандартам и узнать, какой
версии стандарта соответствует ОС. После этого вы сами решите, что из
описанного заслуживает внимания, а что можно выкинуть из головы.
ГЛАВА 1 Фундаментальные концепции 39
Прежде всего, приложение может заявить, какую версию стандарта оно ожидает: для
этого следует определить символ препроцессора перед включением заголовочных
файлов POSIX, таких как unistdh. Это ограничивает стандартные заголовочные файлы
(например stdio.h, unistdh, errno.h) только теми символами, которые определены в
стандарте. Чтобы ограничить себя классическим стандартом POSIX 1990, сделайте
следующее:
«define _P0SIX_S0URCE
«include <sys/types.h> «
«include <unistd.h>
Более новые стандарты POSIX требуют указания другого, более определенного
символа:
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 199506L
«include <sys/types.h>
«include <unistd.h>
Обратите внимание: в отличие от _P0SIX_S0URCE символ _POSIX_C_SOURCE включает число,
которое обычно является длинным целым, включающим год и месяц утверждения
стандарта (в нашем примере июнь 1995 года). Другим возможным длинным целым
числом является 200112L. Однако в случае POSIXI990 используется значение 1, а не
199009 L*
Спецификации SUS, характеризующие коммерческие системы UNIX,
определяются отдельным символом _X0PEN_S0URCE и двумя специальными числами: 500
(спецификация SUS2) и 600 (SUS3). Так, если вы хотите использовать возможности
системы UNIX 98, т. е. системы, соответствующей спецификации SUS2, сделайте
следующее:
«define _P0SIX_S0URCE
«define _P0SIX_C_S0UBCE 199506L
«define _X0PEN_S0URCE 500
«include <sys/types.h>
«include <unistd.h>
Для указания спецификации SUS1 нужно определить символ _X0PEN_S0URCE без
значения и символ _X0PEN_S0URCE_EXTENDED со значением 1:
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 2
«define _X0PEN_S0URCE
«define _X0PEN_S0URCE_EXTENDED 1
«include <unistd.h>
' Вы также можете указать значение 2, если вам нужны связывания С, определенные в
стандарте 1003.2-1992.
40 ГЛАВА 1 Фундаментальные концепции
Формально вам не нужно определять два первых символа POSIX, если вы в
действительности работаете с системой SUS2, так как они будут определены
автоматически. Однако ваш исходный код, возможно, будет использоваться на более старой
системе, поэтому вы должны сказать ей о том, что вам нужен стандарт POSIX, на
понятном ей языке. После _P0SIX_C_S0URCE нужно указать 2, если символ _XOPEN_SOURCE
определен без значения, 199506L — если он определен со значением 500, и 200112L
— если после _X0PEN_S0URCE указано число 600.
Вы запутались? Плохо, потому, что это далеко не все. Чтобы чуть помочь вам, я
резюмирую сказанное.
• Решите, какой уровень стандартизации вам нужен.
• Если только POSIX 1990, определите только символ _P0SIX_S0URCE.
• Если POSIX 1993 или POSIX 1996, определите символы _P0SIX_S0URCE и _P0SIX_C_S0URCE
и укажите соответствующий номер из таблицы 1.2 раздела 1.5.1.
• Если SUS1, SUS2 или SUS3, определите символ _X0PEN_S0URCE (указав после него
число 500 или 600 или ничего не указав), а также два символа POSIX с
соответствующими числами.
• В случае спецификации SUS1 определите также символ _XOPEN_SOURCE_EXTENDED со
значением 1.
• Забудьте о стандарте XPG3 (и XPG4, про который я даже не упоминал) —
думаю, вам и без него хватит забот.
Мы можем легко написать заголовочный файл, задающий нужную комбинацию
символов на основе единственного символа, указываемого вами перед
включением файла. Вот код такого заголовочного файла suvreq.h* (я покажу, как он
используется, в следующем разделе):
/•
Заголовочный файл для эапрооа информации о поддержке конкретного
отандарта. Перед его включением нужно определить один из оледупщих
оимволов (отандарт 1003.1-1988 не поддерживается):
SUV_P0SIX1990 в олучае 1003.1-1990
SUV_P0SIX1993 в олучае 1003.1Ь-1993 - технологии реального времени
SUV_P0SIX1996 в олучае 1003.1-1996
SUV.SUS1 в олучае Single UNIX Specification, v. 1 (UNIX 95)
SUV_SUS2 в олучае Single UNIX Speoifioation, v. 2 (UNIX 98)
SUV_SUS3 в олучае Single UNIX Speoifioation, v. 3 (UNIX 03)
•/
#if defined(SUV_P0SIX1990)
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 1
#elif defined(SUV_P0SIX1993)
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 199309L
' SUV расшифровывается как Standard UNIX Version. Как вы думаете, что это за версия?
ГЛАВА 1 Фундаментальные концепции 41
#ellf deflned(SUV_P0SIX1996)
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 199506L
«ellf deflned(SUV_SUS1)
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 2
«define _X0PEN_S0URCE
«define _X0PEN_S0URCE_EXTEN0E0 1
«ellf deflned(SUV_SUS2)
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 199506L
«define _X0PEN_S0URCE 500
«define _X0PEN_S0URCE_EXTENDED 1
«ellf defined(SUV.SUS3)
«define _P0SIX_S0URCE
«define _P0SIX_C_S0URCE 200112L
«define _X0PEN_S0URCE 600
«define _X0PEN_S0URCE_EXTENDED 1
«endlf
Ограничение программы только стандартными возможностями — прекрасная идея,
если вы хотите написать по-настоящему портируемое приложение, потому что это
помогает гарантировать, что вы не воспользуетесь случайно чем-то нестандартным.
Однако при разработке других приложений это ограничение может быть слишком
строгим. Например, FreeBSD официально соответствует только стандарту POSIX1990,
но в ней реализованы сообщения System V, которые относятся к уровню SUS1. Если
вам нужно использовать сообщения System V или другие возможности,
реализованные в ОС FreeBSD после принятия стандарта POSIX1990, вы не захотите
определять символ _PDSIX_SDURCE. Ну и не надо: это совершенно необязательно. Именно
так мне пришлось поступить, чтобы написанные мной примеры заработали под
управлением FreeBSD.*
Так... Я рассказывал про все эти символы и числа, а теперь говорю, что вы, скорее
всего, не захотите их использовать. Что ж, добро пожаловать в мир стандартов!
1.5.3 Как спросить у системы, что она предлагает
То, что вы запросили определенные возможности, указав символы _PDSIX_SDURCE,
_PDSIX_C_SDURCE и _XDPEN_SDURCE, не означает, что вы их получите: если система со-
Кроме того, большинство компиляторов и ОС поддерживают дополнительные символы,
соответствующие полезным нестандартным возможностям, такие как _GNU_S0URCE (символ
gcc) или EXTENSIONS (символ Solaris). Более подробную информацию об этом вы
найдете в документации, поставляемой с вашей системой.
42 ГЛАВА 1 Фундаментальные концепции
ответствует только стандарту POSIX1993, большего вы от нее не добьетесь. Если вам
этого достаточно, все великолепно. Если этого мало, вам нужно отказаться от
компиляции (при помощи директивы #error) и заблокировать необязательную
возможность своего приложения или включить ее альтернативную реализацию.
Чтобы узнать, что предлагает ОС, вы должны после включения файла unistd.h
проверить символ _P0SIX_VERSI0N и, если вы имеете дело с системой SUS, символы
_XOPEN_UNIX и _X0PEN_VERSI0N. Четыре символа «SOURCE» из предыдущего раздела
позволяют вам обратиться к системе, а при помощи только что названных
символов система дает вам ответ.
Конечно, эти символы могут даже оказаться неопределенными, хотя для символа
_P0SIX_VERSI0N это было бы странным, так как вы смогли успешно включить файл
unistd.h. Таким образом, нужно все тщательно проверить с использованием
препроцессора:
#if _P0SIX_VERSI0N >= 199009L
/* оиотема ооответотвует некоторому отандарту P0SIX */
#else
#error "Can be oompiled only on a POSIX system!"
#endif
Если символ _P0SIX_VERSI0N не определен, он будет заменен нулем.
Значением символа _P0SIX_VERSI0N может быть одно из чисел (например 199009L),
указанных в таблице из раздела 1.5.1. Если вы работаете хотя бы с системой SUS1,
будет определен символ _X0PEN_UNIX (без значения), но лучше проверять символ
_X0PEN_VERSI0N, который будет равен 4, 500 или 600, а когда-нибудь, возможно, и
большему числу.
Давайте узнаем, что заголовочные файлы скажут нам после того, как мы запросим
уровень совместимости SUS2. Напишем для этого небольшую программу (код
заголовочного файла suvreq.h приведен в предыдущем разделе):
#define SUV_SUS2
#include "suvreq.h"
#inoIude <unistd.h>
#inoIude <stdio.h>
int main(void)
{
printf("Request:\n");
#ifdef _P0SIX_S0URCE
printf("\t_POSIX_SOURCE defined\n");
printf("\t_POSIX_C_SOURCE = XId\n", (long)_POSIX_C_SOURCE);
#else
printf("\t_P0SIX_S0URCE undefined\n");
#endif
ГЛАВА 1 Фундаментальные концепции 43
#ifdef _X0PEN_S0UR0E
#if _X0PEN_S0UR0E +0=0
printf("\t_X0PEN_S0UR0E defined (no value)\n">;
#eise
printf("\t_X0PEN_S0UR0E = Xd\n", _X0PEN_S0UR0E);
#endif
«else
p rintf("\t_X0PEN_S0UR0E undef ined\n");
#endif
#ifdef _X0PEN_S0UR0E_EXTENDED
printf("\t_X0PEN_S0UR0E_EXTENDED defined\n");
«else
printf("\t_X0PEN_S0UR0E_EXTENDED undefined\n");
#endif
printf("Oiaims:\n");
#ifdef _P0SiX_VERSi0N
printfC'\t_P0SiX_VERSi0N = *id\n", _P0SiX_VERSi0N);
#eise
printf("\tNot P0SiX\n");
#endif
#ifdef _XOPEN_UNiX
printf("\tX/Open\n");
#ifdef _XOPEN_VERSiON
printf("\t_XOPEN_VERSiON = Xd\n", _XOPEN_VERSiON);
#eise
printf("\tError: _XOPEN_UNiX defined, but not "
"_XOPEN_VERSiON\n");
#endif
#eise
printf("\tNot X/0pen\n");
#endif
return 0;
>
Выполнив этот код под управлением ОС Solaris 8, я получил такие результаты:
Request:
_P0SiX_S0UR0E defined
_P0SiX_0_S0UR0E = 199506
_X0PEN_S0UR0E = 500
_X0PEN_S0UR0E_EXTEN0E0 defined
Olaims:
_P0SiX_VERSI0N = 199506
X/0pen
_X0PEN_VERSi0N = 500
44 ГЛАВА 1 Фундаментальные концепции
Итак, Solaris готова быть системой, соответствующей спецификации SUS2. А что будет,
если мы определим в первой строке символ SUV_SUS1? Давайте узнаем:
Request:
_P0SIX_S0URCE defined
_P0SIX_C_S0URCE = 2
_X0PEN_S0URCE defined (no value)
_X0PEN_S0URCE_EXTEN0E0 defined
Claims:
_P0SIX_VERSI0N = 199506
X/0pen
_X0PEN_VERSI0N = 4
Это означает, что Solaris также может вести себя как система SUS1, скрывая от нас
любые возможности SUS2, отсутствующие в спецификации SUS1.
FreeBSD 4.6 дала мне такой ответ:
Request:
_P0SIX_S0URCE defined
_P0SIX_C_S0URCE = 2
_X0PEN_S0URCE defined (no value)
_X0PEN_S0URCE_EXTEN0E0 defined
Claims:
_P0SIX_VERSI0N = 199009
Not X/0pen
Иначе говоря, FreeBSD проигнорировала мои пожелания и ответила, что она
соответствует только стандарту POSIX1990.
Как я уже говорил, вам, скорее всего, не захочется выполнять такой запрос при работе
с системой FreeBSD, потому что он блокирует слишком много ее возможностей,
причем некоторые из них, наверное, соответствуют стандартам, принятым после
POSIX1990. Когда я выполнил тот же код, не включив заголовочный файл suvreq.h,
я получил такие результаты:
Request:
_P0SIX_S0URCE undefined
_X0PEN_S0URCE undefined
_X0PEN_S0URCE_EXTEN0E0 undefined
Claims:
_P0SIX_VERSI0N = 199009
Not X/0pen
FreeBSD остается верной себе. Интересно отметить, что если то же самое проделать
с Solaris, она говорит, что является системой XPG3, так что мы можем назвать XPG3
уровнем по умолчанию. Почему по умолчанию Solaris заявляет о соответствии
спецификации XPG3, а не SUS2? Лично для меня это загадка, особенно если учесть, что
Solaris обозначается как система UNIX 98. Вот информация, которую вывела Solaris-.
ГЛАВА 1 Фундаментальные концепции 45
Request:
_P0SIX_S0UR0E undefined
_X0PEN_S0UR0E undefined
_X0PEN_S0UR0E_EXTEN0E0 undefined
Olaims:
_P0SIX_VERSI0N = 199506
X/Open
_X0PEN_VERSI0N = 3
SuSE Linux 8.0 (Linux version 2.4, glibc 2.953) в ответ на запрос с символом SUV_SUS2
возвратила следующее:
Request:
_P0SIX_S0UR0E defined
_P0SIX_0_S0UR0E = 199506
_X0PEN_S0UR0E = 500
_X0PEN_S0UR0E_EXTEN0E0 defined
Olaims:
_P0SIX_VERSI0N = 199506
X/0pen
_X0PEN_VERSI0N = 500
Итак, Linux готова быть системой SUS2. Но по умолчанию она является системой
SUS1, потому что удаление директивы, включающей файл suvreq.h, приводит к
таким результатам:
Request:
_P0SIX_S0UR0E defined
_P0SIX_0_S0UR0E = 199506
_X0PEN_S0UR0E undefined
_X0PEN_S0UR0E_EXTEN0E0 undefined
Claims:
_P0SIX_VERSI0N = 199506
X/0pen
_X0PEN_VERSI0N = 4
Сравните это с информацией, которую возвратила Solaris, когда я не включил
заголовочный файл suvreq.h — как видите, при включенном файле unistdh ОС Linux
сама определила для нас символ _P0SIX_S0UR0E. Пожалуй, это имеет некоторый смысл,
так как его определение всегда было требованием POSIX.
ОС Darwin — сама скромность: она заявляет лишь о соответствии стандарту
POSIX1988 (при включенном файле suvreq.h). Вот что она сообщает:
Request:
_P0SIX_S0UR0E defined
_P0SIX_0_S0UR0E = 199506
_X0PEN_S0UR0E = 500
_X0PEN_S0UR0E_EXTEN0E0 defined
Claims:
4 6 ГЛАВА 1 Фундаментальные концепции
_P0SIX_VERSI0N = 198808
Not X/0pen
Как бы то ни было, во всех примерах я перед включением файла suvreq.h
определяю символ SUV_SUS2. Это не относится только к системам FreeBSD и Darwin, у
которых я вообще ничего не спрашиваю. Как в таком случае узнать, с какой
системой мы работаем: с FreeBSD или Darwin? Ответ ищите в разделе 1.5.8.
1.5.4 Необязательные возможности
Я уже говорил, что стандарт POSIX1996 (IEEE Std 1003.1-1996) охватил технологии
реального времени и многопоточность, но это не означает, что соответствующие
ему системы должны поддерживать эти возможности, потому что они необязательны.
Если система не поддерживает необязательную возможность (опцию), она должна
сообщать об этом стандартным образом. Сейчас я это поясню.
Оказывается, что какой бы сложной уже ни была система символов, она слишком
груба, чтобы с ее помощью можно было узнать о поддержке опций. Поэтому для
групп опций (например для средств работы с потоками сообщений POSIX) и
некоторых их подгрупп был определен целый ряд дополнительных символов. Я не
буду рассматривать все группы, а только расскажу, как проверить, поддерживается
ли опция. По мере принятия стандартов POSIX1990, SUS1 и SUS3 правила
выполнения такой проверки все усложнялись и в итоге превратились в нечто пугающее,
поэтому мое объяснение будет упрощенным (подробное обсуждение опций со
ссылками на релевантные функции можно найти на Web-странице [Dre2003]).
На практике я столкнулся с таким неприятным положением дел.
• Основные коммерческие системы (такие как Solaris) поддерживают все опции.
Они также корректно обрабатывают символы проверки опций, хотя
использовать эти опции можно и без проверки, так как все они поддерживаются.
• Системы с открытым исходным кодом — Linux, FreeBSD и Darwin — не
поддерживают многие опции, поэтому их важно проверять. Однако эти ОС
обрабатывают команды проверки опций некорректно, поэтому код проверки зачастую
не работает.
Ясно, что никакие усилия комитетов по стандартизации не улучшат эту ситуацию,
если разработчики ОС не будут соблюдать уже принятые стандарты.
Как бы то ни было, каждой опции соответствует символ препроцессора,
определенный в файле unistdh. Проверяя эти символы, вы можете узнать,
поддерживается ли та или иная опция. Например, символ _P0SIX_ASYNCHR0N0US_I0 соответствует опции
Asynchronous Input and Output (асинхронный ввод-вывод), которая охватывает
системные вызовы aio.read, aio.write и некоторые другие (более подробно мы
обсудим их в главе 3). Если ваша программа использует эту опцию, вы должны сначала
протестировать указанный символ:
#if _P0SIX_ASYNCHR0N0US_I0 <= 0
/• выведите сообщение, выполните какие-то другие действия
ГЛАВА 1 Фундаментальные концепции 47
или откажитесь ст компиляции, используя директиву #errcr */
telse
/* кед, использующий асинхронный введ-вывед •/
«endif
Что это означает? То, что вы не можете использовать системные вызовы,
выполняющие асинхронный ввод-вывод, и не можете включить ассоциированный
заголовочный файл (aio.h), если символ _P0SIX_ASYN0HR0N0US_I0 не определен или
определен с нулевым или отрицательным значением. Если этот символ определен с
положительным значением, можно считать, что опция всегда поддерживается.*
Поддержка некоторых опций зависит от используемого вами файла, и тестировать
их следует иначе. Например, если опция _P0SIX_ASYN0HR0N0US_I0 поддерживается, то,
чтобы узнать, поддерживается ли асинхронный ввод-вывод для конкретного
файла, вы должны проверить субопцию _P0SIX_ASYN0_I0. Правила тестирования
субопций немного различаются в зависимости от конкретной опции, но, как правило,
если опция имеет отношение к файлам, сначала нужно проверить, определена ли
она вообще. Если опция определена, значение -1 говорит о том, что она не
поддерживается ни для какого файла, а любые другие значения — о том, что она
поддерживается для всех файлов. Если опция не определена, вы должны использовать
для ее тестирования вызов pathcenf или fpathccnf (см. раздел 1.5.6).
Только что описанные правила тестирования опций, зависящих от файлов, относятся
к спецификации SUS2. В случае стандартов, предшествовавших SUS2, а именно
POSIX1993 и POSIX1996, правила были другими: проверка файла требовалась
только тогда, когда опция верхнего уровня (в нашем примере _P0SIX_ASYN0HR0N0US_I0) не
была определена; если она была определена со значением, отличающимся от -1, вы
могли полагать, что она поддерживается для всех файлов.
Linux ведет себя немного странно: она утверждает, что является системой SUS2,
однако следует правилам, предшествовавшим спецификации SUS2.
Чтобы все упростить, вы можете инкапсулировать тестирование опций в
функциях, создав по одной функции для каждой группы нужных вам необязательных
системных вызовов. Например, следующая функция проверяет, поддерживаются ли
функции асинхронного ввода-вывода, учитывая при этом все особенности Linux
(системный вызов pathcenf описан в разделе 1.5.6):
typedef enura {0PT_N0 = 0, OPT.YES = 1, OPT.ERROR = -1} OPT.RETURN;
OPT_RETURN cpticn_async_ic(const char «path)
{
" Мой тест слишком упрощен, потому что я не рассматриваю ситуации, когда символ не
определен или имеет нулевое значение, как специальные случаи. Для некоторых символов и
некоторых версий SUS есть заголовочные файлы и функции-заглушки, позволяющие
протестировать символ во время выполнения при помощи вызова sysconf. Однако некоторые
системы, в том числе Linux, пренебрегают заглушками, если символ не определен. Таким
образом, мой тест не охватывает системы, в которых символ имеет нулевое значение, но
вызов sysconf сообщит, что возможность поддерживается.
4 8 ГЛАВА 1 Фундаментальные концепции
#if _P0SIX_ASYN0HR0N0US_I0 <= 0
return 0PT_N0;
#eiif _X0PEN_VERSI0N >= 500 && !defined(LINUX)
#if !defined(_POSIX_ASYNO_iO)
errno = 0;
if (pathoonf(path, _P0_ASYN0_I0) == -1)
if (errno == 0)
return 0PT_N0;
eise
E0_FAIL
eise
return 0PT_YES;
E0_0LEANUP_BGN
return 0PT_ERR0R;
E0_0LEANUP_END
#eiif _P0SIX_ASYN0_I0 == -1
return 0PT_N0;
«else
return 0PT_YES;
#endif /• _P0SIX_ASYN0_I0 */
#eiif _P0SIX_VERSI0N >= 199309L
return 0PT_YES;
«else
errno = EINVAL;
return 0PT_ERR0R;
#endif /* _P0SIX_ASYN0HR0N0US_I0 */
}.
Имейте в виду, что вы не можете использовать эту функцию для определения того,
скомпилируется ли код, использующий опцию. Для этого нужно использовать
препроцессор и символ опции (например _P0SIX_ASYNCHR0N0US_I0) непосредственно. Иначе
говоря, вы должны сделать что-то подобное:
#if _P0SIX_ASYN0HR0N0US_I0 <= 0
/* выведите оообщение, выполните какие-то другие дейотвия
или откажитеоь от компиляции, иопользуя директиву «error
•/
«else
/*
код, вызывающий функцию option_asyno_io и иопользуюций
опцию, если функция возвратит ОРТ YES
*/
#endif
Итак, символ _P0SIX_ASYNCHR0N0US_I0 служит для проверки того, поддерживает ли
вообще система асинхронный ввод-вывод; если мы работаем с системой SUS2 и
ГЛАВА 1 Фундаментальные концепции 49
хотим узнать, поддерживается ли эта опция для конкретного файла, мы должны также
проверить символ _P0SIX_ASYNC_I0 — это можно сделать во время выполнения при
помощи вызова pathconf или fpathconf.
Даже не пытайтесь запомнить символы всех опций: их слишком много,
большинство из них вам никогда не понадобятся, а в их поведении слишком много
несоответствий. Описывая необязательные системные вызовы, я, как правило, буду
указывать, какие символы следует проверять.* Однако проверять опции в программах-
примерах я обычно не буду.
1.5.5 Системный вызов sysconf
Системный вызов sysconf позволяет не только проверить во время выполнения,
поддерживается ли необязательная возможность (см. раздел 1.5.4), но и узнать
различные предельные значения, зависящие от реализации системы — например,
максимальное число открытых файлов. Для получения более подробной
информации о возможностях вызова sysconf обратитесь к соответствующей странице кап
или к документу [SUS2002].
sysconf — получает ин
чение
«inolude <unistd.h>
long sysoonf(
Int nane
);
формацию о поддержке опции
1* Возвращает информацию о
ошибке устанавливает errno)
/•
или предельное зна-
название опции или характеристики, предельное
значение которой нужно получить */
поддержке опции/предельное
V
значение или -1 (при
Используя вызов sysconf, вы можете допустить единственную ошибку (ошибки,
обнаруживаемые во время компиляции, не считаются): указать неизвестный ему
символ, в случае чего sysconf возвращает -1 и присваивает errno значение EINVAL.
Если же значение errno не установлено, возвращаемое значение -1 просто
говорит о том, что опция не поддерживается или что проверяемая характеристика не
имеет предельного значения. Таким образом, перед вызовом sysconf вы дожны
обнулить errno. Кроме того, не используйте макрос ec.negl, потому что он думает, что
все возвращаемые значения -1 свидетельствуют об ошибках. В следующем
примере, определяющем предельное значение, мы обрабатываем возможные ошибки сами
и используем для информирования об ошибках макрос EC.FAIL (макросы «ее» были
рассмотрены в разделе 1.4.2):
Не путайте необязательные возможности, которые все-таки стандартизированы, с нестан-
дартизированными. Читая эту книгу, вы встретите всего лишь несколько нестандартизиро-
ванных вызовов.
50 ГЛАВА 1 Фундаментальные концепции
long value;
#lf defined(_SC_ATEXIT_HAX)
errno = 0;
if ((value = sysoonf(_SC_ATEXIT_MAX)) == -1)
If (errno == 0)
prlntf("max atexlt registrations: unllnilted\n");
else
EC_FAIL
else
prlntf("fliax atexit registrations: Xld\n", value);
«else
prlntf("_SC_ATEXIT_HAX undeflned\n");
«endlf
Выполнив этот код под управлением Linux, я получил следующие результаты:
max atexlt registrations: 2147483647
Полученное число является максимальным значением типа long (которое равно
значению символа LONG.MAX стандартного С). Наверное, это просто означает, что Linux
хранит информацию о зарегистрированных функциях в связном списке, а не в
массиве фиксированного размера. Однако FreeBSD дала мне такой ответ:
_SC_ATEXIT_HAX undefined
Это тоже нормально, так как символ _SC_ATEXIT_MAX не определен в стандарте
POSIX1990, которому соответствуют все версии FreeBSD. Это просто
иллюстрирует важность проверки того, определен ли символ (так как в спецификации
стандартного С говорится, что функция atexit позволяет зарегистрировать не менее 32
функций, можно предположить, что это справедливо и для FreeBSD).
1.5.6 Системные вызовы pathconf и fpathconf
Системный вызов sysoonf позволяет проверить только общесистемные опции и
предельные значения, однако некоторые опции и значения зависят от того, с каким
файлом мы имеем дело. Проверить их можно с помощью функций fpathconf и pathconf:
pathconf — получает информацию о поддержке опции
чение, опираясь на путь
Klnolude <unistd.h>
long pathoonf(
oonst char «path,
lnt name
);
/• Возвращает информацию о
ошибке устанавливает errno)
/*
/*
путь »/
или предельное зна-
название опции или характеристики, предельное
значение которой нужно
поддержке опции/предельное
*/
получить */
значение или -1 (при
ГЛАВА 1 Фундаментальные концепции 51
fpathconf — получает
значение, опираясь на
«include <unistd.h>
long fpathccnf(
int fd,
int name
v
информацию о поддержке опции или
дескриптор файла
/* дескриптор файла */
предельное
/* название епции или лимитированной
характеристики */
1* Возвращает информацию с поддержке опции/предельнее значение
сшибке устанавливает
еггпс) »/
или
-1 (при
Эти функции принимают один и тот же второй аргумент и возвращают один и тот
же результат, только fpathccnf используется, если у вас есть открытый файл, a pathccnf
— если у вас есть путь к файлу, который может быть как открыт, так и закрыт.
Чтобы узнать допустимые значения аргумента name, обратитесь к документу [SUS2002]
или к соответствующей странице man.
Возвращаемое значение интерпретируется так же, как и в случае вызова sysconf (см.
предыдущий раздел). В частности, значение -1 свидетельствует об ошибке, если только
было изменено значение еггпс, поэтому перед вызовом pathccnf или fpathccnf вы должны
обнулить еггпс. Код, использующий вызов pathccnf, был приведен в разделе 1.5.4.
1.5.7 Системный вызов conf st г
Вызов ccnfstr используется так же, как й sysccnf, но работает со строковыми
значениями, а не численными:
confstr — получает конфигурационную строку
«include <unistd.h>
size_t ccnfstr(
int name,
char *buf,
size_t len
);
/* Возвращает длину
errno) */
строки
/* название опции или лимитированной
характеристики */
/* возвращаемое строковое значение */
/» размер буфера (buf)*/
или 0 в случае сшибки (при сшибке устанавливает
Выполняя этот вызов, вы присваиваете аргументу len размер буфера, указатель на
который передается в функцию в качестве аргумента buf, а на выходе получаете в
буфере строку, заканчивающуюся нулевым символом. Если вся строка в буфер не
помещается, она обрезается. Возвращаемым значением является размер буфера,
необходимый для хранения всей строки. Если возвращается 0, значение еггпс
характеризует ошибку, но только в том случае, если оно изменилось. Если значение
52 ГЛАВА 1 Фундаментальные концепции
еггпо не изменилось, это означает, что был указан некорректный аргумент паше. Таким
образом, перед вызовом confstr тоже следует обнулять еггпо.
1.5.8 Определение конкретной ОС
Лучше всего определять возможности системы при помощи символов POSIX/SUS,
но иногда нам нужно узнать, с чем именно мы имеем дело: с Solaris, HP/UX, AIX,
FreeBSD, Darwin, Linux или с чем-то другим. Иногда нужно узнать даже основной и
дополнительный номера версии ОС. Мы уже знаем одну ситуацию, в которой это
уместно: нам не хотелось бы определять символ _P0SIX_S0URCE для систем FreeBSD
и Darwin. А вот еще пример: ОС содержит ошибки или поддерживает возможности
(например сообщения System V), не определенные в стандарте, которому
соответствует ОС (см. следующий раздел).
Универсального способа проверки ОС не существует, а некоторые системы
(например Solaris) не поддерживают вообще никаких способов. Большинство систем
позволяют задать нужный символ после включения какого-то заголовочного файла,
но информация об ОС нужна нам гораздо раньше, даже до включения файла unistdh,
который, как правило, должен быть первым включаемым заголовочным файлом
POSIX или стандартного С. Таким образом, мы устанавливаем символ во время
компиляции, удостоверяясь в том, что каждой системе соответствует свой символ,
например:
$ gcc -DS0LARIS -с xyz.c
На самом деле командные строки являются более сложными и хранятся в make-
файлах, но, думаю, идею вы поняли.
1.5.9 Дополнительные возможности
Системы вроде FreeBSD и Darwin весьма консервативны в своих заявлениях о
соответствии POSIX, однако они гораздо более функциональны, чем гипотетическая
система, поддерживающая только те возможности, которые определены в стандарте
POSIX I988 или POSIXI990. Например, помимо всего прочего FreeBSD
поддерживает сетевые сокеты и механизмы межпроцессного взаимодействия System V,
которые большей частью работают так, как положено.'
ОС не может установить и уровень POSIX, и символ опции, говорящий о том, что
система имеет дополнительные возможности, потому что (I) некоторые
возможности вроде тех, что я назвал, не были определены ни в каком стандарте POSIX до
SUS3 (POSIX2001, если хотите) и (2) в SUS3 они обязательны, поэтому они не
имеют никакого символа. FreeBSD не может решить эту проблему, заявив, что она
соответствует, например, спецификации SUS1, потому что это не так.
* Darwin 6.6 — версия, реализованная в Mac OS X 10.2.6, — не поддерживает сообщения System
V, хотя другие механизмы межпроцессного взаимодействия System V ею поддерживаются.
ГЛАВА 1 Фундаментальные концепции 53
5 - з же время нам совсем не хочется включать в свой код многочисленные провер-
•:-' символа FREEBSD, потому что это не то, что мы имеем в виду: на самом деле мы
-чеем в виду «сокеты» или «механизмы межпроцессного взаимодействия System V».
Неоправданное создание зависимостей от ОС осложняет портирование кода, пото-
«г. что программисты, выполняющие портирование, не будут знать, зачем вы
написали зависящий от системы тест. Программист, портирующий программу на Linux,
ч :жет быть экспертом по Linux, но тонкости FreeBSD могут быть ему неизвестны.
Таким образом, для так называемых дополнительных возможностей —
определенных в каком-то стандарте, но не в том, которому соответствует ОС — нам нужны
с: полнительные символы. Я буду представлять их по ходу дела, и иногда они будут
встречаться вам в программах-примерах. Выглядеть это будет подобным образом.-
*:' (defined(_XOPEN_SOURCE) && _X0PEN_S0URCE >= 4) || defined(BSD_DERIVED)
♦aefine HAVE_SYSVMSG
»endif
'Универсальный заголовочный файл, приведенный в следующем разделе,
определяет символ BSD_DERIVED, если определен символ FREEBSD или DARWIN (см. также раз-
сел 1.5.8).
7с. что я делаю, можно рассматривать как реализацию идеи «частичного
соответствия» — хоть разработчикам стандартов она и внушает ужас, программисты иногда
находят ее полезной.
1.6 Универсальный заголовочный файл
Зо всех программах-примерах этой книги я включаю универсальный
заголовочный файл defs.h, который содержит запрос уровня соответствия SUV_SUS2 и
включает файл suvreq.h, о котором я говорил в разделе 1.5-3. Кроме того, он содержит
чного директив «include, включающих часто используемые заголовочные файлы,
но ничего лишнего я не включаю. Менее популярные заголовочные файлы мы бу-
сем включать по мере необходимости. Хоть ниже это и не показано, файл defs.h
содержит также прототипы функций-утилит, таких как syserrmsg (см. раздел 1.4.1).
Вот основная часть файла defs.h или, по крайней мере, той его версии, которую я
использовал при написании этой книги; самую свежую версию этого файла
можно найти на Web-сайте [AUP2003]:
«if defined(FREEBSD) || defined(DARWIN)
wefine BSD_DERIVED
tendif
*:f !defined(BSD_DERIVED) /• _P0SIX_S0URCE налагает чрезмерные ограничения */
«efine SUV_SUS2
«include "suvreq.h"
«endif
54 ГЛАВА 1 Фундаментальные концепции
«ifdef _GNUC_
((define _GNU_SOURCE /* для иаксимальнсгс ссстветствия GNU стандарту С99 •/
#endif
«include <unistd.h>
«Ifndef cpiuspius
«include <stdbcci.h> /* тслькс С99 */
«endif
«include <sys/types. h >
«include <tlme.h>
«include <limits.h>
«ifdef SOLARIS
«define _VA_LIST /* заблскирсвать определения в файле stdic.h */
«endlf
«include <stdlc.h>
«Ifdef SOLARIS
«undef _VA_LIST
«endlf
«Include <stdarg.h> /* месте для определения _VA_LIST */
«include <stdlib.h>
«Include <stddef.h>
«include <strlng.h>
«include <ctype.h>
«Include <errnc.h>
«include <fcntl.h>
«include <assert.h>
«include "ec.h"
Директивы «ifdef SOLARIS во второй половине файла ясно показывают, почему иногда
нужен код, зависимый от ОС. В зависимости от стандарта макросы «va»
(используемые в С при работе*с функциями, принимающими разное число аргументов)
могут определяться в файле stdio.h или stdarg.h. Solaris в этом смысле отличается от
других систем, поэтому я использовал небольшую хитрость для блокирования
определений в файле stdio.h. Это некрасиво и, наверное, следовало бы придумать что-
то другое, но иногда просто нет другого способа.
Очередность включения файлов немного необычна и, вероятно, их можно было
бы частично упорядочить по алфавиту, но когда я пытался это сделать, я
сталкивался с зависимостями и не мог без проблем скомпилировать программу на той
или иной системе. В итоге я пришел к тому, что вы видите. Предполагается, что
зависимостей от порядка быть не должно, потому что в заголовочном файле
нужно включать другие файлы, от которых он зависит, но они есть.
ГЛАВА 1 Фундаментальные концепции 55
1.7 Дата и время
В UNIX часто используются два вида времени: календарное время и время
выполнения. В этом разделе я опишу системные вызовы и функции, служащие для
работы с временем. К теме времени имеют отношение таймеры интервалов, которые
обсуждаются в разделе 97.
1.7.1 Календарное время
Календарное время используется в качестве времени последнего обращения к файлу,
времени последнего изменения файла или его статуса, а также для определения
момента регистрации пользователя в системе, для отображения даты и текущего
времени и т. д.
Календарное время обычно представляется как:
• арифметический тип time_t, представляющий собой число секунд, прошедших
с момента начала эпохи, которым традиционно считается полночь 1 января 1970
года по UTC Этот способ хорошо подходит для хранения времени в файлах и
передачи в функции, потому что он компактен и не зависит от часового пояса;
• структура timeval, которая содержит время в секундах и микросекундах
(используется для представления и календарного времени, и времени выполнения);
• структура tin, которая содержит время, разделенное на год, месяц, день, час, минуту,
секунду и кое-что другое;
• строка, такая как Tue Jul 23 09:44:17 2002.
Есть целый набор библиотечных функций — большей частью функций
стандартного С — служащих для преобразования времени из одной формы в другую.
Схема, поясняющая работу этих девяти функций, изображена на рис. 1.1. Как видите,
имена этих функций не имеют никакого смысла (ну почему их нельзя было назвать
как-нибудь вроде cvt_tm_to_time?). Десятая функция — time — позволяет получить
текущее время.
Ниже приведены краткие описания структур tin и timeval" и основных функций,
предназначенных для работы с календарным временем.'"
Универсальное синхронизированное время (Universal Time Coordinated), ранее известное
как среднее время по Гринвичу (Greenwich Mean Time, GMT).
' Есть также одна устаревшая структура timet), которая содержит время в секундах и
миллисекундах, и устаревшая функция fti»e, служащая для ее заполнения. Забудьте о них и
используйте вместо этого функцию gettimeofday.
" Некоторые из этих функций стандартного С имеют также реентерабельные формы
(например ctime.r), не использующие статический буфер, но я их описывать не буду.
3 Зак. 4030
56 ГЛАВА 1 Фундаментальные концепции
Рис. 1.1. Функции преобразования времени
struct tm — структура, хранящая время, разделенное на компоненты
struct tm {
int tm_sec; /* секунда [0,61] (2 корректировочных секунды) */
int tmjnin; /* минута [0,59] */
int tmjicur; /* час [0,23] */
int tmjnday; /* день месяца [1,31] */
int tffl_mcn; /• месяц [0,11] */
int tm_year; /* гсд, отсчитываемый ст 1900 */
int tm_wday; /* день недели [0,6] (0 = воскресенье) */
int tm.yday; /* день года [0,365] •/
int tm_isdst; /* флаг перехода на летнее время •/
>;
struct timeval —
struct timeval
time_t tv_
suseocnds_
>;
структура,
{
sec;
t tv_usec;
используемая функцией
/*
/•
секунды */
микросекунды •/
gettimeofday
time — возвращает текущие дату
«include <time.h>
time_t time(
time_t «t
/•
Возвращает время
или
/•
и время
NULL или
как тип timet
возвращаемое
-1 в случае ошибки (errnc не
время
•/
определено)
*/
ГЛАВА 1 Фундаментальные концепции 57
gettimeofday — возвращает текущие дату и время
#inciude <sys/time.h>
int gettimeofday(
struct timevai «tvaibuf,
void «dummy
);
1* Возвращает О а случае успеха
устанавливать errno) */
/•
/•
или
возвращаемое
асегда NULL
-1 (возможно)
как тип timevai
ареня */
•/
при ошибке
(может
localtime — преобразует тип
поненты
«include <time.h>
struct tin *iocaitime(
const time_t *t
time_t в
/* аремя
/* Возвращает время, разделенное на
(устанавливает errno) */
местное
•/
компоненты
время,
разделенное на ком-
или NULL
8 случае ошибки
gmtime — преобразует
#inciude <time.h>
struct tin *gmtime(
const time_t *t
);
/* Возаращает аремя,
(устанавливает errno)
тип time
/*
t во время
аремя */
UTC,
разделенное на компоненты
*/
разделенное
или
NULL в
на компоненты
случае
ошибки
mktime — преобразует местное
time_t
#inciude <time.h>
time_t mktime(
struct tm *tmbuf
);
/* Возвращает аремя или
/*
время,
время,
разделенное
разделенное
-1 а случае ошибки (errno
на
не
на компоненты,
компоненты
определено)
*/
*/
В ТИП
с time — преобразует тип
«include <time.h>
char *ctime(
const time_t *t
);
/* Возаращает строку
или
timet в строчное местное время
/* время •/
NULL а случае ошибки (errno не определено) */
58 ГЛАВА 1 Фундаментальные концепции
asctime — преобразует
стное время"
«include <time.h>
char *asctime(
const struct tm '
);
/* Возвращает строку
время
tmbuf
разделенное на
/* время,
или NULL в случае
компоненты, в строчное
разделенное на
сшибки
(errnc
компоненты */
не определено)
*/
ме-
strftime — преобразует время, разделенное на компоненты, в
отформатированную строку
«include <time.h>
size_t strftime(
char *buf,
size_t bufsize,
const char «format,
const struct tm «tmbuf
);
/« Возвращает числе байтов
/• выходной буфер «/
/• размер буфера •/
/• фермат */
/* время, разделенное на компоненты */
или 0 в случае сшибки (errnc не определено) */
wesftime — преобразует время, разделенное на компоненты, в
отформатированную строку широких символов
«include <wchar.h>
size_t wcsftime(
wchar_t «buf,
size_t bufsize,
const wchar_t «format,
const struct tm «tmbuf
/• выходной буфер •/
/* размер буфера */
/* фермат •/
/* время, разделенное на компоненты */
/• Возвращает числе символов wchar_t или 0 в случае сшибки (errnc не
определено) •/
getdate — преобразует строку во время, разделенное на компоненты, по
заданным правилам
«include <time.h>
struct tm *getdate(
const char «s /* преобразуемая строка */
);
/• Возвращает время, разделенное на компоненты, или NULL в случае сшибки
(устанавливает getdate_err) */
' В SUS не сказано, что функция asctime возвращает при ошибке NULL, но проверять это стоит.
ГЛАВА 1 Фундаментальные концепции 59
strptime — преобразует строку во время, разделенное на компоненты, с
использованием формата
«include <tirae.h>
char »strptlrae(
ccnst char »s, /» преобразуемая строка */
const char «format, /» фермат »/
struct tm «tmbuf /* время, разделенное на компоненты (вывод) «/
);
/* Возвращает указатель на первый необработанный символ или NULL в случае
сшибки (еггпс не определено) */
Согласно стандарту, никакие из этих функций не устанавливают еггпс, а
большинство из них даже не возвращают никаких признаков ошибки, хотя я все равно
советую проверять любые возвращаемые указатели на предмет того, не равны ли они
NULL. Функция getdate ведет себя по-настоящему странно: при ошибке она
присваивает больше нигде не используемой переменной (или макросу) getdate_err
значение от 1 до 8, характеризующее природу проблемы; более подробную
информацию об этом можно найти в [SUS2002].
В большинстве систем UNIX тип time.t реализован как длинное целое число,
которое часто является лишь 32-разрядным (со знаком); этого хватит для подсчета
секунд до 19 января 2038 года.' Рано или поздно он будет сделан 64-разрядным."
Чтобы подготовиться к этому, всегда используйте time.t (вместо, скажем, типа long) и не
делайте никаких предположений о том, что это такое. Тип time.t уже не годится для
представления исторических дат, так как на многих системах он (если использовать
максимальное отрицательное число) позволяет вернуться лишь к 1901 году.
Не зная, чем представлен тип time.t, трудно вычесть один его экземпляр из
другого, поэтому для решения этой популярной задачи была разработана специальная
функция:
difftime — вычитает одно значение time
«include <time.h>
double difftime(
time.t tirael,
time.t timeO
);
/« Возвращает интервал
возвращается) */
/•
/•
tirael -
время
время
timeO
•/
•/
_t из другого
в секундах
(информация
об
сшибках не
' Если эта книга и далее будет переиздаваться с периодичностью 19 лет, в 2038 году я буду
работать над ее четвертым изданием.
" Ясно, что столько разрядов не нужно, но такова уж природа компьютеров: за 32 часто
следует 64.
60 ГЛАВА 1 Фундаментальные концепции
Функция gettimeofday — это более точная альтернатива time, но никакие другие
функции не принимают в качестве аргумента структуру tlmeval. Однако они
примут ее поле tv.seo, потому что оно имеет тип time.t, а с микросекундами вы
можете работать отдельно. О типе tv_useo можно с уверенностью сказать только то, что
он является каким-то целочисленным типом со знаком. Однако я не думаю, что он
имеет более 32 разрядов, так как в секунде только миллион микросекунд, поэтому
приведение поля tv.useo к типу long будет работать без проблем. Чтобы определить
время выполнения какой-то операции, вы можете вызвать в ее начале и конце
функцию gettimeofday, после чего использовать функцию difftime для нахождения
разности полей tv_seo и обычное вычитание для определения разности полей tv.useo.
Некоторые системы (FreeBSD, Darwin и Linux, но не Solaris) используют второй
аргумент функции gettimeofday для возврата информации о часовом поясе, но это
нестандартное поведение. Большинство систем информируют об ошибке,
возвращая -1, и даже устанавливают errno. Те, которые этого не делают, всегда
возвращают 0, так что вы всегда можете без опасений проверять возвращаемое значение на
предмет ошибки.
Время time.t всегда представляется в терминах UTC, но время, разделенное на
компоненты (struot tm), и строковое время могут быть представлены и как UTC, и как
местное время. При вводе или выводе времени удобно использовать местное
время, что приводит к некоторым сложностям. Компьютеру необходим какой-то
механизм, позволяющий узнать, в каком часовом поясе он работает и каким является
текущее время: стандартным или летним. Хуже того, при выводе времени,
относящегося к прошлому, компьютер должен узнать, стандартным или летним оно было
тогда. Функции, работающие с местным временем, справляются с этой проблемой
довольно хорошо.
Часовой пояс пользователя хранится в переменной среды TZ или, если TZ не
задана, как системный параметр по умолчанию. Для обеспечения совместимости
систем предусмотрены одна функция и три глобальных переменных, служащих для
работы с параметрами часового пояса и летнего времени:
tzset — задает информацию
«inolude <time.h>
/* В FreeBSD/Darwin три
extern int daylight;
extern long timezone;
extern ohar *tzname[2];
void tzset(void);
о часовом поясе
оледующих переменных не определены */
/* иопользуетоя ли летнее время? */
/» омещение чаоового пояоа в оекундах */
/* отроки, характеризующие чаоовой пояо »/
Функция tzset позволяет приложению каким-то образом получить часовой пояс (из
TZ или откуда-то еще), устанавливая при этом три глобальных переменных.
ГЛАВА 1 Фундаментальные концепции 61
• Переменной daylight присваивается ноль (false), если время часового пояса
никогда не должно корректироваться с использованием летнего времени, и
ненулевое значение (true) в обратном случае. Механизм перехода на летнее время
и обратно встроен в библиотечные функции и зависит от конкретной реализации.
• Переменной timezone присваивается разница в секундах между UTC и местным
стандартным временем (чтобы получить разницу в часах, разделите это
значение на 3600).
• tzname — это массив из двух строк: элемент tznametO] определяет локальный
часовой пояс в контексте стандартного времени, a tzname[1] — летнее время.
Эти глобальные переменные отсутствовали в стандартах POSIX вплоть до POSIX2002
и не определены в FreeBSD и Darwin. Однако вызывать tzset или работать
непосредственно с глобальными переменными обычно не требуется. Стандартные
функции делают это по мере надобности.
Тем не менее, пример я приведу:
tzset();
printf("daylight = Xd; timezone = Xld hrs.; tzname = Xs/Xs\n",
daylight, timezone / 3600, tzname[0], tzname[1]);
Выполнение этого кода приводит к таким результатам:
daylight = 1; timezone = 7 hrs.; tzname = MST/MDT
Подробное описание функций работы с календарным временем, особенно тех, что
используют строки формата, можно найти в [SUS2002] или хорошей книге по
стандартному С, такой как [Наг2002]. Вот некоторые факты, о которых стоит помнить.
• Игнорируя возможность возврата ошибки, функцию ctime(t) определяют как
asctime(lccaltime(t)).
• Значение поля tm.sec структуры tm может достигать 61 (а не 59) — это нужно
для поддержки корректировочных секунд.
• Значение поля tm.year структуры tm отсчитывается от 1900 года. Это вполне
соответствует всем требованиям Y2K, но кажется странным (2002 год
представляется как 102).
• Не менее странно и то, что функции ctime и asctime завершают выходную
строку символом новой строки.
• Некоторые функции принимают указатель на переменную типа time_t, хотя ее
можно было бы передавать и по значению. Это сохранилось с ранних дней UNIX,
когда в С не было типа данных long, и вместо него нужно было передавать
массив, включающий две 16-разрядных переменных int.
• Аргумент time_t *, принимаемый функцией time, совершенно не нужен, и ему
следует присваивать значение NULL. Все выглядит так, как будто он
предоставляет функции time буфер, но она не нуждается ни в каком буфере, потому что
возвращает значение time_t, имеющее арифметический тип.
62 ГЛАВА 1 Фундаментальные концепции
• В большинстве представленных здесь функций, возвращающих указатель на
структуру tm, используется статический буфер, так что используйте этот буфер
или копируйте его перед вызовом другой такой функции. Существуют
реентерабельные версии функций, которые вы при необходимости можете
использовать в многопоточных программах. Имена этих версий включают суффикс _г.
• Функции, использующие формат, — strftime, wcsftime, getdate и strptime — очень
сложны, но при этом очень полезны и потому достойны изучения. Первые две
функции входят в стандартный С, последние две определены в SUS. Функция
getdate не реализована в ОС FreeBSD и Darwin, хотя ее версия есть в
библиотеке GNU С, которую при желании вы можете установить.
• Для получения дат и времен, вводимых пользователями, используйте функцию
getdate, а при чтении файлов, содержащих даты — strptime или getdate.
Почему? Потому что функция strptime принимает только один формат, из-за чего
пользователям нужно приложить слишком много усилий, чтобы корректно
отформатировать дату и время. Функция getdate поддерживает целый ряд
форматов, пробуя их по очереди в поисках подходящего варианта.
1.7.2 Время выполнения
Время выполнения используется для измерения временных интервалов и
длительности выполнения процесса, а также для учета разных показателей. Ядро
записывает время выполнения каждого процесса автоматически. Интервалы измеряются
в разных единицах: от наносекунд до сотых долей секунды.
С типами и функциями, служащими для работы со временем выполнения,
разобраться не легче, чем с типами и функциями календарного времени. Опишу
сначала основные типы:
• clock_t — это арифметический тип стандартного С (как правило, он
реализован как long, но не полагайтесь на это), представляющий временной интервал в
элементах CLOCKS_PER_SEC или тактах часов;
• структура timeval представляет временной интервал в секундах и
микросекундах (как было сказано в предыдущем разделе, она используется и для
представления календарного времени);
• структура timespec представляет временной интервал в секундах и наносекундах.
Вот структура timespec (структуру timeval вы уже видели), которая определена в
заголовочном файле <time.h>:
struct timespec — структура, представляющая временной интервал
и наносекундах
struct timespec {
time_t tv_sec;
long tv_nsec;
};
/•
/•
секунды */
наносекунды
•/
в секундах
ГЛАВА 1 Фундаментальные концепции 63
Для измерения временных интервалов предназначены три основных функции: getti-
meofday, которая была описана в предыдущем разделе, olook и times. Описания двух
последних функций и структуры, используемой функцией times, приведены ниже:
clock — возвращает время выполнения
«inolude <time.h>
olook_t olook(vold);
I* Возвращает время, предотавленное в единицах CLOCKS_PER_SEC, или -1
олучае ошибки (еггпо не определено) */
times — возвращает информацию о времени выполнения процесса и
дочернего процесса
#inolude <sys/tlmes.h>
olook_t times(
struot tins «buffer /* возвращаемая информация о времени */
);
/* Возвращает время, предотавленное в тактах чаоов, или -1 в олучае ошибки
(устанавливает еггпо) */
struct tms — структура, используемая системным вызовом times
struot tins {
•/
>;
olook_t tms_utiine;
olook_t tms_outiine
olook_t tms_stiine;
olook_t tins_ostinie;
/* время выполнения процеооа в
пользовательоком режиме */
/* оуммарное время выполнения завершившихся
дочерних процеооов в пользовательоком режиме
/* время выполнения процеооа в режиме ядра */
/• оуммарное время выполнения завершившихся
дочерних процеооов в режиме ядра */
Функция times возвращает время, прошедшее с некоторого момента (как правило,
с момента загрузки системы), которое иногда называют реальным временем. Этим
она отличается от функций из предыдущего раздела, которые возвращают время,
прошедшее с начала эпохи (обычно с 1 января 1970 года).
Функция times представляет время в тактах часов; узнать число тактов,
совершаемых часами в секунду, можно при помощи вызова sysoonf (см. раздел 1.5.5):
olook_tioks = sysoonf(_SC_CLK_TCK);
Кроме того, функция times заносит в структуру tms более специфичную информацию:
• tms_utirne — время выполнения кода процесса в пользовательском режиме.
Учитывается только время, потраченное процессором, но не время, проведенное в
ожидании;
64 ГЛАВА 1 Фундаментальные концепции
• tms.stime — время, потраченное процессором на выполнение системных
вызовов по поручению процесса;
• tms_cutime — суммарное время, потраченное процессором на выполнение всех
завершившихся дочерних процессов конкретного процесса и дочерних
процессов, для которых родительский процесс выполнил системный вызов wait,
в пользовательском режиме (вызов wait обсуждается в главе 5);
• tms.cstime — суммарное время, потраченное процессором на выполнение
завершившихся дочерних процессов и на ожидание завершения дочерних
процессов, в режиме ядра.
Хотя функция clock возвращает тот же тип, что и times, возвращаемое ею значение
представляет не реальное время, а время, израсходованное процессором с
момента запуска процесса. Измеряется оно в единицах, определяемых макросом CLOCKS.
PER.SEC, а не в тактах часов. Оно эквивалентно (если закрыть глаза на различие единиц
измерения) сумме tms_utime + tms.stime.
CLOCKS_PER_SEC определяется в SUS как 1 000 000 (т. е. единицами измерения
являются микросекунды), но системы, соответствующие другим спецификациям, могут
придерживаться другого мнения, так что всегда используйте макрос (в FreeBSD
CL0CKS_PER_SEC определяется как 128, а в Darwin — как 100).
Если clock.t является 32-разрядным целым числом со знаком (а зачастую именно
это и имеет место) а в качестве единиц используются микросекунды, предельное
значение достигается примерно за 36 минут, но функция clock хотя бы начинает
отсчет при запуске процесса. Функция times, использующая такты часов, может
работать дольше, но она и приступает к делу раньше — намного раньше, если
система UNIX работает недели и месяцы. Если используются такты часов,
32-разрядное значение clock_t со знаком достигает предела примерно за 250 дней, поэтому,
перезагружая систему раз в полгода, вы можете решить эту проблему.
Однако вы не можете полагать, что значение clock_t является 32-разрядным целым
числом со знаком, что оно имеет знак или вообще является целым. Этот тип
может быть реализован как unsigned long или даже как double.
В оставшейся части этой книги я доя измерения времени выполнения различных
программ буду использовать функцию times, потому что она предоставляет нам сразу
и реальное время, и время, проведенное процессором в пользовательском режиме
и режиме ядра. Вот код двух вспомогательных функций, одна начинает отсчет
времени, а вторая останавливает его и выводит результаты:
«include <sys/times.h>
static struct tins tbufl;
static clock.t reall;
static long clock.ticks;
void timestart(void)
{
ec_neg1( reall = times(itbufl) )
ГЛАВА 1 Фундаментальные концепции 65
/* рассматривайте все возвращаемые значения -1 как ошибки */
ec_neg1( clcck_ticks = sysccnf(_SC_CLK_TCK) )
return;
EC_CLEANUP_BGN
EC_FLUSH("timestart");
EC_CLEANUP_END
}
void timestcp(char *rasg)
{
struct tins tbuf2;
clcck_t real2;
ec_neg1( real2 = times(&tbuf2) )
fprintf(stderr,"Xs:\n\tV'Tctal (user/sys/real)\", X.2f, X.2f, X.2f\n"
"\t\"Child (user/sys)\", X.2f, X.2f\n", rasg,
((dcuble)(tbuf2.tras_utime + tbuf2.tms_cutime) -
(tbuf1.tras_utirae + tbuf1.tms_cutirae)) / clcck_ticks,
((dcuble)(tbuf2.tras_stirae + tbuf2.tras_cstiine) -
(tbuf1.tms_stirae + tbuf1.tras_cstirae)) / clcck_ticks,
(dcuble)(real2 - realD / clcck_ticks,
(double)(tbuf2.tms_cutime - tbufl.ti«s_cutii«e) / olcok_ticks,
(dcuble)(tbuf2.tras_cstirae - tbufl.tms_ostime) / clock_tioks);
return;
EC_CLEANUP_BGN
EC_FLUSH("timestcp");
EC_CLEANUP_END
}
Если нам нужно измерить какой-то временной интервал, мы просто вызываем в его
начале функцию timestart, а в конце — timestop. В следующем примере я именно
это и делаю, выполняя также для сравнения результатов вызовы gettimeof day и clock:
«define REPS 1DDDDDD
«define TV_SUBTRACT(t2, t1)\
(double)(t2).tv_sec + (t2).tv_useo / 1DDDDDD.D -\
((double)(t1).tv_seo + (t1).tv_usec / 1DDDDDD.D)
int raain(void)
<
int i;
char msg[1DD];
olcok_t 01, o2;
struct tiraeval tv1, tv2;
snprintf(fflsg, sizeof(nisg), "Xd getpids", REPS);
ec_neg1( d = oicck() )
gettimeofday(itv1, NULL);
66 ГЛАВА 1 Фундаментальные концепции
timestartO;
for (i - 0; i < REPS; i++)
(void)getpidO;
(void)sleep(2);
tirnestop(msg);
gettimeofday(&tv2, NULL);
eo_neg1( o2 = olook() )
printf("olook(): X.2f\n", (double)(c2 - o1) / CL0CKS_PER_SEC);
printf("gettimeofday(): X.2f\n", TV_SUBTRACT(tv2, tv1));
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
>
Выполнив этот код под управлением Linux, я увидел следующее (на Solaris, FreeBSD
и Darwin были получены похожие результаты):
1000000 getpids:
"Total (user/sys/real)", 0.72, 0.44, 3.19
"Child (user/sys)", 0.00, 0.00
clock(): 1.16
gettimeofdayO: 3.19
Итак, функция clock возвратила 1,1б", что равняется сумме 0,72 и 0,44, a gettimeofday
возвратила такое же значение истекшего времени, как и times. Это хорошо, так как
другие результаты потребовали бы дополнительных объяснений.
Если точность функции times недостаточна, используйте вместо нее getrusage (см.
раздел 5.16).
1.8 О коде примеров
Эта книга содержит много кода, который вы можете свободно использовать при
обучении и даже включать в коммерческие продукты, но тогда вы должны
отметить это — например, в диалоговом окне About или на специальной странице
руководства или книги. Более подробную информацию об этом можно найти по
адресу www.basepath.com/aup/copyrightJotm.
Некоторые фрагменты кода ради экономии места были сокращены (например, код
обработки ошибок, приведенный в этой главе, и код файла defs.h), но вы можете
найти полные файлы в Интернете по адресу www.basepath.com/aup [AUP2003] (этот
сайт содержит и много другой информации, такой как список ошибок и опечаток).
По мере обнаружения ошибок в этой книге опубликованный в Интернете код
будет обновляться, так что если что-то покажется вам неверным, проверьте сначала
' Работая над изданием 1985 года, я за 1,43 секунды смог выполнить лишь 1000 вызовов getpid!
ГЛАВА 1 Фундаментальные концепции 67
более новый код. Если после этого вы останетесь при своем мнении, напишите мне
по адресу aup@basepath.com. Если вы иначе смотрите на какие-то вещи, если вам
известен более эффективный способ решения какой-то задачи или если вы
просто захотите сообщить мне, что это самая лучшая книга из всех, что когда-либо
попадались вам в руки, я с удовольствием прочитаю и такие письма.
Примеры кода написаны на С99, но даже если вы используете более старый
компилятор, новые возможности языка (например типы intptr_t и bool) не должны
привести к проблемам, так как вы можете определить собственные макросы и
директивы typedef. Основную часть кода я протестировал, используя компилятор gcc,
на компьютере с процессором Intel под управлением SuSE Linux 8.0 (на базе Linux
2.4), FreeBSD 4.6 и Solaris 8, а также на компьютере iMac на базе процессора PowerPC
под управлением Darwin 6.6 (входящей в состав Mac OS X 10.2.6). С течением
времени код будет протестирован и на других системах, а вся заслуживающая
внимания информация будет опубликована на Web-сайте книги.
1.9 Полезные ресурсы
Ниже приведен список ресурсов (кроме этой книги!), которые окажутся
полезными каждому UNIX-программисту.
• Хорошее руководство по вашему языку. Лучшим руководством по С является
книга «С A Reference Manual» (Harbison and Steele [Har2002]), a no C++ — наверное,
книга «C++ Programming Language» (Bjarne Stroustrup [Str2000]). Суть этой
рекомендации в том, что у вас должна быть книга, в которой, пусть и приложив некоторые
усилия, можно было бы найти ответы на все вопросы. Раздобудьте такую книгу, и
вы не только освободите рабочий стол, но и сэкономите много времени.
• Спецификация Open Group SUS (Single UNIX Specification), которую можно
найти по адресу ivww.unix.org/version3. Это фантастический сайт. В левом
фрейме вы выбираете имя функции, а в правом отображается ее спецификация.
Спецификацию SUS можно также приобрести на компакт-диске и в печатном виде,
но Web-сайтом вы можете пользоваться бесплатно.
• Дискуссионная группа Usenet comp.unix.programmer. Каким бы сложным ни
казался вам какой-то вопрос по UNIX, задайте его в этой группе, и вы почти
наверняка получите ответ. Возможно, он окажется неверным, но, скорее всего,
он укажет вам на правильный путь (для доступа к дискуссионным группам
нужен почтовый клиент, поддерживающий работу с ними и с рассылками
новостей. Чтобы получать рассылки новостей, обратитесь к своему
интернет-провайдеру или заплатите администрации сайта вроде wwwsupernews.com или
www.giganews.com. Кроме того, вы можете получить доступ к дискуссионным
группам при помощи сайта Google Groups, расположенного по адресу hup://
groupsgoogle.com/grphp). В Интернете есть много форумов, посвященных UNIX,
например www.unix.com, но они не идут ни в какое сравнение с
comp.unix.programmer. Задавайте в этой группе вопросы, посвященные только UNIX (или Linux,
или FreeBSD) — спросив что-нибудь о С или C++, вы рискуете нарваться на гру-
68 ГЛАВА 1 Фундаментальные концепции
бость. Для других тем предназначены отдельные дискуссионные группы (всего
их насчитывается несколько десятков тысяч). Кроме того, сайт Google Groups
позволяет искать информацию в архивах Usenet — поройтесь в них, и,
возможно, вы обнаружите, что на ваш вопрос уже был дан ответ лет 20 назад.
• Поисковая система Google. Вам кажется, что системный вызов sigwait
реализован в системе FreeBSD неверно? Вы не знаете, поддерживает ли Linux
асинхронный ввод-вывод? Вы можете искать нужную информацию методичным образом
(например, на сайтах FreeBSD или Linux) или же можете вместо этого немного
«погуглить». Примерно в половине случаев я нахожу то, что мне нужно, не на
официальном сайте, а на каком-нибудь таинственном форуме или чьем-то
личном Web-сайте, проиндексированном в Google.
• Библиотека GNU С — отличный источник примеров кода, которые можно
загрузить по адресу: wivwgnu.org/software/libc,
• Страницы man, входящие в состав вашей ОС. Искать информацию в этой
системе несколько неудобно (команда apropos может немного помочь), но зато,
обнаружив нужную страницу, вы получите исчерпывающий ответ на свой
вопрос. Посвятите некоторое время изучению страниц man, чтобы использовать их
максимально эффективно, когда придет время.
Упражнения
1.1. Напишите на С программу, выводящую «Hello World», используя заголовочный
файл defs.h и, по меньшей мере, один макрос обнаружения ошибок (скажем,
ес_пед1). Я включил в книгу это упражнение, чтобы вы установили примеры
программ, удостоверились в нормальной работе компилятора С и убедились
в наличии нужных инструментов, таких как текстовый редактор и утилита make.
Если у вас нет доступа к системе UNIX или Linux, решение этой проблемы тоже
является частью данного упражнения.
1.2. Напишите программу, выводящую информацию о том, какой версии POSIX и
SUS соответствует ваша система. Сделайте это, используя разные запросы
(например SUV.SUS2).
1.3. Напишите программу, отображающую все значения sysconf, используемые вашей
системой. Придумайте методику, позволяющую сгенерировать все строки,
содержащие вызовы sysconf, не печатая каждый символ один за одним.
1.4. Сделайте то же самое, что и в пункте 1.3, используя вызов pathconf.
Спроектируйте программу так, чтобы путь можно было ввести в качестве аргумента.
1.5. Сделайте то же самое, что и в пункте 1.3, используя вызов confstr.
1.6. Напишите версию команды date, не использующую параметры командной
строки. Дополнительное задание: используйте как можно больше параметров.
Напишите эту команду в соответствии со спецификацией SUS или одним из
стандартов POSIX. Постарайтесь выполнить это упражнение, не заглядывая в
исходный код Gnu или любых других подобных примеров.
Базовые операции
файлового
ввода-вывода
2.1 Введение в операции файлового
ввода-вывода
В этой главе мы рассмотрим базовые операции ввода-вывода, применимые к
обычным файлам. Изучение этой темы будет продолжено в главе 3, где будут описаны
операции ввода-вывода с применением дополнительных системных вызовов.
Операции ввода-вывода над специальными файлами будут обсуждаться в главе 4, над
каналами — в главе 6, над именованными каналами — в главе 7 и над сокетами —
в главе 8.
Для начала я продемонстрирую простой пример, в котором используются возможно
уже знакомые вам четыре системных вызова: open, read, write и close. Эта функция
копирует данные из одного файла в другой (подобно команде ср):
«define BUFSIZE 512
void oopy(ohar «from, ohar *to) /* оодержит ошибку */
{
int fromfd = -1, tofd = -1;
ssize_t nread;
ohar buf[BUFSIZE];
eo_neg1( fromfd = open(from, O.RDONLY) )
eo_neg1( tofd = open(to, O.WRONLY | O.CREAT | O.TRUNC,
S_IRUSR | S_IWUSR) )
while ((nread = read(fromfd, buf, sizeof(buf))) > 0)
70 ГЛАВА 2 Базовые операции файлового ввода-вывода
if (write(tofd, buf, nread) != nread)
EC.FAIL
if (nread == -1)
EC.FAIL
eo_neg1( olose(fronifd) )
eo_neg1( olose(tofd) )
return;
EC_CLEANUP_BGN
(void)olose(fromfd); /* здеоь нельзя иопользовать макрос ec_neg1 I */
(void)close(tofd);
EC_CLEANUP_END
}
Попробуйте отыскать ошибку в теле функции (ключ к разгадке — в разделе 1.4.1).
Если вам это не удалось, то ответ вы найдете в разделе 2.9-
Сейчас я лишь вкратце опишу эту функцию, подробнее мы обсудим ее позднее.
Первый вызов open открывает исходной файл на чтение (о чем свидетельствует ключ
0.RD0NLY) и возвращает дескриптор файла, который будет использоваться в
последующих системных вызовах. Второй вызов open создает новый файл (о чем
свидетельствует ключ 0_CREAT) либо, если файл с заданным именем уже существует,
усекает его размер до нуля (0_TRUNC). В любом случае этот вызов открывает файл на
запись и возвращает его дескриптор. Третий аргумент вызова open — биты набора
прав доступа, которые будут присвоены вновь созданному файлу (в данном случае
права на чтение и запись выдаются только владельцу файла). Вызов read читает
данные в буфер. Указатель на буфер передается во втором аргументе. Размер
буфера в байтах передается в третьем аргументе. Он возвращает количество
прочитанных байт или ноль в случае достижения конца файла, или -1 в случае
возникновения ошибки. Вызов write записывает в файл данные из буфера. Указатель на буфер
передается во втором аргументе. Размер буфера в байтах передается в третьем
аргументе. Он возвращает число записанных байт, которое можно толковать как
признак ошибки, если оно не совпадает с заданным. И, наконец, вызовы close
закрывают дескрипторы файлов.
Для большего удобства, вместо условных операторов if, вызовов fprintf и
перехода на обработку ошибок, я использовал макроопределение eo_neg1. Оно завершает
работу функции, если его аргумент равен -1. Кроме того, я использовал
макроопределение EC.FAIL, которое всегда завершается с признаком ошибки (фактически
производит переход к коду обработки ошибочных ситуаций, ограниченному
макросами EC_CLEANUP_BGN и EC_CLEANUP_END). Эти макросы были описаны в разделе 1.4.2.
2.2 Файловые дескрипторы и системная
таблица файлов
Каждый процесс в UNIX владеет набором файловых дескрипторов, которые
нумеруются от 0 до N, где N — максимально возможное число одновременно открытых
ГЛАВА 2 Базовые операции файлового ввода-вывода 71
файловых дескрипторов. Число N зависит от версии ОС UNIX и ее конфигурации.
В любом случае это число не бывает меньше 16, а зачастую — много больше.
Чтобы получить фактическое значение числа N, можно вызвать sysconf (см. раздел 1.5.5)
с аргументом _sc_OPEN_MAX, например, так:
printf("_SC_OPEN_MAX = Xld\n", sysconf(_SC_OPEN_MAX));
В операционной системе Linux 2.4 мы получим число 1024, в FreeBSD — 957, в Solaris
— 256. На сегодняшний день вероятно уже не осталось ОС, где это число было бы
равно 16, разве только в некоторых встраиваемых системах.
2.2.1 Стандартные дескрипторы
Согласно принятым соглашениям, первые три файловых дескриптора передаются
процессу уже открытыми. Дескриптор с номером 0 соответствует устройству
стандартного ввода, дескриптор с номером 1 — устройству стандартного вывода, а с
номером 2 — устройству стандартного вывода сообщений об ошибках. Эти три
дескриптора обычно используются для управления терминалом. В текстах программ,
вместо номеров гораздо удобнее использовать константы stdin_FUENO, stdout_fileno
И STDERR.FILENO.
Утилиты UNIX должны выполнять чтение входных данных из STD1N_FILEN0 и
записывать результаты в STD0UT_FILEN0, благодаря этому командная оболочка может
объединять их в конвейеры. Для выдачи важных сообщений следует использовать
устройство STDERR_F1LEN0, поскольку любой вывод в STD0UT_FILEN0 может быть
перенаправлен в файл или передан дальше по конвейеру другой утилите, что является
обычной практикой при работе с командной оболочкой.
Любой из стандартных дескрипторов может соответствовать обычному файлу,
каналу, именованному каналу FIFO, устройству и даже сокету. Это отличный способ
обеспечить независимость от типа устройства ввода и/или вывода, однако это
возможно далеко не всегда. Например, экранный редактор наверняка не сможет
работать, если устройство стандартного вывода не является терминальным устройством.
К моменту запуска процесса, три стандартных дескриптора уже открыты и готовы
к использованию в вызовах read и write. Процесс может открывать другие
дескрипторы, которые используются для работы с файлами, каналами и т. п. Родительский
процесс может «завещать» потомку не только эти три стандартных дескриптора,
но и те, что открыл сам. Мы увидим это в главе 6, когда будем соединять процессы
каналами.
2.2.2 Использование файловых дескрипторов
Вообще, UNIX использует файловые дескрипторы для работы с чем угодно, что ведет
себя подобно файлам, если допустимы операции чтения и/или записи. Файловые
дескрипторы не используются в механизмах взаимодействия между процессами,
таких как очереди сообщений, к которым неприменимы вызовы read и write (для
этих целей предназначены специальные вызовы).
72 ГЛАВА 2 Базовые операции файлового ввода-вывода
Существует не так много способов открыть новый файловый дескриптор. Хотя мы
с вами пока не готовы рассмотреть их во всех подробностях, будет нелишним хотя
бы перечислить их:
• open — используется в большинстве случаев, когда можно указать путь к файлу,
включая обычные файлы, специальные файлы и именованные каналы;
• pipe — создает и открывает неименованный канал (глава 6);
• socket, accept и connect — используются для создания сетевых соединений.
Процесс А
Г
Дескрип- ~
торы -£
_7_
Ч
Процесс В
Дескрип- ~2
торы "J
V
Рис. 2.1. Файловые дескрипторы, описания открытых файлов и индексные узлы
Язык программирования С не предусматривает отдельного типа для описания
переменных-дескрипторов (как, например, для идентификаторов процессов), поэтому
мы просто используем обычный тип int'.
2.2.3 Записи в таблице файлов и их совместное
использование
Как уже говорилось в первой главе, дескриптор файла — это просто индекс в
таблице дескрипторов, которая имеется у каждого процесса. Каждый элемент этой
таблицы содержит указатель на запись в единой для всей системы таблице
описаний открытых файлов (так же известной под названием «системная таблица
файлов»). Запись в таблице файлов в свою очередь ссылается на данные в файле
(через копию индексного узла в памяти). Разные файловые дескрипторы, даже при-
Я подумывал было о том, чтобы в рамках этой книги ввести тип fid_t. Это сделало бы
листинги более понятными, но потом решил отказаться от этой идеи, поскольку в реальной
жизни вы постоянно будете сталкиваться с обычным типом int, так почему бы не
постараться привыкнуть к этому.
Описание
файла 1
(текущая позиция,
состояние,
режим доступа)
Описание
файла 2
(текущая позиция,
состояние,
режим доступа)
ГЛАВА 2 Базовые операции файлового ввода-вывода 73
надлежащие разным процессам, могут ссылаться на одну и ту же запись в таблице
файлов, как это показано на рис. 2.1.
Каждый вызов open или pipe создает новую запись в таблице и новый дескриптор.
На рисунке показано, как процесс А открыл один и тот же файл дважды и получил
дескрипторы с номерами 5 и 6. В результате были созданы две записи в системной
таблице файлов — 1 и 2. Затем с помощью механизма копирования дескрипторов
(копирование дескрипторов выполняют системные вызовы dup, dup2 и fork)
процесс А создает дескриптор 7 как копию дескриптора 5. Это означает, что теперь
дескрипторы 5 и 7 ссылаются на одну и ту же запись в таблице файлов. Процесс В,
потомок процесса А, получил копию родительского дескриптора 5, для него это
дескриптор 3.
Время от времени мы будем возвращаться к схеме на рис. 2.1, поскольку она
достаточно красноречиво говорит о многом. Например, когда мы будем говорить о
текущей позиции в файле (раздел 2.8), то увидим, что дескрипторы 5 и 7 процесса
А и дескриптор 3 процесса В, имеют одну и ту же текущую позицию в файле,
поскольку ссылаются на одну и ту же запись в системной таблице файлов.
2.3 Идентификаторы наборов прав доступа
В разделе 1.1.5 уже упоминалось, что для каждого файла определяются 9 бит прав
доступа: права на чтение, запись и исполнение для владельца файла, группы и всех
остальных. Вы можете наблюдать их в выводе команды is:
-rwxr-xr-x 1 marc users 29808 Aug 4 13:45 hello
Большинство представляют их себе как набор из 9 бит, следующих друг за другом
в определенном порядке (владелец, группа, остальные), но это совершенно не
обязательно. Так, стандартом POSIX1988 вместо восьмеричных чисел для описания прав
доступа были рекомендованы к применению специальные идентификаторы.
Принцип их именования следует шаблону s_lpwww, где р определяет режим доступа (R,
W или X), a www — кому выдается право на этот режим доступа (USR, GRP или ОТН).
Например, для предыдущего файла вместо восьмеричного числа 755 можно записать:
S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | - S_IXGRP | S_IR0TH | S_IX0TH
Существуют отдельные идентификаторы для USR, GRP и ОТН, которые описывают
полные права доступа. Именование этих идентификаторов следует форме S_lRWXw.
Здесь символ w определяет, кому выдается полное право доступа к файлу (от англ.
«whom» — «кому») — U, G или О. Таким образом, предыдущий пример может быть
записан в следующем виде:
S_IRWXU | S_IRGRP | S.IXGRP | S_IR0TH | S_1X0TH
Использование идентификаторов дает разработчикам ОС свободу выбора порядка
следования бит, описывающих права доступа к файлу. Даже несмотря на то, что они
74 ГЛАВА 2 Базовые операции файлового ввода-вывода
менее удобны в обращении и могут стать потенциальным источником ошибок".
В большинстве случаев вы будете использовать в своих программах ограниченное
число комбинаций прав доступа (одна-две — для файлов и немногим больше — для
каталогов). Поэтому будет совсем неплохо, если каждый из вариантов вы опишете
отдельным макроопределением и будете использовать их вместо длинных
последовательностей из s_l*. В этой книге мы будем пользоваться макросами, которые
описаны в файле defs.h (см. раздел 1.6):
«define PERM_DIRECTORY S_IRWXU
«define PERM_FILE (S.IRUSR | S.IWUSR | S_IRGRP | S_IR0TH)
Обратите внимание: макросам присвоены имена, которые соответствуют их
назначению, а не набору бит прав доступа. Имя PERH_FILE далеко не то же самое, что
PERM_RWURGO. Кроме того, изменением единственного макроса мы можем изменить
политику назначения прав доступа к файлам во всем приложении.
2.4 Системные вызовы open и с reat
open — открывает или
«include <sys/stat.h>
«include <fcntl.h>
int open(
const char *path,
int flags,
mode_t perms
создает файл
/* полное имя файла */
/* флаги */
/* права доступа (если создается новый файл) */
);
/* Возвращает дескриптор файла или -1 в случае ошибки (код ошибки находится
в глобальной переменной еггпо) */
Вызов open открывает существующий файл (обычный, специальный или
именованный канал) или создает новый, но в данном случае это может быть только
обычный файл. Специальные файлы создаются вызовом mknod (см. раздел 3-8.2), а
именованные каналы — вызовам mkfifo (см. раздел 7.2.1). После того, как файл будет
открыт, вы можете использовать полученный дескриптор в вызовах read, write, lseek,
close и многих других, которые мы будем обсуждать в следующей главе.
2.4.1 Открытие существующего файла
Для начала поговорим об открытии существующего файла, имя которого задается
аргументом path. Если через аргумент flags передается значение 0_RD0NLY, то файл
Один из рецензентов отметил, что в случае использования восьмеричных чисел, ядро
автоматически выполняет их преобразование во внутреннее представление, используемое
файловой системой.
ГЛАВА 2 Базовые операции файлового ввода-вывода 75
открывается только для чтения, 0.WR0NIY — только для записи, O.RDWR — как для
записи, так и для чтения*.
Процесс, который должен читать, писать или и то и другое, открывает файл с
использованием алгоритма, который был описан в разделе 1.1.5. Например, если
эффективный идентификатор пользователя у процесса совпадает с
идентификатором владельца файла, то для него должны быть установлены права доступа на
чтение и/или на запись.
В случае открытия существующего файла, аргумент perms игнорируется и в текстах
программ, как правило, вообще опускается. Таким образом, функция open
вызывается всего с двумя аргументами.
Ниже приводится пример кода, который открывает существующий файл:
int fd;
ec_neg1( fd = open("/home/marc/oldfile", 0_RD0NLY) )
Существует масса причин, по которым вызов open может завершиться с ошибкой.
В большинстве случаев у нас есть возможность сообщить пользователю о конкретной
проблеме. Неверное указание пути к файлу (enoent) требует иного решения, чем
отсутствие прав доступа (EACCESS). Используемая нами методика проверки на
наличие ошибок и выдачи сообщений замечательно справляется с этой задачей
(макроопределения «ее» были описаны в разделе 1.4.2).
Вызов open присваивает файловому дескриптору наименьший из доступных
номеров, но, как правило, вас это число не интересует. Однако, знание этого
обстоятельства может порой сослужить неплохую службу. Например, когда вам
необходимо перенаправить один из стандартных дескрипторов — 0, 1 или 2 (см. раздел
2.2.1), нужно просто закрыть этот дескриптор и открыть файл, который получит
номер только что закрытого дескриптора.
2.4.2 Создание нового файла
Если файл с указанным именем не существует, вызов open создаст его, при условии,
что аргумент flags содержит флаг O.CREAT. Безусловно, вы можете создать новый файл,
открыв его только на чтение, но в этом едва ли есть хоть какой-то смысл,
поскольку вновь созданный файл пуст. По этой причине, флаг 0_creat обычно
дополняется флагом 0_WR0NIY или O.RDWR. Кроме того, на этот раз необходимо определить права
доступа к файлу, например:
ec_neg1( fd = openC/home/inarc/newfile", O.RDWR | O.CREAT, PERM.FILE) )
Зачем нужны три разных флага? Разве нельзя заменить флаг O.RDWR комбинацией O.roonly
| O.wronly? Нет, потому что идентификатор o.roonly имеет значение ноль, и не соответствует
какому-либо биту.
76 ГЛАВА 2 Базовые операции файлового ввода-вывода
Аргумент perms используется только при создании нового файла, в противном
случае он игнорируется.
Окончательный набор прав доступа к создаваемому файлу формируется как результат
логического умножения аргумента perms, переданного в системный вызов, на
логическое дополнение маски прав доступа. Маска обычно устанавливается во время
регистрации пользователя в системе (командой umask) или системным вызовом umask
(см. раздел 2.5). Фраза «логическое умножение на логическое дополнение»
означает: если некий бит маски установлен, то соответствующий ему бит в
окончательном наборе прав доступа сбрасывается. Так, маска 002 приведет к тому, что бит S_IW0TH
(право на запись для всех остальных) будет сброшен, даже если в системный
вызов open будет передан флаг S_IW0TH. Однако, как программисту, вам нет нужды
задумываться о маске, поскольку этой проблемой занимаются сами пользователи,
чтобы наложить ограничения на доступ к своим файлам.
Что произойдет, если вы создадите файл с флагом o_wronly или 0_rdwr, но при этом
не дадите ему права на запись? Поскольку файл был только что создан, он пока еще
доступен для записи. Однако, когда в следующий раз вы попытаетесь открыть его,
все наложенные вами ограничения вступят в силу, как это было описано в
предыдущем разделе.
Иногда возникает необходимость удалять содержимое файла при открытии.
Делается это с помощью передачи флага 0_TRUNC:
ec_neg1( fd = open("/hone/паrc/newfile", O.WRONLY | 0_CREAT | 0_TRUNC,
PERM_FILE) )
Использование флага 0_TRUNC приводит к уничтожению всех данных в файле,
поэтому его допустимо указывать только при открытии существующего файла и при
условии, что процесс имеет право на запись в открываемый файл. По той же
причине, флаг o_trunc не может использоваться совместно с флагом 0_R00NLY.
Чтобы создать новый файл (с флагом 0_CREAT), процесс должен иметь право на
запись в каталог, поскольку ссылка на файл будет записана в файл каталога. При
открытии существующего файла права доступа к каталогу не имеют значения. В этом
случае в расчет принимаются только права доступа к файлу.
Следует также заметить, что процесс должен обладать правом на поиск (исполнение)
в промежуточных каталогах (т.е. home и marc). Однако, это общеизвестное требование
для любых действий с каталогами и мы не будем постоянно напоминать о нем.
Флаг o_TRUNC не должен использоваться без 0_CREAT, если предполагается создание
нового файла. Сам по себе он означает следующее: «если файл существует —
удалить из него все данные, если файл не существует — завершиться с признаком
ошибки». (Хотя подобный эффект можно было бы использовать для запуска
системы журналирования: нет файла журнала — журналирование отключается.)
С другой стороны, комбинация флагов 0_WR0NLY | O.CREAT | 0.TRUNC настолько обычна
(«создать файл или очистить его и открыть на запись»), что для этой цели
существует специальный системный вызов:
ГЛАВА 2 Базовые операции файлового ввода-вывода
77
creat — создает новый или очищает существующий файл и открывает его
на запись
«include <sys/stat.h>
«include <fcntl.h>
int creat(
ccnst char «path, /♦ полнее имя файла */
mcde_t perms /* права доступа */
);
I* Возвращает дескриптор файла или -1 в случае сшибки (кед сшибки находится
в глобальней переменней еггпс) */
При обращении к системному вызову open для открытия существующего файла,
можно указывать только первый и второй аргументы (path и flags). Системный вызов
creat использует только первый и третий аргументы. Фактически, вызов creat можно
было бы оформить в виде макроопределения:
«define creat(path, perms) cpen(path, 0_WR0NLY | 0_CREAT | 0_TRUNC, perms)
Почему бы нам не забыть о creat и везде использовать вызов open? Один
системный вызов запомнить проще, чем два, к тому же флаги режима доступа к файлу всегда
явно указываются. Звучит неплохо! В этой книге мы так и сделаем'.
Мы пропустили один важный момент: кто назначается в качестве владельца вновь
созданному файлу? Согласно разделу 1.1.5, каждому файлу назначаются
идентификатор пользователя-владельца и идентификатор группы-владельца, которые для
краткости мы далее будем называть как «владелец» и «группа». Правила назначения
владельца и группы выглядят следующим образом.
• В качестве владельца файлу назначается эффективный идентификатор
пользователя процесса.
• В качестве группы устанавливается ли^бо идентификатор группы каталога, в
котором создается файл, либо эффективный идентификатор группы процесса.
У приложения нет возможности выбрать, какая из двух групп будет присвоена файлу,
но оно может узнать это с помощью системного вызова stat (см. раздел 3.5.1). Чтобы
принудительно назначить файлу нужную группу, можно воспользоваться
системным вызовом chown (см. раздел 37.2), хотя подобные требования к приложениям
предъявляются довольно редко.
Существует еще один флаг — 0_EXCL, который в сочетании с 0_CREAT вызывает ошибку
при попытке открыть существующий файл. Системный вызов среп, используемый
без флага o_CREAT, означает: «открыть файл, если он существует, в противном
случае — завершиться с ошибкой». С комбинацией флагов O.CREAT | o_EXCL — с
точностью до наоборот- «создать новый файл, если он не существует, в противном случае
— завершиться с ошибкой».
• История появления системного вызова ceat восходит к тем стародавним временам, когда
вызов open имел всего два аргумента.
78 ГЛАВА 2 Базовые операции файлового ввода-вывода
Одно из интересных применений флага 0.EXCL — работа с файлами блокировок,
этот прием будет продемонстрирован в следующем разделе. Другая область
применений — обслуживание временных файлов, которые должны удаляться при
нормальном завершении приложения. Если приложение на запуске обнаруживает, что
временный файл уже существует, то это может означать аварийное завершение
программы на предыдущем запуске. В таком случае можно попробовать выполнить
сборку мусора и восстановить утерянные данные. Проще говоря, если
необходимо, чтобы попытка открытия существующего файла терпела неудачу — флаг o.excl
как раз то, что вам нужно.
int fd;
while ((fd = openCVtmp/apptemp", O.RDWR | O.CREAT | 0_TRUNC | O.EXCL,
PERM_FILE)) == -1) {
if (errno == EEXIST) {
if (cleanup_previous_run())
continue;
errno = EEXIST; /» восстановить код ошибки, на всякий случай */
}
EC_FAIL /* какая-то другая ошибка или сбой при восстановлении */
}
/* файл открыт, можно двигаться дальше */
Здесь мы не использовали макрос ес_пед1, поскольку нам требуется проверять
содержимое переменной errno. Код ошибки EEXIST определен специально на случай
использования флага o_EXCL. В случае неудачи при создании нового файла, вызывается
функция cleanup_previous_run() (здесь она не показана) и попытка повторяется, именно
по этой причине использован цикл while. Если вызов cleanup_previous_run()
потерпел неудачу, то восстанавливается содержимое глобальной переменной errno. Нам
не известно, что успела сделать cleanup_previous_run() — это могут быть тысячи строк
кода, и она вполне могла изменить переменную errno. (Цикл while можно заменить
циклом for с двумя итерациями, чтобы обслужить случай, когда cleanup_previous_run()
возвращает true, и завершить работу цикла, если попытка удалить временный файл
провалилась.)
Наш пример совершенно не подходит, когда запускаются несколько копий одного
и того же приложения. В этом случае понятие «временный» начинает приобретать
более глубокий смысл, а удаление временного файла конкурирующим приложением
может привести к непоправимым последствиям. Если программа допускает
одновременную работу нескольких своих копий, то необходимо обеспечить
уникальность именования временных файлов для каждой из них. В разделе 2.7 я покажу,
как это можно сделать. Если необходимо исключить возможность
одновременного доступа к какому-либо ресурсу, то придется прибегнуть к использованию
механизма блокировок. Этот вопрос станет темой обсуждения следующего раздела.
ГЛАВА 2 Базовые операции файлового ввода-вывода 79
2.4.3 Использование файлов блокировок
Процессы, которые требуют предоставления исключительного доступа к ресурсу,
могут придерживаться следующего протокола: прежде чем получить доступ к
ресурсу, они должны попытаться создать файл (с заранее оговоренным именем),
используя флаг 0.EXCL. Это удастся сделать только одному процессу, все остальные
вызовы open будут терпеть неудачу. В случае появления ошибки, процесс может
немного подождать или заняться выполнением других действий и попробовать
получить доступ к ресурсу позднее. В случае успешного создания файла, процесс
выполняет необходимые действия с ресурсом и затем удаляет файл. После этого
другой конкурирующий процесс сможет получить доступ к ресурсу.
Такой способ блокировки будет работать, только если обеспечена атомарность
процесса проверки существования файла и его создания — никакой другой
процесс не должен иметь возможность исполняться в промежутке между этими двумя
действиями. Иначе может возникнуть ситуация, когда второй процесс создаст файл
уже после того, как первый убедился в его отсутствии. По этой причине пример кода,
приведенный ниже, совершенно не годится:
if (aocess( ... ) == 0) /• файл не найден */
... ореп(... ) ... /* ооздать его */
Нам необходим более надежный способ, гарантирующий атомарность операции.
Простой механизм, подобный этому, носит название мъютекс (mutex) — от
английского «mutual exclusion» («взаимное исключение»), двоичный семафор (который
может считать только до 1), или блокировка. На протяжении книги мы еще не раз
столкнемся с этими понятиями. В мире UNIX, слово «мьютекс» чаще всего
встречается, когда речь идет о потоках, а слово «семафор» предполагает использование
семафоров ОС UNIX. Поэтому, в данном разделе, мы будем употреблять термин
«блокировка».
Протокол, описанный выше, прекрасно укладывается в вызов двух функций: look и
unlook, примерно таким образом:
if (lookC'aooounts")) <
... дейотвия о реоуроом aooounts ...
unlock ("aooounts");
>
else
... невозможно уотановить блокировку ...
Имя блокировки «accounts» выбрано произвольно — оно никак не зависит от
имени файла. Если теперь два процесса попытаются выполнить этот участок
программы, то блокировка предотвратит одновременное исполнение защищенной секции
кода («действия с ресурсом accounts»). Запомните: если процесс не вызовет
функцию lock, он лишится «защиты» от конкуренции. Такие блокировки относятся к
80 ГЛАВА 2 Базовые операции файлового ввода-вывода
разряду рекомендуемых, а не обязательных. (Более подробно о различиях между
этими двумя типами блокировок вы сможете прочитать в разделе 7.11.5)
Ниже приводится исходный код функций look, unlook и небольшой
вспомогательной функции lookpath:
«define L0CK0IR "Amp/"
«define MAXTRIES 10
«define NAPLENGTH 2
statio ohar *lookpath(ohar «name)
{
statio ohar path[100];
if (snprintf(path, sizeof(path), "XsXs", L0CK0IR, name) > sizeof(path))
return NULL;
return path;
}
bool look(ohar *name)
{
ohar *path;
int fd, tries;
eo_null( path = lockpath(name) )
tries = 0;
while ((fd = open(path, O.WRONLY | 0_CREAT | O.EXCL, 0)) == -1 &&
errno == EEXIST) {
if (++tries >= MAXTRIES) {
errno = EAGAIN;
EC.FAIL
}
sleep(NAPLENGTH);
}
if (fd == -1)
EC.FAIL
eo_neg1( olose(fd) )
return(true);
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool unlock(ohar «name)
{
ohar «path;
ГЛАВА 2 Базовые операции файлового ввода-вывода 81
ec_null( path = lockpath(name) )
ec_neg1( unlink(path) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Функция lockpath собирает полное имя файла, который будет использоваться в
качестве блокировки. Мы поместили файл в каталог /tmp, потому что он присутствует
в любой UNIX-системе и доступен на запись для любого пользователя. Обратите
внимание: если функция snprintf возвращает слишком большое число, то это не
означает, что произошел выход за пределы переданного ей буфера, а говорит лишь
о том, какой объем памяти требуется для размещения созданной строки.
Функция lock пытается установить блокировку за счет создания нового файла с
флагом 0.EXCL. Как и в предыдущем разделе, ошибка EEXIST обрабатывается особым
образом.
Количество попыток ограничено числом HAXTRIES. Между попытками процесс
«засыпает» на NAPLENGTH секунд (функция sleep входит в стандарт языка С и описывается в
разделе 9.7.2). После создания файл тут же закрывается (см. раздел 2.11), потому что
мы ничего не собираемся в него писать — файл либо существует, либо отсутствует,
это все что нам от него нужно. Он создается даже без указания прав доступа. В
качестве небольшого усовершенствования мы могли бы записать в файл идентификатор
процесса и время установки блокировки. Благодаря этому любой другой процесс,
ожидающий на блокировке, сможет узнать, кого он ждет и как долго закрыт доступ
к ресурсу. Разумеется, в этом случае нужно выдать файлу права на чтение. Все, что
должна сделать функция unlock — это удалить ссылку на файл вызовом unlink. После
этого, первая же попытка другого процесса установить блокировку увенчается
успехом. Описание системного вызова unlink я дам в разделе 2.6.
Теперь рассмотрим небольшую тестовую программу,-
void testlock(void)
{
int i;
for (i = 1; i <= 4; i++) {
if (lockf'accounts")) {
printf("Процесс Xld установил блокировку\п", (long)getpid());
sleep(rand() X 5 + 1); /* работа с ресурсом "accounts" */
ec_false( unlock("accounts") )
>
else {
if (errno == EAGAIN) {
printf("Процесс Xld не смог захватить блокировку\п",
82 ГЛАВА 2 Базовые операции файлового ввода-вывода
(long)getpidO);
eo_reinit(); /* забыть про эту ошибку •/
}
else
EC_FAIL /* произошло что-то оерьезное •/
}
sleep(rand() X 5 + 5); /* оделать что-то еще */
}
return;
EC_CLEANUP_BGN
EC_FLUSH("testlook")
EC_CLEANUP_END
}
Программа выполняет последовательность действий
«захватить/обработать/освободить» в цикле четыре раза. Этап «обработать» эмулируется задержкой на
случайное число секунд, в диапазоне от 1 до 5. Если процесс не смог «захватить»
блокировку, то выводит сообщение и двигается дальше. Затем задержкой на срок от 5 до
9 секунд эмулируются действия, не связанные с обработкой ресурса accounts,
после чего цикл повторяется. В вызове функции printf для получения
идентификатора процесса используется системный вызов getpid (см. раздел 513)- Для проверки
я запустил одной командой сразу три экземпляра программы:
$ tst & tst & tst &
И получил такой результат:
Процеоо 9232 уотановил блокировку
Процеоо 9233 уотановил блокировку
Процеоо 9234 уотановил блокировку
Процеоо 9232 уотановил блокировку
Процеоо 9233 уотановил блокировку
Процеоо 9232 уотановил блокировку
Процеоо 9233 уотановил блокировку
Процеоо 9234 уотановил блокировку
Процеоо 9232 уотановил блокировку
Процеоо 9233 уотановил блокировку
Процеоо 9234 уотановил блокировку
Процеоо 9234 уотановил блокировку
Сначала ход событий вполне предсказуем, но в строке 6 начинают назревать
довольно интересные события, а в самом конце — процесс 9234 вообще остался в
одиночестве, в то время как другие уже закончили работу.
Теперь оценим «за» и «против» использования файлов в качестве блокировок. Итак,
«за»: с такими блокировками легче работать (во второй главе книги мы еще не можем
считать себя многоопытными «зубрами»). Будучи файлами, они могут хранить в себе
ГЛАВА 2 Базовые операции файлового ввода-вывода 83
дополнительную информацию. Они могут существовать столь долго, сколько
вообще могут существовать файлы, это бывает полезным при установке блокировок
на очень длительное время. Однако последний пункт списка «за» открывает
список «против»: если процесс завершился аварийно и не снял блокировку, то не
поможет даже перезагрузка, разве только система не настроена на автоматическую
очистку каталога /tmp. Такие блокировки очень медлительны, потому что создание
файла, даже в случае неудачи, огромная по своим масштабам операция. С этим еще
можно мириться, когда блокировка запрашивается приложением всего несколько
раз. Но, например, в системах управления базами данных или в приложениях
реального времени могут потребоваться более скоростные типы блокировок.
Хуже того, когда процесс не может захватить блокировку, он начинает чередовать
периоды ожидания с попытками захвата, такая последовательность действий
называется голосованием. Это огромная нагрузка на центральный процессор и все лишь
для того, чтобы получить ответ на простой вопрос: «Моя очередь еще не подошла?».
Блокировка может освободиться, когда процесс еще «спит», и он не узнает об этом,
пока не «проснется», а это тоже лишняя трата времени.
К счастью, среди прочих возможностей, в UNIX существует так называемая
система блокировок. Она обеспечивает «пробуждение» процесса, когда происходит
ожидаемое им событие. Более подробно мы поговорим о ней в разделе 7.11.
2.4.4 Краткое описание флагов системного вызова open
Системному вызову open можно передать большее количество флагов, нежели я описал,
но о них мы будем говорить позднее. Например, флаг ONOCTTY имеет отношение
к терминалам, поэтому рассказ о нем пойдет в главе 4. В табл. 2.1 перечислены все
флаги, определенные в SUS3", с краткими описаниями и указанием раздела книги,
где эти флаги подробно обсуждаются.
Таблица 2.1. Флаги системного вызова open
Флаг Описание
0_R00NLY' Открыть только для чтения (раздел 2.4.1)
0_WR0NLY Открыть только для записи (раздел 2.4.1)
0.R0WR Открыть для чтения и для записи (раздел 2.4.1)
0_APPEN0 Открыть для дополнения в конец файла (раздел 2.8)
O.CREAT Создать новый, если не существует (раздел 2.4.2)
O.DSYNC Установить режим синхронного ввода-вывода (раздел 2.16.3)
0.EXCL Не открывать, если существует. Используется в комбинации с O.CREAT
(раздел 2.4.2)
0_N0CTTY He делать устройство управляющим терминалом (раздел 4.10.1)
0_N0NBL0CK" Неблокирующий режим работы с именованными каналами и
специальными файлами (разделы 4.2.2 и 7.2)
(см. след. стр.)
' Single UNIX Specification, Version 3 (единая спецификация ОС UNIX, версия 3); см. раздел
1.5.1.
84 ГЛАВА 2 Базовые операции файлового ввода-вывода
Таблица 2.1 {окончание)
Флаг Описание
0.RSYNC Установить режим синхронного ввода-вывода (раздел 2.16.3)
0.SYNC Установить режим синхронного ввода-вывода (раздел 2.16.3)
0.TRUNC Удалить содержимое файла — усечь его размер до нуля (раздел 2.4.2)
Один из первых трех обязателен.
" Прежде назывался 0_ndelay, правда, с несколько иной семантикой.
2.5 Системный вызов umask
В разделе 2.4.2 мы уже упоминали о маске, накладываемой на набор прав доступа
при создании файла. Наложение маски выполняет системный вызов umask, который
редко используется где-либо еще, кроме как в команде umask.
umask — устанавливает и возвращает
емых файлов.
((include <sys/stat.h>
mode_t umask(
mode_t craask /* новая
);
/. возвращает предыдущее
предусмотрен) */
маска */
маску прав доступа для вновь создава-
значение маски (возврат
кодов
ошибок
не
Так как каждый процесс имеет маску и любая комбинация из 9 бит считается
допустимой, umask не предусматривает выход по ошибке. Он всегда возвращает
прежнее значение маски. Чтобы узнать текущее значение маски не изменяя ее, нужно
выполнить два последовательных вызова umask. Первый — чтобы получить текущее
значение маски (передав в вызов любое значение новой маски), второй — чтобы
восстановить ее в первоначальное состояние.
2.6 Системный вызов unlink
unlink — удаляет запись в
((include <unistd.h>
int unlink(
const char «path /*
);
/* Возвращает О в случае
в переменной errno) »/
каталоге
полное имя
успеха, -1
файла
*/
в случае
ошибки
(код ошибки -
Системный вызов unlink удаляет из каталога ссылку на файл и уменьшает на 1 счетчик
ссылок в индексном узле. Если счетчик ссылок стал равен 0, файловая система уда-
ГЛАВА 2 Базовые операции файлового ввода-вывода 85
лит файл, после этого дисковое пространство и сам индексный узел станут
доступными для повторного использования. Процесс должен иметь право на запись в
каталог, из которого удаляется ссылка.
С помощью этого вызова можно удалить ссылку на файл любого типа, включая
обычные файлы, сокеты, именованные каналы, специальные файлы и т. д. Но
удалить ссылку на каталог может только суперпользователь, а в некоторых системах
даже ему это будет не под силу. В любом случае, для удаления ссылки на каталог
следует использовать системный вызов rmdir (см. раздел 3-6.3), а не unlink.
Если счетчик ссылок в индексном узле достиг значения 0, но в системе есть
процессы, которые держат файл открытым, файловая система откладывает момент
освобождения дискового пространства до тех пор, пока последний процесс не
закроет удаляемый файл, чтобы избежать обрушения работающих процессов. Эта
особенность часто используется при работе с временными файлами, которые нужны
только на время работы программы, примерно таким образом:
ec_neg1( fd = open("temp", 0_RDWR | 0_CREAT | 0_TRUNC I 0_EXCL, 0) )
ec_neg1( unlink("temp") )
Такой подход имеет два преимущества: первое — если процесс завершает работу
по какой-либо причине, файл будет удален автоматически. Благодаря этому
отпадает необходимость регистрировать функцию выхода из программы вызовом atexit
(см. раздел 1.3.4), чтобы обеспечить удаление ненужных временных файлов.
Второе — поскольку ссылка на файл удаляется вызовом unlink немедленно,
уменьшается вероятность обрушения другого процесса из-за использования флага o_EXCL в
вызове open и случайного совпадения имени временного файла. Тем не менее,
такое совпадение все еще может произойти, если вызов open второго процесса
попадает в промежуток между вызовами open и unlink первого процесса.
Один из вариантов решения этой проблемы — использовать блокировки (см.
раздел 2.4.3):
ec_false( lockCopentemp") )
ec_neg1( fd = open("temp", 0_RDWR | 0_CREAT | 0.TRUNC | 0.EXCL, 0) )
ec_negl( unlinkO'temp") )
ec_false( unlockCopentemp") )
Предложенное решение довольно надежное, но оно имеет один недостаток —
используемая блокировка не является обязательной. К тому же выбрано весьма
распространенное имя для временного файла. Таким образом, есть вероятность, что
другое приложение откроет временный файл с тем же именем без использования
блокировки, а это может привести к появлению конфликтов между процессами, из-
за флага 0_EXCL в вызове open. Более надежное решение заключается в выборе
уникальных имен временных файлов, но об этом мы поговорим в разделе 2.7,
который посвящен временным файлам.
На первый взгляд, здесь кроется еще одна проблема: имя временного файла temp
одновременно используется разными процессами. Получается, что они читают и
86 ГЛАВА 2 Базовые операции файлового ввода-вывода
пишут в один и тот же файл? Нет, это не так. Если вы сегодня создали файл с
именем myfile, потом удалили его, а на следующий день опять создали файл с тем же
именем, то это будут совершенно разные файлы. То, что первый процесс держит
временный файл открытым (со своими данными), не имеет никакого значения,
потому что он использует индексный узел, отличный от того, который будет
выделен второму процессу. Когда приложение с помощью unlink удаляет ссылку на
временный файл, индексный узел становится безымянным (анонимным) и таким
образом будет недоступен другим процессам.
2.7 Создание временных файлов
В предыдущем разделе мы использовали фиксированные имена для временных
файлов и прибегали к блокировкам, чтобы предотвратить конфликты между
приложениями. Такой подход достаточно сложен. Для программ в UNIX более
характерен иной способ разрешения конфликтов, он заключается в использовании
уникальных имен. В стандарте языка С для этих целей предусмотрена функция tmpnam:
char «pathname;
ec_null( pathname = tmpnam(NULL) )
ec_neg1( fd = open(pathname, 0_RDWR | O.CREAT | O.TRUNC | 0_EXCL, 0) )
ec_neg1( unlink(pathname) )
Функция tmpnam гарантирует уникальность на момент вызова, потому что она
проверяет отсутствие файла с таким именем. Но файл не будет создан, пока не будет
сделан вызов open, так что существует небольшая вероятность того, что два процесса
почти одновременно вызовут tmpnam и получат одинаковые имена файлов. (Мы
постоянно должны помнить о том, что промежуток времени между исполнением
двух соседних строк программы может быть произвольным и в течение этого
периода управление может быть передано другому процессу.) Тогда из-за флага 0_EXCL
один из процессов не сможет создать и открыть файл. Это не так опасно, как
чтение-запись в один и тот же файл, однако такой вариант немногим лучше
методики использования фиксированного имени. Вероятность появления конфликтов имен
хотя и очень мала, но по-прежнему не равна нулю.
Решение этой проблемы:
mkstemp — создает и открывает файл с уникальным именем
«include <stdlib.h>
int mkstemp(
char «template /* шаблон имени файла •/
);
/* Возвращает дескриптор открытого файла или -1 в случае ошибки (содержимое
переменной еггпо может не соответствовать фактической ошибке) */
ГЛАВА 2 Базовые операции файлового ввода-вывода 87
Вызов mkstemp дает полную гарантию уникальности имени созданного файла. Он
принимает шаблон имени файла, завершающийся шестью символами X, заменяет
их уникальной комбинацией, создает и открывает файл на чтение и на запись. Права
доступа к файлу, созданному таким образом, никак не регламентируются в SUS3,
хотя в большинстве реализаций они ограничиваются правом на чтение и на запись
для владельца файла (S_IRUSR | S_IWUSR).
При разработке переносимых программ возникает вопрос — что находится в
глобальной переменной errno, если mkstemp возвращает значение -1? Стандарты не
определяют никаких кодов ошибок, возвращаемых mkstemp, но, тем не менее, всегда
следует полагаться на то, что в любой из реализаций, mkstemp будет возвращать
корректный код ошибки в errno. Поэтому мы везде используем макрос ес_пед1
(который зависит от содержимого errno). Постарайтесь не забывать записывать в нее
значение 0 перед вызовом mkstemp, чтобы предотвратить появление вводящих в
заблуждение сообщений об ошибках в тех реализациях, где mkstemp не
устанавливает errno должным образом.
Вызов mkstemp имеется в SUS1, Linux, FreeBSD и Darwin (история появления
корнями уходит в BSD), таким образом, вы можете считать, что он имеется практически
в любой UNIX-подобной ОС.
Ниже приводится пример, который просто выводит имя файла:
char pathname[] = "/tnp/dataXXXXXX";
errno = 0; /* mkstemp может не записывать код ошибки »/
ec_neg1( fd = mkstemp(pathname) )
ec_neg1( unlink(pathname) )
printf("Xs\n", pathname);
Вывод:
/tmp/dataKdByOu
Вы по-прежнему можете тут же удалить ссылку на файл, в противном случае вам
необходимо будет сделать это позднее (скажем, в функции выхода из программы,
зарегистрированной через atexit). Если имя файла нужно передать в другую часть
программы или внешней утилите, вы-не должны удалять ссылку на файл сразу же
после создания, например:
int status;
char cmd[100];
ec_neg1( fd = mkstemp(pathname) )
/* здесь должен находиться код, который заполняет файл */
snprintf(cmd, sizeof(omd), "sort Xs", pathname);
ec_neg1( status = system(cmd) )
Комментарий («здесь должен находиться код...») показывает, что на его месте
должен располагаться код, который заполнит файл данными. Функция system входит в
4 Зак. 4030
88 ГЛАВА 2 Базовые операции файлового ввода-вывода
стандарт языка С и предназначена для запуска внешних программ (мы будем ее
рассматривать в главе 5).
Между прочим, в стандарт языка С включена функция tmpfile, которая выполняет
те же действия, что и mkstemp, но вместо файлового дескриптора она возвращает
указатель на структуру типа FILE.
Существует еще одна функция — mktemp, название которой вам наверняка
приходилось слышать. По своей функциональности она скорее напоминает trnpnam, чем
mkstemp, потому что не создает файл, а возвращает его имя. Ей присущи те же
проблемы, что и trnpnam, к тому же она не является стандартом, поэтому старайтесь
воздерживаться от использования этой функции.
Краткая сводка.-
• приветствуются: mkstemp и tmpfile;
• нежелательны: mktemp и trnpnam.
2.8 Текущая позиция в файле и флаг 0_append
Этот раздел описывает флаг o_append, с которым мы уже встречались в разделе 2.4.4,
и знакомит с некоторыми особенностями системных вызовов read, write, lseek, pread
и pwrite, которые более полно будут описаны в оставшейся части этой главы.
Текущая позиция в файле — это характеристика обычного файла, которая
определяет место (позицию), куда будет производиться запись или откуда будет
выполняться чтение ближайшими вызовами write/read. Это его единственное назначение.
Другие типы файлов — каталоги, сокеты, именованные каналы и символические
ссылки — не имеют этой характеристики. Специальные файлы могут иметь, а
могут не иметь маркер позиции в файле, в зависимости от реализации (см. раздел 3-2).
Во времена, предшествовавшие появлению ОС UNIX (еще до распада группы Биттлз),
большинство операционных систем работали с файлами двух типов —с
последовательным доступом и с произвольным доступом. ОС UNIX реализовала
единственный тип файлов — с изменяемым маркером текущей позиции, что позволяло
осуществлять доступ к произвольным участкам файла. В те дни это было
существенное новшество.
Каждый раз, когда открывается файл, вы получаете независимую текущую позицию,
потому что при этом создается новая запись в системной таблице файлов (см. рис. 2.1).
Таким образом, в следующем примере:
int fd1, fd2, fd3;
ec_neg1( fd1 = open("myfile", 0_WR0NLY | 0_CREAT | O.TRUNC, PERM_FILE) )
ec_neg1( fd2 = open("myfile", 0_RD0NLY) )
ec_neg1( fd3 = openC'yourfile", 0_RDWR | 0_CREAT | 0_TRUNC, PERM_FILE) )
дескрипторам fdl и fd2 соответствуют их собственные текущие позиции в файле,
что позволяет писать в fdl и читать из fd2 независимо друг от друга. Несколько иначе
ГЛАВА 2 Базовые операции файлового ввода-вывода 89
обстоит дело с fd3, в этом случае операции чтения и записи будут работать с
одной и той же текущей позицией в файле.
Если файл был открыт без флага 0.APPEND, то первоначально текущая позиция в файле
получает значение 0. Затем она автоматически передвигается вызовами read и write
на число байт, которое было прочитано или записано. Таким образом, если
текущая позиция в файле не меняется умышленно, то чтение и/или запись
выполняются последовательно. Вы считываете порцию данных из файла, затем считываете
очередную порцию и так далее. То же самое относится и к операции записи.
Допустим, текущая позиция в файле равна 0. Если записать 100 байт в fdl, а затем
прочитать 100 байт из fd2, то мы получим те же самые 100 байт, которые только
что записали. Но если выполнять операции записи/чтения над fd3, то мы
прочитаем данные, которые находятся в файле сразу же после тех, что были записаны, и/
или признак конца файла, если дальше в файле ничего нет. Это происходит
потому, что каждая запись в таблице файлов определяет единственную текущую
позицию, которая используется обоими вызовами: read и write.
Переустановка текущей позиции может быть выполнена с помощью системного
вызова lseek (см. раздел 2.13). Новое значение текущей позиции окажет влияние
на первый же вызов read или write.
Позднее, в главе 6 мы рассмотрим, как создаются дубликаты открытых
дескрипторов. Под словом «дубликат» я подразумеваю не простое копирование:
fdl = fd2;
а использование специального системного вызова, который создает дубликат
дескриптора, например dup. В любом случае, суть заключается в том, что дубликаты
дескрипторов совместно используют одну и ту же запись в системной таблице
файлов, а значит и текущая позиция в файле у них общая.
Если файл открыт с флагом 0.APPEND, то при записи, вызову write будет
предшествовать неявный и атомарный вызов lseek, переустанавливающий текущую позицию в
конец, благодаря этому запись всегда будет производиться в конец файла. Даже если
в этот файл одновременно будут писать несколько процессов, они не будут затирать
данные друг друга. Подобного эффекта вам не удастся достичь с помощью
последовательности вызовов lseek и write (если файл открыт без флага 0.APPEND), потому что,
как мы видели это в других ситуациях, между двумя системными вызовами существует
некоторый промежуток времени, в результате может получиться следующее:
• процесс А перемещает текущую позицию в конец файла (пусть это будет число
1000);
• процесс В так же перемещает текущую позицию в конец файла (то же самое число
1000);
• процесс В записывает 200 байт (начиная с позиции 1000);
• процесс А записывает 200 байт (начиная с позиции 1000) и благополучно
затирает данные, записанные процессом В!
90 ГЛАВА 2 Базовые операции файлового ввода-вывода
Мы могли бы использовать механизм блокировок, но у нас есть лучшее решение:
если процессы А и В откроют файл с флагом 0_APPEND, то это гарантирует нам
такую последовательность действий:
• процесс А переустанавливает текущую позицию в конец файла (число 1000) и
записывает 200 байт;
• процесс В переустанавливает текущую позицию в конец файла (число 1200) и
записывает свои данные.
Таким образом, флаг O.APPEND прекрасно подходит для ведения файлов журналов и
в других ситуациях, когда необходимо собирать в файл данные, поступающие от
нескольких процессов.
Чтение и запись с определенной позиции в файле можно осуществлять и без
использования вызова lseek — для этого существуют вызовы pread и pwrite (см. раздел 2.14).
Они не используют значение текущей позиции в файле и не изменяют его.
Поскольку вы уже достаточно знаете о системных вызовах read и write, можете сейчас
сразу перейти к разделу 2.13, прочитать о системном вызове lseek, рассмотреть
примеры его использования, а затем вернуться к разделу 2.9 и продолжить
изучение материала.
2.9 Системный вызов write
write — выполняет запись в файловый дескриптор
«include <unistd.h>
ssize_t write(
int fd, /* дескриптор */
const void *buf, /• адрес буфера с записываемыми данными •/
size_t nbytes /* количество данных для записи »/
);
I* Возвращает количество записанных байт или -1 в случае ошибки (код ошибки
- в переменной еггпо) */
Мы уже изрядно говорили о вызове write, не пора ли поближе познакомиться с ним?
Системный вызов write записывает nbytes байт из буфера buf в открытый файл,
который представлен дескриптором fd. Запись начинается с текущей позиции в
файле, а по ее окончании текущая позиция смещается на число записанных байт.
В вызывающую программу возвращается число байт, которое было записано или
-1, если возникла ошибка. Напомню еще раз: если файл был открыт с флагом O.APPEND,
то непосредственно перед записью текущая позиция автоматически будет
перемещаться в конец файла.
Вызов write может также использоваться для записи в каналы, в специальные
файлы или в сокеты, но в этих случаях он имеет некоторые особенности. Самое
важное отличие: в подобных применениях вызов write может блокироваться, это
означает, что запись приостанавливается до наступления некоторого события, напри-
ГЛАВА 2 Базовые операции файлового ввода-вывода 91
мер освобождения места для записи очередной порции данных. Если вызов write
заблокирован, он может быть прерван поступившим сигналом (см. раздел 9-1.4); в
этом случае вызывающему процессу возвращается -1 и код ошибки EINTR в
переменной errno. Более подробное обсуждение этого вопроса вы найдете в главах 4, 6
и 8, а сейчас вернемся к обычным файлам.
Простота вызова write обманчива. На первый взгляд кажется, что он сначала
записывает данные, а потом возвращает управление, но несложные расчеты
показывают, что это невозможно — возврат в программу происходит слишком быстро. Здесь
явно какой-то обман!
Действительно мы имеем дело с «обманом». На самом деле, вызов write не
выполняет запись на диск. Он просто переписывает данные в буферный кэш ядра и
заканчивает свою работу, как бы возвращая от ядра заявление, примерно
следующего содержания:
«Я принял во внимание ваш запрос и убедился, что с дескриптором
файла все в порядке. Я благополучно скопировал ваши данные и проверил,
достаточно ли места на диске. Позднее, в более подходящий для меня момент,
при условии сохранения своей работоспособности, я попробую записать ваши
данные на диск. Если обнаружится какая-либо ошибка, я попытаюсь выдать
соответствующее сообщение на консоль, но вам конкретно я ничего не
скажу (потому что вы к тому времени уже можете завершить свою работу). Если
вы или какой-нибудь другой процесс попытаетесь прочитать эти данные до
того, как я запишу их на диск, я возьму их из кэша. Таким образом, если все
будет в порядке, вы никогда не узнаете, в какой момент времени я закончил
работу над вашим запросом. В дальнейшем вы не должны беспокоить меня
по этому поводу. Доверьтесь мне и скажите спасибо за быстрый ответ —
надеюсь, вам этого будет достаточно.»
Когда все идет хорошо, мы получаем фантастический выигрыш! Смысл тот же, как
если бы мы фактически выполнили запись на диск, только намного быстрее.
Однако, если возникнет сбой в работе дискового устройства или ядро «рухнет» по какой-
либо причине, мы обнаружим, что на диске нет данных, которые были «записаны».
В дополнение к неопределенности со временем фактической записи данных на диск,
техника отложенной записи имеет еще две проблемы. Первая — процесс,
инициировавший запись, не может быть проинформирован об ошибках, возникших во
время записи на диск. На самом деле, буферы файловой системы не принадлежат
ни одному из процессов — если несколько процессов пытаются записывать
данные в один и тот же участок одного и того же файла, то их данные попадут в один
и тот же буфер. Конечно, можно было бы предусмотреть механизм передачи
сигнала «write error» всем процессам, которые писали в этот буфер, но что тогда
должен делать процесс с данными, которые были записаны после него? И как
уведомить процессы, которые уже закончили работу?
Вторая проблема состоит в том, что физический порядок записи буферов не
поддается регулированию. Порядок часто имеет значение. Например, при изменении
92 ГЛАВА 2 Базовые операции файлового ввода-вывода
структуры связного списка в файле, предпочтительнее сначала записать в файл
новый элемент списка, а затем обновить ссылки на него, чем наоборот, потому
что элемент, на который нет ни одной ссылки, вызывает гораздо меньше проблем,
чем ссылки, указывающие в никуда. Даже если серия системных вызовов write
следует в определенном порядке, это не гарантирует тот же порядок записи буферов
на диск.
К счастью, у нас есть возможность принудительно синхронизировать запись на диск.
Об этом я расскажу в разделе 2.16.
С другой стороны, не стоит переоценивать значимость этих проблем. Учитывая
надежность современных компьютеров и реализаций UNIX, можно смело надеяться,
на то, что аварийные ситуации в ядре будут встречаться крайне редко.
Большинство пользователей предпочитают иметь выигрыш по времени, который дает
буферный кэш, и никогда не знать о том, что ядро «обманывает» их.
Теперь еще раз посмотрим на пример копирования файла, приведенный в начале
этой главы. Ошибка находится в строке, где производится проверка записи:
if (write(tofd, buf, nread) 1= nread)
EC.FAIL
Если вызов write вернул число записанных байт меньше, чем ему было указано, то
это нельзя считать ошибкой. Такая ситуация возможна при записи данных в канал,
когда он быстро переполняется, или когда размер обычного файла достиг
предельного значения. Фактически, ошибка произойдет при следующем вызове write'.
Она заключается в том, что макрос EC.FAll выдаст значение переменной errno,
которая будет содержать верный код ошибки только тогда, когда вызов write вернет
-1. Мы можем изменить текст функции так, чтобы учесть возможность частичной
записи данных и продолжать запись до тех пор, пока не появится настоящая ошибка:
«define BUFSIZE 512
void copy2(char «from, char *to)
{
int fromfd = -1, tofd = -1;
ssize_t nread, nwrite, n;
char buf[BUFSIZE];
ec_neg1( fromfd = open(from, 0_RD0NLY) )
ec_neg1( tofd = open(to, O.WRONLY | 0_CREAT | O.TRUNC,
S_IRUSR | S.IWUSR) )
while ((nread = read(fromfd, buf, sizeof(buf))) > 0) {
' Хотя даже в этом случае возможно, что перед очередным вызовом write, проблема
нехватки места разрешится сама собой. Тогда и следующая попытка записи не вернет признак
ошибки. У нас нет абсолютно надежного способа узнать, почему read или write выполнили
свою задачу не полностью.
ГЛАВА 2 Базовые операции файлового ввода-вывода 93
nwrite = 0;
do {
eo_neg1( n = write(tofd, 4buf[nwrite], nread - nwrite) )
nwrite += n;
} whiie (nwrite < nread);
}
if (nread == -1)
EC.FAIL
eo_neg1( oiose(fromfd) )
eo_neg1( oiose(tofd) )
return;
EC_CLEANUP_BGN
(void)oiose(fronifd); /* здеоь нельзя иопользовать eo_neg1l */
(void)oiose(tofd);
EC_CLEANUP_EN0
}
В реальной жизни появление таких проблем с обычными файлами может служить
поводом для серьезного беспокойства, хотя подобную технику мы будем
использовать в следующих главах, когда подойдем к рассмотрению терминалов и
каналов, которые порой требуют выполнения повторных попыток. Возможно, для
записи в обычные файлы больше подойдет это простое решение:
if ((nwrite = write(tofd, buf, nreed)) != nread) {
if (nwrite != -1)
errno = 0;
EC.FAIL
}
Или, может быть, вы предпочтете такой вариант:
errno = 0;
ec_false( write(tofd, buf, nread) == nread )
В этих примерах в errno записывается 0, чтобы получить сообщение с указанием
номера строки, но без вводящего в заблуждение кода ошибки. Единственное, чего
вам определенно не стоит делать, так это полностью игнорировать неполную
запись данных вызовом write.-
ec_neg1( write(tofd, buf, nread) ) /• неверно */
Ниже приводится пример функции writeell. Она реализует подход «повторных
попыток», рассмотренный нами в примере функции сору2. Мы будем пользоваться
этой функцией при рассмотрении других тем этой книги (разделы 4.10.2 и 8.5), когда
будет нужна абсолютная уверенность в том, что данные записаны полностью.
Обратите внимание: здесь не используются макросы «ее», потому что функция
задумана как прямая замена вызову write:
94 ГЛАВА 2 Базовые операции файлового ввода-вывода
ssize_t writeaii(int fd, const void *buf, size_t nbyte)
{
ssize_t nwritten = 0, n;
dc {
if ((n = write(fd, &((ccnst char *)buf)[nwritten],
nbyte - nwritten)) == -1) {
if (errnc == EINTR)
continue;
eise
return -1;
}
nwritten += n;
} whiie (nwritten < nbyte);
return nwritten;
}
Здесь особым образом обслуживается ошибка с кодом EINTR, потому что в
действительности — это не совсем ошибка. Код EINTR говорит лишь о том, что работа
вызова write была прервана неким сигналом до того, как он успел хоть что-нибудь
записать, поэтому мы продолжаем работу, как ни в чем не бывало. Работа с сигналами и
с прерванными системными вызовами более подробно будет рассматриваться в
разделе 91.4. В следующем разделе вы найдете пример аналогичной функции — readaii.
2.10 Системный вызов read
read -
- выполняет чтение из файлового дескриптора
#inciude <unistd.h>
ssize_t read(
);
/•
int fd, /•
vcid *buf, /•
size_t nbytes /*
дескриптор */
адрес буфера для принимаемых
объем принимаемых данных •/
Возвращает количестве прочитанных байт или -1 в
сшибки - в переменней
еггпс) */
данных
случае
•/
сшибки
(кед
Системный вызов read является противоположностью вызову write. Он считывает
nbytes байт из файла, представленного дескриптором fd, и размещает их в буфере
buf. Чтение начинается с текущей позиции в файле по окончании и текущая
позиция смещается на количество прочитанных байт. В вызывающую программу
возвращается число прочитанных байт, 0 (по достижении конца файла), или -1, как
признак ошибки.
В отличие от write, вызов read не имеет таких широких возможностей в «обмане».
Он не может сначала передать данные вызывающему процессу, а потом прочитать
их с диска. Если в кэше нет требуемых данных (возможно попавших туда в
результате других операций ввода-вывода), то процесс вынужден ждать, пока ядро про-
ГЛАВА 2 Базовые операции файлового ввода-вывода 95
читает их с диска. Иногда ядро считывает с диска не только затребованные
данные, а сразу несколько соседних блоков, пытаясь таким способом предугадать
потребности программы и повысить скорость чтения данных из файла. Если
система работает с небольшой нагрузкой, данные могут храниться в буферах
достаточно долго и часто выполняется чтение соседних блоков данных, то такой прием
опережающего чтения дает существенный выигрыш.
Проблема частичного выполнения запроса, с которой мы столкнулись в вызове write,
свойственна и вызову read. Поскольку несоответствие количества запрошенных и
прочитанных байт не является ошибкой, переменная еггпо в этом случае не будет
содержать код ошибки, который мог бы указать нам причину такого поведения. Ниже
приводится пример функции readall, которая выполняет чтение данных с учетом этой
особенности (сравните ее с функцией writeall из предыдущего раздела).
ssize_t readaii(int fd, void «buf, size_t nbyte)
{
ssize_t nread =0, n;
do {
if ((n = read(fd, &((char «)buf)[nread], nbyte - nread)) == -1) {
if (еггпо == EINTR)
continue;
else
return -1;
}
if (n == 0)
return nread;
nread += n;
} while (nread < nbyte);
return nread;
}
Мы увидим эту функцию в действии в разделе 8.5.
Как и в случае с write, чтение из канала, из специального файла или из сокета может
быть заблокировано и, следовательно, прервано каким-либо сигналом (см. раздел 9-1.4)-
В результате вызывающему процессу возвращается число -1 и код ошибки EINTR в
переменной еггпо.
2.11 Системный вызов close
close — закрывает дескриптор файла
«include <unistd.h>
int ciose(
int fd /* дескриптор
);
/• Возвращает 0 в случае
в переменной еггпо) •/
файла •/
успеха, -1 в
случае
ошибки
(код
ошибки -
96 ГЛАВА 2 Базовые операции файлового ввода-вывода
Самое важное, что нужно знать о системном вызове close, это то, что он
практически ничего не делает. Он не выталкивает на диск содержимое буферов ядра —
он просто освобождает дескриптор файла для повторного использования. Когда
закрывается последний дескриптор, ссылающийся на запись в таблице файлов (см.
раздел 2.2.3), эта запись также может быть удалена. В свою очередь, когда
удаляется последняя запись в таблице файлов, ссылающаяся на копию индексного узла в
памяти, она также может быть удалена. И еще один момент: если счетчик ссылок
данного индексного узла оказался равным 0, то с диска удаляется индексный узел
и все связанные с ним данные (см. раздел 2.6).
Поскольку вызов close не приводит к выталкиванию данных из кэша на диск и
вообще никак не ускоряет этот процесс, нет никакой необходимости закрывать файл,
чтобы прочитать из него только что записанные данные. Ядро гарантирует
получение записанных вами данных. Другими словами, наличие механизма
буферизации в ядре никак не сказывается на семантике read, write, lseek и любого другого
системного вызова.
Фактически, закрывать дескрипторы файлов совсем необязательно, поскольку они
будут автоматически утилизированы по завершении процесса. Однако все
структуры ядра лучше освобождать своевременно, а в текст программы добавлять
комментарий, который будет сообщать об окончании работы с файлом. Это поможет вам
во время отладки и предотвратит использование дескриптора файла по ошибке.
Применительно к каналам и другим необычным файлам, вызов close имеет ряд
побочных эффектов. Но о них мы будем говорить при изучении файлов этих типов.
2.12 Буферизация ввода-вывода в приложениях
2.12.1 Буферизация в приложении и в ядре
В первой главе говорилось, что файловая система UNIX построена поверх
специального файла блочного устройства и поэтому все операции ввода-вывода, в том
числе и механизм буферизации в ядре, оперируют целыми блоками данных.
Размер блока может быть любым, но наиболее часто используются размеры, кратные
числу 512, например: 1024, 2048 или 409.6. Размеры блоков для разных устройств
не обязательно должны совпадать, они могут зависеть даже от того, как был создан
дисковый раздел.
Операции чтения и записи выполняются быстрее, если они производятся над
целыми блоками данных. Для демонстрации правоты этого заявления мы
скомпилировали функцию сору2 (из раздела 2.9) с размером буфера в 1 байт, для чего
изменили строку:
«define BUFSIZE 1
ГЛАВА 2 Базовые операции файлового ввода-вывода 97
Мы измерили время, которое потребовалось обеим версиям функций для
копирования файла размером 4 Мб'. Результаты эксперимента приведены в табл. 2.2
(время в секундах).
Таблица 2.2. Временные характеристики операций ввода-вывода
Размер буфера
512 байт
1 байт
User
0.07
18.43
System
0.58
204.98
Всего
0.65
223.41
Приведенные цифры были получены в ОС Linux, в FreeBSD результаты были те же
самые.
В колонке «User» приводится время, которое было затрачено на выполнение команд
в пользовательском процессе. В колонке «System» — время, которое было
затрачено на исполнение команд в ядре от имени процесса.
Маленький размер буфера настолько снижает производительность операций
ввода-вывода над обычными файлами, что используется только в очень редких и
необычных ситуациях (например, чтение из файла задом наперед, см. раздел 2.13).
Значительная потеря производительности обусловлена, по большей части, просто
большим количеством обращений к ядру. Чтобы проверить, насколько велики
потери от неправильно выбранного размера буфера, мы повторили опыт, но на этот
раз установили BUFSIZE равным 1024 («хороший» размер) и 1100 («плохой» размер).
Потеря производительности была уже меньше — в Linux она составила 75%
(общее время 4.25 и 7.4 секунд соответственно). В ОС FreeBSD и Solaris разница была
много меньше, порядка 10-20%.
Основная проблема состоит в том, что потребности программ редко совпадают с
оптимальным размером буфера. Чаще всего приходится иметь дело с текстовыми
строками переменной длины и разнообразными структурами, размер которых не
всегда совпадает с размером блока. Решение проблемы состоит в том, чтобы
помещать данные в блоки и записывать их по мере заполнения. Аналогично для
чтения — выполнять чтение блоками и извлекать из них данные по мере
необходимости. То есть в довесок к механизму буферизации ядра добавлять буферизацию в
приложении. Учитывая, что данные могут пересекать границы блоков, приходится
прибегать к некоторым ухищрениям, каким — я покажу в следующем разделе.
Для начала ответим на вопрос «Какой размер буфера выбрать?». Можно
использовать фактический размер блока файловой системы, с которой вы собираетесь
работать". Однако, как показывает опыт, больший размер буфера предпочтительнее.
Если размер буфера приложения меньше фактического размера блока, но при этом
укладывается в блок целое число раз, ядро может достаточно эффективно «уложить»
записываемые данные в свои буферы и записывать их на диск по мере
заполнения. Если размер буфера приложения больше фактического размера блока в целое
' В первой редакции книги, вышедшей в 1985 году, эксперимент проводился над файлом в 4000
байт. Что самое интересное — цифры, полученные в то время, очень близки к нынешним!
" Узнать его можно с помощью системного вызова stat (см. раздел 351).
9 8 ГЛАВА 2 Базовые операции файлового ввода-вывода
число раз, это так же положительно сказывается на эффективности размещения
данных в буферах ядра, к тому же уменьшает количество обращений к ядру, что
тоже немаловажно. Подобные рассуждения справедливы и для операции чтения.
Таким образом, самый простой путь — воспользоваться макросом BUFSIZ, который
определен стандартом языка С. Будучи по своей сути константой, он может не
соответствовать фактическому размеру блока используемой файловой системы, но
это не имеет большого значения. Вы можете даже использовать буферы размером
512 байт, этого будет вполне достаточно.
2.12.2 Функции для работы с буферами в приложении
Для работы с буферами очень удобно использовать специализированный набор
функций, которые выполняют операции чтения, записи, установки текущей
позиции и т. п., никогда не отклоняясь от модели блочного ввода-вывода. Отличным
примером такого пакета может служить так называемая «стандартная библиотека
ввода-вывода», описанная во многих книгах, посвященных языку С.
Основные приемы работы с буферами я продемонстрирую на примере
упрощенного пакета, который называется BUFI0. Он поддерживает операции чтения и
записи, но не имеет возможности переустанавливать текущую позицию. Сначала —
заголовочный файл bufio.h, который должен подключаться к приложению
(прототипы функций опущены):
typedef struot {
int fd; /* деокриптор файла */
ohar dir; /* направление: г или w (г - чтение, w - запиоь) •/
ssize_t total; /* размер буфера в байтах */
ssize_t next; /* оледуощий байт в буфере (аналог текущей позиции) */
unsigned char buf[BUFSIZ]; /* ооботвенно буфер */
} BUFIO;
Далее следует содержимое файла реализации (bufio.c):
BUFIO *Bopen(oonst ohar «path, oonst ohar *dir)
{
BUFIO »b = NULL;
int flags;
swltoh (dlr[0]) {
oase 'r':
flags = 0_RD0NLY;
break;
oase 'w':
flags = 0_WR0NLY | 0_CREAT | 0_TRUNC;
break;
default:
errno = EINVAL;
*
ГЛАВА 2 Базовые операции файлового ввода-вывода 99
EC.FAIL
}
ec_null( b = callcc(1, sizecf(BUFIO)) )
ec_neg1( b->fd = cpen(path, flags, PERM_F1LE) )
b->dlr = dlr[0];
return b;
EC_CLEANUP_BGN
free(b);
return NULL;
EC_CLEANUP_ENO
}
static bccl readbuf(BUFIO *b)
{
ec_neg1( b->tctal = read(b->fd, b->buf, sizecf(b->buf)) )
if (b->tctal = 0) {
errnc = 0;
return false;
}
b->next = 0;
return true ;
EC_CLEANUP_B6N
return false;
EC_CLEANUP_EN0
static bccl writebuf(BUFIO *b)
{
ssize_t n, tctal;
tctal = 0;
while (total < b->next) {
ec_neg1( n = wrlte(b->fd, ib->buf[tctal], b->next - tctal) )
tctal += n;
}
b->next = 0;
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_EN0
}
int Bgetc(BUFI0 *b)
{
100 ГЛАВА 2 Базовые операции файлового ввода-вывода
if (b->next >= b->totai)
if (Ireadbuf(b)) {
if (errno == 0)
return -1;
EC.FAIL
}
return b->buf[b->next++];
EC.CLEANUP.BGN
return -1;
EC_CLEANUP_END
}
booi Bputo(BUFIO «b, int o)
{
b->buf[b->next++] = o;
if (b->next >= sizeof(b->buf))
ec_faise( writebuf(b) )
return true;
EC.CLEANUP.BGN
return faise;
EC.CLEANUP.END
}
booi Boiose(BUFIO »b)
{
if (b != NULL) {
if (b->dir == 'w')
eo_faise( writebuf(b) )
eo.negK oiose(b->fd) )
free(b);
}
return true;
EC.CLEANUP.BGN
return faise;
EC.CLEANUP.END
}
Теперь перепишем функцию копирования файла с использованием нового пакета:
#inoiude "bufio.h"
booi oopy3(ohar «from, ohar «to)
{
BUFIO «stfrom, «stto;
int o;
ГЛАВА 2 Базовые операции файлового ввода-вывода 101
ec_null( stfrcm = Bcpen(frci», "r") )
ec_null( sttc = Bcpen(tc, "w") )
while ((c = Bgetc(stfrcm)) != -1)
ec_false( Bputc(sttc, c) )
if (errnc != 0)
EC.FAIL
ec_false( Bclcse(stfrcm) )
ec_false( Bclcse(sttc) )
return true;
EC_CLEANUP_BGN
(vcid)Bclcse(stf rem);
(void)Bclcse(sttc);
return false;
EC_CLEANUP_END
>
Вы не могли не заметить сильного сходства функций из пакета BUFI0 и
стандартной библиотеки ввода-вывода. Для сравнения ниже приводится текст этой же
функции, написанной с использованием библиотечных вызовов-.
bcol ccpy4(char *frem, char *tc)
{
FILE «stfrcm, «sttc;
int c;
ec_null( stfrcm = fcpen(frcm, "r") )
ec_null( sttc = fcpen(tc, "w") )
while ((c = getc(stfrcm)) != EOF)
ec_ecf( putc(c, sttc) )
ec_false( Iferrcr(stfrcm) )
ec_eof( fclcse(stfrom) )
ec_ecf( fclcse(sttc) )
return true;
EC_CLEANUP_BGN
(vcid)fclcse(stfrem);
(vcid)fclose(sttc);
return false;
EC_CLEANUP_END
}
В таблице ниже приводится время, затраченное на копирование файла с
использованием функций BUFI0, стандартной библиотеки ввода-вывода и метода прямых
системных вызовов (функция ссру2).
102 ГЛАВА 2 Базовые операции файлового ввода-вывода
Таблица 2.3. Сравнение буферизованных и не буферизованных операций ввода-вывода
Метод User' System Всего
BUFIO
Solaris 1.00 0.51 1.51
Linux 1.00 0.28 1.28
FreeBSD 1.00 0.45 1.45
Стандартная библиотека ввода-вывода
■к
Solaris 0.57 0.24 0.81
Linux 11.32 0.15 11.48
FreeBSD 1.02 0.20 1.22
Метод прямых системных вызовов (размер буфера — BUFSIZ)
Solaris 0.00 0.52 0.52
Linux 0.00 0.23 0.23
FreeBSD 0.01 0.37 0.38
Все измерения нормированы по времени копирования методом BUFIO (User), для каждой
из систем.
»
Т
Используя буферизацию ввода-вывода в приложении, мы добились практически
наилучшего сочетания скорости и гибкости: мы можем читать и писать данные хоть
по одному байту, при этом время работы в ядре от имени процесса (составляющая
System) для методов BUFIO и прямых системных вызовов, практически одинаково. Это •
позволяет утверждать, что буферизация операций ввода-вывода в приложениях —
правильный выбор. Метод с использованием стандартной библиотеки
ввода-вывода, за исключением Linux, показывает наилучшие результаты, но он не дает такой
гибкости, как метод BUFIO. Время, затраченное на выполнение в пользовательском
процессе (составляющая User), для ОС Linux резко выделяется из общего ряда (11.32).
Простые исследования показали следующее: для сборки тестовых программ на всех
платформах использовался компилятор дсс, причем в Linux в компиляции участво- 1
вала дсс-версия заголовочного файла stdio.h, в FreeBSD — BSD-версия, в Solaris —
версия, основанная на System V. Похоже на то, что все дело в этом".
По иронии судьбы, библиотека стандартного ввода-вывода получила очень
широкое распространение. Возможность ввода-вывода данных произвольного объема *
для обычных файлов всегда была сильной стороной UNIX и все же, на практике
она редко бывает достаточно эффективной.
Дело в том, что в каждом вызове putc выполняется проверка блокировки потока; другие
системы грамотней подходят к этому и выполняют проверку только тогда, когда это
действительно необходимо. К моменту, когда вы читаете эти строки, данная ошибка возможно уже
исправлена.
♦
$ ГЛАВА 2 Базовые операции файлового ввода-вывода 103
2.13 Системный вызов lseek
Системный вызов lseek' используется для переустановки текущей позиции в
файле. Он не выполняет никаких операций ввода-вывода и не отдает команд
контроллеру диска.
lseek — устанавливает и возвращает текущую позицию в файле
«include <unistd.h>
off_t lseek(
int fd, /* дескриптор файла */
off_t pos, /* позиция в файле »/
int whence /* интерпретация аргумента pos */
);
/* Возвращает новую позицию в файле или -1 в случае ошибки (код ошибки -
в переменной еггпо) */
Аргумент whence может принимать одно из следующих значений:
t SEEK_SET Аргумент pos содержит смещение от начала файла (абсолютная позиция
|> в файле).
SEEK_CUR Аргумент pos содержит смещение от текущей позиции в файле. Может быть
положительным числом, нулем и отрицательным числом.
Указав в аргументе pos значение 0 — мы получим текущую позицию в файле.
SEEK_END Аргумент pos содержит смещение от конца файла. Может быть
положительным числом, нулем и отрицательным числом. Указав в аргументе pos
значение 0 — мы установим текущую позицию в конец файла.
Результатом работы вызова может быть любое неотрицательное число, даже
превышающее размер файла. Если новая текущая позиция оказалась за пределами файла,
ближайший вызов write вставит «недостающий» кусок в конец файла и заполнит
его байтами со значением 0. Вызов read, с текущей позицией установленной в
конец файла или за его пределами, вернет признак конца файла — 0. Попытка
чтения из интервала, который заполнил вызов write при вставке «недостающего»
куска, увенчается успехом и в вызывающую программу будет возвращен буфер,
заполненный нулями, как того и следовало ожидать.
Когда производится запись за пределами файла, большинство реализаций ОС UNIX
не хранит вставленные таким образом пустые блоки. Это дает возможность,
например, на диске объемом в 3 000 000 блоков хранить файлы с суммарной длиной более
3 000 000 блоков. Такая особенность может породить серьезные проблемы при
восстановлении файлов из резервных копий, потому что при создании резервной
копии на устройство хранения будет передан полный объем файлов, т. е. более
3 000 000 блоков!
До того, как в языке С появился тип long, этот вызов назывался seek. В те времена, чтобы
установить новую текущую позицию в файле, приходилось указывать номер блока и
номер байта в блоке.
104 ГЛАВА 2 Базовые операции файлового ввода-вывода
Из всех возможных вариантов вызова lseek, наиболее часто используются следующие:
Первый — для установки абсолютной позиции в файле.-
ec.negK lseek(fd, offset, SEEK.SET) )
Второй — для перемещения в конец файла:
ec.negK lseek(fd, О, SEEK.END) )
Третий — для получения текущей позиции в файле:
cff_t where;
ec.negK where = lseek(fd, 0, SEEK.CUR) )
Другие способы использования lseek применяются довольно редко.
Как уже говорилось в разделе 2.8, большая часть операций по установке текущей
позиции в файле выполняется ядром неявно. Так, вызов среп устанавливает
текущую позицию на первый байт в файле. Вызовы read и write увеличивают значение
текущей позиции на количество прочитанных или записанных байт. Когда файл
открывается с флагом 0.APPEND, текущая позиция переустанавливается в конец файла
перед каждым вызовом write.
Для иллюстрации работы с системным вызовом lseek, приведем пример функции
backward, которая выводит содержимое файла задом наперед. Например, пусть файл
содержит следующие строки:
dcg
bites
man
Тогда функция backward выведет:
man
bites
dcg
Исходный код функции:
vcid backward(char «path)
{
char s[256], c;
int i, fd;
cff.t where;
ec.negK fd = cpen(path, 0.RD0NLY) )
ec.negK where = lseek(fd, 1, SEEK.END) )
i = sizecf(s) - 1;
s[i] = '\0';
ГЛАВА 2 Базовые операции файлового ввода-вывода 105
do {
ec_neg1( where = lseek(fd, -2, SEEK_CUR) )
switch (read(fd, &c, 1)) {
case 1:
if (c == '\n'> {
prlntfCXs", &s[l]);
1 = sizeof(s) - 1;
}
if (i <= 0) {
errno = E2BIG;
EC.FAIL
}
s[-i] = c;
break;
case -1:
EC.FAIL
break;
default: /* невероятный случай */
errno = 0;
EC.FAIL
}
} while (where > 0);
printfO'Xs", &s[l]);
ec_neg1( close(fd) )
return;
EC_CLEANUP_BGN
EC_FLUSH("backward");
EC_CLEANUP_END
>
Здесь есть два интересных момента, которые заслуживают особого внимания. Во-
первых, поскольку вызов read неявно перемещает текущую позицию вперед, перед
каждым следующим считыванием необходимо переустанавливать ее на два байта
назад, по этой же причине в начале функции мы устанавливаем текущую позицию
на один байт дальше конца файла. Во-вторых, вызов read не предусматривает
возврат признака «начало файла», поэтому мы следим за текущей позицией в файле
(переменная where) и прекращаем итерации, как только будет прочитан первый байт
в файле. В качестве альтернативы можно было бы продолжать итерации до того
момента, когда lseek попытается присвоить текущей позиции в файле
отрицательное число. Но это неразумно, поскольку код ошибки, который будет возвращен в
этом случае (EINVAL), может подразумевать иные типы ошибок, связанные с
неправильными аргументами вызова, поэтому лучше отказаться от такого подхода.
106 ГЛАВА 2 Базовые операции файлового ввода-вывода
2.14 Системные вызовы pread и pwrite
pread — выполняет чтение из файла, начиная с заданной позиции
«include <unistd.h>
ssize_t pread(
int fd, /* дескриптор файла */
vcid «buf, /« адрес буфера для принимаемых данных */
size_t nbytes, /* сбъем принимаемых данных */
cff_t offset /* позиция в файле */
);
/* Возвращает количестве прочитанных байт или -1 в случае сшибки (кед
сшибки - в переменней еггпс) */
pwrite — выполняет запись в файл,
«include <unistd.h>
ssize_t pwrite(
int fd,
censt vcid *buf,
size_t nbytes,
cff_t offset
/*
/*
/*
/*
1%
/* Возвращает количестве
- в переменней еггпс
) */
дескриптор
в заданную позицию
файла */
адрес буфера с данными
количестве
позиция в
данных для
файле */
записанных байт или -
для записи
записи »/
1 в случае
*/
сшибки
(кед
сшибки
Системные вызовы pread и pwrite no своей функциональности практически полностью
совпадают с комбинацией вызова Iseek и последующим вызовом read или write
соответственно, за исключением:
• они не пользуются текущей позицией в файле, так как позиция для записи или
чтения передается явно, через аргумент offset;
• они не изменяют текущую позицию в файле. Фактически, текущая позиция
полностью игнорируется этими вызовами.
Для файлов, открытых с флагом o_APPEND (см. раздел 2.8), поведение pwrite
полностью совпадает с поведением вызова write (аргумент offset игнорируется).
Удобство применения одного вызова вместо двух неоспоримо, но, что более
важно, вызовам pread и pwrite несвойственны проблемы, связанные с изменением
текущей позиции в файле между Iseek и последующими вызовами read или write из
другого процесса или потока. Не забывайте: это может произойти при
использовании одного дескриптора несколькими потоками приложения или при
использовании дубликатов дескрипторов несколькими процессами, как это было
описано в разделе 2.2.3. То же относится и к файлам, открытым с флагом 0_APPEND (см.
ГЛАВА 2 Базовые операции файлового ввода-вывода 107
раздел 2.8). Так как ни pread, ни pwrite не используют указатель текущей позиции в
файле, им несвойственны ошибки связанные с неверным позиционированием.
Чтобы показать вызов pread в действии, приведем переработанную версию
функции backward (из предыдущего раздела). На этот раз она получилась более
прямолинейной и более простой в написании и отладке:
void backward2(char «path)
<
char s[256], с;
int i, fd;
cff_t file_size, where;
ec_neg1( fd = cpen(path, 0_RD0NLY) )
ec_neg1( file.size = lseek(fd, 0, SEEK_END) )
i = sizecf(s) - 1;
s[i] = '\0';
fcr (where = file_size - 1; where >= 0; where-)
switch (pread(fd, &c, 1, where)) <
case 1:
if (c == '\n') <
printf("Xs", &s[i]);
i = sizecf(s) - 1;
}
if (i <= 0) {
errn^ = E2BIG;
EC_FAIL
}
s[-i] = c;
break;
case -1:
EC.FAIL
break;
default; /* невероятный случай */
еггпс = 0;
EC.FAIL
}
printfCXs", &s[i]);
ec_neg1( clcse(fd) )
return;
EC_CLEANUP_BGN
EC_FLUSH("backward2");
EC_CLEANUP_END
}
108 ГЛАВА 2 Базовые операции файлового ввода-вывода
2.15 Системные вызовы readv и writev
readv — чтение вразброс
#include <sys/uic.h>
ssize_t readv(
int fd,
const struct icvec *
int icvcnt
);
/* Возвращает количестве
iov,
/•
/*
/•
дескриптор
файла */
массив буферов для
количестве
буферов
прочитанных байт или -1 в
сшибки - в переменней еггпс)
•/
принимаемых
•/
случае
данных
сшибки (кед
•/
writev — запись со
«include <sys/uic
ssize_t writev(
int fd,
const struct
int icvcnt
);
/* Возвращает кол
- в переменней ег
слиянием
.h>
icvec *
ичестве
rnc) •/
icv,
/•
/•
h
дескриптор файла
массив буферов с
количестве буфере
записанных байт или -1 в
•/
данными
в ./
случае
•/
сшибки
(кед
сшибки
Вызовы readv и writev очень похожи на вызовы read и write, но в отличие от
последних имеют дело не с одним, а с несколькими буферами. Иногда их называют
«чтение вразброс» и «запись со слиянием». В файле (в канале, в сокете или чему там
соответствует открытый дескриптор) данные по-прежнему располагаются подряд,
друг за другом, но буферы могут быть разбросаны в памяти процесса.
Другими словами, если вам необходимо записать в файл три структуры, то вместо
трех вызовов write это можно сделать одним вызовом writev, однако, взглянув на
второй аргумент, вы без труда поймете — чтобы использовать эти вызовы,
придется немного потрудиться, но стоят ли они наших усилий? Вскоре мы подойдем к
ответу на этот вопрос и посмотрим как можно использовать эти вызовы.
Перед вызовом любой из этих функций; вы должны создать массив iov (элементов
iovent), в котором каждый элемент содержит указатель на буфер с данными и
размер буфера, по сути — второй и третий аргументы вызовов read и write.
struct iovec
— структура
struct icvec {
vcid
size.
};
*icv_base;
t icv_len;
/•
/•
используемая в системных вызовах
адрес буфера с
размер буфера
данными
•/
•/
readv и writev"
' А также sendmsg и reevmsg, см. раздел 8.6.3
ГЛАВА 2 Базовые операции файлового ввода-вывода 109
5 своих программах вы можете объявить статический массив из struct iovec, а также
поместить его в динамической памяти вызовом malice или другим удобным для вас
:~эсобом. Максимальное число элементов массива для разных ОС может несколь-
•: отличаться, но оно никогда не бывает меньше 16. В ОС, удовлетворяющих
спецификации SUS, идентификатор I0V_HAX соответствует максимальному числу
з- :ементов массива. Если этот идентификатор не определен, можно воспользоваться
санкцией sysccnf (см. раздел 1.5.5) с аргументом _SC_I0V_HAX. Но в большинстве
реальных применений 16 элементов более чем достаточно.
Няже приводится фрагмент программы, которая использует writev, для записи в файл
структуры-оглавления и двух структур с данными. Сначала приведем объявления
структур:
«define VERSION 506
«define STR.MAX 100
struct header {
int h_versicn;
int h_nura_items;
} hdr, *hp;
struct data {
enum {TYPE.STRING, TYPE.FLOAT} d_type;
unicn {
flcat d_val;
char d_str[STR_HAX];
} d_data;
} d1, d2, «dp;
struct icvec v[3];
(Версия — это просто некоторое число, которое помогает идентифицировать тип
оглавления.)
Затем инициализируются структура с оглавлением, две структуры с данными и
массив icvec:
hdr.h_versicn = VERSION;
hdr.h_num_items = 2;
d1.d_type = TYPE_STRING;
strcpy(d1.d_data.d_str, "Данные для записи в файл");
d2.d_type = TYPE.FLOAT;
d2.d_data.d_val = 123.456;
v[0].icv_base = (char »)4hdr; /* иногда icv_base - указатель типа char* */
v[0].icv_len = sizecf(hdr);
v[1].icv_base = (char *)4d1;
v[1].icv_Ien = sizecf(dl);
v[2].icv_base = (char *)&d2;
v[2].icv_Ien = sizeof(d2);
110 ГЛАВА 2 Базовые операции файлового ввода-вывода
И потом выполняется запись всех трех структур одним вызовом writev:
ec_neg1( n = writev(fd, v, sizeof(v) / sizeof(v[0])) )
Обратите внимание: при инициализации iov_base выполняется приведение типа.
В большинстве случаев, необходимость в этом отсутствует, так как SUS определяет
это поле как указатель типа void*. Но в ОС FreeBSD это поле имеет тип char*. Таким
образом, подобное приведение типов удовлетворяет обоим условиям.
Чтобы показать, что данные в файле действительно расположились друг за другом,
мы прочитаем их и выведем на экран. Читаться данные будут в один большой
буфер обычным вызовом read и затем извлекаться из буфера с помощью указателей
соответствующих типов (hp и dp).
Обратите внимание: для возврата к началу файла, который был открыт с ключом
0.RDWR (здесь не показано), используется вызов iseek:
ec_nuii( buf = maiicc(n) )
ec.negK iseek(fd, 0, SEEK.SET) )
ec_neg1( read(fd, buf, n) )
hp = buf;
dp = (struct data *)(hp + 1);
printf("Версия = Xd\n", hp->h_versicn);
for (i = 0; i < hp->h_num_iteins; i++) {
printf("#Xd: ", i);
switch (dp[i].d_type) {
case TYPE.STRING:
printf("Xs\n", dp[i].d_data.d_str);
break;
case TYPE.FLOAT:
printf("X.3f\n", dp[i].d_data.d_vai);
break;
default:
errno = 0;
EC_FAIL
>
>
ec_neg1( ciose(fd) )
free(buf);
В результате работы фрагмента мы получим:
Версия = 506
#0: Данные для записи в файл
#1: 123.456
Имеются ли еще какие-нибудь преимущества использования одного вызова
вместо нескольких, кроме очевидного удобства? В табл. 2.4 приведены результаты
тестовых испытаний, в каждом из которых 50 000 раз производилась запись 16
буферов с данными (по 200 байт в каждом) вызовами writev и write. В каждом испыта-
ГЛАВА 2 Базовые операции файлового ввода-вывода 111
нии был получен файл длиной 160 Мб. Результаты были нормированы таким
образом, чтобы время, которое было затрачено на исполнение команд в ядре от
имени процесса (составляющая system), было представлено числом 50'.
Таблица 2.4. Скорость исполнения write и writev
Системный вызов
User
System
Solaris
writev
write
1.35
8.45
50.00
67.61
Linux
writev
write
0.17
1.83
50.00
27.67
FreeBSD
writev
write
0.39
4.90
50.00
209.89
Для Solaris, и особенно для FreeBSD, использование вызова writev действительно дает
увеличение производительности по сравнению с write. Но в случае с Linux все
оказалось совсем не так. В ходе изучения исходных текстов ядра Linux мы
обнаружили, что вызов writev в этой ОС просто обходит в цикле элементы массива и
вызывает write для каждого из них! В случае сокетов, для которых собственно и
разрабатывались системные вызовы readv и writev, Linux ведет себя более достойщ
полностью передавая массив функциям нижнего уровня. Мы не проводили тестовых
испытаний для сокетов, но из исходных текстов явно следует, что применение writev
к сокетам действительно заслуживает внимания.
2.16 Синхронизированный ввод-вывод
В этом разделе говорится о том, как обойти механизм буферизации в ядре,
который был кратко описан в разделе 2.12.1, и как принудительно «выталкивать»
буферы ядра на диск.
« 2.16.1 Понятия синхронизированный и синхронный
В человеческом языке слова «синхронизированный» и «синхронный» означают
практически одно и то же, но с точки зрения операций ввода-вывода в UNIX они
имеют различное значение.
' Изучая содержимое таблицы, не забывайте, что мы не сравниваем производительности на
4 разных платформах, скажем Linux против FreeBSD. Нормирование было произведено
отдельно для каждой из платформ. Нас интересуют только сравнительные характеристики
вызовов write и writev.
112 ГЛАВА 2 Базовые операции файлового ввода-вывода
• Синхронизированный ввод-вывод означает, что вызов write (и родственные ему
pwrite и writev) не вернет управление до тех пор, пока данные полностью не
будут записаны на устройство вывода (обычно — диск). Как я уже отмечал в
разделе 2.9, обычный вызов write не является синхронизированным — он перемещает
данные в кэш ядра и сразу же возвращает управление в вызывающую программу.
Таким образом, в случае краха системы, данные могут быть потеряны.
• Синхронный ввод-вывод означает, что вызов read (и родственные ему) не вернет
управление до тех пор, пока запрошенные данные не будут доступны для чтения,
а вызов write (и родственные ему) — пока записываемые данные не попадут, по
крайней мере, в буфер ядра и далее — на устройство вывода, если ввод-вывод
синхронизирован. Все описанные нами системные вызовы, выполняющие ввод-
вывод (read, pread, readv, write, pwrite и writev), являются синхронными.
Следовательно, обычные операции ввода-вывода в UNIX являются синхронными, но
не синхронизированными. Фактически, операции чтения и записи до
определенной степени являются асинхронными из-за особенностей функционирования
буферного кэша. Однако, как только вы попытаетесь синхронизировать операцию
записи (принудительно выталкивая буферы ядра на устройство вывода при
каждой операции), тут же резко возрастет время ожидания возврата из вызова write.
Производительность программы упадет настолько, что вы наверняка вернетесь к
идее асинхронной записи и заявите самому себе: «Лучше я запущу процесс записи
и займусь чем-нибудь более полезным, а о результатах узнаю позднее, если на это
останется время». Операции чтения, особенно не следующие друг за другом,
всегда до некоторой степени синхронизированы (ядро не может вернуть данные, если
их нет в кэше).
Чтобы синхронизировать операции чтения и записи, при открытии файла
вызовом open необходимо добавить некоторые флаги или выполнить системные
вызовы, которые вытолкнут буферы ядра на устройство. Это и станет темой
обсуждения данного раздела. Асинхронные операции ввода-вывода выполняются
совершенно иными системными вызовами (например aio.write). О них мы будем говорить
в разделе 39, где я более четко обозначу границы между понятиями
«синхронизированный» и «синхронный».
2.16.2 Системные вызовы для синхронизации
Самый древний, наиболее известный и наименее эффективный:
sync —
планирует
«include <unistd
void
sync(void);
выталкивание
.h>
буферов
из кэша
Вызов sync просто сообщает ядру о необходимости сбросить буферы кэша на
устройство вывода и сразу же возвращает управление, таким образом, собственно
запись происходит немного позднее. Мы все еще не можем уверенно обозначить
ГЛАВА 2 Базовые операции файлового ввода-вывода 113
момент, когда данные попадут на устройство вывода. Кроме того, вызов sync
приводит к выталкиванию всех буферов в кэше, а не только тех, которые связаны с
интересующим нас файлом.
Основное назначение этого системного вызова — реализовать команду
синхронизации, которая запускается при остановке системы или перед размонтированием
сменного носителя. Для использования в приложениях есть более подходящие
варианты.
Следующий системный вызов fsync ведет себя как минимум подобно sync, но
воздействует только на те буферы, которые имеют отношение к конкретному файлу.
Этот вызов поддерживается системами, отвечающими спецификации SUS2 и более
ранними, в которых определен идентификатор .POSIX.FSYNC.
fsync — планирует или принудительно выталкивает
данного файла
#include <unistd.h>
int fsync(
int fd /* дескриптор
);
/* Возвращает 0 в случае
в переменней еггпс) •/
файла */
успеха, -1
в случае
ошибки
буферы
из кэша для за-
(кед сшибки -
Мы говорим «как минимум», потому что только в том случае, когда системой
поддерживается синхронизация ввода-вывода (см. раздел 1.5.4), гарантируется возврат
управления в вызывающую программу только после передачи данных
контроллеру устройства или в случае появления ошибки".
Ниже приводится исходный код функции, которая проверяет присутствует ли в
системе поддержка синхронизации ввода-вывода. За подробными разъяснениями
обратитесь к разделу 1.5.4.
OPT_RETURN cpticn_sync_ic(ccnst char «path)
{
#if _POSIX.SYNCHRONIZED.IO <= 0
return 0PT.N0;
#elif _X0PEN_VERSI0N >= 500 && Idefined(LINUX)
#if ! defined(_P0SIX_SYNC_I0)
errnc = 0;
if (pathccnf(path, _PC_SYNC_I0) == -1)
if (errnc == 0)
return 0PT.N0;
else
Это означает, что к моменту возврата данные все еще могут находиться в кэш-памяти
контроллера, но они очень быстро будут перенесены на носитель. Как правило, даже в случае
краха системы, устройства все еще остаются под напряжением и способны довести
начатое дело до конца.
114 ГЛАВА 2 Базовые операции файлового ввода-вывода
EC.FAIL
else
return 0PT_YES;
EC_CLEANUP_BGN
return 0PT_ERR0R;
EC_CLEANUP_END
#elif _P0SIX_SYNC_I0 == -1
return 0PT_N0;
«else
return OPTJfES;
#endif /* _P0SIX_SYNC_I0 */
#elif _P0SIX_VERSI0N >= 199309L
return 0PT_YES;
«else
errno = EINVAL;
return 0PT_ERR0R;
«endif /* _POSIX_SYNCHRONIZED_IO »/
}
Последний системный вызов, выполняющий синхронизацию — fdatasync, более
быстрая форма fsync, выталкивающая только данные и не затрагивающая
служебную информацию, такую как «время модификации» и т. п. Для большинства
критических применений этого вполне достаточно — служебная информация будет
дописана ядром позднее.
fdatasync — принудительно выталкивает из кэша только данные файла
«include <unistd.h>
int fdatasync(
int fd /• дескриптор файла */
);
/* Возвращает 0 в случае успеха, -1 в случае ошибки (код ошибки -
в переменной errno) */
При наличии поддержки синхронизированного ввода-вывода оба вызова, fsync и
fdatasync, могут возвращать истинные коды ошибок ввода-вывода, в этом случае
переменная errnc содержит код ЕЮ.
И последний момент: если синхронизированный ввод-вывод не поддерживается
системой, то реализация системных вызовов sync и fsync вообще не обязана
предусматривать каких-либо действий (вызов fdatasync в этом случае вообще
отсутствует). В противном случае реализация системных вызовов обязана обеспечить
высокий уровень сохранности данных, несмотря на то, что во многом это зависит от
драйвера устройства и от самого устройства.
ГЛАВА 2 Базовые операции файлового ввода-вывода 115
2.16.3 Флаги синхронизации в системном вызове open
Обычно вы вызываете fsync или fdatasync, чтобы окончательно увериться в
сохранении данных, например перед тем, как сообщить пользователю об успешном
завершении транзакции с базой данных. В еще более критических случаях файл можно
открыть с дополнительными флагами, которые обеспечат неявный вызов f sync или
fdatasync при каждой операции записи. Я уже упоминал флаги 0_SYNC, 0.DSYNC и 0.RSYNC
в табл. 2.1. Они доступны, только если поддерживается синхронизированный ввод-
вывод.
0_SYNC После каждой операции записи неявно вызывается fsync.
0_DSYNC После каждой операции записи неявно вызывается fdatasync.
0_RSYNC Выполняет синхронизацию операции чтения. Должен использоваться
в комбинации с флагом 0.SYNC или O.DSYNC.
Когда мы говорим «операция записи», подразумеваем вызовы write, pwrite и writev.
То же относится и к словам «операция чтения».
Единственное, что дает флаг O.RSYNC — обновление времени последнего доступа к
файлу в индексном узле, выбранным методом синхронизации, т. е. в комбинации
с флагом 0_DSYNC, вероятно, вообще не даст никакого эффекта. Время последнего
доступа к файлу в большинстве применений не является критически важной
характеристикой, требующей непременной синхронизации, поэтому, насколько она
целесообразна — решать вам. В некоторых системах этот флаг может
использоваться для отключения опережающего чтения.
Ниже приводится текст программы тестирования синхронизированного и не
синхронизированного режимов записи в файл, который открыт с флагом 0.DSYNC и без него:
#define SYNCREPS 5000
«define PATHNAME "trap"
vcid synctest(vcid)
{
int i, fd = -1;
char buf[4096];
#if !defined(_P0SIX_SYNCHR0NIZE0_I0) 11. _P0SIX_SYNCHR0NIZE0_I0 == -1
printf("Синхронизация не поддерживается - сравнение невсзмсжнс\п");
«else
/* Создать файл, чтобы можно было выполнить проверку */
ec_neg1( fd = cpen("trap", 0_WR0NLY | 0_CREAT, PERH_FILE) )
ec_neg1( clcse(fd) )
switch (cpticn_sync_ic(PATHNAME)) {
case OPT.YES:
break;
case 0PT_N0:
printf("синхронизация для Xs не псддерживается\п", PATHNAME);
return;
11 б ГЛАВА 2 Базовые операции файлового ввода-вывода
case 0PT_ERR0R:
EC.FAIL
}
memset(buf, 1234, sizeof(buf));
ec_neg1( fd = open(PATHNAME, O.WRONLY | OJTRUNC | 0_DSYNC) )
timestartO;
for (i = 0; i < SYNCREPS; i++)
eo_neg1( write(fd, buf, sizeof(buf)) )
eo_neg1( oiose(fd) )
timestop("synchronized");
ec_neg1( fd = open(PATHNAME, O.WRONLY | OJTRUNC) )
timestartO;
for (i = 0; i < SYNCREPS; i++)
ec_neg1( write(fd, buf, sizeof(buf)) )
ec_neg1( olose(fd) )
tiinestop( "unsynohronized");
«endif
return;
EC_CLEANUP_BGN
EC_FLUSH("baokward");
(void)olose(fd);
EC_CLEANUP_END
}
Функции timestart и timestop используются для получения временных отметок — мы
уже говорили о них в разделе 1.7.2. Обратите внимание: функция option_sync_io
требует, чтобы проверяемый файл был создан до обращения к ней. Набор флагов во всех
трех вызовах open несколько непривычен: первый использует флаг 0.CREAT без OJTRUNC,
от него требуется только гарантировать наличие файла (после открытия файл
сразу же закрывается). Последующие два вызова open используют флаг OJTRUNC без О.СЯЕАТ,
так как мы точно знаем, что файл уже существует.
Результаты, которые были получены в Linux, приводятся в табл. 2.5 (в Solaris
результаты получились практически те же самые, a FreeBSD не поддерживает режим
синхронизации).
Таблица 2.5. Результаты тестирования синхронизированного и не синхронизированного
режимов записи
Режим
Синхронизированный
Не синхронизированный
User*
0.03
0.02
System
5.57
1.13
Факт
266.45
1.15
Время в секундах
ГЛАВА 2 Базовые операции файлового ввода-вывода 117
В колонке «Факт» приводится общее время работы, включая время ожидания
завершения операции ввода-вывода.
Как видите, синхронизация ввода-вывода довольно дорогостоящая штука. Именно
по этой причине у вас может возникнуть желание перейти на асинхронный ввод-
вывод. Как — я расскажу в разделе 3-9' •
2.17 Системные вызовы truncate и ftruncate
truncate — изменяет разм
«include <unistd.h>
int truncate(
ccnst char «path, /•
cff_t length /»
);
/• Возвращает О в случае
в переменней errnc) */
ер файла, заданного по имени
полнее
нсвый
успеха
имя файла •/
размер файла */
-1 в случае сшибки
(кед
сшибки -
ftruncate — изменяет
«include <unistd.h>
int ftruncate(
int fd, 1*
cff_t length /*
);
/• Возвращает О в с/
в переменней errnc)
размер файла, заданного дескриптором
дескриптор файла •/
нсвый размер файла */
1учае успеха, -1 в случае
*/
ошибки
(кед
ошибки -
Системные вызовы truncate и ftruncate используются для изменения размеров файла
как в большую, так и в меньшую сторону. Как можно увеличить размер файла без
фактической записи данных в него, я уже рассказывал, когда обсуждал системный
вызов lseek. Хотя truncate и ftruncate могут увеличивать размер файла, все же чаще
они используются для уменьшения (усечения) файлов. Ранее, до появления этих
вызовов в UNIX, усечение файлов было невозможным. (Приходилось создавать новый
файл и затем переименовывать его.) -
Ниже приводится несколько искусственный пример. Обратите внимание на то, как
выполняется проверка вызова write. Я подробно касался этой темы в конце раздела 2.9:
void ftruncate_test(void)
{
int fd;
ccnst char s[] = "Это мои правила.\п"
"Если они вам не нравятся - тем хуже для вас.\п"
"Yt-Гручо Маркс\п";
' Если что-то в этом параграфе для вас оказалось непонятным, перечитайте раздел 2.16.1.
118 ГЛАВА 2 Базовые операции файлового ввода-вывода
eo_neg1( fd = open("tmp", O.WRONLY | O.CREAT | O.TRUNC, PERM.FILE) )
errno = 0;
eo_false( write(fd, s, sizeof(s)) == sizeof(s) )
(void)system("ls -1 trap; oat tup");
eo_neg1( ftrunoate(fd, 17) )
(void)system("ls -1 tmp; oat trap");
eo_neg1( olose(fd) )
return;
EC_CLEANUP_BGN
EC_FLUSH("ft runoate.test");
EC_CLEANUP_END
}
Вот результат работы программы:
-rw-r-r- 1 maro sysadrain 80 Oot 2 14:03 tmp
Это мои правила.
Еоли они вам не нравятоя - тем хуже для вао.
-Гручо Марко
-rw-r-r- 1 maro sysadrain 25 Oot 2 14:03 tmp
Это мои правила.
Вызов ftrunoate, кроме того, используется для изменения размеров блока общей
памяти (см. раздел 7.14).
Упражнения
2.1. Измените функцию look в разделе 2.4.3 так, чтобы она сохраняла в файле
блокировки регистрационное имя пользователя (функция getlogin, раздел 35.2).
Добавьте в функцию look аргумент, в котором она могла бы возвращать имя
пользователя, захватившего блокировку.
2.2. Напишите программу, которая открывает файл для записи с флагом 0_APPEND
и затем записывает в него текстовую строку. Запустите одновременно несколько
экземпляров программы и убедитесь, что перемешивания строк не
происходит. После этого уберите из программы флаг 0_append, а переход в конец
файла выполните с помощью lseek. Запустите одновременно несколько
экземпляров программы и посмотрите, что получилось.
2.3. Повторите тесты из раздела 2.12.2, с размерами буфера 2, 57, 128, 256, 511,513
и 1024 байт. Если желаете, можете попробовать подставить другие числа. Если
у вас есть доступ к различным версиям UNIX, опробуйте тесты на каждой
из них. Запишите полученные результаты в таблицу и изучите их.
2.4- Добавьте в пакет BUFI0 (раздел 2.12.2) возможность открытия файла
одновременно на запись и чтение.
2.5. Добавьте в пакет BUFI0 функцию Bseek.
2.6. Напишите свой вариант команды oat, которая не имеет опций командной
строки. Дополнительно попробуйте реализовать столько опций, сколько вам
будет под силу. В качестве руководящего документа можете использовать SUS.
2.7. Попробуйте реализовать свою версию команды tail (аналогично п. 2.6).
- ш en ■ ■ ■ о
з& Ш Ш Ш Ш Ш J
Дополнительные
операции файлового
ввода-вывода
3.1 Введение
В этой главе будет продолжено обсуждение темы ввода-вывода, начатое во второй
главе. Сначала мы рассмотрим использование уже известных нам системных
вызовов для доступа к специальным файлам дисковых устройств. Заглянем с их помощью
внутрь файловой системы. Затем разберем дополнительные системные вызовы, с
помощью которых можно создавать ссылки на существующие файлы, создавать,
удалять и читать каталоги, получать или изменять информацию о файлах.
Обсуждаемые здесь системные вызовы на практике используются не так широко,
как те, с которыми мы уже ознакомились. Однако знание принципов работы с ними
вам будет далеко не лишним, потому что оно дает полное представление о
принципах функционирования подсистемы ввода-вывода.
Примеры программ в этой главе более объемные. Их детальное изучение стоит
потраченного времени, так как они иллюстрируют некоторые моменты, которые явно
не описаны в тексте.
3.2 Специальные файлы дисковых устройств
и файловые системы
Этот раздел посвящен операциям ввода-вывода над специальными файлами
дисковых устройств, которые используются для доступа к внутреннему устройству
файловой системы. Здесь также будет освещен процесс мо! ггирования и размон-
тирования файловых систем.
5 Зак. 4030
120 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
3.2.1 Операции ввода-вывода над специальными файлами
дисковых устройств
До сих пор мы рассматривали операции ввода-вывода, производимые
исключительно с помощью модуля ядра, обслуживающего файловую систему, используя
относительно высокоуровневые абстракции, такие как: файл, каталог и i-node
(индексный узел). Как уже отмечалось в разделе 2.12, файловая система реализована
поверх блочной системы ввода-вывода, которая использует буферный кэш'. Обращение
к блочной системе ввода-вывода производится через специальный файл блочного
устройства или блочное устройство, которое непосредственно связано с диском.
Диск, в свою очередь, может рассматриваться как последовательность блоков с
размерами, кратными размеру сектора. Как правило, размер сектора равен 512 байт.
В системе может иметься несколько физических дисков, а каждый из них может
быть поделен на части, которые называются томами, разделами или файловыми
системами. (Термин «файловая система» довольно неоднозначен, поскольку под ним
также подразумевается часть кода ядра (модуль ядра), однако контекст, в котором
он употребляется, дает достаточно четкую его трактовку.)
Каждому разделу диска соответствует специальный файл, имя которого обычно
формируется из названия устройства, например «hd», вслед за которым следуют
цифры и символы латинского алфавита, однозначно идентифицирующие раздел
физического диска. Как правило, специальные файлы устройств размещаются в
каталоге /dev, хотя это и не является обязательным требованием. Например, в
операционной системе Linux, файл /dev/hdb3 соответствует третьему разделу (цифра 3)
второго жесткого диска (символ «Ь» — второй символ латинского алфавита).
В принципе, к специальному файлу устройства можно обращаться как к обычному
файлу. Его можно открыть на чтение и/или запись, прочитать или записать
данные (блоками произвольного размера), переместиться в определенную позицию
(с точностью до байта) и закрыть его. Данные, которые буферизируются в кэше
(поскольку это специальный файл блочного устройства), относятся ко всему
разделу в целом — здесь нет ни каталогов, ни файлов, ни индексных узлов, ни прав
доступа, ни владельцев, ни размеров и пр. Это просто огромный массив
пронумерованных блоков.
На практике операции ввода-вывода не-могут выполняться обычными
пользователями, поскольку для них доступ к файлу запрещен. Чтение из файла устройства,
который содержит файлы других пользователей, может поставить под угрозу их
конфиденциальность, даже несмотря на то, что информация на диске выглядит
случайным набором байтов (порядок соответствия блоков файлам и каталогам в
UNIX достаточно неочевиден). Запись на диск в обход модуля ядра,
обслуживающего файловую систему, вообще может привести к плачевным последствиям.
Более новые реализации вместо буферного кэша используют подсистему виртуальной
памяти, но, тем не менее, понятие «кэш» служит отличной абстракцией модели
взаимодействия ядра с файлами.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 121
С другой стороны, если раздел закреплен за определенным пользователем и в нем
нет файлов и каталогов других пользователей, то отсутствуют и конфликты с
системой безопасности. Некоторые системы управления базами данных и сбора
информации могут потребовать наличия отдельного раздела и обращаться к нему через
специальный файл блочного устройства. Разумеется, количество специальных
файлов дисковых устройств ограничено количеством разделов на дисках. Обычно, когда
файл устройства используется неким приложением, то оно является единственным
или, по крайней мере, основным в системе.
Потенциально, даже более скоростными, чем специальные файлы блочных
дисковых устройств, являются так называемые «неструктурированные» или «плоские» (raw)
специальные файлы дисковых устройств. Они соответствуют тем же самым
областям диска, с той лишь разницей, что являются символьными устройствами, а не
блочными. Это означает, что они не следуют блочной модели и не имеют
буферного кэша. Но в чем-то они даже имеют преимущество.
Когда процесс инициирует операцию чтения или записи в неструктурированный
специальный файл устройства, он блокируется в памяти (во избежание
вытеснения в файл подкачки), чтобы физические адреса областей с данными не могли
измениться. После этого, если аппаратура и драйвер устройства позволяют, обмен
между диском и сегментом данных процесса выполняется в обход ядра, с
использованием механизма прямого доступа к памяти (DMA). При этом объем одной
порции данных может превышать размер блока.
Обычно операции ввода-вывода для «плоских* файлов не обладают той гибкостью,
которую могут дать обычные файлы или специальные файлы блочных устройств.
Аппаратура прямого доступа к памяти (DMA) может накладывать свои
ограничения на размещение буферов в адресном пространстве процесса. Установка
текущей позиции может выполняться только по границам блоков. Впрочем, эти
ограничения мало беспокоят разработчиков файловых систем, ориентированных на
работу с базами данных, поскольку они находят очень удобным представление диска
в виде набора страниц фиксированного размера.
До сих пор мы встречались с различиями, которые зависят лишь от версии UNIX.
В данном случае мы имеем дело с различиями, обусловленными аппаратной
платформой, драйверами устройств и даже типом установки. Компьютеры могут очень
сильно отличаться друг от друга в части аппаратной реализации устройств ввода-
вывода, следовательно, они могут значительно отличаться и используемыми
драйверами устройств. Кроме того, основная цель реализации «плоских» файлов
устройств — достичь как можно более высокой скорости ввода-вывода, хотя для
настольных систем общего назначения это не так актуально.
Подобно синхронизированному вводу-выводу (см. раздел 2.16), ввод-вывод в
«плоские» файлы устройств имеет одно громадное преимущество и один существенный
недостаток. Преимущество заключается в том, что процесс ожидает завершения
операции ввода-вывода и поэтому не возникает вопроса о принадлежности дан-
122 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
ных, вследствие чего процесс может быть проинформирован о возникшей
ошибке через возвращаемое значение -1 и код ошибки в глобальной переменной еггпо.
Недостаток состоит в том, что архитектура UNIX не допускает одновременного
обращения к нескольким системным вызовам read или write из одного процесса, в
результате процесс вынужден ждать полного завершения операции ввода-вывода. Это
становится существенной проблемой, например, в многопользовательских системах
управления базами данных, когда чтением/записью данных в «неструктурированный»
файл устройства занимается один-единственный процесс. Как вариант решения этой
проблемы можно было бы предложить организовать многопоточную обработку (см.
раздел 5.17) или использовать асинхронный ввод-вывод (см. раздел 39).
Другой, менее существенный недостаток использования «плоских» файлов устройств,
от которого впрочем довольно легко избавиться, обусловлен тем, что блокировка
процесса в памяти, на время выполнения ввода-вывода, может не дать отработать
другим процессам из-за нехватки свободного места в оперативной памяти. Это тем
более обидно, что на время ввода-вывода через прямой доступ к памяти
процессор остается практически не задействованным, и мог бы обслужить другой процесс.
Решение этой проблемы очень простое — добавить дополнительные модули памяти
в систему. На сегодняшний день цены на память настолько низкие, что едва ли можно
найти серьезные причины, чтобы не купить ее, особенно для систем, в которых
интенсивно используются «плоские» файлы устройств.
В табл. 3.1 приводятся функциональные отличия между обычными файлами,
специальными файлами блочных устройств и неструктурированными файлами устройств.
Таблица 3.1. Сравнение характеристик файлов разных типов
Характеристика Обычный файл Файл блочного «Плоский» файл
устройства устройства
Каталоги, файлы, индексные Да Нет Нет
узлы, права доступа и пр.
Кэш Да Да Нет
Возврат ошибки ввода-вывода, Нет Нет Да
прямой доступ к памяти
Чтобы продемонстрировать различия в производительности, мы измерили в ОС
Solaris время, которое потребовалось для чтения 10 000 блоков размером 65 536 байт
каждый. В эксперименте участвовали обычный файл, файл блочного устройства (/
dev/dsk/cOdOpO) и «плоский» файл устройства (/dev/rdsk/cOdOpO). Результаты
измерений приводятся в секундах. Поскольку чтение «плоского» файла устройства
можно сравнить с чтением обычного файла, открытого с флагом 0_RSYNC (см. раздел
2.16.3), я включил в результаты испытаний оба варианта чтения обычного файла.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 123
Таблица 3.2. Сравнение производительности
Тип файла
Обычный файл
Обычный файл, O.RSYNC | 0.0SYNC
Файл блочного устройства
«Плоский» файл устройства
User
0.40
0.25
0.38
0.10
System
21.28
25.50
22.50
2.87
Факт
441.75
412.05
562.78
409.70
Как видите, преимущество «плоских» файлов в скорости просто огромно. В Linux
я получил похожие результаты. Я не проверял скорости записи, потому что два типа
файлов из этого списка используют кэш — благодаря чему без особого труда
выиграют гонку.
3.2.2 Низкоуровневый доступ к файловой системе
Сейчас мы займемся изучением внутренней структуры файловой системы, читая
ее как дисковое устройство (полагаю, что вы имеете право на чтение файла
устройства). В прикладных программах вам, скорее всего никогда не придется этого
делать, но подобными действиями занимаются такие утилиты обслуживания
файловой системы, как fsck.
Разновидностей файловых систем разработано даже больше, чем разновидностей
UNIX: для Solaris — UFS (UNIX File System), для FreeBSD - FFS (Fast File System, иногда
называется как UFS), для Linux — ReiserFS и Ext2fs. Но практически все они
основаны на оригинальной версии файловой системы UNIX (образца 70-х годов прошлого
столетия) и FFS.
Информация на диске располагается в виде последовательности блоков
фиксированного размера, например 2048 байт (в рамках нашего обсуждения фактический
размер блока не имеет большого значения). Первый блок диска резервируется под
программу-загрузчик (если диск загружаемый), метку диска и прочие сведения
административного характера.
Начинается файловая система суперблоком, расположенным на фиксированном
расстоянии от начала дискового раздела. Суперблок содержит разнообразную
информацию о файловой системе: количество индексных узлов, объем дискового
раздела в блоках, заголовок связного списка свободных блоков и тому подобное. Каждый
файл (будь то обычный файл, каталог, специальный файл и пр.) имеет свой
индексный узел и должен быть связан как минимум с одним и? .отлогов. Файлы,
которые хранят некоторые данные, обычные файлы, каталоги --■ -мволические
ссылки — могут занимать несколько дисковых блоков, указатель на список блоков,
принадлежащих файлу, также хранится в индексном узле. Индексные узлы
располагаются в месте, определяемом суперблоком.
124 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
Индексные узлы с номерами 0 и 1 не используются*. Индексный узел с номером 2
зарезервирован под корневой каталог (/) файловой системы. Остальные номера
индексных узлов не имеют предопределенного назначения и могут быть
использованы без каких-либо ограничений.
Следующая программа демонстрирует один из вариантов организации доступа к
суперблоку и индексному узлу через обращение к «плоскому» специальному
файлу дискового устройства /dev/ad0s1g, который содержит дерево каталогов /us г, в чем
можно убедиться, взглянув на вывод команды mount:
$ mount
/dev/ad0s1a on / (ufs, NFS exported, looal)
/dev/ad0s1f on /tmp (ufs, looal, soft-updates)
/dev/ad0s1g on /usr (ufs, looal, soft-updates)
/dev/ad0s1e on /var (ufs, local, soft-updates)
proofs on /proo (proofs, looal)
Программа специально написана для FreeBSD и файловой системы FFS:
«lfndef FREEBS0
«error "Программа предназначена только для ОС FreeBSO."
«endlf
«Include "defs.h"
«Include <sys/param.h>
«lnolude <ufs/ffs/fs.h>
«lnolude <ufs/ufs/dlnode.h>
«define OEVICE "/dev/ad0s1g"
lnt nmln(lnt argo, ohar *argv[])
<
lnt fd;
long lnumber;
ohar sb_buf[((sizeof(struot fs) / DEV_BSIZE) + 1) * OEV.BSIZE];
struot fs *superblock = (struot fs *)sb_buf;
struct dlnode *d;
ssize_t nread;
off_t fsbo, fsba;
ohar *lnode_buf;
size_t lnode_buf_size;
if (argo < 2) {
printfCFlopflflOK использования: inode <номер_индеконого_узла>\п");
exit(EXIT_FAILURE);
' Индексный узел с номером 0 означает «пустой», или отсутствующий индексный узел.
Индексный узел с номером 1 исторически используется в качестве «головы» списка
поврежденных блоков.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 125
}
inumber = atoi(argv[1]);
eo_neg1( fd = open(DEViCE, O.RDONLY) )
eo.negK iseek(fd, SBLOCK * DEV.BSiZE, SEEK_SET) )
switoh (nread = read(fd, sb_buf, sizeof(sb_buf))) {
oase 0:
errno = 0;
printf("flooTHrHyT конец файла, read (1)\n");
EC_FAiL
oase -1:
EC_FAil
default:
if (nread l= sizeof(sb_buf)) {
errno = 0;
printf("BMeoTo Xd байт прочитано только Xd\n",
sizeof(sb_buf), nread);
EC.FAiL
}
}
printf("Сведения из оуперблока для Xs:\n", DEVICE);
printf("\tepeMfl пооледней запиои = Xs", otime(&superblook->fs_time));
printf("\tKofl-Bo блоков * Xid\n", (iong)superbiook->fs_size);
printf("\tKOfl-eo блоков о данными = Xid\n",
(long)superbiook->fs_dsize);
printf("\tpa3Mep ооновного блока = Xid\n",
(iong)superbiock->fs_bsize);
printf("\tpa3Mep фрагм. блока = Xid\n",
(iong)superbiook->fs_fsize);
printf("\tT04Ka монтирования * Xs\n", superbiook->fs_fsmnt);
inode.buf.size = superbiook->fs_bsize;
eo_nuii( inode_buf = roaiioo(inode_buf_size) )
fsba = ino_to_fsba(superbiook, inumber);
fsbo = ino_to_fsbo(superbiook, inumber);
eo.negK iseek(fd, fsbtodb(superbiook, fsba) * DEV.BSIZE, SEEK.SET) )
switoh (nread = read(fd, inode_buf, inode_buf_size)) <
oase 0:
errno = 0;
printfCfloo^rHyT конец файла, read (2)\n");
EC_FAIL
oase -1:
EC.FAIL
default:
if (nread != inode_buf_size) {
errno = 0;
printf("BMecTO Xd байт прочитано только Xd\n",
inode_buf_size, nread);
EC.FAIL
126 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
}
}
d = (struct dincde *)4incde_buf[fsbc * sizecf(struct dincde)];
printf("\пСведения об incde с номером Xld:\n", inumber);
printf("\tnpaea доступа = 0Xc\n", d->di_mcde);
printf("\tKon-ec ссылок = Xd\n", d->di_nlink);
printf ("^владелец = Xd\n", d->di_uid);
printf("XtepeMfl модификации = Xs", ctime((tine_t *)&d->di_mtirae));
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_ENO
}
Объявление буфера sb.buf выглядит довольно запутанно:
char sb_buf[((sizecf(struct fs) / OEV.BSIZE) + 1) * 0EV_BSIZE];
Дело в том, что размер его должен быть округлен в большую сторону до
ближайшей границы сектора, что совершенно необходимо при работе с «плоскими»
файлами устройств в ОС FreeBSD.
Первый вызов lseek устанавливает текущую позицию в файле на начало
суперблока. Идентификатору SBL0CK соответствует число 16, а идентификатору 0EV.BSIZE —
число 512 (размер сектора в подавляющем большинстве случаев). Суперблок
всегда располагается начиная с 16-го сектора от начала файловой системы. Первая
группа обращений к функции printf выводит содержимое некоторых полей из
ОГРОМНОЙ Структуры fs:
Сведения из суперблска для /dev/ad0s1g:
время последней записи = Men Oct 14 15:25:25 2002
ксл-вс блсксв = 1731396
ксл-вс блсксв с данными = 1704331
размер сснсвнсгс блска = 16384
размер фрагм. блска = 2048
течка ментирсвания = /usг
Далее программа отыскивает заданный индексный узел (argv[1]), используя
макросы, определенные в одном из заголовочных файлов. Я получил номер
индексного узла командой Is с ключом -i и после этого передал номер программе:
$ Is -li x
383642 -rwxr-xr-x 1 marc marc 11687 Sep 19 13:29 x
Для данного узла программа выдала следующее:
Сведения сб incde с нсмерсм 383642:
права доступа = 0100755
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 127
кол-во ооылок = 1
владелец = 1001
время модификации = Thu Sep 19 13:29:30 2002
Я больше не собираюсь заострять ваше внимание на низкоуровневом доступе к
дискам. Я лишь хотел показать родство обычных файлов и специальных файлов
устройств. Однако вы можете поэкспериментировать с программой и попытаться
извлечь с ее помощью дополнительную информацию или даже приспособить ее
под другую версию UNIX.
3.2.3 Системные вызовы statvfs и fstatvfs
Безусловно, уметь обращаться с суперблоком через «плоский» файл устройства при
помощи системного вызова read бывает довольно полезным, но для этих целей лучше
пользоваться специальными системными вызовами statvfs и fstatvfs, которые
определены спецификацией SUS1 (см. раздел 1.5.1):
statvfs — возвращает сведения о файловой системе по имени каталога
#inolude <sys/statvfs.h>
int statvfs(
oonst ohar «path, /* путь к файловой оиотеме */
struot statvfs «buf /* возвращаемые оведения •/
);
/• Возвращает 0 в олучае уопеха, -1 в олучае ошибки (код ошибки -
в переменной еггпо) •/
fstatvfs — возвращает сведения о файловой системе по дескриптору файла
«inolude <sys/statvfs.h>
int fstatvfs(
oonst int fd,
struot statvfs *buf
);
/* Возвращает 0 в олучае
в переменной еггпо) »/
/♦ деокриптор
файла
*/
/* возвращаемые оведения */
уопеха, -1 в
олучае
ошибки
(код ошибки -
На практике наиболее часто используется вызов statvfs, который возвращает
сведения о файловой системе, расположенной по указанному пути. Однако, если в
программе имеется открытый файл, можно воспользоваться вызовом fstatvfs, который
вернет те же самые сведения.
Стандарты определяют полный перечень полей структуры statvfs, но каждая
конкретная реализация не обязана поддерживать их все. Кроме того, в большинстве
реализаций присутствуют свои дополнительные поля и определяется
дополнительный набор флагов для поля f.flag. За более точной информацией по этому
вопросу вам следует обратиться к страницам справочного руководства (man). Если в ва-
128 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
шем случае какие-либо из стандартных полей не поддерживаются, то это значит,
что на их месте находятся поля с информацией иного характера.
unsigned long f_bsize;
unsigned long f_frsize;
fsblkcnt_t f_blccks;
fsblkcnt_t f_bfree;
/
/
/
/
struct statvfs — структура, используемая при обращении к statvfs и fstatvfs
struct statvfs {
размер блока */
фундаментальный размер блока (fblcck) */
общее числе блсксв fblcck */
количестве свободных блсксв
для суперпельзевателя */
fsblkcnt_t f_bavail; /* количестве свободных блсксв
для сбыкнсвеннсгс пользователя »/
общее количестве индексных узлов •/
количестве свободных индексных узлев */
для суперпельзевателя */
ксличество свебодных индексных узлев */
■ для сбыкнсвеннсгс пользователя */
идентификатор файловой системы */
флаги (см. ниже) */
/* максимальная длина имени файла */
fsfilcnt.t f_files;
fsfilcnt_t f_ffree;
fsfilcnt_t f_favail;
unsigned long f_fsid;
unsigned leng f.flag;
unsigned long f_naraeroax;
};
/«
/
/
/
/
h
/•
/•
Некоторые замечания по этой структуре.
• Чтобы повысить скорость доступа, большинство файловых систем выделяют
файлам блоки большого размера, который задается полем f_bsize, но конец файла
размещается в блоках маленького размера с целью экономии дискового
пространства. Размер таких блоков-фрагментов называется фундаментальным и
хранится в поле f_f rsize; сокращенно такие блоки называются fblcck. Поля
структуры, имеющие тип fsblkcnt.t, хранят количество фрагментов блоков; чтобы
получить величину, выраженную в байтах, необходимо имеющееся там
значение умножить на фундаментальный размер блока (f_frsize).
• Типы fsblkcnt.t и fsfilcnt_t являются беззнаковыми, но от реализации к
реализации размер их может отличаться. Наиболее часто встречаются
определения long и long long. Если вам необходимо вывести значения этих полей
функцией printf и ваш компилятор соответствует стандарту С99, приведите
значение поля к типу uintmax_t и используйте спецификатор формата Xju, в
противном случае — приводите к типу unsigned long long и используйте
спецификатор Xllu.
• Спецификации SUS определяют всего два флага для поля f_flag — они
информируют о том, как была смонтирована файловая система:
ш ST_RDONLY — только для чтения;
ш STNOSUID — в целях безопасности отключена возможность изменения прав
доступа для исполняемых файлов.
• В большинстве систем для нужд суперпользователя выделяется немного
больше пространства, чем для обычного пользователя. Таким способом создается
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 129
минимально необходимый резерв на случай отсутствия свободного места в
файловой системе.
ОС FreeBSD была разработана до появления спецификации SUS и потому не
поддерживает системные вызовы statvfs и fstatvfs, но в ней имеется похожий вызов
— statfs. В этот вызов передается структура, которая отличается названием и
набором полей. Пример следующей программы демонстрирует способ извлечения
основных сведений о файловой системе с автоматическим выбором того или иного
системного вызова (statfs или statvfs):
#if _X0PEN_S0URCE >= 4
«include <sys/statvfs.h>
«define FCN_NAME statvfs
«define STATVFS 1
«elif defined(FREEBSD)
«include <sys/param.h>
«include <sys/iacunt. h>
«define FCN.NAME statfs
«else
«error "Необходима поддержка вызсва statvfs или функции, егс заменяющей"
«endif
vcid print_statvfs(ccnst char «path)
{
struct FCN_NAME buf;
if (path == NULL)
path = ".";
ec_neg1( FCN_NAME(path, ibuf) )
«ifdef STATVFS
printf("размер блска = Xlu\n", buf.f_bsize);
printf("фундаментальный размер блска = Xlu\n", buf.f_frsize);
«else
printf("размер блска = Xlu\n", buf.f_icsize);
printf("фундаментальный размер блска = Xlu\n", buf.f_bsize);
«endif
printf("ебщее числе блсксв = Xllu\n",
(unsigned leng lcng)buf.f_blccks);
printf("количестве свободных блсксв для суперпельзевателя = Xllu\n",
(unsigned leng lcng)buf.f_bfree);
printf("количестве свободных блсксв для обыкновенного пользователя = Х11и\п",
(unsigned long lcng)buf.f_bavail);
printf("общее количестве индексных узлев = Xllu\n",
(unsigned leng lcng)buf.f_files);
printf("количестве свободных индексных узлов для суперпельзевателя = Х11и\п",
(unsigned long long)buf.f_ffree);
130 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
«ifdef STATVFS
printf("количеотво овободных индеконых узлов для обыкновенного пользователя =
Xiiu\n",
(unsigned long iong)buf.f_favaii);
printf("идентификатор файловой оиотемы = Xiu\n", buf.f_fsid);
printf("только для чтения = Xs\n",
(buf.f.fiag & ST.RDDNLY) == ST_RDDNLY ? "да" : "нет");
printf("возможность setuid/setgid отключена = Xs\n",
(buf.f.fiag 4 ST_NDSUID) == ST_NOSUID ? "да" : "нет");
printf("максимальная длина имени файла = Xiu\n", buf.f_nanenax);
#eise
printf("только для чтения = Xs\n",
(buf.f_fiags & MNTJTODNLY) == MNT.RDDNLY ? "да" : "нет");
printf("возможность setuid/setgid отключена = Xs\n",
(buf.f.fiags & HNT.NDSUID) == MNT.NDSUID ? "да" : "нет");
«endif
printf("\nCBo6oflHoe проотранотво = X.DfXX\n",
(doubie)buf.f_bfree * 1DD / buf.f.biooks);
return;
EC_CLEANUP_BGN
EC.FLUSH("p rint.statvfs");
EC_CLEANUP_END
}
В результате работы этой функции в ОС Solaris для каталога /home/maro/aup было
получено следующее (в Linux — результаты те же самые):
размер блока = 8192
фундаментальный размер блока = 1D24
общее чиоло блоков = 4473D46
количеотво овободных блоков для оуперпользователя = 3683675
количеотво овободных блоков для обыкн. пользователя = 3638945
общее количеотво индеконых узлов = 566912
количеотво овободных индеконых узлов для оуперпользователя = 565782
количеотво овободных индеконых узлов для обыкн. пользователя = 565782
идентификатор файловой оиотемы = 26738695
только для чтения = нет
возможнооть setuid/setgid отключена = нет
макоимальная длина имени файла = 255
Свободное проотранотво = 82Х
В FreeBSD для каталога /usr/home/marc/aup было получено:
размер блока = 16384
фундаментальный размер блока = 2D48
общее чиоло блоков = 17D4331
количеотво овободных блоков для оуперпользователя = 12D9974
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 131
количеотво овободных блоков для обыкн. пользователя = 1073628
общее количеотво индеконых узлов = 428030
количеотво овободных индеконых узлов для оуперпользователя = 310266
только для чтения = нет
возможность setuid/setgid отключена = нет
Свободное проотранотво = 71X
Системный вызов statvfs (или statfs) лежит в основе хорошо известной утилиты
df («disk free» — см. Упражнение 32). Ниже приводится результат работы этой
утилиты в FreeBSD:
$ df /usr
Filesystem 1K-blooks Used Avail Capacity Mounted on
/dev/ad0s1g 3408662 988696 2147274 32X /usr
Здесь число блоков размером 1 Кбайт — 3408662, соответствует числу блоков
размером 2 Кбайта — 1704331, полученному нашей программой print_statvfs.
Также существуют стандартные способы извлечения информации из индексных узлов,
таким образом, вам не нужно будет обращаться к файлу устройства, как мы это делали
в предыдущем разделе. Подробнее эта возможность будет рассмотрена в разделе 3.5.
3.2.4 Монтирование и отмонтирование файловых систем
ОС UNIX может обслуживать несколько файловых систем (на жестких и гибких дисках,
на компакт-дисках, на DVD и т. п.), но все они располагаются в едином дереве
каталогов, которое начинается от корня. Процесс подключения файловой системы к
иерархии каталогов называется монтированием, а процесс отключения — размон-
тированием, или отмонтированием. На рис. 3-1 показана большая файловая
система со своим корнем (индексный узел с номером 2 и два каталога — х и у), и
маленькая отмонтированная файловая система со своим собственным корнем (индексным
узлом с номером 2). (Не забывайте: индексный узел с номером 2 всегда отводится
для корневого каталога.)
Рис. 3.1. Две не связанных между собой файловых системы
132 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
Рис. 3.2. Файловые системы после монтирования
Чтобы примонтировать вторую файловую систему, вам необходимо знать две вещи:
имя устройства, в данном случае /dev/ad0s1m, и каталог, куда выполняется
монтирование (точка монтирования), в данном случае /у/а (индексный узел 221). Пример
команды, которая создает дерево каталогов из рисунка 3-2:
# mount /dev/ad0s1m /у/а
После монтирования старое содержимое каталога будет сокрыто, а все ссылки на
него автоматически станут указывать на индексный узел ad0slm:2 (такая нотация
означает — «индексный узел с номером 2 на устройстве adOslm»). Таким образом,
файл ad0slm:107 теперь доступен под полным именем /y/a/f. После отмонтирова-
ния устройства adOslm, его содержимое станет недоступно, зато откроется доступ
к прежнему содержимому ad0slg:22\
Любая UNIX-система имеет в своем распоряжении системные вызовы mount и umount
(иногда unmount), но в зависимости от разновидности UNIX они могут несколько
отличаться наборами аргументов и поведением. Подобно другим функциям,
доступным только суперпользователю, они не стандартизованы. На практике эти
системные вызовы используются для реализации утилит mount и umount и почти
никогда не вызываются напрямую из приложений. В качестве примера я приведу
краткое описание, взятое из Linux, но мы не будем глубоко погружаться в изучение этих
вызовов:
* Фактически, каталоги, которые выступают в качестве точки монтирования, редко
используются для хранения каких-либо файлов, но иногда системные администраторы
используют этот эффект для сокрытия содержимого некоторых каталогов.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 133
mount — монтирует файловую систему (не стандартизован)
«include <sys/mcunt.h>
int mcunt(
const char *scurce, /* устройстве */
censt char «target, /* каталог */
const char *type, /* тип (например ext2) */
unsigned leng flags, /* флаги ментирсвания (например MS_RD0NLY) */
censt vcid «data /* данные, зависящие ст типа файлсвей системы */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменной еггпс) */
umount — отмонтирует файловую
«include <sys/mcunt.h>
int umcunt(
const char «target /«
);
/* Возвращает О в случае
в переменней еггпс) */
каталог
успеха,
систему (не стандартизован)
•/
-1 в
случае
сшибки
(кед сшибки -
Как правило, если вызов umcunt завершается с ошибкой, то чаще всего с кодом EBUSY,
из-за того, что какой-либо файл или каталог в отмонтируемой файловой системе
остается в использовании. Обычно в системе существует альтернативный
системный вызов — umcunt2, которому в качестве второго аргумента передается флаг,
указывающий на необходимость принудительного отмонтирования. Необходимость
в этом иногда возникает, когда нужно выполнить остановку системы.
С точки зрения прикладного программирования, за редким исключением, нас
совершенно не интересует, находится ли некий файл (или каталог) в отдельной
файловой системе или нет. Нас интересует только правильность заданного пути к файлу
или каталогу. Фактически, все доступные файловые системы, и даже корень, были
смонтированы в свое время, возможно в ходе загрузки. Одна из проблем, которые
могут нам встретиться, связана с символическими ссылками, о чем и пойдет речь в
следующем разделе.
3.3 Жесткие и символические ссылки
Запись в файле каталога, содержащая имя и номер индексного узла, называется
жесткой ссылкой. Существует еще один тип ссылок — символические ссылки, о
которых мы тоже будем говорить в этом разделе. На рис. 33 показаны оба типа
ссылок (шестнадцатеричные числа в скобках — это идентификаторы устройств, о
которых мы поговорим позднее).
134 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
Рис. 3.3. Жесткие и символические ссылки
3.3.1 Создание жесткой ссылки (системный вызов link)
Вы получаете жесткую ссылку, когда создаете файл любого типа, включая
каталоги. Вы можете создать дополнительные жесткие ссылки на файлы, не являющиеся
каталогами*, с помощью системного вызова link:
link — создает жесткую ссылку
«include <unistd.h>
int link(
ccnst char «cldpath,
ccnst char «newpath
);
/* Возвращает О в случае
в переменней errnc) */
/* старее
/* невсе
успеха, -
им
имя
1 в
1 файла
файла
случае
«/
>/
сшибки
(кед сшибки -
Первый аргумент (cldpath) должен быть именем существующей жесткой ссылки — она
представляет номер используемого индексного узла. Второй аргумент (newpath)
задает имя новой жесткой ссылки. Старая и новая ссылки абсолютно равноправны, так как
UNIX не различает первичные и вторичные ссылки. Процесс, создающий жесткую
ссылку, должен иметь право на запись в каталог, где она будет размещаться. Имени
ссылки, представленной во втором аргументе, не должно существовать — системный
вызов link не умеет изменять существующие ссылки. В случае необходимости,
существующая ссылка должна быть сначала удалена вызовом unlink (см. раздел 2.6) или
переименована вызовом rename (будет обсуждаться в следующем разделе).
' Некоторые системы допускают возможность создания жестких ссылок на существующие
каталоги, но это означает потерю древовидной структуры дерева каталогов, усложняя
системное администрирование.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 135
3.3.2 Переименование файлов и каталогов
(системный вызов rename)
На первый взгляд операция переименования выглядит достаточно простой, если под
ней подразумевается: «изменить название элемента каталога». Все, что нужно сделать
— это создать вторую жесткую ссылку вызовом link и удалить старую ссылку
вызовом unlink, но при таком подходе есть ряд моментов, сильно осложняющих дело.
• Файл может быть каталогом, для которого нельзя создать вторую жесткую ссылку.
• Иногда возникает необходимость изменить имя так, что в результате
переименованный объект оказывается в другом каталоге. Если все происходит в рамках
одной файловой системы, то особых проблем не возникает, но если «новое» имя
должно оказаться в другой файловой системе, то это уже большая проблема,
потому что link может создавать жесткие ссылки только в пределах одной
файловой системы. Разумеется, можно создать символическую ссылку (см.
следующий раздел) в новом каталоге, но это совершенно не то, что большинство из
нас подразумевает под словом «переименовать».
• Если необходимо «переименовать» (фактически — «переместить») каталог из
одной файловой системы в другую, то придется переместить целое дерево
подкаталогов со всем их содержимым. Переместить лишь пустые каталоги будет
недостаточно.
• Если существует несколько жестких ссылок на один и тот же файл и при этом
новое имя остается в пределах той же самой файловой системы, то тут тоже
проблем не возникает, потому что все они ссылаются на индексный узел по его
номеру, который не изменяется. Но если вы собираетесь переместить файл в
другую файловую систему, то все старые ссылки окажутся неверными, потому
что фактически вы скопируете файл в новое место, создадите новую жесткую
ссылку и удалите старую. Но при этом «старая» копия файла останется на
месте, она никуда не денется, так как файл нельзя удалить, пока на него есть хоть
одна ссылка. Таким образом, вместо «переименования» вы получите две копии
файла. Нехорошо как-то получается.
• Если символическая ссылка содержит полный путь к файлу и вы по тем или иным
причинам измените этот путь, то в результате получите «битую» ссылку. Снова
нехорошо.
Этот список можно было бы продолжать и продолжать, стоит лишь немного
задуматься. Однако суть вам должна быть понятна. Вот почему утилита mv, которую мы
используем для «переименования» файлов и каталогов, считается одной из самых
сложных.
В любом случае утилита mv как-то должна перемещать каталоги в пределах
файловой системы, а для этого нужен один из альтернативных механизмов.
• Всегда копировать каталог и вложенные подкаталоги со всем их содержимым,
а затем удалять старый каталог, даже если он просто переименовывается,
оставаясь в том же самом родительском каталоге.
136 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
• Системный вызов link, который может работать с каталогами, пусть даже
вызвать его может только суперпользователь (для исполняемого файла mv можно
установить бит set-user-ID).
• Новый системный вызов, который будет в состоянии выполнять перемещение
каталогов внутри файловой системы.
Первый вариант сразу отметается — слишком большой объем работы необходимо
выполнить, если вам нужно только поменять пару символов в имени каталога! Второй
вариант был отвергнут POSIX в пользу третьего, нового системного вызова rename.
Хотя на самом деле не такой уж и новый — он был включен в стандарт языка С и
уже существовал в BSD. Это единственный способ переименовать каталог без
полного копирования его содержимого.
rename — переименовывает файл
«include <stdio
int renarae(
const char
const char
);
/* Возвращает О
в переменной ег
h>
•oldpath,
•newpath
в случае
гпо) */
/» отарое
/* новое
успеха, -
полное
полное
имя
имя
1 в олучае
V
•/
ошибки
(код
ошибки -
Вызов rename выполняет примерно следующую последовательность действий:
1. Если newpath существует, то он удаляется с помощью unlink или rmdir.
2. Выполняется link (oldpath, newpath), даже если oldpath является каталогом.
3. Вызовом unlink или rmdir удаляется oldpath.
Кроме того, он имеет свои особенности и ограничения.
• Как я уже упоминал, шаг 2 работает и с каталогами, даже если процесс не
обладает правами суперпользователя (процессу достаточно иметь право на запись
в родительский каталог newpath).
• Если newpath существует, тогда и newpath, и oldpath должны быть одного типа, либо
файлы, либо каталоги.
• Если newpath существует и является каталогом, он должен быть пуст
(аналогичное правило существует и у rmdir). Нд шаге 3, если oldpath — каталог, он
удаляется, даже если это непустой каталог, так как его содержимое уже имеется в newpath.
• Если rename терпит неудачу, то все остается без изменений.
На первый взгляд кажется, что шаги 1, 2 и 3 можно оформить в виде
библиотечной функции, но, к сожалению, нет способа заставить ее работать подобно
системному вызову.
Утилита mv может выполнять более широкий диапазон действий, чем системный
вызов rename. Она умеет перемещать отдельные файлы и каталоги между файловыми си-
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 137
стемами. Она может перемещать группы файлов и каталогов. Системный вызов rename
дает команде mv пусть и небольшую, но основополагающую долю функциональности.
И заключительное примечание: если oldpath является символической ссылкой, то
rename работает с ней, а не с тем, на что она ссылается.
3.3.3 Создание символических ссылок
(системный вызов symlink)
Как показано на рис. 3-3, если вы хотите создать в каталоге /у жесткую ссылку на
файл /х (adOslg: 107), нужно выполнить системный вызов:
ec_negl( link("/x". "/У/о") )
Он сделает ссылки /х и /у/с эквивалентными, так как adOslg: 107 не «входит» ни в
один из каталогов.
Невозможно создать жесткую ссылку из /у на /y/a/f (ad0slm:l07), потому что для
идентификации объектов используются номера индексных узлов, а не пары
значений — «устройство: номер узла». (Я позаботился о том, чтобы на рисунках в разных
файловых системах присутствовали файлы с одинаковыми номерами индексных
узлов — 107.) Если вы попытаетесь выполнить:
ec_neg1( link("/y/a/f", "/У/b") )
тогда link вернет вам код ошибки EXDEV.
Решение хорошо известно каждому опытному пользователю — нужно создать
символическую ссылку. В отличие от жесткой ссылки, символическая ссылка —
просто маленький файл, который хранит строку полного имени объекта в текстовом
виде. На рис. 3-3 символическая ссылка изображена в виде эллипса. У нее есть
собственный индексный узел (adOslg: 118), но практически любая попытка получить
доступ к /у/о интерпретируется ядром как попытка доступа к /y/a/f, который
находится в другой файловой системе.
Символические ссылки могут ссылаться на другие символические ссылки. Обычно,
когда системному вызову open передается символическая ссылка такого рода, ядро
разыменовывает их по цепочке до тех пор, пока не встретит объект, который не
является символической ссылкой. С жесткими ссылками этого не происходит,
потому что они прямо ссылаются на индексный узел, который не может быть другой
жесткой ссылкой (хотя он может быть каталогом, который содержит жесткую ссылку).
Другими словами, если путь к объекту определяется жесткой ссылкой, он может
рассматриваться буквально. Если символической ссылкой, то фактический путь к объекту
зависит от содержимого промежуточных символических ссылок.
Создаются символические ссылки командой In с ключом -s, однако существует
системный вызов symlink, который выполняет туже работ)-:
138 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
symlink — создает символическую ссылку
«include <unistd.h>
int symlink(
const char *oldpath,
const char «newpath
);
/* Возвращает О в случае
в переменной егтпо) */
/•
/•
возможное старое
новое
успеха,
полное имя
-1 в случае
полное
*/
ошибки
имя
(код
•/
ошибки -
Главным образом, symlink работает аналогично link. В обоих случаях создается новая
жесткая ссылка с именем newpath. Однако в случае символической ссылки, эта
новая жесткая ссылка указывает на файл символической ссылки, который содержит
строку oldpath.
В комментариях к системному вызову использовано уточнение — «возможное».
Сделано это потому, что symlink вообще никак не проверяет корректность аргумента
oldpath. Вы можете сделать даже такой вызов:
ec_neg1( symlink("Сегодня в 12:30 обед с Майком", "напоминание") )
и затем просмотреть «напоминание» командой Is:
$ Is -1 напоминание
lrwxrwxrwx 1 marc sysadmin 24
Oct 16 12:04 напоминание -> Сегодня в 12:30 обед с Майком
Такая особенность символических ссылок заложена в них преднамеренно — они
могут ссылаться на несуществующие объекты, к коим можно причислить объекты,
находящиеся в отмонтированных файловых системах.
С другой стороны, подобная независимость от объекта ссылки имеет свои
недостатки — ядро ничего не делает с символической ссылкой, когда удаляется объект
ссылки. Под словом «удаляется» я имею ввиду удаление жесткой ссылки. Не
забывайте, что физически файл удаляется с диска только тогда, когда счетчик ссылок в
индексном узле достигает значения 0.
Удалить символическую ссылку можно с помощью системного вызова unlink (см.
рис. 3-3):
ec_neg1( unllnk("/y/b") )
Этот вызов удалит символическую ссылку /у/b и никак не повлияет на сам файл
/y/a/f. Таким образом, unlink удаляет саму жесткую ссылку (в данном случае — файл
символической ссылки), которая передана в качестве аргумента.
Хорошо, а как тогда удалить файл, на который указывает символическая ссылка?
Для этого нужно получить путь к удаляемому файлу, прочитав содержимое
символической ссылки системным вызовом readlink:
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 139
readlink — читает содержимое файла символической ссылки
«include <unistd.h>
ssize_t readlink(
const char *path
char »buf,
size_t bufsize
);
/• Возвращает О в сл^
в переменней errnc) <
/•
/*
/•
^чае
/
полнее имя »/
возвращаемый текст
размер буфера */
успеха, -1 в случае
*/
сшибки
(кед сшибки -
Системный вызов readlink возвращает полное имя объекта ссылки, но без
завершающего символа «\0», который в UNIX обычно служит маркером конца строки.
Поэтому в качестве третьего аргумента следует передавать число, на 1 меньше
фактического размера буфера (зарезервировав, таким образом, место для символа «\0»),
а после получения заполненного буфера необходимо добавить в него символ <\0»,
примерно таким образом:
ssize.t n;
char buf[1024];
ec_neg1( n = readlink("/hcme/marc/mylink", buf, sizeof(buf) - 1) )
buf[n] = Л0';
Теперь можно удалить объект, на который ссылается символическая ссылка /home/
marc/mylink и саму символическую ссылку:
ec_neg1( unlink(buf) )
ec_neg1( unlink("/hcme/marc/mylink") )
Системный вызов readlink не единственный, который может обслуживать файлы
символических ссылок непосредственно. Другой системный вызов, который работает
с символическими ссылками напрямую — stat (см. раздел 35.1), точнее его
разновидность — lstat. Он возвращает информацию, извлекаемую из индексного узла.
В примере с readlink есть одна маленькая проблема. Она связана с
использованием константы 1024. Строго говоря, е.сли фактическое полное имя объекта ссылки
окажется больше размера буфера, то это не значит, что нам грозит крах системы
или программы — мы просто получим «усеченный» путь к файлу, хотя это
немногим лучше.
Но если исходить из предположения, что максимальная длина имени файла не
превосходит 255 символов, а глубина вложения подкаталогов не известна, какого
размера буфер нам потребуется? Ответ 1024 не подходит, в самом тяжелом случае
его хватит, чтобы добраться лишь до каталога 4-го уровня! Более точный ответ на
поставленный вопрос дает следующий раздел.
140 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
3.4 Полные имена объектов файловой системы
В этом разделе рассказывается, как определить максимальную длину полного
имени объекта в файловой системе и как получить полное имя текущего каталога.
3.4.1 Какой длины может быть полное имя?
Если бы существовало ограничение на максимальную длину полного имени
файла, скажем, константа _POSIX_MAX_PATH, то это означало бы наличие
ограничений на глубину вложения подкаталогов. Даже если бы это число было достаточно
большим, данное ограничение породило бы немало проблем, поскольку для UNIX
является обычным делом, например, монтировать файловые системы других
компьютеров посредством NFS (Network File System — сетевая файловая система).
Исходя из этого, максимальная длина полного имени определяется динамически, во
время исполнения и может изменяться файловой системой.
По сути это то, чем являются функции pathconf и fpathconf (см. раздел 1.5.6). В
системе, соответствующей стандарту POSIX1990, вы можете использовать их примерно
таким образом:
static long get_max_pathname(const char «path)
<
long max_path;
errno = 0;
max_path = pathconf(path, _PC_PATH_MAX);
if (max_path == -1) {
if (errno == 0)
raax_path = 4096; /* лучше так, чем никак */
else
EC_FAIL
}
return raax_path + 1;
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_EN0
}
Я увеличил полученное от pathconf значение на 1, потому что в документации,
которая была у нас на руках, четко не оговаривается, предусматривает ли этот размер
наличие завершающего символа «\0». я склонен считать, что не предусматривает.
Я опробовал эту функцию на всех трех моих тестовых системах, как на локальных,
так и на NFS-томах. В результате, в FreeBSD и Solaris я получил число 1024, в Linux
— 4096. Но эти результаты характерны для моих систем с моими настройками.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 141
3 вашем же случае, лучше не полагаться на мои цифры, а постараться
самостоятельно получить их с помощью pathconf.
Чисто технически, для аргумента _РС_РАТН_МАХ функция pathconf возвращает
наибольший, но не абсолютный, а относительный размер имени файла, принимая за
точку отсчета аргумент path. Таким образом, чтобы соблюсти предельную точность, в
качестве первого аргумента нужно передавать путь к корневому каталогу
файловой системы, которая вас интересует и затем добавлять длину пути до корня
файловой системы. Однако подобные рассуждения могут завести слишком далеко,
большинство же из нас предпочитают просто вызывать функцию с аргументом «/» или
Сказанное выше соответствует стандартам POSIX и SUS. На практике даже
системы, в которых pathconf и fpathconf возвращают некоторое число, не
рассматривают его как предел при создании каталогов, но они используют его при проверке
корректности аргументов системных вызовов, возвращая код ошибки ENAHET00L0NG,
если вы попытаетесь передать слишком длинное имя файла, например, вызову open.
3.4.2 Системный вызов getcwd
Это очень простой системный вызов, который служит для получения имени
текущего каталога. Подобно вызову readlink, единственная сложность — определить, какой
величины должен быть буфер.
getcwd — возвращает полное имя текущего каталога
«include <unistd.h>
char *getcwd(
char *buf, /* возвращаемое
size_t bufsize /* размер буфера
);
/* Возвращает 0 в случае успеха, -1
в переменной еггпо) */
полное имя текущего каталога
*/
в случае ошибки (код
ошибки
•/
-
Ниже приводится пример функции, которая автоматически размещает буфер в
динамической памяти и затем вызывает getcwd. Если передать функции значение
t rue, то она освободит буфер.
static char *get_cwd(bool cleanup)
{
static char *cwd = NULL;
static long max_path;
if (cleanup) {
free(cwd);
cwd = NULL;
}
else {
142 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
if (cwd == NULL) {
ec_neg1( max_path = get_max_pathname(".") )
ec_null( cwd = malloc((size_t)max_path) )
}
ec_null( getcwd(cwd, max_path) )
return cwd;
}
return NULL;
EC_CLEANUP_BGN
return NULL;
EC_CLEANUP_END
}
Ниже приводится фрагмент кода, аналогичный по своему действию команде pwd:
char *cwd;
ec_null( cwd = get_cwd(false) )
printf("Xs\n", cwd);
(void) get_cwd(true);
Когда я запустил его на Solaris, то получил:
/home/marc/aup
Мы еще не раз встретимся с вызовом getcwd в этой главе, а дополнительные
примеры по его использованию вы найдете в разделе 3-6.4.
Существует еще один системный вызов — getwd, очень похожий на предыдущий.
Однако он уже устарел и не рекомендуется к использованию в современных
программах.
3.5 Получение и отображение метаданных файла
В этом разделе мы рассмотрим способы получения сведений о файле, таких как
владелец, время последнего изменения и способы их правильного отображения.
3.5.1 Системные вызовы stat, fstat и lstat
Индексный узел содержит полный набор метаданных о файле — все, кроме имени,
которое в действительности никак не связано с индексным узлом и данными, на
которые он указывает. Процедура чтения данных из индексного узла прямо из файла
устройства, как мы это делали в разделе 32.2, является слишком низкоуровневой. К
счастью, для получения информации из индексных узлов, существуют три
стандартных системных вызова — stat, lstat и fstat и одна очень известная команда — Is.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 143
stat — возвращает сведения о файле по его имени
«include <sys/stat.h>
int stat(
const char *path, /* полнее имя файла */
struct stat *buf /* возвращаемая информация */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменной еггпс) •/
lstat — возвращает сведения о файле по его имени
волических ссылок
«Include <sys/stat.h>
int lstat(
censt char «path, /*
struct stat *buf /*
);
/* Возвращает О в случае
в переменней еггпс) */
полнее имя файла */
возвращаемая информация
успеха,
без разыменования
*/
-1 в случае сшибки
(кед сшибки -
сим-
fstat
— возвращает сведения о файле
«include <sys/stat.h>
int fstat(
);
/•
в
censt int fd, /*
struct stat «buf /*
Возвращает О в случае
переменней еггпс) */
по дескриптору
дескриптср файла •/
возвращаемая
успеха, -1
информация */
в случае сшибки (кед
сшибки -
Системный вызов stat принимает полное имя файла и находит соответствующий
ему индексный узел. Вызов fstat принимает дескриптор открытого файла и
отыскивает соответствующий индексный узел в таблице активных узлов ядра. Вызов lstat
аналогичен вызову stat за одним исключением — если в качестве имени файла была
передана символическая ссылка, то будет возвращена информация о самой
ссылке, а не об объекте, который она представляет*. Все три вызова возвращают одни и
те же метаданные, через структуру типа stat, в первом аргументе.
Ниже приводится определение структуры stat. Однако в некоторых реализациях
поля структуры могут быть переставлены местами, могут быть добавлены новые поля,
Вы не найдете системного вызова f lstat, потому что невозможно получить дескриптор файла,
открытый для символической ссылки. Любая попытка передать символическую ссылку
системному вызову open приведет к открытию не файла ссылки, а связанного со ссылкой объекта.
Даже если бы это было возможно, то все равно хватило бы одного вызова fstat.
144 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
поэтому старайтесь всегда лишний раз убедиться, что подключаете к программе
локальный заголовочный файл sys/stat.h.
struct stat — структура для приема сведений из stat, fstat и lstat
struct stat {
dev_t st_dev;
inc_t st_inc;
mcde_t stjncde;
niink_t st_nlink;
uid_t st_uid;
gid_t st_gid;
dev_t st_rdev;
cff_t st_size;
tinie_t st_atime;
time_t st_ratinie;
time_t st_ctirae;
biksize_t st_blksize;
bikcnt_t st_biccks;
};
/•
/•
/•
/•
/•
/•
/•
/*
/•
/•
/*
/•
/•
/•
/•
идентификатор устройства файлсвсй системы */
нсмер индексного узла •/
режим (см. ниже) */
ксл-вс жестких ссылок */
идентификатор пользователя •/
идентификатор группы »/
идентификатор устройства */
(для специального файла) */
размер в байтах */
время последнего обращения */
время последнего изменения */
время псследнегс изменения индексного узла */
оптимальный размер блска */
для операций ввсда-вывсда */
ксл-вс блоков по 512 байт */
Некоторые замечания по этой структуре.
• Идентификатор устройства (типа dev_t) — это число, уникальное для каждой
смонтированной файловой системы, даже для NFS-томов. Таким образом,
комбинации st.dev и st.inc однозначно идентифицируют любой индексный узел в
системе. По существу это очень похоже на нотацию, которую мы использовали
в разделе 3.2.4 (например «adOslg:221»). Взгляните еще раз на рис. 3-3 — под
именами устройств указаны их шестнадцатеричные идентификаторы. Стандарты
не оговаривают алгоритм определения идентификатора устройства, но в
большинстве реализаций он представляет собой комбинацию старшего и младшего
номеров устройства, где старший номер идентифицирует драйвер, а младший
— собственно устройство. Как правило, младший номер устройства — это
самый младший байт.
• Поле st_dev — это устройство, на котором находится индексный узел. Поле st_rdev
используется только для специальных файлов устройств и является устройством,
которое специальный файл представляет. Возьмем, например, специальный файл
устройства /dev/adOslg. Этот файл находится в корневой файловой системе,
поэтому поле st.dev содержит идентификатор устройства корневой файловой
системы, но поле st_rdev содержит идентификатор устройства,
представленного этим файлом.
• Поле st_size интерпретируется в зависимости от типа файла и реализации. Для
обычных файлов, каталогов и символических ссылок — это объем данных на диске
(для символических ссылок — длина пути). Для объектов в разделяемой памяти
— объем занимаемой памяти. Для каналов — количество данных в канале.
• Время последнего обращения (st.atime) изменяется при чтении файла.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 145
• Время последнего изменения (st.mtime) обновляется при записи данных в файл
и при добавлении или удалении жестких ссылок для каталогов.
• Время последнего изменения индексного узла (st.ctime) иногда называется как
«время последнего изменения статуса». Обновляется при записи данных в файл
или при явном изменении информации в индексном узле (например, при
смене владельца файла или при изменении счетчика ссылок), но не обновляется
при чтении данных из файла.
• Поле st.blksize предназначено для изменения размера блока файла. В
большинстве случаев, вероятно, имеет то же назначение, что и аналогичное поле в
суперблоке (см. раздел 3-2.3).
• Если файл имеет «дыры», полученные как результат установки текущей позиции
в файле за его пределы и последующей записи каких-либо данных, значение
st.blccks * 512 может оказаться меньше значения st.size.
• Системный вызов f stat особенно востребован в ситуациях, когда требуется
получить сведения об элементе, не имеющем пути в файловой системе, например,
о неименованном канале или сокете. Для этих объектов поля st.mcde, st.inc, st.dev,
st.uid, st.gid, st.atime, st.ctime и st.mtime содержат актуальные значения, но что
касается других полей — все зависит от реализации. Тем не менее, для
именованных и неименованных каналов поле st.size, как правило, содержит
количество непрочитанных байт в канале.
Поле st.mcde содержит биты, определяющие тип файла (обычный файл, каталог, сокет
и пр.), права доступа и ряд других характеристик. Для большей переносимости
приложений, при анализе этого поля рекомендуется пользоваться специальными
макросами. Для начала рассмотрим макросы, с помощью которых можно
определить тип файла.
st_mode — битовая маска и значения для файлов различных типов
S_IFMT /• все биты, кстсрые определяет тип файла •/
S.IFBLK /* специальный файл блечнеге устрейства */
S.IFCHR /* специальный файл симвсльнсгс устрейства •/
S_IFDIR /* каталег */
S_IFIF0 /* именованный или неименованный канал */*
S_IFLNK /• символическая ссылка */
S.IFREG /* сбычный файл */
S.IFS0CK /* сскет ./
Макрос S.IFMT — это маска всех битов, которые относятся к типу файла в поле st.mcde.
Прочие комбинации битов типа файла являются не маской, а значением, поэтому
следующий вариант проверки — является ли файл сокетом, выполнен неправильно:
if ((buf.st.mcde & S.IFSOCK) == S.IFSOCK) /* неверие •/
Подобного рода проверки должны выполняться так:
• Исторически сложилось так, что в это название вкралась орфографическая ошибка. Более
правильным было бы назвать этот макрос, как S.IFFIF0.
146 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
if ((buf.st_.mode & S.IFMT) == S.IFSOCK)
А еще лучше использовать для этих целей специальные макросы, которые
возвращают 0 в случае несоответствия и ненулевое значение — в случае соответствия
заданному типу
st_mode — макросы для проверки соответствия заданному типу
S_ISBLK(mode)
S_ISCHR(mode)
S_ISDIR(mode)
/•
/•
/•
S_ISFIFO(mode) /♦
S_ISLNK(mode)
S_ISREG(mode)
S.ISSOCK(mode)
/•
/*
/•
являетоя
являетоя
являетоя
являетоя
являетоя
являетоя
является
опециальнын файлом
специальным файлом
каталогом */
блочного уотройотва
•/
символьного уотройотва */
именованным или неименованным каналом
оимволичеокой ооылкой */
обычным файлом */
оокетом •/
-/
Проверка — является ли файл сокетом, с помощью макроса может быть выполнена так:
if (S_ISSOCK(buf.st_mode))
Для определения прав доступа в поле st.mode имеется 9 бит. В разделе 2.3, когда речь
шла о системном вызове open, я уже рассказывал о макросах, с помощью которых
можно анализировать права доступа. Имена этих макросов следуют форме записи:
s_lpwww, где символ р — режим доступа (R, w или X), a www — кому выдано право на
этот режим доступа (USR, GRP или ОТН).
На этот раз мы имеем дело с битовыми масками, например, так можно выполнить
проверку прав на чтение и на запись для группы:
if ((buf.st.mode & (S.IRGRP | S.IWGRP)) == (S_IRGRP I S.IWGRP))
В попытке объяснить еще лучше (или хуже — уж простите!), приведу пример, как
выполнить проверку прав на чтение или на запись для группы:
if ((buf.st.mode & S_IRGRP) == S.IRGRP ||
(buf.st.mode & S.IWGRP) == S.IWGRP)
В поле st.mode есть еще несколько бит, имеющих отношение к правам доступа. Их
я тоже помещу в рамочку, чтобы было проще найти, когда позднее вы вернетесь
на эту страницу.
St
mode -
S.Ixwww
- маски прав доступа
/•
/•
S.ISUID /*
S.ISGID
/•
/•
/•
S.ISVTX /•
и сопутствующих им
x = R|W|X, www = USR|GRP|OTH */
примеры: S.IRUSR,
право на изменение
(set-user-ID) */
право на изменение
(set-group-ID) ♦/
ограниченный режим
S.IW0TH */
идентификатора
идентификатора
битов
пользоватепя при
группы
удаления из каталога
исполнении */
при пополнении
*/
•/
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 147
Назначение прав изменения идентификаторов пользователя и группы при
исполнении (set-user-ID и set-group-ID) я уже объяснял в разделе 1.1.5. Флаг S_ISVTX, если
• становлен, означает, что удалить файл из каталога может только
суперпользователь, владелец каталога или владелец файла. Если флаг не установлен (по
умолчанию) — удалить файл сможет любой, кто имеет право на запись в каталог*.
Хороший способ продемонстрировать, как можно использовать макросы для
интерпретации поля st_mode — попытаться отобразить режимы в виде 10-символьной
последовательности, как это делает команда Is (например drwxr-xr-x). Ниже
приводится список правил, которыми пользуется is.
• Первый символ обозначает тип файла.
• Следующие 9 символов — это три группы (по три символа в каждой) прав
доступа для владельца, группы и остальных. Каждый бит отображается своим
символом (г, w или х), если он установлен, и дефисом — в противном случае.
• В позиции символа х, отвечающего за право исполнения для владельца или
группы, выводится маленький символ s, если вместе с правом на исполнение
установлен соответствующий бит изменения идентификатора пользователя или
группы (set-user-ID и set-group-ID). Если бит права на исполнение сброшен, а
соответствующий ему бит изменения идентификатора установлен, то в позиции
символа х следует выводить большой символ S. Такая комбинация возможна, но
бессмысленна, за исключением случая, когда право на исполнение для группы
отсутствует, но бит изменения идентификатора группы установлен. В
некоторых системах такая комбинация используется для принудительной
блокировки файла, более подробно об этом — в разделе 7.11.5.
• В позиции символа х, отвечающего за право исполнения для других, выводится
маленький символ t, если установлен бит ограниченного режима удаления (I_SVTX)
и бит права на исполнение (поиск в каталоге). Если бит права на исполнение
сброшен, а бит ограниченного режима удаления установлен — выводится
большой символ Т.
Следующая функция поможет вам полнее понять этот алгоритм:
«define TYPE(b) ((statp->st_mode & (S.IFMT)) == (b))
«define MODE(b) ((statp->st_mode & (b)) == (b))
static void print_mode(const struct stat *statp)
{
if (TYPE(S_IFBLK))
putchar('b');
else if (TYPE(S_IFCHR))
' Это определение SUS. Исторически этот флаг называется «sticky bit» (бит «навязчивости»)
и устанавливается на исполняемые файлы часто используемых программ (например shell).
Если этот бит был установлен, то по завершении программы сегмент кода оставался в
памяти и при повторном запуске он не загружался с диска, а брался из памяти. В
современных UNIX-системах назначение этого бита изменилось. (Символы SVTX произошли от «SaVe
TeXt», так как сегмент исполняемого кода так же называется «text».)
148 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
putchar('c');
else If (TYPE(S_IFDIR))
putchar('d');
else If (TYPE(S_IFIF0)) /* именно так, а не S_IFFIF0! ♦/
putchar('p');
else If (TYPE(S_IFREG))
putchar('-');
else If (TYPE(S_IFLNK))
putchar('r);
else if (TYPE(S_IFSOCK))
putchar('s');
else
putchar('?');
putchar(MODE(S_IRUSR) ? 'r" : '-');
putchar(MODE(S_IWUSR) ? 'w' : '-');
if (MODE(S_ISUID)) {
If (MODE(S_IXUSR))
putchar('s');
else
putchar('S');
}
else if (MODE(S_IXUSR))
putchar('x');
else
putcharC-');
putchar(MODE(S_IRGRP) ? 'r" : '-');
putchar(MODE(S_IWGRP) ? V : '-');
if (MODE(S_ISGID)) {
if (MODE(S_IXGRP))
putcharC s');
else
putcharC S');
>
else If (MODE(S_IXGRP))
putcharCx');
else
putcharC-');
putchar(MODE(S_IROTH) ? 'r" : '-');
putchar(MODE(S_IWOTH) ? 'w' : '-');
If (MODE(S_IFDIR) && MODE(S_ISVTX)) {
if (M0DE(S_IX0TH))
putcharC t');
else
putchar('T');
}
else if (M0DE(S_IX0TH))
putchar('x');
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 149
else
putcharC-');
>
Эта функция не завершает вывод информации символом перевода строки. Почему
— вскоре станет понятно. Думаю, это будет приятный сюрприз! Ниже приводится
отрывок тестовой программы:
struct stat statbuf;
ec_neg1( lstatCsomefile", istatbuf) )
print_mode(4statbuf);
putchar('\n');
ec_neg1( system("ls -1 somefile") )
и результат его работы:
prw-w-wprw-
w-w- 1 marc sysadmin 0 Oct 17 13:57 somefile
Обратите внимание: в тестовой программе использовался вызов lstat, а не stat,
потому что в данной ситуации нам нужна информация о самих символических ссылках,
а не об объектах ссылки.
Следующая функция выводит количество жестких ссылок на индексный узел. Она
намного проще, чем printjnode:
static void print_numlinks(const struct stat *statp)
{
printf("X51d", (long)statp->st_nlink);
}
Вы можете спросить: для чего выполняется приведение к типу long? Все просто, мы
на самом деле не знаем таких реализаций, в которых тип nlink.t был бы не целого
типа, а приведение выполняется для того, чтобы конкретизировать тип и
гарантировать его соответствие спецификатору формата в функции printf. To же самое
относится и к другим типам в структуре stat, в этой главе мы еще не раз
встретимся с подобным явлением.
Затем нужно вывести имена владельца и группы, но для этого необходимо
познакомиться еще с двумя библиотечными функциями.
3.5.2 Системные вызовы getpwuid, getgrgid и getlogin
Числовые идентификаторы группы и владельца получить несложно — они находятся
в полях st_gid и st_uid структуры stat, но нам нужно преобразовать эти
идентификаторы в имена. Для этих целей предназначены функции getgrgid и getpwuid, которые на
самом деле не являются системными вызовами, так как вся необходимая им инфор-
150 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
мация находится в файлах со списками пользователей и групп. Проблема состоит в
том, что стандарты никак не регламентируют местоположение этих файлов.
Ниже приводится пример типичной записи из файла паролей ОС Solaris для
пользователя «marc»:
$ grep marc /etc/passwd
marc:x:100:14::/hcme/marc:/bin/sh
Единственное, чего нет в файле паролей, так это самого пароля! Пароли хранятся
в зашифрованном виде в другом файле, который доступен только
суперпользователю.
В данной системе пользователь «marc» входит в состав нескольких групп, но
группой по умолчанию является группа 14:
$ grep 14 /etc/grcup
sysadmin::14:
getpwuid — возвращает запись из файла паролей
«include <pwd.h>
struct passwd *getpwuid(
uid_t uid /* числовой идентификатор пользователя */
);
/* Возвращает указатель на структуру или NULL в случае сшибки (кед сшибки
в переменней еггпс) */
struct passwd — структура для возвращаемого значения getpwuid
struct passwd {
char *pw_naiee;
uid_t pw_uid;
gid_t pw_gid;
char *pw_dir;
char *pw_shell;
};
/* имя пользователя (login) */
/* числовой идентификатор пользователя */
/* числовой идентификатор группы пс умолчанию */
/* домашний каталог */
/* командный интерпретатор пс умолчанию */
getgrgid — возвращает запись из файла со списком групп
«include <grp.h>
struct group *getgrgid(
gid_t gid /* числовой идентификатор группы */
);
/* Возвращает указатель на структуру или NULL в случае сшибки (код сшибки
в переменней еггпс) */
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 151
struct group — структура для возвращаемого значения getgrgid
struct group {
char «gr_name; /* имя группы */
gld_t gr_gid; /* числовой идентификатор группы •/
char ««grjnem; /• массив-список членов группы */
Как я уже говорил, в структурах указаны только те поля, которые определены
стандартами. В каждой конкретной реализации эти структуры могут иметь
дополнительные поля, за более точными сведениями вам следует обратиться к
соответствующим заголовочным файлам.
Теперь попробуем использовать эти две функции для вывода имени пользователя
и группы или просто числовых идентификаторов, если их имена неизвестны. Имена
могут быть неизвестны, если анализируются файлы на сетевых файловых
системах — в этом случае пользователь одной системы может отсутствовать на другой.
static void print_cwner(ccnst struct stat «statp)
{
struct passwd «pwd = getpwuid(statp->st_uid);
if (pwd == NULL)
printf(" X-8id", (icng)statp->st_uid);
eise
printf(" X-8s", pwd->pw_name);
}
static vcid print_grcup(ccnst struct stat «statp)
{
struct grcup «grp = getgrgid(statp->st_gid);
if (grp == NULL)
printf(" X-8id", (icng)statp->st_gid);
eise
printf(" X-8s", grp->gr_name);
}
Заодно упомяну еще одну функцию. Она возвращает имя, под которым
пользователь зарегистрировался в системе: .
getlogin — возвращает
в системе
«inciude <unistd.h>
char •geticgin(vcid);
/* Возвращает строку
еггпс) «/
имя, под которым пользователь зарегистрировался
или
NULL в
случае
сшибки
(кед
ошибки -
- в переменней
6 Зак. 4030
152 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
3.5.3 Отображение дополнительных сведений
Продолжаем рассматривать содержимое структуры stat. На этот раз выведем
размер файла. Однако, для специальных файлов мы будем выводить старший и
младший номера устройства, так как размер в этом случае не имеет значения. Как я уже
упоминал в разделе 35, порядок декодирования идентификатора устройства
стандартами не оговаривается, но в большинстве случаев младшие 8 бит представляют
младший номер устройства:
static vcid print_size(ccnst struct stat *statp)
{
switch (statp->st_racde & S_IFHT) {
case S_IFCHR:
case S.IFBLK:
printf("X4u,X4u", (unsigned)(statp->st_rdev » 8),
(unsigned)(statp->st_rdev 4 OxFF));
break;
default:
printf("X9iu", (unsigned icng)statp->st_size);
}
}
Настал черед даты и времени последнего изменения файла. Эту тяжелую работу мы
поручим стандартной функции strftime. Функции time и difftime также входят в
стандарт языка С (см. раздел 1.7.1). Обычно при выводе даты мы опускаем год, если
миновало менее 6 месяцев'.
static vcid prlnt_date(ccnst struct stat *statp)
{
tlme_t new;
double dlff;
char buf[100], *fmt;
if (time(&ncw) == -1) {
printfC ????????????");
return;
}
diff = difftirae(ncw, statp->st_mtime);
if (diff < 0 || diff > 60 * 60 • 24 • 182.5) /* примерно 6 месяцев •/
fmt = "Xb Xe XY";
else
fmt = "Xb Xe XH:XM";
strftime(buf, sizecf(buf), fmt, lccaltime(&statp->st_nitime));
printfC Xs", buf);
* Мы взяли за правило всегда проверять возвращаемые значения на наличие ошибок, кроме
ошибок форматирования и печати. Функция difftine — не исключение, но она относится
к тем функциям, которые никогда не возвращают признак ошибки.
ГЛАВА 3 Дополнительные операции файлового ввода-еывода 153
И последнее, что нам нужно вывести — имя файла, которое отсутствует в
структуре stat. И еще один интересный момент: для символических ссылок нужно
вывести не только имя файла-ссылки, но и имя объекта ссылки:
static veld print_name(ccnst struct stat *statp, const char «name)
{
if (S_ISLNK(statp->st_ncde)) {
char «contents = i»aiicc(statp->st_size + 1);
ssize_t n;
if (contents != NULL && (n = readiink(name, contents,
statp->st_size)) l= -1) {
ccntents[n] = '\0'; /* добавить завершающий символ '\0' */
printf(" Xs -> Xs", name, contents);
>
else
printf(" Xs -> [не могу прочитать ссылку]", name);
free(ccntents);
}
else
printf(" Xs", name);
>
Вспомните.- в разделе 333 я говорил о том, как узнать максимальный размер
полного имени файла, чтобы правильно установить размер буфера для вызова readlink.
Эта функция демонстрирует более прямолинейный подход — точный размер
имени объекта ссылки (за исключением завершающего символа «\0») для
символических ссылок хранится в поле st.size'. Обратите внимание: я задаю размер буфера
на один байт больше, чем значение поля st_size, но передаю в readlink размер, равный
st.size, резервируя место под завершающий символ «\0».
А теперь — тестовая программа, которая объединяет все наши функции вывода:
int i»ain(int argc, char *argv[])
{
int 1;
struct stat statbuf;
for (1 = 1; i < argc; 1++) {
ec.negK lstat(argv[i], istatbuf) )
is_lcng(4statbuf, argv[i]);
}
exit(EXIT_SUCCESS);
Наиболее современные файловые системы хранят короткие имена объектов ссылки
прямо в индексном узле, не занимая дополнительных блоков на диске, но даже в этом случае
поле st.size содержит корректное значение.
154 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
>
static vcid ls_lcng(ccnst struct stat *statp, ccnst char «name)
<
print_mcde(statp);
print_numlinks(statp);
print_cwner(statp);
print_grcup(statp);
print_size(statp);
print_date(statp);
print_name(statp, name);
putchar('\n');
}
И результат работы программы:
$ aupls /dev/tty
crw-rw-rw- 1 rcct rcct 5, 0 Mar 23 2002 /dev/tty
$ aupls a.tmp a.cut util
lrwxrwxrwx 1 marc sysadmin 5 Jul 29 13:30 a.tmp -> b.tmp
-rwxr-xr-x 1 marc sysadmin 8392 Aug 1 2001 a.out
drwxr-xr-x 3 marc sysadmin 512 Aug 28 12:26 util
Обратите внимание на последнюю строку: наша программа не знает, как вывести
содержимое каталога, она просто выводит его имя, как обычный файл. Чтобы
добавить возможность вывода содержимого каталога, нам нужно узнать, как их
читать. Этим мы и займемся в следующем разделе.
3.6 Каталоги
В этом разделе мы узнаем, как читать и удалять каталоги, как сменить текущий каталог
и как выполнить обход дерева каталогов.
3.6.1 Чтение содержимого каталогов
Каталоги в UNIX почти всегда организуются как обычные файлы, в индексном узле
которых установлен специальный бит, не позволяющий ядру производить прямую
запись в эти файлы. Некоторые системы допускают чтение каталогов системным
вызовом read, но стандарты POSIX и SUS не оговаривают такую возможность, а также
не определяют внутренний формат представления информации в
файлах-каталогах. Однако, ради интереса, я написал небольшую программу, которая читает и
выводит первые 96 байт из файла-каталога в шестнадцатеричном представлении:
static void dir_read_test(void)
<
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 155
int fd;
unsigned char buf[96];
ssize_t nread;
ec_neg1( fd = cpen(".", O.RDONLY) )
ec.negK nread = read(fd, buf, sizecf(buf)) )
dump(buf, nread);
return;
EC_CLEANUP_BGN
EC_FLUSH("dir_read_test");
EC_CLEANUP_ENO
}
static void dump(ccnst unsigned char »buf, ssize_t n)
{
int i, j;
fcr (i = 0; i < n; i += 16) {
printf("X4d ", i);
fcr (j = i; j < n 44 j < i + 16; j++)
printf(" Xc", isprint((int)buf[j]) ? buf[j] : ' ');
printf("\n ");
fcr (j = i; j < n 44 j < i + 16; j++)
printf(" X.2x", buf[j]);
printf("\n\n");
}
printf("\n");
}
В Linux я получил сообщение об ошибке (через макрос «ее»):
*** EIS0IR (21: "Is a directory") •**
Но в FreeBSD и Solaris программа дала такой результат.-
0 ' .V
60 dB 05 00 0с 00 04 01 2е 00 00 00 56 dB 05 00
16 . . b
0с 00 04 02 2е 2е 00 00 62 dB 05 00 0с 00 0В 02
32 in 2 у к С
6d 32 00 сО 79 dB 05 00 0с 00 0В 01 6Ь 00 43 сб
4В pcsync_s
ЬЗ da 05 00 1В 00 ОВ Ос 70 63 73 79 бе 63 5f 73
64 i g с
69 67 2е 6f 00 00 00 00 еЗ еЗ 05 00 10 00 ОВ 06
80 t i и е . с г
74 69 6d 65 2е 6f 00 сб 72 dB 05 00 Ос 00 ОВ 02
156 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
На первый взгляд это выглядит как случайный набор символов, но при более
детальном изучении начинают выявляться некоторые детали, не лишенные смысла.
Здесь видны шесть имен:«.»,«..», «ш2», «k», «psync.sig. с» и «time. с». Известно, что номера
индексных узлов этих файлов также должны находиться здесь. Чтобы отыскать их,
мы воспользуемся командой Is с ключом -i. А чтобы перевести числа из
десятичной в шестнадцатеричную систему счисления обратимся к утилите do («desk
calculator» — настольный калькулятор), комментарии курсивом были добавлены позднее:'
$ Is -ldlf . .. m2 k
383072 drwxr-хг-х 2 marc marc 2560 Oct 18 11:02 .
383062 drwxr-xr-x 9 marc marc 512 Oct 14 18:05 ..
383074 -rwxrwxrwx 1 marc marc 55 Jul 25 11:14 m2
383097 -rwxr-r- 1 marc marc 138 Sep 19 13:28 k
$$$ dc
16c вывод в шестнадцатеричной системе счисления
383072р втолкнуть номер индексного узла в стек и напечатать его
50860 dc выводит число в шестнадцатеричном виде
383062р ... та же самое ...
50856
q завершить работу
Можете убедиться сами: числа 5D860 и 5D856 находятся во второй строке (первая
строка с шестнадцатеричными числами). Для каждой записи имеется еще ряд
чисел, например длина имени файла, но мы не будем погружаться слишком глубоко.
Лучше рассмотрим стандартные способы чтения содержимого каталогов с
помощью системных вызовов:
opendir — открывает каталог
«Include <dlrent.h>
0IR *cpendlr(
ccnst char «path /* имя каталога */
);
/* Возвращает указатель на DIR или NULL в случае сшибки (кед сшибки -
в переменней еггпс) */
closedir — закрывает каталог
«Include <dlrent.h>
lnt clcsedlr(
0IR «dlrp /• указатель на 0IR
);
/* Возвращает 0 в случае успеха, -
в переменней еггпс) */
полученный
1 в случае
ст
cpendlr
ошибки
(кед
•/
сшибки -
* Набор флагов «-ldif» команды Is означает: вывести длинный список, не входя в
подкаталоги, показывать номера индексных узлов и не выполнять сортировку.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода
157
readdir — читает запись из каталога
«Include <dlrent.h>
struct dirent *readdir(
OIR *dirp /* указатель на OIR, полученный ст cpendlr */
);
/* Всзаращает указатель на структуру или NULL nc достижении конца каталога
или а случае сшибки (кед сшибки - а переменней еггпс) •/
struct dirent — структура для размещения одной записи вызовом readdir
struct dirent {
inc_t d_inc; /* нсмер индексного узла */
char d_name[]; /* имя */
};
rewinddir
«include
- переход в начало каталога
<dirent
void rev»inddir(
OIR
);
♦dirp /»
h>
указатель
OIR,
полученный
ст
cpendir
*/
seekdir — переход к требуемому местоположению (аналог «текущей
позиции» в файлах)
«include <dirent.h>
vcid seekdir(
OIR »dirp, /» указатель OIR, полученный ст cpendir */
leng lec /* местоположение */
);
telldir — получить текущую позицию в каталоге
«include <dirent.h>
lenfl telldir(
OIR *dirp /* указатель OIR, полученный ст cpendir */
);
/* Всзаращает текущую позицию в каталоге (кеды ошибск не предусмотрены) •/
Назначение этих функций достаточно очевидно: работа с каталогом начинается с
вызова cpendir, который возвращает указатель на структуру DIR (аналог указателя
на структуру FILE, возвращаемого стандартной функцией fopen). Затем этот
указатель используется как аргумент во всех остальных функциях. Для чтения
содержимого каталога, в цикле вызывается readdir до тех пор, пока он не вернет NULL. Если
содержимое переменной errno не изменилось, то это означает, что достигнут
конец каталога. (Чтобы не попасть впросак, записывайте 0 в переменную еггпс перед
158 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
вызовом readdir.) Структура dirent, которую возвращает readdir, содержит номер
индексного узла и имя элемента каталога. По окончании работы с каталогом он
закрывается вызовом clcsedir.
Номер индексного узла, возвращаемый вызовом readdir в поле d_inc, не имеет
особой ценности, так как в случае, когда элемент каталога является точкой
монтирования, это поле содержит номер индексного узла в объемлющей файловой
системе, а не в смонтированной. Например, для элемента каталога /у/а (см. рис. 33 в
разделе 3-3), readdir вернет в поле d_inc значение 221, не смотря на то, что
действующий номер индексного узла равен ad0s1m:2. Таким образом, во время обхода
дерева каталогов (я остановлюсь на этом подробнее, когда буду объяснять, как
получить имя текущего каталога в разделе 3-6.4) вам необходимо будет получать
корректный номер индексного узла через обращение к системному вызову stat, а не
брать его из поля d_inc.
Вызов rewinddir бывает порой необходим, когда нужно возобновить чтение
каталога с самого начала, не закрывая и повторно не открывая его. Системные вызовы
seekdir и telldir используются довольно редко. Аргумент 1сс вызова seekdir
представляет собой нечто, что возвращается вызовом telldir — вы не должны считать
этот аргумент порядковым номером элемента каталога. Также вы не должны думать,
что все записи в каталоге имеют фиксированную длину — это далеко не так, в чем
вы можете убедиться, просмотрев предыдущий пример еще раз.
Системный вызов readdir один из немногих, которые возвращают указатель на
единственную структуру, размещенную статически. Это достаточно удобно, но мало
подходит для многопоточных приложений. В подобных ситуациях можно
воспользоваться системным вызовом readdir_r:
readdir_r — читает запись из каталога
«include <dirent.h>
int readdir_r(
DIR «restrict
struct dirent
struct dirent
);
/* Возвращает О в
не изменяется) */
dirp,
•entry,
**result
/•
/•
Л
/•
указатель
структура
DIR, полученный
для размещения
сб элементе каталога »/
результат
случае успеха или код
(указатель или
ст cpendir
информации
4ULL) */
сшибки (переменная еггпс
•/
•/
Вы должны передать вызову readdir.г указатель на структуру dirent, в которой под
имя элемента выделен достаточно большой объем, по крайней мере NAHE_MAX + 1.
Значение NAHE_MAX вы можете определить с помощью pathccnf или fpathccnf, с
именем каталога в качестве аргумента (примерно так, как мы делали это в разделе 3-4).
Результат возвращается в аргументе result, который интерпретируется
аналогично возвращаемому значению вызова readdir, но на этот раз, в случае появления
ошибки, ее код передается как возвращаемое значение. Для проверки можно использо-
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 159
вать макрос ec.rv (раздел 1.4.2), как я сделал это в следующем примере, который
выводит список имен элементов текущего каталога с номерами индексных узлов:
static vcid readdir_r_test(vcid)
{
boci ck = faise;
icng namejnax;
DIR *dir = NULL;
struct dirent «entry = NULL, «resuit;
errnc = 0;
/* возврат с ксдсм сшибки, равным 0, сзначает, чтс значение не найденс */
ec_neg1( namejnax = pathccnf(".", _PC_NAME_MAX) )
ec_nuii( entry = maiicc(cffsetcf(struct dirent, djiame) +
namejnax + 1) )
ec_nuii( dir = opendir(".") )
whiie (true) {
ec_rv( readdir_r(dir, entry, iresuit) )
if (resuit == NULL)
break;
printf("nMfl: Xs; номер узла: Xid\n", resuit->djiame,
(icng)resuit->d_inc);
}
ck = true;
EC.CLEANUP
EC_CLEANUP_BGN
if (dir != NULL)
(vcid)cicsedir(dir);
free(entry);
if (!ck)
EC_FLUSH("readdi r_r_test");
EC_CLEANUP_END
}
Некоторые замечания по поводу этого примера.
• Значение -1, возвращаемое функцией pathccnf, когда errnc == 0, означает, что
на длину имени нет ограничений, чего в принципе не может быть. Но мы
взялись в наших примерах полностью обслуживать все возможные и невозможные
ситуации, поэтому будем следовать этому принципу и дальше. Комментарий я
поместил для тех, кто может столкнуться с этой, теоретически невозможной,
ситуацией.
• Выделение памяти с помощью malice требует дополнительного объяснения. Мы
точно знаем, что структура dirent содержит не одно поле, а в некоторых
реализациях она может содержать ряд нестандартных полей. Мы также знаем, что поле
160 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
имени стоит последним в структуре. Таким образом, смещение поля d_nane от
начала структуры, плюс размер этого поля, дают нам искомый объем памяти,
который необходимо выделить.
• Макрос EC.CLEANUP осуществляет переход к коду завершения функции, как это
было описано в разделе 1.4.2. Булева переменная ok сообщает была ли
обнаружена ошибка в ходе исполнения.
Выглядит довольно сложно! Намного проще в использовании системный вызов
readdir, который прекрасно подходит для однопоточных приложений или для случая,
когда чтением содержимого каталогов занимается конкретный поток:
statio void readdir_test(void)
{
DIR *dir = NULL;
struot dirent «entry;
eo_nuii( dir = opendir(".") )
whiie (errno = 0, (entry = readdir(dir)) != NULL)
printf("MMa: Xs; номер узла: Xid\n", entry->d_nane,
(iong)entry->d_ino);
ec_nzero( errno )
EC.CLEANUP
EC_CLEANUP_BGN
if (dir != NULL)
(void)olosedir(dir);
EC_FLUSH("readdir.test");
EC_CLEANUP_END
}
Единственный здесь интересный момент — использование readdir в условной
части оператора while. Первая часть оператора обнуляет переменную errno, вторая
определяет результат проверки всего условия в целом и принимается в учет
оператором while. Вы можете использовать не такой изощренный способ чтения
каталога, но старайтесь избегать такой формы:
errno = 0;
while ((entry = readdir(dir)) != NULL) { /* неверно */
I* обработка элемента каталога •/
}
потому что переменная errno может быть переустановлена в процессе обработки
элемента каталога. Обнулять ее следует перед каждым вызовом readdir.*
" Я знаю, вы можете подумать, что половина всех сложностей программирования под UNIX
заключается в обслуживании переменной errno. И вы будете правы! Пожалуйста, не бейте
меня — я говорю лишь то, что есть! Более простой способ вы сможете подсмотреть в
приложении В.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода
161
В любом случае, каждый из приведенных вариантов дает такой результат:
имя: .; номер узла: 383072
имя: ..; номер узла: 383062
имя: №2; номер узла: 383074
имя: к; номер узла: 383097
Теперь, после того как мы научились читать содержимое каталогов, попробуем
объединить readdir с функцией is.long из раздела 3.5.3 чтобы получить аналог
команды is, которая «умеет» выводить содержимое каталогов:
int main(int argo, ohar *argv[]) /» здеоь еоть ошибка */
{
booi ok = faise;
int i;
OIR *dir = NULL;
struot dirent «entry;
struot stat statbuf;
for (i = 1; i < argo; i++) {
eo_neg1( istat(argv[i], istatbuf) )
if (IS_ISDIR(statbuf.st_mode)) {
is_iong(&statbuf, argv[i]);
ok = true;
EC_CLEANUP
}
eo_nuil( dir = opendir(argv[i]) )
whiie (errno = 0, ((entry = readdir(dir)) != NULL)) {
eo_neg1( istat(entry->d_name, istatbuf) )
is_iong(4statbuf, entry->d_name);
}
eo_nzero( errno )
}
ok = true;
EC.CLEANUP
EC_CLEANUP_BGN
if (dir != NULL)
(void)oiosedir(dir);
exit(ok ? EXIT.SUCCESS : EXIT.FAILURE);
EC_CLEANUP_END
}
Эта программа работает безупречно, если заставить ее выводить содержимое
текущего каталога. Результат смотрите ниже (показаны только первые несколько строк):
$ aupis .
drwxr-xr-x 2 ваго наго 2560 0ot 18 12:20 .
162 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
drwxr-xr-x 9 marc marc 512 Oct 14 18:05 ..
-rwxrwxrwx 1 marc marc 55 Jul 25 11:14 «2
-rwxr-r- 1 marc marc 138 Sep 19 13:28 к
Но когда я попытался вывести с ее помощью содержимое каталога /trap, я получил
следующее:
$ aupls /tmp
drwxr-хг-х 2 marc marc 2560 Oct 18 12:20 .
drwxr-xr-x 9 marc marc 512 Oct 14 18:05 ..
ERROR: 0: main [/aup/c3/aupls.c:422] lstat(entry->d_name, Sstatbuf)
*** EN0ENT (2: "Nc such file cr directory") ***
Получается, что lstat в цикле чтения содержимого каталога не может найти
заданное имя, даже не смотря на то, что мы его только что прочитали. Небольшой анализ
показал, что крах программы происходил на полном имени auplcg.tmp. Такой путь к
файлу был бы верным, если бы каталог /tmp был текущим, но ведь это не так!*
Попробуем теперь перейти в каталог /tmp и опять запустить программу (показаны только
первые несколько строк):
$ cd /tmp
$ /usr/hcme/marc/aup/aupls .
drwxrwxrwt 3 rcct wheel 1536 Oct 18 12:20 .
drwxr-xr-x 18 rcct wheel 512 Jul 25 08:01 ..
-rw-r-r- 1 marc wheel 189741 Aug 30 11:22 auplcg.tmp
-rw— 1 marc wheel 0 Aug 5 13:23 ed.7GHAhk
Чтобы исправить ошибку, можно поместить обращение к системному вызову chdir
перед входом в цикл чтения каталога, но об этом в следующем разделе.
3.6.2 Системные вызовы chdiг и fchdiг
Что делает команда cd известно практически всем, а теперь я представлю вам
системные вызовы, которые делают то же самое:
chdir — делает заданный
«include <unistd.h>
int chdir(
ccnst char «path /*
каталог
текущим
имя каталега
>•
/. Возвращает 0 в случае успеха,
в переменней еггпс) */
-1 в
*/
случае
сшибки
(кед сшибки -
' Первые две итерации прошли благополучно по той простой причине, что элементы *.» и
«..» присутствуют в любом каталоге. Однако те, которые были выведены в примере,
никакого отношения к каталогу /tmp не имеют!
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 163
fchdir — делает текущим каталог по
«include <unistd.h>
int fchdir(
int fd /* дескриптор
);
/* Возвращает 0 в случае
в переменней еггпс) */
файла */
успеха, -1
дескриптору
в случае
сшибки
(кед
сшибки -
Как и следовало ожидать, аргумент системного вызова chdir может быть и
абсолютным, и относительным маршрутом к каталогу.
Системный вызов принимает в качестве аргумента дескриптор, открытый для
каталога. Но минуточку! Разве я не говорил в разделе 3-6.1, что использование
системного вызова среп для открытия каталога — есть вещь нестандартная? Впрочем,
кажется, не говорил. Я говорил, что некоторые системы допускают
нестандартную возможность чтения каталогов вызовом read. Есть как минимум одна причина,
по которой иногда бывает полезным открыть каталог вызовом open — это получить
дескриптор для использования в вызове fchdir. Вы должны открывать каталог с
флагом 0_RD0NLY, открывать его для записи недопустимо.
Но из-за проблем с переносимостью программ мы не можем читать каталог
вызовом read, так почему бы не использовать повсеместно системный вызов chdir с
указанием абсолютного пути? Потому что в паре с open, системный вызов fchdir
служит прекрасным инструментом для создания закладок на каталоги. Сравните
следующие две методики:
1. Получить полное имя текущего 1. Открыть текущий каталог вызовом
каталога. open.
2. Сделать что-нибудь, что изменит 2. Сделать что-нибудь, что изменит
текущий каталог. текущий каталог.
3. Вызвать chdir, используя имя 3. Вызвать fchdir, используя дескриптор,
каталога, полученное на первом шаге. полученный на первом шаге.
Методика, которая приведена в левом списке, несколько уступает той, что
приведена справа, потому что:
• есть ряд сложностей с получением имени текущего каталога. Мы обсуждали этот
вопрос в разделе 3-4.2;
• это довольно трудоемкий процесс, в чем мы убедимся на собственном опыте,
когда попробуем сделать это в разделе 3-6.4.
В некоторых случаях, когда вам необходимо вернуться лишь на один уровень выше,
можно использовать такой подход:
ec_neg1( chdir("..") )
Здесь вам не требуется знать полное имя каталога, но по-прежнему пара open/fchdir
выглядит предпочтительнее. Потому что в этом случае вы совершенно не зависите
164 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
от того, насколько глубоко вы ушли вниз по дереву каталогов. Однако такой
вариант нельзя использовать, если у вас нет права на чтение каталога.
Итак, попробуем исправить программу aupls из предыдущего раздела так, чтобы она
делала каталог текущим, перед тем как читать его. После обзора каталога мы
должны вернуться в первоначальную позицию, потому что программа может получить
несколько аргументов, и каждый из них предполагает неизменность текущего
каталога. Для краткости приведу только исправленный фрагмент:
ec_null( dir = cpendir(argv[i]) )
ec_neg1( fd = cpen(".", O.RDONLY) )
ec_neg1( chdir(argv[i]) )
while (errnc « 0, ((entry = readdir(dir)) l= NULL)) {
ec_neg1( lstat(entry->d_nai»e, &statbuf) )
ls_lcng(&statbuf, entry->d_name);
}
ec_nzerc( errnc )
ec_neg1( fchdir(fd) )
Но и этот фрагмент содержит ошибку. Если хотите найти ее самостоятельно —
займитесь этим прямо сейчас, а чтение продолжите потом.
Проблема заключается в том, что если между chdir и fchdir произойдет ошибка, то
будет выполнен переход на код завершения функции, обращения к вызову fchdir
не будет и в результате программа не восстановит положение текущего каталога.
Для этой программы такая ошибка не страшна, поскольку любая ошибка вызывает
ее завершение и она никак не влияет на другие программы или на командный
интерпретатор. Но что если программа с подобной ошибкой используется для
передачи данных другой программе, например по конвейеру?
Чтобы исправить данную ошибку, нужно поместить второе обращение к
системному вызову fchdir в код завершения функции, тогда он будет исполнен в любом
случае. Вы можете инициализировать переменную fd значением -1 и проверить
ее, перед тем как обратиться к fchdir.
3.6.3 Системные вызовы mkdir и rmdir
Следующие два системных вызова создают и удаляют каталоги:
mkdir — создает каталог
«include <sys/stat.h>
int mkdir(
ccnst char *path, /*
mcde_t perms /*
);
/* Возвращает О в случае
в переменней errnc) */
полнее
права )
успеха,
имя
каталега
цеступа */
-1
в случае
*/
сшибки
(кед сшибки -
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 165
rmdlr — удаляет каталог
«include <unistd.h>
int rmdir(
ccnst char «path /*
>;
/• Возвращает О в случае
в переменней еггпс) */
полнее
успеха
имя
-1
катал с га
е случае
•/
сшибки
(кед сшибки -
Окончательные права доступа к каталогу, создаваемому вызовом mkdir,
определяются в соответствии с маской, аналогично созданию файла вызовом среп (см.
раздел 2.4). Ссылки на каталоги «.» и «..» создаются автоматически..
Вызов rmdir во многом похож на unlink*. Единственное ограничение — перед
удалением каталог должен быть пустым. Если каталог не пуст, то сначала придется
удалить все вложенные подкаталоги и файлы серией вызовов unlink и rmdlr. Если
вы действительно желаете удалить каталог со всем его содержимым, то гораздо проще
написать:
ec_neg1( system("rm -rf scmedir") )
поскольку команда rm в состоянии самостоятельно обойти все дерево
подкаталогов и удалить его.
Ниже приводится иллюстрация правоты моих слов:
veld rmdir_test(vcid)
{
ec_neg1( mkdir("somedir", PERM.DIRECTORY) )
ec_neg1( rmdir("scmedir") )
ec.negK mkdir("scmedir", PERM.DIRECTORY) )
ec.negK cicse(cpen("scnedir/x", 0_WR0NLY | O.CREAT, PERM.FILE)) )
ec_neg1( system("is -Id scmedir; is -i scmedir") )
if (rmdir("scmedir") == -1)
реггсг("Ожидаемая сшибка");
ec_neg1( system("rm -rf somedir") )
ec.negK system("is -id scmedir") )
return;
EC.CLEANUP.BGN
EC_FLUSH("rmdir_test");
EC_CLEANUP_END
}
Макроопределения perm.directory и perh.file были описаны в разделе 1.1.5.
Строка, в которой создается файл, на первый взгляд кажется малопонятной, но такая
* В некоторых системах суперпользователю позволено использовать unlink для каталогов, но
это отступление от стандартов.
166 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
запись очень удобна, если вам нужно лишь создать файл. Единственная
неприятность — если open завершится с ошибкой, то в вызов olose будет передан аргумент
-1 и нам останется лишь строить догадки, почему файл не был создан.
Результат работы функции:
drwx— 2 шаго users 72 Oot 18 15:32 soraedir
total 0
-rw-r-r- 1 наго users 0 Oot 18 15:32 x
Ожидаемая ошибка: Direotory not empty
Is: somedir: No suoh file or direotory
3.6.4 Реализация вызова getcwd
(обход дерева каталогов снизу вверх)
В разделе 3-4.2 мы лишь вскользь коснулись системного вызова getcwd, из-за
сложностей, связанных с определением размера буфера. Давайте вернемся немного назад
и ради интереса попробуем написать собственную версию этой функции!
Основная идея заключается в том, чтобы, начиная с текущего каталога получить его
номер индексного узла, затем подняться в родительский каталог и по номеру,
полученному ранее, определить имя вложенного каталога. И так до тех пор, пока выше
двигаться будет некуда.
Что значит «выше некуда»? Это означает, что
ohdirC"..")
вернет -1с кодом EN0ENT, либо мы останемся в том же самом каталоге — возможны
оба варианта развития событий, и оба они означают, что мы находимся в
корневом каталоге.
По мере продвижения вверх, мы будем получать новые элементы пути и
накапливать их в связанном списке, помещая более новые элементы в начало списка. Так,
если текущий каталог grandchild находится в каталоге child, который в свою
очередь — в каталоге parent, расположенном в корневом каталоге, то по достижении
вершины дерева мы получим связанный список, представленный на рис. 3-4.
Начало списка
"Г
parent
grandchild
Рис 3.4. Связанный список из элементов пути к текущему каталогу
Ниже показана структура элементов списка и функция, которая создает новый
элемент и вставляет его в начало списка:
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 167
struct pathlist_ncde {
struct pathlist_ncde *c_next;
char c_name[1]; /* размер массива будет устанавливаться динамически */
>;
static bccl push_pathlist(struct pathlist_node **head, ccnst char «name)
{
struct pathlist_ncde *p;
ec_null( p = inallcc(sizecf(struct pathlist_ncde) + strlen(naine)) )
strcpy(p->c_naine, name);
p->c_next = «head;
•head = p;
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
Размер каждого из узлов списка (pathlist_ncde) будет выбираться динамически. Сама
структура, в том виде, в каком она объявлена, предполагает хранение в поле c_name
лишь символа «\0», завершающего строку. Поэтому при создании нового элемента
списка к размеру структуры добавляется длина строки с именем очередного
каталога, что можно видеть в вызове функции malice. Обратите внимание: новый
элемент списка вставляется в начало, чтобы сохранить естественное следование
элементов пути, которое необходимо для функции get_pathlist:
static char *get_pathlist(struct pathlist_ncde «head)
{
struct pathlist_ncde *p;
char «path;
size_t tctal = 0;
fcr (p = head; p l= NULL; p = p->c_next)
tctal += strlen(p->c_name) + 1;
ec_null( path = mallcc(tctal + 1) )
path[0] = '\0';
fcr (p = head; p != NULL; p = p->c_next) {
strcat(path, "/");
strcat(path, p->c_name);
}
return path;
EC_CLEANUP_BGN
return NULL;
EC_CLEANUP_END
168 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
Она дважды проходит по списку. На первом проходе подсчитывается общее
количество символов (в каждой итерации добавляется 1 для символа «/»), необходимое
для размещения полного имени каталога, на втором проходе выполняется сборка
результирующей строки.
По окончании работы с деревом каталогов необходимо освободить память,
занимаемую СПИСКОМ:
static void free_pathiist(struct pathiist_node **head)
{
struct pathiist_node *p, «p_next;
for (p ■ «head; p != NULL; p = p_next) {
p_next = p->c_next;
free(p);
У
•head = NULL;
}
Для хранения указателя на следующий элемент списка использована временная
переменная p_next, поскольку поле c.next элемента списка нельзя использовать после
вызова функции free.
И, наконец, отрывок программы, которая создает список, показанный на рис. 3-4:
struct pathiist_node «head = NULL;
char *path;
ec_faise( push_pathiist(&head, "grandchild") )
ec_faise( push_pathiist(&head, "chiid") )
ec_faise( push_pathiist(&head, "parent") )
ec_nuii( path = get_pathiist(head) );
f ree_pathiist(&head);
printf("Xs\n", path);
free(path);
и выводит его:
/parent/child/grandchiid
Одно из немаловажных удобств такого способа — не нужно заранее предсказывать
размер буфера при работе со стандартным вызовом getcwd, как это приходилось
делать в разделе 3.4.2.
Теперь мы готовы написать функцию getcwdx — нашу собственную версию вызова
getcwd, которая движется по дереву каталогов снизу-вверх, вызывает на каждом уровне
push_pathlist и останавливается по достижении корня. Для начала напишем
макрос, который определяет — не представляют ли две структуры stat один и тот же
индексный узел, проверяя идентификаторы устройств и номера индексных узлов:
ГЛАВА 3 Дополнительные операции файлоеого ееода-выеода 169
«define SAME_IN0DE(s1, s2) ((s1).st_dev « (s2).st_dev 44\
(sD.st.ino — (s2).st_ino)
Мы дважды будем пользоваться этим макросом в функции getowdx:
char «getowdx(void)
{
struot stat stat.ohiid, stat_parent, stat_entry;
OIR *sp = NULL;
struot dirent «dp;
struot pathiist_node «head = NULL;
int dirfd ■ -1, rtn;
ohar «path = NULL;
eo.negK dirfd ■ open(".", O.RDONLY) )
eo.negK istat(".", 4stat_ohiid) )
whiie (true) {
eo.negK istat("..", 4stat_parent) )
/» перейти на один уровень вверх и проверить - не доотигли ли корня */
if (((rtn = ohdir("..")) == -1 44 еггпо == ENOENT) ||
SAHE_INOOE(stat_ohiid, stat.parent)) {
if (head == NULL)
6o_faise( push_pathiist(4head, "") )
eo_nuii( path = get_pathiist(head) )
EC.CLEANUP
}
eo_neg1( rtn )
/• прочитать оодержимое каталога в поиоках потомка */
ec_nuii( sp = opendir(".") )
whiie (еггпо = 0, (dp = readdir(sp)) != NULL) {
eo.negK istat(dp->d_nai»e, 4stat_entry) )
if (SAME_INOOE(stat_ohiid, stat.entry)) {
6o_faise( push_pathiist(4head, dp->d_nane) )
break;
}
}
if (dp == NULL) {
if (еггпо =s 0)
еггпо = ENOENT;
EC.FAIL
}
stat_chiid = stat_parent;
}
EC_CLEANUP_BGN
if (sp != NULL)
170 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
(void)clcsedir(sp);
if (dirfd != -1) {
(void)fchdir(dirfd);
(vcid)clcse(dirfd);
}
f ree_pathlist(&head);
return path;
EC_CL.EANUP.END
}
Некоторые замечания по этой функции.
• Для возврата в текущий каталог по завершении функции, мы использовали
технику cpen/f chdir.
• Внутри цикла переменная stat_child служит для хранения структуры stat потомка,
имя которого мы ищем. Переменная stat.entry хранит структуру stat
очередного элемента в каталоге, который мы получаем вызовом readdir. Переменная
stat.parent хранит структуру stat родительского каталога. При перемещении на
один уровень вверх родительский каталог становится потомком.
• Порядок проверки на достижение корневого каталога был описан в начале
раздела: либо chdir завершается с кодом ошибки ENOENT, либо мы остаемся в том же
самом каталоге.
• Если мы достигли корня с пустым списком, то в список заносится пустое имя,
что в результате даст нам путь к каталогу «/».
• В цикле чтения каталога мы могли бы перепрыгивать через элементы, которые
не являются каталогами, но макрос SAME.INODE сделает это быстрее.
В заключение напишем маленькую программу, которая ведет себя подобно
команде pwd:
int main(vcid)
{
char «path;
ec_null( path = getcwdxQ )
printf("Xs\n", path);
free(path);
exit(EXIT_SUCCESS);
EC.CLEANUP.BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 171
3.6.5 Реализация f tw (обход дерева каталогов сверху вниз)
В стандартной библиотеке языка С существует функция f tw («file tree walk» — обход
дерева каталогов), которая предназначена для организации рекурсивного обхода
дерева каталогов. Она принимает указатель на функцию-обработчик, которая
вызывается для каждого встреченного элемента каталога. Однако здесь мы не будем
обсуждать функцию f tw — гораздо интереснее для нас попробовать написать
собственную реализацию функции обхода, используя знания, полученные в этой главе.
Первое, что необходимо выяснить — действительно ли структура каталогов является
деревом, потому что рекурсивный алгоритм очень чувствителен к этому. Если программа
столкнется со второй ссылкой на тот же самый каталог (здесь не имеются в виду ссылки
«.•> и «..»), она дважды обойдет одно и тоже дерево каталогов со всеми
подкаталогами. Существуют две проблемы, на которых следует остановиться особо.
1. Установка символических ссылок на каталоги — довольно обычное дело, поэтому
мы вполне можем встретить две ссылки на один каталог (вторая — жесткая
ссылка). Кроме того, мы можем столкнуться с циклическими символическими
ссылками.
2. Некоторые системы допускают возможность создания второй жесткой ссылки
на каталог с помощью системного вызова link. Это встречается крайне редко,
но наша программа может столкнуться и с этим.
Проблема №1 решается легко — достаточно просто игнорировать символические
ссылки. Проблему №2 мы пока оставим в стороне, хотя вы столкнетесь с ней в
упражнении 35 в конце этой главы. Таким образом, будем исходить из
предположения, что дерево каталогов, образованное жесткими ссылками, действительно имеет
древовидную структуру.
Итак, нам необходимо, чтобы программа aupls (расширенная версия программы
из раздела 35-3) вела себя подобно команде is. Чтобы она корректно
обрабатывала ключ -R, выполняя рекурсивный обход дерева каталогов, и ключ -d, выводя
имена подкаталогов без входа в них. Без этих аргументов программа aupls должна
полностью повторять функциональность предыдущей версии.
С ключом -R программа должна сначала вывести содержимое указанного каталога,
включая имена вложенных подкаталогов, а затем выполнить рекурсивный обход
каждого из вложенных подкаталогов, например:
$ aupls -R /aup/coraraon
/aup/ooraraon:
-rwxr-xr-x 1 root root 1145 Oot 2 10:21 makefile
-rwxr-xr-x 1 root root 171 Aug 23 10:41 logf.h
-rwxr-xr-x 1 root root 1076 Aug 26 15:24 logf.o
drwxr-xr-x 1 root root 4096 Oot 2 12:20 of
-rwxr-xr-x 1 root root 245 Aug 26 15:29 notes.txt
/aup/ooraraon/of:
-rwxr-xr-x 1 root root 1348 Oct 3 13:52 cf-ec.c-ec_push.htm
172 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
-rwxr-xr-x 1 root root 576 Oot 3 13:52 of-eo.o-eo_print.htm
-rwxr-xr-x 1 root root 450 Oot 3 13:52 of-eo.o-eo_reinit.htm
-rwxr-xr-x 1 root root 120 Oot 3 13:52 of-eo.o-eo_warn.htm
Вскоре вы увидите, что для каждого уровня вложения выполняется два прохода (цикл
с readdir). На первом — выводится содержимое каталога, на втором для каждого из
подкаталогов выполняется рекурсивный вход и проход №1.
Информация о проходе будет передаваться в каждую из функций в виде
структуры. Это намного удобнее, чем глобальные переменные или длинные списки
аргументов:
typedef enui» {SH0W_PATH, SH0W.INF0} SH0W_0P;
struot traverse_info {
bool ti_reoursive; /• Клвч -R? */
ohar *ti_name; /* текущий элемент •/
struot stat ti_stat; /* stat для ti_nane */
bool (*ti_fon)(struot traverse_info *, SH0W_0P); /* функция-обработчик •/
};
Ниже приводится исходный код функции-обработчика:
statio bool show_stat(struot traverse_info *p, SH0W_0P op)
{
switoh (op) {
oase SHOW.PATH:
eo_false( print_owd(false) )
break;
oase SH0W_INF0:
ls_long(&p->ti_stat, p->ti_nane);
>
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
Если аргумент op функции show_stat принимает значение SH0W_PATH, выводится
заголовок блока, SH0W_INF0 — информация об элементе каталога. Функция Is.long, из
раздела 35.3 остается без изменений, а функция print_owd практически идентична
примеру из раздела 3-4.2:
statio bool print_owd(bool cleanup)
{
char *cwd;
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 173
if (cleanup)
(vcid)get_cwd(true);
eise {
ec_nuii( cwd = get_cwd(faise) )
printf("\nXs:\n", cwd);
}
return true;
EC_CLEANUP_BGN
return faise;
EC_CLEANUP_END
}
Позднее мы увидим, для чего используется print_cwd(true), а теперь перейдем к
функции main и посмотрим, как происходит инициализация и начинается процесс
обхода:
«define USAGE "Псрядск испсльзсвания: aupis [-Rd] [dir]\n"
static icng tctai_entries = 0, tctai_dirs = 0;
int maindnt argc, char *argv[])
{
struct traverse_infc ti = {0};
int c, status = EXIT_FAILURE;
bcci stat_cniy = faise;
ti.ti_fcn = shcw_stat;
while ((c = getcpt(argc, argv, "dR")) l= -1)
switch(c) {
case 'd':
stat_cnly = true;
break;
case 'R':
ti.ti_recursive = true;
break;
default:
fprintf(stderr, USAGE);
EC.CLEANUP
}
switch (argc - cptind) {
case 0:
ti.ti_name = ".";
break;
case 1:
ti.ti_name = argv[cptind];
break;
174 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
default:
fprlntf(stderr, USAGE);
EC.CLEANUP
}
ec_false( dc_entry(&tl, stat_cnly) )
prlntf("\nBcerc элементов: Xld; каталогов = Xld\n", tctal_entrles,
tctal_dirs);
status = EXIT.SUCCESS;
EC.CLEANUP
EC_CLEANUP_BGN
prlnt_cwd(true);
exlt(status);
EC.CLEANUP.END
}
Для хранения счетчиков общего числа элементов и каталогов используются
глобальные переменные tctal.entrles и tctal.dlrs, которые выводятся при
завершении работы программы. Вскоре мы увидим, где они наращиваются.
Стандартная библиотечная функция getcpt (часть POSIX/SUS, а не стандарт С)
принимает в третьем аргументе список допустимых опциональных символов. После
выбора всех опций она устанавливает глобальный индекс (cptind) на следующий
входной аргумент, в нашем случае это необязательное имя каталога. Если оно не
задано, мы начинаем обрабатывать текущий каталог.
Основная работа выполняется функцией dc.entry:
static bccl dc_entry(struct traverse_infc *p, bccl stat_cnly)
{
bccl ls_dir;
ec_neg1( lstat(p->ti_name, &p->ti_stat) )
ls.dlr = S_ISDIR(p->ti_stat.st_nicde);
If (stat.cnly || lis.dlr) {
tctal_entries++;
If (is_dlr)
tctal_dlrs++;
ec_false( (p->tl_fcn)(p, SHOW.INFO) )
}
else If (is_dlr)
ec_false( dc_dlr(p) )
return true;
EC.CLEANUP.BGN
return false;
EC_CLEANUP_END
}
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 175
Функция do.entry используется одним из двух способов: если аргумент stat.only
содержит true или текущий элемент не является каталогом, то наращиваются
глобальные счетчики и вызывается функция-обработчик с аргументом SH0W.INF0. Эта
ветка отрабатывает, когда программе передан ключ -d, либо когда аргумент
командной строки не является каталогом, либо на первом проходе чтения содержимого
каталога. В противном случае для каталогов вызывается функция do.dir, которая
выполняет чтение содержимого указанного каталога:
statio bool do_dir(struot traverse.info *p)
<
OIR *sp = NULL;
struct dirent *dp;
int dlrfd = -1;
bool result = false;
eo.negK dlrfd = open(".", 0.R00NLY) )
If ((sp = opendlr(p->tl_name)) == NULL || ohdir(p->ti_name) == -1) {
If (errno == EACCES) {
fprlntf(stderr, "Xs: В доотупе отказано.\n", p->ti_name);
result = true;
EC.CLEANUP
}
EC.FAIL
>
if (p->tl_reoursive)
eo_false( (p->ti_fon)(p, SH0W_PATH) )
while (errno = 0, ((dp = readdir(sp)) l= NULL)) {
if (stromp(dp->d_name, ".") == 0 ||
stromp(dp->d_name, "..") == 0)
oontlnue;
p->ti_name = dp->d_naine;
eo_false( do_entry(p, true) )
>
if (errno != 0)
syserr_print("Обзор каталога (Проход 1)");
if (p->ti_reoursive) {
rewinddir(sp);
while (errno = 0, ((dp = readdir(sp)) != NULL)) {
if (strorap(dp->d_narae, ".") == 0 ||
stromp(dp->d_name, "..")==0)
continue;
p->ti_narae = dp->d_name;
eo_false( do_entry(p, false) )
}
If (errno l= 0)
syserr_print("Обзор каталога (Проход 2)");
>
176 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
result = true;
EC.CLEANUP
EC_CLEANUP_8GN
if (dirfd l= -1) {
(vold)fohdlr(dlrfd);
(vold)olose(dlrfd);
}
if (sp l= NULL)
(vold)olosedlr(sp);
return result;
EC_CLEANUP_ENO
}
Функция do.dir открывает дескриптор dirfd для текущего каталога, чтобы можно
было вернуться назад. Затем открывает каталог вызовом opendir и переходит в него,
чтобы обрабатывать элементы относительно этого каталога. Вполне может
возникнуть ситуация, когда каталог недоступен для чтения (opendir завершается с
ошибкой) или для поиска (ohdir завершается с ошибкой), в этих случаях функция
просто выводит сообщение и продолжает работу.
Затем, если был задан ключ -R, вызывается функция-обработчик, которая выводит
заголовок блока — имя текущего каталога. Это совершенно нормально, когда весь
вывод производится одной функцией-обработчиком
После этого начинается первый проход чтения содержимого каталога (на каждой
итерации функции do.entry передается значение true в аргументе stat.only). По
окончании первого прохода, если был задан ключ -R, начинается второй проход
чтения каталога (на каждой итерации функции do.entry передается значение false
в аргументе stat_only). На втором проходе возникает рекурсия, потому что
обращение к функции do.entry может привести к рекурсивному вызову do.dir.
Обратите внимание: оба цикла чтения каталогов перепрыгивают через ссылки на
специальные каталоги «.» и *..», которые команда Is не выводит по умолчанию.
Мы решили считать ошибки вызова readdir нефатальными, чтобы иметь
возможность продолжить работу. Поэтому мы не передаем вызов readdir макросу eo.nzero,
а сами выводим сообщения об ошибках с помощью функции syserr.print:
void syserr_print(oonst ohar *msg)
{
ohar buf[200];
fprintf(stderr, "ОШИБКА: Xs\n", sysern»sg(buf, sizeof(buf), nsg,
errno, EC.ERRNO));
}
Функция syserrmsg была описана в разделе 1.4.1.
В результате мы получили свою собственную рекурсивную версию команды Is!
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 177
3.7 Изменение индексного узла
Семейство системных вызовов stat (см. раздел 3.5.1) извлекает данные из
индексного узла. Я уже объяснял назначение различных полей индексного узла и как они
изменяются при том или ином воздействии на файл. Некоторые поля могут
напрямую изменяться системными вызовами, которые будут обсуждаться в этом
разделе. В табл. 33 показано, какие системные вызовы, какие поля изменяют. Прочерк
означает, что это поле вообще не изменяется, оно получает свое значение в
момент создания индексного узла. Примечание «побочный эффект» означает, что это
поле изменяется только в результате побочного эффекта от какого-либо
воздействия, например, в результате создания новой ссылки системным вызовом link, но
не может изменяться непосредственно.
Таблица 3.3. Изменения полей индексных узлов
Поле
Описание
Каким вызовом изменяется
st_dev идентификатор устройства
файловой системы
st.lno номер индексного узла
st.mode режимы
st.nllnk количество жестких ссылок
st.uid идентификатор пользователя
st.gid идентификатор группы
st.rdev идентификатор устройства
(для специальных файлов устройств)
st.slze размер в байтах
st.atline время последнего обращения
st_nit.li»e время последнего изменения
st_ctl*e время последнего изменения
индексного узла
st.blkslze оптимальный размер блока
для операций ввода-вывода
st_blooke размер в блоках по 512 байт
oh«od, fohnod
побочный эффект
chown, fohown, lohown
ohown, fohown, lohown
побочный эффект
побочный эффект
utiae
utlne
побочный эффект
3.7.1 Системные вызовы chmod и fchmod
chmod — изменяет режимы доступа
«include <sys/stat.h>
lnt chmod(
const ohar «path, /•
uode_t node /»
);
/* Возвращает О в олучае
в переменной errno) */
имя файла
по имени
•/
новые режимы
уопеха, -1
в
•/
олучае
файла
ошибки (код
ошибки -
178 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
fchmod — изменяет режимы доступа по дескриптору файла
«include <sys/stat.h>
int fchracd(
int fd, /* дескриптор файла */
mcde_t mcde /* новые режимы */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
Системный вызов chmcd изменяет режимы доступа к существующему файлу любого
типа. Он не может использоваться для изменения типа файла — ему доступны только
флаги S_1SUID, S.1SG1D, S.1SVTX и биты прав доступа. Системный вызов fchnicd очень
похож на chmcd, но в качестве аргумента принимает не имя, а дескриптор файла.
Изменить режимы доступа может только процесс, имеющий права
суперпользователя, или идентичный файлу действующий идентификатор пользователя.
Недостаточно иметь права на запись в файл. Кроме того, чтобы установить флаг S.ISGID,
процесс должен иметь идентичный файлу действующий идентификатор группы
(кроме суперпользователя).
Если вы не собираетесь менять режимы доступа целиком, то сначала нужно
получить существующие режимы вызовом stat, затем установить или сбросить
необходимые биты и потом вызвать chmcd для изменения режимов.
Как правило, вызов chmcd не используется в приложениях, потому что режимы
доступа задаются во время создания файла. С другой стороны, пользователи
довольно часто пользуются командой chmcd.
3.7.2 Системные вызовы chown, fchown и lchown
Системный вызов chewn изменяет идентификаторы пользователя и группы файла.
Эта операция доступна только владельцу файла (процессу с идентичным файлу
действующим идентификатором пользователя) и суперпользователю. Если один из
аргументов имеет значение -1, то соответствующий ему идентификатор остается
без изменений.
chown — изменяет владельца и группу файла по его имени
«include <unistd.h>
int chcwn(
const char «path
uid_t uid,
gid_t gid
);
/* Возвращает 0 в ел)
в переменней еггпс) <
1*
/•
/•
/чае
1
имя файла •/
нсвый идентификатор
нсвый идентификатор
успеха, -1 в случае
пользователя */
группы
сшибки
•/
(кед сшибки -
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 179
fchown — изменяет владельца и группу файла по дескриптору
«include <unistd.h>
int fchcwn(
int fd, /♦ дескриптор файла */
uid_t uid, /* нсвый идентификатор пользователя */
gid t gid /* новый идентификатор группы */
I* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
! lchown — изменяет владельца и группу символической ссылки по ее имени
«include <unistd.h>
J int lchcwn(
I censt char *path, /* имя ссылки */
j uid_t uid, /* нсвый идентификатор пользователя */
gid_t gid /* новый идентификатор группы */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
I
Системный вызов fchewn идентичен chcwn, только принимает в качестве аргумента
не имя файла, а дескриптор. Системный вызов lchcwn воздействует на саму
символическую ссылку, а не на объект ссылки.
Если вызывающий процесс не обладает правами суперпользователя, то данные
вызовы сбрасывают биты set-user-ID и set-group-ID для предотвращения вполне
очевидной формы атак:
$ ер /bin/sh mysh [получить персональную копию командного интерпретатора]
$ chmcd 4700 mysh [включить бит set-user-10]
$ chcwn rcct mysh [сделать суперпельзевателя владельцем]
$ my sh [получить права суперпельзевателя]
Если вы хотите иметь персональный командный интерпретатор с правами
суперпользователя, вам нужно изменить порядок вызова команд chmcd и chcwn. Однако,
если вы не суперпользователь, то вы не сможете выполнить команду chmcd. Таким
образом лазейка закрыта.
Если в системе имеется макрос _P0SIX_CH0WN_RESTRlCTED (проверьте у себя с помощью
pathcenf или fpathccnf), правила игры могут немного отличаться. В этом случае
правом изменения владельца файла обладает только суперпользователь, но владелец файла
может изменить группу файла на действующую группу процесса или одну из
дополнительных групп. Другими словами: владелец не может «отдать» свой файл, самое
большее — изменить группу файла на одну из ассоциированных с процессом.
180 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
Поскольку владелец файла меняется, как правило, с помощью команды chcwn
(которая обращается к системному вызову chcwn), эти правила редко затрагивают
прикладные программы. Чаще всего они касаются системного администратора и
самих пользователей.
3.7.3 Системный вызов utime
utime — устанавливает время последнего обращения и изменения файла
«include <utime.h>
int utime(
const char *path, /* имя файла */
const struct utimbuf «timbuf /* новее время */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменной еггпс) */
struct utimbuf — используемая при обращении к вызову utime
struct utimbuf {
time_t actime; /* время последнего обращения */
time_t medtime; /« время последнего изменения */
};
Системный вызов utime изменяет время последнего обращения и последнего
изменения файла любого типа*. Тип time.t описывает число секунд, прошедших с начала
эпохи (см. раздел 1.7.1). Только владелец и суперпользователь могут изменить
время последнего обращения и изменения.
Если вместо указателя на timbuf передается значение NULL, то используется текущее
время. Сделано это для того, чтобы иметь возможность имитировать обновление
файла без необходимости перезаписи и в первую очередь для удобства команды
touch. Любой процесс, обладающий правом на запись в файл, может обратиться к
этому вызову. Этот вызов часто применяется к файлам, полученным из резервных
копий или по сети, когда времена устанавливаются в их первоначальные значения.
Время последнего изменения индексного узла не может быть переустановлено, но
этого и не требуется, поскольку индексные узлы не перемещаются — они каждый
раз создаются заново.
3.8 Дополнительные вызовы для работы
с файлами
Здесь будут рассмотрены системные вызовы для работы с файлами, которые не
вошли в предыдущий раздел.
' В некоторых системах существует очень похожий, но устаревший системный вызов utimes.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 181
3.8.1 Системный вызов access
В отличие от других системных вызовов, которые имеют дело с правами доступа,
access выполняет проверку реальных, а не действующих идентификаторов
пользователя и фуппы у процесса.
i access — определяет доступность файла
«include <unistd.h>
int access(
ccnst char *path, /• имя файла */
int what /• проверяемые права •/
I );
/• Возвращает О, если доступен, или -1, если недоступен или в случае сшибки
(кед сшибки - в переменней еггпс) •/
В качестве аргумента what можно передавать следующие флаги, причем первые три
можно объединять оператором ИЛИ:
R_0K /• праве на чтение */
W_0K /* праве на запись */
Х_0К /• праве на испслнение (псиск) •/
F_0K /• проверка на существование */
Если реальный идентификатор пользователя процесса и файла совпадают —
проверяются права владельца, иначе, если реальный идентификатор группы процесса
и файла совпадают — проверяются права группы, иначе проверяются права для
остальных.
Вызов access используется в двух основных случаях.
• Чтобы проверить, имеет ли право реальный пользователь или группа сделать
что-нибудь с файлом в случае, когда установлены биты изменения прав
пользователя или группы при запуске (set-user-ID или set-group-ID). Например,
представим, что имеется утилита, которая приобретает права суперпользователя при
запуске, но она требует проверки права реального пользователя на выполнение
некоторой операции, например, на удаление файла. Не стоит надеяться, что unlink
завершится в таком случае с кодом ошибки EACCES, поскольку он будет вызван с
правами суперпользователя.
• Чтобы убедиться в существовании файла. Для этой цели можно использовать
вызов stat, но проще access. Фактически, системный вызов access целиком мог
бы быть оформлен в виде библиотечной функции, которая вызывает stat.
Некоторые системы допускают такую реализацию.
Если в качестве аргумента path передана символическая ссылка, то переход по
ссылкам будет выполняться до тех пор, пока не встретится объект, не являющийся
символической ссылкой*.
' Это справедливо почти для всех системных вызовов, только если их названия не
начинаются с символа «1». Исключения составляют unlink и rename.
182 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
Если при работе с вызовом access вы хотите отличить ошибку EACCES (проверка R_0K,
W_0K и/или Х_0К) от EN0ENT (проверка F_0K), то не стоит обращаться к нему через макрос
ec_neg1. Выполняйте проверку самостоятельно, примерно таким образом:
if (access("trap", F_0K) == 0)
printf("Oaito существует^");
else if (errnc == ENOENT)
printfC'Oatto не найден\п");
else
EC_FAIL
3.8.2 Системный вызов mknod
mknod — создает файл
«include <sys/stat.h>
int rakncd(
ccnst char *path, /*
racde_t perras, /*
dev_t dev /*
);
/* Возвращает О в случае
в переменней errnc) */
имя файла •/
права доступа */
идентификатор устройства «/
успеха, -1 в случае сшибки (кед сшибки -
Системный вызов rakned может создавать обычные файлы, каталоги, специальные
файлы и именованные каналы. Единственная переносимая операция, которую вы
може^р сделать с помощью raknede, не обладая правами суперпользователя, это
создать именованный канал, но для этого существует системный вызов mkf ifo (см. раздел
7.2.1). Вам нет нужды использовать его для создания обычных файлов (для этого
существует open) или каталогов (mkdir). С его помощью нельзя создать ни
символическую ссылку (symlink), ни сокет (bind).
Поэтому основное назначение rakned — создание специальных файлов устройств,
как правило, в каталоге /dev. Делается это обычно во время установки нового драйвера
устройства, когда вызывается команда mkned, которая обращается к одноименному
системному вызову.
Аргумент perras задает права доступа к файлу с использованием битовых масок и
макросов, описанных в разделе 3-5.1. Аргумент dev — это идентификатор
устройства. Если вы устанавливаете новый драйвер устройства, то вы наверняка знаете и
его идентификатор.
3.8.3 Системный вызов fcntl
В разделе 2.2.3, который вы, возможно, захотите перечитать, прежде чем
продолжить, я говорил, что несколько открытых дескрипторов могут ссылаться на одну и
ту же запись в таблице файлов, где хранятся текущая позиция в файле и флаги
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 183
например 0_APPEND или 0_R00NLY). Как правило, флаги устанавливаются один раз —
= момент открытия файла, но есть возможность изменить их в любой момент си-
.темным вызовом fcntl. С его помощью можно получить флаги, не изменяя их.
fcntl — выполняет управляющие операции над открытым файлом
«include <unistd.h>
«include <fcntl.h>
int fcntl(
int fd, /« дескриптор файла «/
int op, /« операция »/
... /• необязательные аргументы, зависит от вида операции «/
);
/• Возвращаемое значение зависит от вида сперации. В случае сшибки
возвращается значение -1 (код сшибки - в переменней еггпс) */
Всего вызовом предусматривается десять различных операций, но в этом разделе
мы будем говорить только о четырех. Остальные будут обсуждаться позднее.
Таблица 3.4. Операции fcntl
Операция
Назначение
Где описывается
-.DUPFD Создать дубликат дескриптора Раздел 6.3
p.GETFD Получить флаги дескриптора Здесь
f.setfo Установить флаги дескриптора Здесь
C_GETFL Получить флаги состояния и режимов Здесь
доступа (использует три дополнительных
аргумента типа int)
F_SETFL Установить флаги состояния и режимов Здесь
доступа (использует три дополнительных
аргумента типа int)
F.GET0WN Используется при работе с сокетами' Раздел 8.7
F_SET0WN Используется при работе с сокетами" Раздел 8.7
F_GETLK Получить состояние блокировки Раздел 7.11.4
F.SETLK Установить или сбросить блокировку Раздел 7.11.4
F_SETLKW Установить или сбросить блокировку Раздел 7.11.4
Слишком сложная тема, даже для того, чтобы вкратце описать ее здесь
При выполнении любой операции «set» вы обязательно должны сначала выполнить
операцию «get», чтобы получить текущее значение, затем установить или сбросить
требуемые флаги, и лишь потом выполнить операцию «set», даже если операция
предусматривает работу с единственным флагом. Благодаря этому ваш код останется
работоспособным и в случае добавления в систему новых флагов, и в случае если
имеются нестандартные флаги, о наличии которых вы даже не подозреваете.
Например, неправильный способ установки флага 0_APPEND:
7 Зак. 4030
184 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
ec_neg1( fcntl(fd, F.SETFL, 0_APPEND) ) /* неверно */
правильный способ:
ec_neg1( flags = fcntl(fd, F_GETFL) )
ec_neg1( fcntl(fd, F.SETFL, flags | O.APPENO) )
Если вы хотите сбросить флаг 0_APPEN0, то вторая строка должна быть такой:
ec_neg1( fcntl(fd, F_SETFL, flags & "0_APPEN0) )
Правило: логическое сложение (ИЛИ) — чтобы установить, логическое умножение
(И) на логическое дополнение (НЕ) — чтобы сбросить.
Единственный стандартный флаг дескриптора — F0_CL0EXEC, который определяет
— будет ли закрыт файл при обращении к системному вызову exec. Этот флаг
может быть установлен только с помощью системного вызова fcntl, однако
подробнее об этом мы поговорим в разделе 5.3.
Операции F.GETFL и F_SETFL предназначены для управления флагами режимов
доступа к файлу. Флаги 0_R00NLY, O.WRONLY и 0_R0WR нельзя установить или сбросить, вы
можете только получить их значение, потому что они представляют собой не
битовые маски, а самостоятельные значения. По этой же причине, чтобы
проанализировать любой из этих флагов, вы должны будете воспользоваться маской 0.АССМ00:
ec_neg1( flags = fcntl(fd, F_GETFL) )
if ((flags & 0_ACCH00E) == 0_R00NLY)
/* файл открыт только для чтения */
Следующие два условных оператора составлены неверно:
if (flags & 0_R00NLY) /* неверно */
if ((flags & 0_ROONLY) == O.ROONLY) /♦ также неверно •/
Наибольший интерес для нас представляют флаги, перечисленные в табл. 2.1
раздела 2.4.4. Вы можете получить и изменить значения флагов 0_APPEN0,0_0SYNC, 0_N0CTTY,
0.N0NBL0CK, 0_RSYNC и O.SYNC. Вы можете получить значения флагов 0_CREAT, OJTRUNC и
0_EXCL, правда, я не вижу особого смысла в том, чтобы изменять их, поскольку они
используются только в момент открытия файла вызовом open. Чуть выше я уже
демонстрировал пример изменения флага 0_append.
Более подробно о флаге 0_N0NBL0CK — см. разделы 4.2.2 и 7.2.
3.9 Асинхронный ввод-вывод
В этом разделе рассказывается о том, как выполняются операции ввода-вывода,
которые возвращают управление в программу, не дожидаясь своего завершения.
Благодаря этому программа может заняться более полезным делом, а результат
выполнения операции проверить позднее.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 185
3.9.1 Еще раз о понятиях «синхронизированный»
и «синхронный»
Материал, излагаемый здесь, опирается на раздел 2.16.1, где были даны начальные
сведения о синхронизации операций ввода-вывода. В данном разделе мы рассмотрим
операции асинхронного ввода-вывода, т. е. такие операции, которые инициируются
системным вызовом (например aio_read) и возвращают управление в программу до
своего полного завершения, а результаты выполнения операции могут быть
получены позднее. Или, если быть точнее:
• термин синхронизированный означает, что операция ввода-вывода
завершается тогда, когда завершается физический ввод-вывод данных на носитель.
Термин несинхронизированный означает, что операции ввода-вывода
производятся между процессом и кэшем ядра;
• термин синхронный означает, что системный вызов возвращает управление, как
только завершится выполнение операции ввода-вывода, как это определено в
предыдущем параграфе. Термин асинхронный означает, что системный вызов
возвращает управление немедленно, как только операция ввода-вывода будет
инициирована, а завершение ее проверяется другим системным вызовом.
То же самое, но в одном предложении: «Понятия синхронный/асинхронный
указывают, ждет ли приложение завершения операции. Понятия синхронизированный/
несинхронизированный определяют значение слова «завершение».
Часть материала этого раздела предполагает, что вы знакомы с сигналами UNIX, в
противном случае вы можете прямо сейчас перейти к главе 9 и прочитать сначала
ее (особое внимание уделите разделу 9.5.6), а потом вернуться к этому разделу.
Как я уже отмечал в разделе 2.9, операция записи в UNIX до некоторой степени
является асинхронной, поскольку она возвращает управление сразу же, как только
данные будут переписаны в кэш. Однако, если файл открыт с флагом 0.SYNC или
0.DSYNC (см. раздел 2.16.3), то вызов write не вернет управление до тех пор, пока не
завершится фактическая запись данных на носитель. Операция чтения являет
собой полную противоположность. Системный вызов read ожидает фактического
завершения операции чтения с носителя, если в кэше этих данных нет.
Асинхронные операции ввода-вывода не ожидают, пока вызов read прочитает
данные или пока синхронизированный (O.SYNC или 0_DSYNC) write выполнит запись. Они
инициируют операцию ввода-вывода и сразу же возвращают управление. Позднее
процесс может обратиться к системному вызову aio_error, чтобы узнать о том,
завершилась ли операция. Кроме того, процесс может получить от системы
уведомление в виде сигнала или через запуск нового потока (см. раздел 517) о
завершении операции ввода вывода.
Термин «завершение» используется здесь исключительно в применении к
системным вызовам read и write. Он не подразумевает синхронизацию ввода-вывода, если
файл не был открыт с одним из флагов, описанных в разделе 2.16.3. Таким
образом, мы имеем четыре различных варианта:
186 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
Таблица 3.5. Синхронизированные и синхронные операции чтения и записи
Синхронные Асинхронные
Несиихронизированные read/write. Флаги 0_SYNC и aio_read/aio_write. Флаги
0.DSYNC сброшены 0.SYNC и 0.DSYNC сброшены
Синхронизированные read/write. Установлен флаг aio_read/aio_write. Установ-
0.SYNC ИЛИ 0.DSYNC лен флаг 0.SYNC ИЛИ 0_DSYNC
Асинхронный ввод-вывод может помочь увеличить производительность операции
чтения (синхронизированной или нет), если есть возможность организовать
приложение так, чтобы оно инициировало операцию чтения еще до того, как эти
данные потребуются. Асинхронная запись выполняется намного быстрее
синхронизированной, но выигрыш, по сравнению с несинхронизированной операцией
записи, получится практически незаметным, потому что кэш ядра делает
практически то же самое.
Не путайте неблокирующий ввод-вывод, который задается флагом 0.N0NBL0CK (см.
разделы 2.4.4 и 4.2.2), с асинхронным вводом-выводом. Неблокирующий вызов
возвращает управление, если он не может выполнить действие, не заблокировав
приложение". Асинхронный вызов возвращает управление, как только инициирует
действие.
Системный вызов lio.listio — единственный в этом разделе, который может
выполнять как синхронный, так и асинхронный ввод-вывод.
3.9.2 Блок управления асинхронными
операциями ввода-вывода
Все асинхронные системные вызовы используют блок управления операцией, в
котором хранится ее состояние. Каждая операция должна иметь свой собственный
блок управления. Разумеется, после завершения операции ее блок управления
может быть использован повторно.
struct aiocb — блок управления асинхронной операцией ввода-вывода
struot aioob {
int aio_fiides;
off_t aio.offset;
volatile void »aio_buf;
size_t aio.nbytes;
int aio.reqprio;
struct sigevent aio_sigevent;
int aio_iio_opoode;
};
/» деокриптор файла */
/* позиция в файле */
/• буфер •/
/• объем буфера »/
/• начальный приоритет запрооа •/
/* введения о оигнале */
/* дейотвие, которое будет выполнено */
' Исключение составляет системный вызов oonnect, см. раздел 8.1.2.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 187
Первые четыре поля структуры в точности повторяют входные аргументы вызо-
зов pread и pwrite: три аргумента вызовов read и write, плюс аргумент для неявного
вызова lseek (см. раздел 2.14).
Структура sigevent подробно описывается в разделе 95.6. С ее помощью вы
можете указать, какой сигнал сгенерировать или какой поток запустить по завершении
операции. Обработчику сигнала или потоку может быть передано произвольное
целое число или указатель, обычно это указатель на блок управления. Если вы не
хотите, чтобы приложение получило сигнал или запустило поток, просто
запишите в поле aio_sigevent.sigev_notify значение S1GEV.N0NE.
Поле aio.regprio задает приоритет операции ввода-вывода и доступно, только если
система поддерживает опции POSIX: _P0S1X_PRI0R1TIZE0_10 и _P0SIX_PR10RITY_SCHE0UL1NG.
Дополнительную информацию по этой теме вы найдете в [SUS2002].
Последнее поле aio_lio_opoode используется системным вызовом lio_listio. Подробнее
об этом вызове читайте в разделе 39-9-
3.9.3 aio.read и aio.write
Функции aio.read и aio.write являются основными операциями асинхронного ввода-
вывода. Аналогично вызовам pread и pwrite, поле aio.offset определяет позицию в
файле для выполнения операции. Этот параметр игнорируется для устройств,
которые не позволяют выполнять перемещение по файлу (например сокеты) или когда
файл открыт с флагом 0.APPEND (см. раздел 2.8). Буфер и размер буфера
определяются внутри блока управления и не передаются как аргументы.
aio_read — выполняет асинхронное чтение из файла
#inolude <aio.h>
int aio_read(
struot aioob «aioobp /* блок управления •/
);
/* Возвращает 0 в олучае уопеха, -1 в олучае ошибки (код ошибки -
в переменной еггпо) »/
aio_write — выполняет асинхронную
#inolude <aio.h>
int aio_write(
struot aioob *aioobp /* блок
);
/• Возвращает 0 в олучае уопеха,
в переменной еггпо) */
запись в файл
управления
-1
в олучае
•/
ошибки
(код ошибки -
Успешное завершение этих функций означает лишь то, что операция была
благополучно инициирована — это совершенно не означает, что операция
ввода-вывода прошла успешно, и даже не означает, что блок управления благополучно про-
188 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
шел проверку на корректность заполнения. Вы всегда должны проверять результат
выполнения с помощью aio.error (см. следующий раздел). Отдельные реализации
могут предусматривать возврат признака ошибки, например в случае, когда
неправильно указана текущая позиция в файле, но чаще alo.read и aio.write возвращают
О, а о плохих новостях вы узнаете позже.
3.9.4 aio.error и aio.return
aio_error — возвращает
да-вывода
#inoiude <aio.h>
int aio_error(
oonst struot
);
/» Возвращает О,
aioob
отчет о выполнении
♦aioobp /<
код ошибки, или
в errno не запиоываетоя
) •/
асинхронной операции
блок управления */
EINPR0GRESS
(в этом
олучае
код
ошибки
вво-
Если операция завершилась, aio.error возвращает 0 в случае успеха или код
ошибки (один из тех, которые могут вернуть системные вызовы read, write, f sync и fdatasyno,
см. разделы 2.9, 2.10 и 2.16.2). Если операция еще не завершилась, возвращается код
EINPROGRESS. Если вы уверены, что операция завершена (например, когда от
системы уже было получено уведомление в виде сигнала), можете обращаться с кодом
EINPROGRESS так же как с любой другой ошибкой и использовать макрос eo_rv, как
мы это делали в разделе 3-6.1:
ec_rv( aio_error(aiocbp) )
Тем не менее, следует отметить, что асинхронный ввод-вывод обычно
используется в довольно сложных приложениях, для которых наши простые макросы «ее»
проверки наличия ошибок могут не подойти. Однако макрос ec.rv может
по-прежнему использоваться в процессе разработки, так как представляет собой
отличное средство трассировки вызовов функций.
Если функция aio.error возвращает значение 0, то с помощью функции aio.return
вы можете получить число прочитанных или записанных байт, которое обычно
возвращают системные вызовы read и write:
aio_return — возвращает статус асинхронной операции ввода-вывода
«inoiude <aio.h>
ssize.t aio_return(
struot aioob *aiocbp /* блок
);
/* Возвращает значение операции
в переменной еггпо) */
управления •/
«ли -1 в олучае
ошибки
(код ошибки -
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 189
Предполагается, что вызов aio_return производится только в случае, когда aio.error
зернула признак успешного завершения операции. Возвращаемое значение -1,
полученное от aio.return не означает, что операция ввода-вывода завершилась с
ошибкой, оно лишь говорит о том, что функция была вызвана по ошибке. Кроме того,
sio.return можно вызвать лишь раз для одной и той же операции, поскольку после
получения возвращаемого значения оно может быть потеряно системой.
3.9.5 Вызов aio.cancel
С помощью aio.oanoei вы можете отменить незавершенную асинхронную
операцию ввода вывода:
aiocancel — отменяет запрос на асинхронную операцию ввода-вывода
«inoiude <aio.h>
int aio_oanoei(
int fd, /* деокриптор файла •/
struot aioob *aioobp /* блок управления */
);
/* Возвращает код результата или -1 в олучае ошибки (код ошибки -
в переменной errno) */
Если в качестве аргумента aioobp передан пустой указатель (NULL), то отменяются
все асинхронные операции, связанные с дескриптором fd. Операции, которые не
были отменены, сообщают о своем завершении обычным способом, отмененные
операции возвращают (через aio.error) код ошибки ECANCELED.
Если функции передан непустой указатель, то она попытается отменить операцию,
которая была запущена с указанным блоком управления. В этом случае значение
аргумента fd должно совпадать со значением поля aio.fildes в блоке управления.
В случае успеха, функция aio_oanoel возвращает одно из следующих значений:
aio.canceled Все запрошенные операции были отменены.
aio.notcanceled Одна или более операций (возможно все) не были отменены,
поскольку они уже находятся в состоянии выполнения. Чтобы
определить, какие именно были отменены, необходимо вызвать
aio.error для каждой из них.
AI0.ALL00NE Ни одна операция не была отменена, поскольку все они уже
завершились.
3.9.6 Вызов aio_f sync
Кроме прямых эквивалентов системных вызовов read и write, существует еще одна
операция асинхронного ввода-вывода —.выталкивание буферов из кэша на
устройства вывода, аналогичная системным вызовам fsyno и fdatasyno (см. раздел 2.16.2):
190 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
aio_fsync — инициирует выталкивание буферов
«include <aic.h>
int aic_fsync(
int cp,
struct aiccb
);
/* Всзаращает О а
h
•aiccbp /*
случае
в переменней еггпс) */
0_SYNC или O.DSYNC
блек
успеха,
управления */
ДЛЯ
*/
-1 а случае сшибки
одного файла
(кед сшибки -
Эта функция использует блок управления операцией несколько иначе, чем все
остальные — на устройство вывода будут вытолкнуты все буферы, связанные с
дескриптором файла из поля aic_f ildes, а не только те, что связаны с данной
операцией. Запрос на синхронизацию так же является асинхронным — он фактически не
выполняет синхронизацию — он только инициирует ее. Проверить выполнение
операции можно обычным способом, т. е. с помощью aic_errcr или посредством
сигнала. Все точно так же, как для aic.read и aic_write, которые, возвращая
значение 0, сообщают лишь о том, что запрос принят.
3.9.7 Вызов aio.suspend
Иногда бывает необходимо подождать завершения асинхронной операции ввода-
вывода. Сделать это можно с помощью aic.suspend:
aio_suspend — ожидает завершения асинхронной операции ввода-вывода
«include <aic.h>
int aic_suspend(
censt struct
int cbent,
censt struct
);
/* Всзаращает О а
aiccb «censt list[],
timespec •timecut
случае успеха, -1 a
а переменней еггпс) */
/*
/*
/*
массив блскса управления */
числе элементсв в массиве */
максимальнее время сжидания */
случае сшибки (кед сшибки -
Если аргумент timecut имеет значение NULL, aic.suspend принимает массив блоков
управления и ожидает завершения хотя бы одной из операций. Каждый блок
управления-в массиве должен соответствовать предварительно инициированной
асинхронной операции. Если хотя бы одна из них уже завершилась, управление
немедленно возвращается в вызывающую программу.
Для удобства повторного использования того же самого массива допускается,
чтобы отдельные элементы имели значение NULL, но cbent должен содержать полное
число элементов в массиве, в том числе и пустых.
Если timecut не NULL, то aic_suspend ожидает не дольше указанного времени, после
чего возвращается с признаком ошибки и кодом ошибки EAGAIN в переменной еггпс.
Описание структуры timespec вы найдете в разделе 1.7.2.
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 191
Подобно большинству блокирующих вызовов, aio_suspend может быть прерван
сигналом (см. раздел 9-1.4). В результате может сложиться очень интересная ситуация: если
операция предусматривает генерацию сигнала по завершении, то вызов aio_suspend будет
прерван этим сигналом и aio_suspend завершится с признаком ошибки и кодом
ошибки EINTR. В некоторых ситуациях эту ошибку можно было бы игнорировать,
но проблема заключается в том, что вызов может быть прерван любым системным
сигналом, даже не связанным с завершением операции. Избежать прерывания
aio.suspend сигналом завершения операции можно одним из двух способов:
• использовать для индикации завершения операции только что-то одно — либо
сигнал, либо aio.suspend;
• установить для сигнала флаг SA_RESTART (см. раздел 9-1.6).
Или просто вызвать aio_error для каждого элемента списка, чтобы узнать причину
прерывания, и перезапустить aio.suspend (возможно в цикле), если ни одна из
операций не была завершена.
3.9.8 Пример сравнения синхронных и асинхронных
операций ввода-вывода
В этом разделе будет рассмотрен пример использования системных вызовов
асинхронного ввода-вывода и продемонстрированы некоторые их преимущества
перед синхронными операциями ввода-вывода.
Для начала рассмотрим программу sio, которая выполняет обычные синхронные
операции файлового ввода-вывода. После каждых 8000 считываний из файла,
программа выполняет чтение с устройства стандартного ввода.
#define PATH "/aup/o3/datafile.txt"
«define FREQ 8000
statio void synohronous(void)
{
int fd, oount = 0;
ssize_t nread;
ohar buf1[512], buf2[512];
eo_neg1( fd = open(PATH, 0_R00NLY) )
timestartO;
while (true) {
eo_neg1( nread = read(fd, bufl, sizeof(bufl)) )
if (nread == 0)
break;
if (oount % FREQ == 0)
eo_neg1( read(ST0IN_FILEN0, buf2, sizeof(buf2)) )
oount++;
}
timestop("synoh ronous");
192 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
printf("прочитано Xd блоков\п", oount);
return;
EC_CLEANUP_BGN
EC_FLUSH("synoh ronous")
EC_CLEANUP_END
}
(Описание timestart и timestop вы найдете в разделе 1.7.2.)
Стандартный ввод программы sio связан каналом со стандартным выводом
программы feed:
$ feed | sio
Программа feed очень «неохотно» выдает данные — она пишет их с частотой 1 раз
в 20 секунд:
int main(void)
{
ohar buf[512];
memset(buf, 'x', sizeof(buf));
whiie (true) {
sieep(20);
write(STD0UT_FILEN0, buf, sizeof(buf));
}
}
Как показал эксперимент, программа sio затратила 0.12 секунды на выполнение
команд в пользовательском процессе, 0.73 секунды — на выполнение команд в ядре
и 200.05 секунды на полное завершение операций ввода. О.чевидно, что большую
часть времени программа провела в ожидании поступления данных из канала.
Следующий пример во многом искусственный, но он ясно демонстрирует, как можно
уменьшить время ожидания с помощью асинхронных операций ввода-вывода. Если
чтение из канала выполняется асинхронно, то параллельно можно производить
чтение файла:
static void asynohronous(void)
{
int fd, oount = 0;
ssize_t nread;
ohar buf1[512], buf2[512];
struot aioob ob;
oonst struot aioob *list[1] = { &ob };
memset(&ob, 0, sizeof(ob));
ob.aio.fildes = STDIN_FILEN0;
ob.aio_buf = buf2;
ГЛАВА 3 Дополнительные операции файлового ввода-вывода 193
cb.aicjibytes = sizecf(buf2);
cb.aio_sigevent.sigev_nctify = SIGEV_NONE;
eo_neg1( fd = cpen(PATH, OJTOONLY) )
timestartO;
while (true) {
ec_neg1( nread = read(fd, bufl, sizecf(bufl)) )
if (nread == 0)
break;
if (count X FREQ == 0) {
if (count > 1) {
ec_neg1( aic_suspend(list, 1, NULL) )
ec_rv( aic_errcr(4cb) )
}
ec_neg1( aic_read(4cb) )
}
ccunt++;
}
timestcp("asynchrcncus");
printf("npc4MTaHC Xd блсксв\п", ccunt);
return;
EC_CLEANUP_BGN
EC_FLUSH("asynchrcncus")
EC_CLEANUP_END
>
Обратите внимание на то, как инициализируется блок управления — в первую
очередь он обнуляется. На каждые 8000 обращений к файлу (определение FREQ было
дано выше), программа запускает асинхронную операцию чтения из канала
вызовом aic_read, после чего задерживается на aic_suspend, чтобы завершить операцию
чтения перед новым запросом. Пока выполняется aic_read, продолжается чтение
очередных 8000 порций из файла. На этот раз у нас получилось 0.18 секунды на
выполнение команд в пользовательском процессе (потому что работы прибавилось),
0.70 секунды — на выполнение команд в ядре и 180.58 секунды на полное
завершение операций ввода, что значительно меньше, чем в предыдущем случае.
В конкретных приложениях выигрыш от применения асинхронного ввода-вывода
будет не так велик, а иногда он будет намного больше, чем в этом примере. Ключ к
успеху заключается в том, чтобы:
• программа выполняла какую-нибудь полезную работу, пока выполняется
асинхронная операция;
• выигрыш от использования асинхронных операций должен перевешивать
проигрыш от увеличившегося количества системных вызовов в программе. Вы
также можете учесть повышенную сложность разработки и то, что асинхронный
ввод-вывод поддерживается не на всех системах.
194 ГЛАВА 3 Дополнительные операции файлового ввода-вывода
3.9.9 Вызов lio.listio
Списки блоков управления могут найти еще одно применение — выполнение
пакетных запросов одним системным вызовом:
Но listio — выполняет пакетный
#include <aic.h>
int lic_listic(
int mcde,
struct aiccb «const list[],
int cbcnt,
struct sigevent *sig
);
/• Возвращает О в случае успеха
в переменней еггпс) */
ввод-вывод для списка блоков управления
/* LI0_WAIT или LI0_N0WAIT •/
/* массив блсксв управления */
/* количестве элементсв массива */
/• NULL или сведения с генерации сигнала •/
-1 в случае ошибки (кед сшибки -
Подобно aic_suspend, вызов lic_listic принимает список блоков управления,
пустые элементы (NULL) в списке игнорируются. Он инициирует запросы без учета
порядка их следования в списке. Тип каждой операции в списке определяется полем
aic_lic_cpccdB блока управления:
LI0.READ Эквивалентно вызову aio.read или pread.
LI0_WRITE Эквивалентно вызову aio.write или pwrite.
LI0_N0P «Нет операции» — блок управления игнорируется.
Аргумент mcde определяет тип операций —синхронные или асинхронные, как
показано в табл. ЗА которая напоминает табл. 35 из раздела 3-9.1.
Таблица 3.6. Синхронизированные и синхронные операции чтения и записи в lic_listic
Синхронные Асинхронные
Несиихроиизироваииые LI0.WAIT. Сброшены LIO.nowait. Сброшены флаги
флаги 0_SYNC И O.DSYNC O.SYNC и O.DSYNC
Синхронизированные LI0_WAIT. Установлен LIO.NOWAIT. Установлен флаг
флаг O.SYNC ИЛИ O.DSYNC O.SYNC ИЛИ O.DSYNC
Таким образом, вызов lic.listic может использоваться не только для выполнения
асинхронных операций, но и для пакетной обработки обычных операций ввода-вывода.
В режиме LI0_N0WAIT вы можете запросить подтверждение выполнения через
сигнал или через запуск потока в приложении, используя для этого аргумент sig
точно так же, как и поле aic.sigevent в блоке управления. Но в этом случае
уведомление будет означать, что все операции из списка завершены. У вас по-прежнему
сохраняется возможность получать индивидуальные уведомления от каждой из
операций через поле aic.sigevent блока управления.
В режиме LI0.WAIT аргумент sig игнорируется.
Как и в случае с другими асинхронными вызовами, успешное завершение lic.listic
говорит лишь о том, что все запросы успешно инициированы. Выполнение
операций необходимо проверять с помощью вызова aic.error.
ГЛАВА 3 Дополнительные сЩ5ЩЯ^ИИ|1йёш и ввода-вывода 195
Упражнения
; '. Измените программу из раздела 3-2.2 так, как это предлагается в последнем
параграфе этого раздела.
; 2 Напишите свою реализацию стандартной команды df.
г 5 Измените программу aupls так, как это предложено в последнем параграфе
раздела 3-6.2.
г 4 Измените функцию getcwdx из раздела 3-6.4 так, чтобы она не изменяла
текущий каталог.
5 5. Разберитесь с Проблемой №2, которая была описана в начале раздела 3-6.5.
5 6. Измените программу aupls из раздела 3-6.5 так, чтобы она сортировала
список по именам.
г ~. Измените программу aupls из раздела 3-6.5 так, чтобы она принимала ключ
-t, позволяющий сортировать список по имени и времени последнего изменения.
? 8. Измените программу aupls из раздела 3.6.5 так, чтобы она могла принимать
другие стандартные ключи команды Is (на ваш вкус).
3 9- Напишите программу копирования дерева каталогов со всем его содержимым.
Она должна принимать два аргумента: корень дерева каталогов (например
/usr/marc/bcck) и корень дерева для копии (например /usr/mark/backup/bcck).
Проблему символических ссылок можно игнорировать (т. е. допускается
создание нескольких копий подкаталогов). Сохранением таких параметров, как
владелец файла, права доступа, время последнего обращения и время
последнего изменения можно пренебречь.
3.10. То же самое, что и в упражнении 39, но на этот раз копирование должно
выполняться с сохранением первоначальных значений таких параметров, как
владелец файла, права доступа, время последнего обращения и время
последнего изменения.
3.11. То же самое, что и в упражнении 3-Ю, но на этот раз с учетом символических
и жестких ссылок на каталоги.
3.12. Почему отсутствует системный вызов lchmod?
3.13- Реализуйте системный вызов access в виде функции. Допускается
использование любых системных вызовов, кроме самого системного вызова access.
3.14. Реализуйте системные вызовы readv и writev (см. раздел 2.15) с помощью
lic_listic. Есть ли какие-нибудь преимущества реализации этих вызовов
таким способом?
3.15. Можно ли реализовать таким же способом системные вызовы read, write, pread,
pwrite, fsync, fdatasync, aic.read, aic.write и aic_fsync? Если да — выполните их.
Появились ли какие-нибудь преимущества?
3.16. Перечислите «за» и «против» каждого из способов проверки завершения
асинхронных операций ввода-вывода: aic_suspend, сигналы, потоки и
периодическая проверка с помощью aic.errcr. (Для успешного выполнения упражнения
вы должны располагать сведениями, излагаемыми в главах 5 и 9)
|S в ■■ ■■ ■■ ■■ A
'■':1- ш Ш Ш Ш Ш 1\
Терминальный
ввод-вывод
4.1 Введение
Тема терминального ввода-вывода настолько сложна, что сама по себе
заслуживает большей части главы. Проблема связана не с обычным терминальным вводом-
выводом, с которого я начну — он даже проще, чем файловый ввод-вывод.
Сложности объясняются многочисленными вариациями атрибутов терминалов. В этой
главе я также объясню, как терминалы связаны с сеансами и группами процессов.
Затем я покажу, как использовать псевдотерминалы, которые позволяют одному
процессу управлять другим процессом, работающим с терминалом.
В UNIX при терминальном вводе-выводе терминал рассматривается как устаревший
печатный телетайп — ужасно шумное устройство с массивной кареткой, которое
теперь можно увидеть только в музеях или старых фильмах. Специфические
средства поддержки экранов (символьных или графических), функциональных клавиш,
мыши и других указательных устройств в ядре не реализованы. С некоторыми
более новыми устройствами можно работать при помощи библиотек, которые
обращаются к стандартным драйверам устройств. Самой известной библиотекой для
работы с символьными дисплеями является пакет Curses, который теперь является
частью спецификации [SUS2002]. Так называемые приложения с графическим
пользовательским интерфейсом (graphical user interface, GUI) обычно создаются для
системы X Window — возможно, при помощи одного из поддерживаемых ей инстру-
ментариев, таких как Motif, Qt, KDE или Gnome.
Так как эта глава посвящена преимущественно драйверам устройств, а не
компонентам ядра вроде файловой системы, описываемые здесь возможности зависят от
версии UNIX сильнее, чем другие аспекты. Иногда даже конкретные учреждения
вносят изменения в драйверы терминалов UNIX. Помните также, что не все
атрибуты терминального ввода-вывода обусловлены природой UNIX. Распространение
интеллектуальных терминалов, локальных сетей и процессоров, выполняющих
198 ГЛАВА 4 Терминальный ввод-вывод
предварительную обработку данных, привело к тому, что сейчас поток символов
претерпевает сильные изменения до того, как его принимает ядро UNIX, и после
того, как оно отправляет его дальше. Детали предварительной обработки и
постобработки слишком сильно различаются, поэтому я их описывать не буду. Как
обычно, я сконцентрируюсь на стандартизированных аспектах терминального ввода-
вывода. Получив базовую информацию, вы сможете разобраться с особенностями
своей системы по входящему в ее состав руководству.
4.2 Чтение данных с терминала
В этом разделе мы обсудим чтение данных с терминала, помимо всего прочего вы
\'знаете, как предотвратить блокировку, если терминал не имеет готовых данных.
4.2.1 Обычный ввод-вывод с использованием терминалов
Сначала мы обсудим обычную работу механизма терминального ввода-вывода,
т е. то, как он работает, когда вы вошли в систему, но еще не настраивали ввод-
вывод при помощи команды stty. Затем я объясню, как использовать системные
вызовы fcntl и tcsetattr для изменения этого обычного поведения.
Есть три способа получения доступа к терминалу для ввода или вывода:
1. Вы можете открыть специальный символьный файл /dev/tty для чтения, записи
или, что делают чаще всего, и для того и для другого. Этот специальный файл
является синонимом соответствующего процессу управляющего терминала (см.
раздел 4.3.1).
2. Если вам известно реальное имя специального файла (например /dev/tty04), вы
можете открыть его, а не файл /dev/tty. Если вам просто нужен управляющий
терминал, этот способ не имеет никаких преимуществ перед использованием
универсального имени /dev/tty. В приложениях он обычно используется для
доступа не к управляющему, а к другим терминалам.
3. Каждый процесс получает три уже открытых дескриптора файлов:
дескрипторы стандартного ввода, стандартного вывода и стандартного вывода
информации об ошибках. Для управляющего терминала они могут быть открыты или
закрыты, но обычно они тем или иным образом используются; если вместо этого
они открыты для обычного файла или канала, это объясняется тем, что
пользователь или родительский процесс перенаправил ввод или вывод.
Базовыми системными вызовами, служащими для терминального ввода-вывода,
являются вызовы open, read, write и close. Вызывать creat не имеет особого смысла,
так как специальный файл уже должен существовать. Вызов lseek не влияет на
терминалы, потому что они не имеют файлового смещения, а это означает, что
вызовы pread и pwrite использовать нельзя.
Если вы не задали флаг 0_N0NBL0CK, вызов, пытающийся открыть терминальное
устройство, ожидает, пока устройство не будет подключено к терминалу. Это
выгодно, в первую очередь, системному процессу, который блокируется в вызове open,
ГЛАВА 4 Терминальный ввод-вывод 199
ожидая подключения пользователя. Затем системный процесс получает значение,
возвращенное вызовом open, и вызывает процесс входа в систему. После того, как
пользователь вошел в систему, вызывается командная оболочка пользователя,
указанная в файле паролей, и пользователь может приступить к работе.
Системный вызов read тоже по умолчанию работает с терминалами не так, как с
файлами: он никогда не возвращает более одной строки введенных данных, и даже
если он запрашивает только один символ, никакие символы не возвращаются, пока
не будет готова вся строка. Это объясняется тем, что пока пользователь не
завершил строку, мы не можем считать, что она представлена в заключительной форме:
пользователь может при помощи символов erase и kill исправить ее или вообще
отменить ввод. Число, возвращаемое вызовом read, позволяет узнать, сколько
символов на самом деле было прочитано (то, что я только что описал, называется
каноническим вводом с терминала, но вы можете отключить его; см. раздел 4.5.9).
Пользователь может завершить строку двумя способами. Чаще всего признаком конца
строки является символ перевода строки. При этом пользователь нажимает
клавишу ввода, но драйвер устройства преобразует символ ввода (восьмеричное 15) в
символ перевода строки (восьмеричное 12). Или же пользователь может
сгенерировать символ конца файла (end-of-file, EOF), нажав Ctrl-d. В этом случае строка
становится доступной вызову read как есть, без завершающего символа перевода
строки. Важно упомянуть один специальный случай: если пользователь сгенерирует
EOF в начале строки, вызов read возвратит ноль, так как строка, заканчивающаяся
символом EOF, пуста. Все будет выглядеть как конец файла, поэтому комбинацию
Ctrl-d можно считать «символом» конца файла.
Для чтения данных с терминала при помощи дескриптора файла стандартного ввода
STDIN_FILENO (определенного как 0) можно использовать следующую функцию. Она
удаляет завершающий символ перевода строки, если таковой имеется, и добавляет
завершающий символ null, преобразуя введенную строку в строку С:
bool getln(char «s, ssize_t max, bool «iseof)
{
ssize_t nread;
switch (nread = read(ST0IN_FILEND, s, max - 1)) {
case -1:
EC_FAIL
case 0:
*iseof = true;
return true;
default:
if (s[nread - 1] == '\n')
nread-;
s[nread] = '\D';
*iseof = false;
return true;
>
200 ГЛАВА 4 Терминальный ввод-вывод
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Возвращаемое функцией значение говорит о том, произошла ли ошибка, поэтому
в качестве признака конца файла используется третий аргумент:
ec_false( getln(s, sizeof(s), idseof) )
if (iseof)
printf("EOF\n");
else
prlntf("Read: Xs\n", s);
При работе с терминалами функция getln весьма эффективна, потому что она
читает всю строку при помощи единственного системного вызова. Более того, она
не ищет конец строки сама, а просто использует для этого индекс, возвращенный
вызовом read. Но для чтения файлов или каналов эта функция не подходит, потому
что из-за отсутствия лимита, предполагающего чтение одной строки, вызов read будет,
как правило, читать слишком много данных. Вместо строки функция getln
прочитает следующие шах - 1 символов (конечно, если символов много).
Более универсальная версия getln должна игнорировать это уникальное свойство
терминалов и читать не более одной строки. Вот версия, которая просто изучает
каждый символ в поисках символа перевода строки:
bool getln2(char «s, ssize_t max, bool «iseof)
{
ssize_t n;
char c;
n = 0;
while (true)
switch (read(STDIN_FlLENO, &c, 1)) {
case -1:
EC_FAIL
case 0:
s[n] = '\0';
«iseof = true;
return true;
default:
if (c == '\n') {
s[n] = '\0';
«iseof = false;
return true;
}
if (n >= max - 1) {
errno = E2BIG;
ГЛАВА 4 Терминальный ввод-вывод 201
EC_FAIL
}
s[n++] = с;
}
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
)
Эта версия рассматривает введенную где бы то ни было комбинацию Ctrl-d как
признак EOF, что отличается от поведения функции getln (которая считала
признаком конца файла только комбинацию Ctrl-d, введенную в начале строки).
Помните, что комбинация Ctrl-d относится только к терминалам; в других случаях
признак EOF — это просто конец файла или канала без открытого дескриптора
записи файла.
Хотя функция getln2 читает терминалы, файлы и каналы правильно, она делает это
медленнее, чем могла бы, потому что она не выполняет буферизацию ввода так, как
было описано в разделе 2.12. Это легко исправить, сделав так, чтобы getln2
вызывала функцию Bgetc, входящую в пакет BUFIO, представленный в том же разделе 2.12.
Для открытия специальных файлов терминалов (например /dev/tty) вполне
подходит функция Всреп. Однако, чтобы мы могли вызывать Bgetc для стандартного ввода,
нам нужна дополнительная функция Bfdcpen (см. упражнение 4.2),
инициализирующая указатель BUFIO на основе уже открытого дескриптора файла, а не пути. Все
это позволило бы нам читать символы из стандартного ввода, будь то терминал,
файл или канал, подобным образом:
ec_null( stin = Bfdopen(STDIN_FILENO, "r") )
while ((с = Bgetc(stin)) != -1)
/* обработайте символ •/
Теперь мы всегда читаем данные с максимальной скоростью: по одному блоку за
раз в случае файлов и каналов и по одной строке в случае терминалов.
Наша реализация пакета BUFIO не позволяет использовать один и тот же указатель
BUFIO для ввода и вывода, так что если вывод нужно отправить на терминал,
необходимо открыть второй BUFIO с использованием дескриптора файла STDOUT.FUENO
(определен как 1).
Стандартная библиотека ввода-вывода UNIX предоставляет для доступа к терминалу
три предопределенных и уже открытых указателя типа FILE: stdin, stdout и stderr,
поэтому функцию fdcpen, которая похожа на нашу Bfdcpen, вызывать для
терминала обычно не нужно.
Выводить данные на терминал легче, чем вводить их, так как при этом не нужно
обрабатывать символы erase/kill и т. д. Сколько бы символов мы ни выводили при
помощи вызова write, они немедленно помещаются в очередь символов, отправля-
202 ГЛАВА 4 Терминальный ввод-вывод
емых терминалу, независимо от того, присутствует среди них символ перевода
строки или нет.
При закрытии дескриптора файла, открытого для терминала, вызов close не
делает ничего такого, что бы он не делал при закрытии файла. Он просто
обеспечивает возможность повторного использования дескриптора файла, однако
дескриптор файла чаще всего имеет значение 0, 1 или 2, поэтому придумать вариант его
повторного использования непросто. Таким образом, в конце программы никто эти
дескрипторы не закрывает."
4.2.2 Неблокируемый ввод
Как уже было сказано, если при вызове read для чтения данных с терминала строка
недоступна, вызов read ожидает данные. Так как процесс в это время ничего делать
не может, эта ситуация называется блокировкой. При работе с файлами такого не
бывает: или данные доступны, или достигнут символ конца файла. Позднее другой
процесс может записать в файл дополнительные данные, но это не имеет
значения: важно только расположение символа конца файла при выполнении вызова read.
Флаг 0_NONBL0CK, устанавливаемый при вызове среп или fcntl, делает вызов read не-
блокируемым. Если после этого при вызове read строка данных окажется
недоступной, read сразу же возвратит значение -1, а переменной errnc будет присвоено
значение EAGA1N."
Было бы неплохо включать и отключать блокировку по желанию, так что давайте
напишем функцию setblcck, выполняющую соответствующим образом вызов fcntl
(используемая в этой функции методика идентична той, которую я применил в
разделе 3-8.3 для установки флага 0_APPEND):
bccl setblcck(int fd, bccl block)
{
int flags;
ec_neg1( flags = fcntl(fd, F_GETFL) )
if (block)
flags &= "(LNONBLOCK;
else
flags |= 0_N0NBL0CK;
ec_neg1( fcntl(fd, F.SETFL, flags) )
return true;
EC_CLEANUP_BGN
return false;
' В главе 5, соединяя два процесса при помощи канала, мы будем их закрывать.
" В некоторых версиях UNIX есть похожий флаг 0_NDELAY. Если он установлен и данные
недоступны, вызов read возвращает 0, что нельзя отличить от возвращаемого признака конца
файла. Таким образом, лучше использовать O.NONBLOCK.
ГЛАВА 4 Терминальный ввод-вывод 203
EC_CLEANUP_END
}
Ниже приведен код, тестирующий функцию setblook. Он отключает блокировку, после
чего читает строки в цикле. Если данных нет, код делает пятисекундную паузу и
снова пытается прочитать данные. Для вывода времени, прошедшего с начала
цикла, я использую системный вызов time, представленный в разделе 1.7.1.
statio void test_setblook(void)
{
char s[100];
ssize_t n;
time_t tstart, tnow;
eo_neg1( tstart = tirae(NULL) )
ec_false( setblook(STDIN_FILENO, false) )
while (true) {
ec_neg1( tnow = time(NULL) )
printf("Waiting for input (X.Of sec.) ...\n",
difftime(tnow, tstart));
switoh(n = read(STDIN_FUENO, s, sizeof(s) - 1)) {
oase 0:
printf("E0F\n");
break;
oase -1:
if (errno == EAGAIN) {
sleep(5);
oontinue;
}
EC_FAIL
default;
if (s[n - 1] = '\n')
n-;
stn] = '\0';
printf("Read \"Xs\"\n", s);
oontinue;
}
break;
}
return;
EC_CLEANUP_BGN
EC_FLUSH("test_setblock")
EC_CLEANUP_END
}
Пример результата, получаемого при выполнении этого кода, приведен ниже.
Я немного подождал, напечатал «hello», еще подождал и ввел Ctrl-d
204 ГЛАВА 4 Терминальный ввод-вывод
Waiting for input (0 seo.) ...
Waiting for input (5 seo.) ...
Waiting for input (10 seo.) ...
hello
Waiting for input (15 seo.) ...
Read "hello"
Waiting for input (15 seo.) ...
Waiting for input (20 seo.) ...
Waiting for input (25 sec.) ...
-DE0F
Пятисекундная пауза — это компромисс между слишком частым использованием
read, приводящим к расходованию ресурсов процессора, и слишком долгим
ожиданием, не позволяющим нормально обработать введенные пользователем данные.
По приведенным мной результатам видно, что между вводом «hello» и моментом,
когда программа наконец прочитала введенные данные и воспроизвела их,
прошло несколько секунд. Таким образом, отключение блокировки и получение
вводимых данных в цикле read/sleep — это, вообще говоря, неудачная идея, и мы
можем поступить гораздо лучше. Как? Расскажу в следующем разделе.
Как я уже сказал, вы также можете установить флаг O.NONBLOCK при открытии
специального файла терминала с помощью вызова open. В этом случае флаг 0_N0NBL0CK
влияет и на read, и на open: если соединения нет, вызов open сразу же возвращает
управление.
Одним из способов применения неблокируемого ввода является мониторинг
нескольких терминалов. Терминалами могут быть лабораторные устройства,
подключенные к системе UNIX посредством портов терминалов. Допустим, что время от
времени эти устройства отправляют системе символы, которые мы хотим
накапливать по мере поступления с сохранением их порядка. Так как мы не можем
узнать, когда конкретный терминал отправит символ, использовать блокируемый ввод-
вывод нельзя, в противном случае может возникнуть ситуация, когда мы будем
ожидать терминал, которому нечего сказать, тогда как другие терминалы,
требующие внимания, в это время будут игнорироваться. Однако при использовании
неблокируемого ввода-вывода мы можем опрашивать каждый терминал по очереди;
если терминал не готов, вызов read возвратит -1 (и присвоит еггпо значение EAGAIN),
и мы просто обратимся к другому терминалу. Если при опросе всех терминалов
готовые данные обнаружены не будут, мы перед повторением цикла можем сделать
паузу на одну секунду, чтобы не перегружать процессор.
Этот алгоритм реализован в функции readany. Первыми ее аргументами являются
массив fds, содержащий дескрипторы файлов, и число nfds дескрипторов файлов,
хранящихся в массиве. Эта функция не возвращает управление, пока при чтении
одного из этих дескрипторов файлов не будет возвращен символ. Посредством
третьего аргумента (whichp) она возвращает индекс дескриптора файла, из
которого был прочитан символ. Сам символ возвращается обычным для функций
образом; значение 0 соответствует концу файла, а -1 свидетельствует об ошибке. Код,
ГЛАВА 4 Терминальный ввод-вывод 205
вызывающий readany, вероятно, накапливал бы входящие данные для дальнейшей
обработки.*
int readany(int fds[], int nfds, int *whichp)
{
int i;
unsigned char c;
for (i = 0; i < nfds; i++)
setblock(fds[i], false); /* вызывать setblcck каждый раз неэффективно */
i = 0;
while (true) {
if (i >= nfds) {
sleep(1);
i = 0;
}
с = 0; /* признак кснца файла */
if (read(fds[i], &c, 1) == -1) {
if (errnc == EAGAIN) {
i++;
continue;
}
EC_FAIL
}
«whichp = i;
return c;
}
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_END
}
Комментарий по поводу неэффективности вызова setblcck подразумевает, что на
самом деле мы не обязаны вызывать setblcck при каждом вызове readany. Есть
более эффективный, хотя и менее модульный подход: возложите эту ответственность
на вызывающий код.
Ниже приведен код функции, тестирующей readany. Она открывает два терминала:
/dev/tty — это, как я объяснил в разделе 4.2.1, управляющий терминал
(приложение telnet, выполняемое на другом компьютере), а /dev/pts/З — это окно xterm на
экране дисплея, подключенного непосредственно к компьютеру, который
работает под управлением SuSE Linux (я узнал имя /dev/pts/З при помощи команды tty).
' Представьте, что лабораторные устройства вводят строки, заканчивающиеся символом
перевода строки. В разделе 4.59 я расскажу, как читать данные, не дожидаясь готовности
целой строки.
206 ГЛАВА 4 Терминальный ввод-вывод
static void readany_test(vcid)
{
int fds[2] = {-1, -1}, which;
int c;
bccl ck = false;
ec_neg1( fds[0] = cpen("/dev/tty", O.RDWR) )
ec_neg1( fds[1] = cpen("/dev/pts/3", O.RDWR) )
while ((c = readany(fds, 2, iwhich)) > 0)
printf("Gct Xc from terminal Xd\n", isprint(c) ? с : '?', which);
ec_neg1( с )
ck = true;
EC.CLEANUP
EC_CLEANUP_BGN
if (fds[0] != -1)
(vcid)clcse(fds[0]);
if (fds[1] l= -1)
(vcid)clcse(fds[1]);
if (Ick)
EC_FLUSH("readany_test1")
EC_CLEANUP_END
}
Вот пример результатов, выведенных на управляющий терминал, куда записывала
данные функция printf:
$ readany_test
dcg
Get d from terminal 0
Get с from terminal 0
Get g from terminal 0
Get ? from terminal 0
Got с from terminal 1
Got с from terminal 1
Got w from terminal 1
Got ? from terminal 1
Этот вывод стал результатом следующих действий. Сначала я напечатал на
управляющем терминале слово «dog», после чего нажал клавишу ввода. Это привело к
повторению «dog» и выводу четырех следующих строк (вопросительный знак
соответствует символу перевода строки). Затем я подошел к компьютеру Linux,
напечатал «cow», нажал клавишу ввода и увидел вот что:
cow: command net fcund
На состоянии управляющего терминала это никак не отразилось. Проблема
заключалась в том, что я выполнял командную оболочку в окне xterm, и она была забло-
ГЛАВА 4 Терминальный ввод-вывод 207
■сирована в вызове read в ожидании ввода данных. Это нормально для покинутого
терминала. Два моих процесса читали один и тот же терминал, при этом оболочка
"олучила буквы первой и, конечно же, интерпретировала их как команду. Я решил
гделать еще одну попытку:
S sleep 10000
Команда sleep, выполняемая на переднем плане, перевела командную оболочку в
режим ожидания, и в этот раз введенные мной данные были получены командой
-eadany.test, которая напечатала четыре последних строки. Это показывает, что
драйвером терминала не владеет никакой конкретный процесс; то, что один
процесс, работающий с открытым терминалом, не читает данные, не означает, что
другой процесс не может этого делать.
Еще один комментарий по поводу тестовой программы.- получение символа EOF
эт любого терминала приводит к завершению программы, но ничто не мешает вам
реализовать свои приложения иначе.
4.2.3 Системный вызов select
Выполнение вызова sleep с целью ожидания имеет два недостатка: во-первых, как я
\же говорил, задержка при обработке входящего символа может достигать целой
секунды, что в некоторых приложениях неприемлемо. Во-вторых, до появления
готового символа мы можем выполнить далеко не один бесполезный цикл опроса.
Идеальным вариантом было бы наличие системного вызова, говорящего системе:
«Переведи меня в сон, пока какой-либо дескриптор файла не предложит мне символ,
готовый к обработке». Если бы у нас был такой вызов, нам даже не нужно было бы делать
вызовы read неблокируемыми, так как read не блокируется, если данные готовы.
На самом деле такой вызов есть, и он называется select (скоро мы доберемся до
похожего вызова pselect):
select — ожидает готовности ввода-вывода
•include <sys/select.h>
int select(
int nfds, /* максимальный нсмер дескриптора файла + 1 */
fd_set *readset, /* набор, служащий для сжидания чтения, или NULL */
fd_set *writeset, /* набор, служаицй для сжидания записи, или NULL */
fd_set *errcrset, /* набор, служащий для сжидания ошибок, или NULL */
struct timeval *timecut /* тайм-аут (в микросекундах) или NULL */
);
/* Возвращает числе установленных битов или -1 в случае сшибки
(устанавливает еггпо) */
208 ГЛАВА 4 Терминальный ввод-вывод
pselect — ожидает готовности ввода-вывода
«include <sys/seiect.h>
int pseiect(
int nfds,
fd_set «readset,
fd_set «writeset,
fd_set «errcrset,
ccnst struct timespec
/• максимальный нсмер дескриптора файла +
/• набор, служащий для ожидания чтения, или
/• набер, служащий для ожидания записи, или
/• набер, служащий для ожидания сшибок, или
•timeout, /* тайм-аут (в наносекундах) или
ccnst sigset_t «sigmask /• маска сигналов •/
);
/* Возвращает числе установленных битсв или -1 в случае сшибки
(устанавливает errnc) */
1 */
NULL
NULL
NULL
NULL
*/
•/
*/
*/
В отличие от нашей функции readany, в которую мы передавали массив
дескрипторов файлов, при использовании select вы в одном из аргументов fd_set
устанавливаете бит' для каждого дескриптора файла, который хотите протестировать. Для
работы с наборами битов предназначены четыре макроса:
FD_ZERO — очищает весь
«inciude <sys/select.h>
vcid FD_ZER0(
fd_set «fdset
);
набор fdset
/• счищаемый набер fd_set •/
FD_SET — задает дескриптор файла в наборе fd_set
«inciude <sys/seiect.h>
vcid FD_SET(
int fd,
fd_set «fdset
);
/• задаваемый дескриптор файла •/
/* набор fd_set */
FD_CLR — очищает дескриптор файла в наборе fdset
«inciude <sys/seiect.h>
vcid FD_CLR(
int fd,
fd_set «fdset
);
/• счищаемый дескриптор файла •/
/• набор fd_set •/
" Дескрипторы могут быть представлены и иначе, но обычно они представляются битами.
Считайте, что я использую термин «бит» в этом контексте в качестве модели.
ГЛАВА 4 Терминальный ввод-вывод 209
FD_ISSET — тестирует дескриптор файла из набора fdset
«include <sys/select.h>
j int FD_ISSET(
j int fd, /* тестируемый дескриптор файла */
fd_set »fdset /• набор fd_set •/
>;
/. Возвращает 1, если дескриптор задан, и 0 в противней случае (информация
сб сшибках не возвращается) •/
В зависимости от того, что вы ожидаете (чтение, запись или ошибку) вы можете
использовать select без наборов или с 1, 2 или 3 наборами. Если все аргументы fd_set
имеют значение NULL, вызов select просто блокируется, пока не истечет заданное
время или пока он не будет прерван сигналом; эта возможность не особо полезна.
Вы инициализируете набор, используя макрос FO.ZERO, а после этого при помощи
макроса F0_SET устанавливаете бит для каждого дескриптора файла, который вас
интересует:
fd_set set;
FD_ZER0(&set);
FD_SET(fd1, &set);
FD_SET(fd2, &set);
Затем вы вызываете select, который блокируется (даже если задан флаг 0_N0NBL0CK),
пока один или несколько дескрипторов файлов не будут готовы к чтению или
записи или не сгенерируют ошибку. Вызов select изменяет переданные вами
наборы, устанавливая бит для каждого готового дескриптора файла, и вы должны
протестировать каждый дескриптор файла, чтобы узнать, задан ли он:
if (FD_ISSET(fd1, &set)) {
/• сделать чте-тс с fd1 в ссстветствии с разновидностью набора fd_set «/
}
Получить список дескрипторов файлов нельзя — вы должны обработать каждый
дескриптор по отдельности. Узнать, к чему готов дескриптор файла, можно по
разновидности тестируемого набора. Следовательно, даже если входные наборы
идентичны, вы не можете использовать один и тот же набор более чем для одного
аргумента. Вам нужны отдельные наборы, иначе вы не сможете получить результаты.
Бит устанавливается на выходе только в том случае, если он был установлен и на
входе. Это справедливо, даже если соответствующий дескриптор файла был готов.
Иначе говоря, вы получаете ответы только на те вопросы, которые задали.
В случае успеха select возвращает общее число битов, установленных во всех
наборах, которое полезно разве что для проверки числа дескрипторов файлов,
полученного вами при вызовах (возможно, в цикле) макроса FD_ISSET.
210 ГЛАВА 4 Терминальный ввод-вывод
Аргумент nfds — это не число битов, установленных на входе. Это длина наборов,
которые должны быть учтены вызовом select. Иначе говоря, в каждом входном
наборе все установленные биты должны находиться в диапазоне дескрипторов
файлов от 0 до nfds - 1 включительно. Для определения nfds вы можете подсчитать
максимальный номер дескриптора файла и добавить 1. Если вы не хотите делать
это, вы можете использовать константу FD.SETSIZE, представляющую максимально
возможное число дескрипторов файлов в наборе. Однако это число довольно
велико (скажем, 1024), поэтому обычно гораздо эффективнее предоставить вызову
select фактическое число.
Последним аргументом вызова select является длительность тайм-аута, представленная
структурой timeval, которую мы обсудили в разделе 1.7.1. Если за это время ничего
не происходит, select возвращает нулевое значение — ошибка при этом не
возникает, а биты не устанавливаются. Имейте в виду, что select может изменить эту
структуру, поэтому перед следующим вызовом select ее нужно инициализировать заново.
Если аргумент timeout имеет значение NULL, тайм-аута нет; это эквивалентно
бесконечному временному интервалу. Если он не равен NULL, но интервал времени
равен нулю, вызов select выполняет тесты и затем сразу же возвращает управление;
вы можете использовать этот вариант для реализации опроса вместо ожидания. Это
имеет смысл, если у вашего приложения есть другие дела: вы можете выполнять опрос
время от времени, пока приложение делает другую работу, а когда работа будет
завершена, вы можете выполнить блокировку (присвоив аргументу timeout
ненулевой интервал времени или значение NULL).
Используя select, программисты часто допускают три ошибки.
• Первому аргументу nfds присваивают максимальный номер дескриптора
файла, а не максимальный номер плюс 1.
• Иногда программист выполняет повторный вызов select с теми же наборами,
которые были изменены вызовом, не понимая, что при возврате установлены
только те биты, которые представляют готовые дескрипторы файлов. Перед
каждым вызовом вы должны инициализировать входные наборы заново.
• Не все разработчики знают, что понимается под «готовностью». Речь идет не о
готовности данных, а лишь о том, что вызовы read или write с неустановленным
флагом D.NDNBLDCK не заблокируются. Возврат нулевого значения (EOF),
ошибки или данных — вот все возможные варианты. Не имеет значения, установлен
ли для дескриптора файла флаг D.NDNBLDCK, а это означает, что вызов никогда не
заблокируется; вызов select считает дескриптор готовым только в том смысле,
что при неустановленном флаге D.NDNBLDCK не произойдет блокировка.
Итак, вот гораздо более удачная реализация функции readany:
int readany2(int fds[], int nfds, int *whichp)
{
fd_set set.read;
int i, maxfd = D;
unsigned char c;
ГЛАВА 4 Терминальный ввод-вывод 211
FD_ZERD(&set_read);
for (i = D; i < nfds; i++) {
FD_SET(fds[i], &set_read);
if (fds[i] > niaxfd)
raaxfd = fds[i];
}
ec_neg1( select(niaxfd + 1, &set_read, NULL, NULL, NULL) )
for (i = D; i < nfds; i++) {
if (FD_ISSET(fds[i], &set_read)) {
с = D; /* признак конца файла */
ec_neg1( read(fds[i], 4c, 1) )
«whichp = i;
return c;
}
}
/* здесь оказаться "невозможно" */
errno = 0;
EC_FAIL
£C_CLEANUP_BGN
return -1;
£C_CLEANUP_END
}
Эта версия очень эффективна. Вызовы read остаются блокируемыми, нет цикла
опроса и нет вызовов sleep. Все ожидание выполняется в select, а когда он
возвращает управление, мы можем прочитать символ и сразу же возвратить его.
Если есть вероятность того, что другой поток будет читать тот же дескриптор файла,
что и блокируемый вызов read, данные, о готовности которых сообщил вызов select,
могут исчезнуть до того, как код доберется до read, а это может привести к тому,
что read заблокируется навсегда. Чтобы исправить это, достаточно сделать вызов
read неблокируемым и переходить к select, если read возвращает -1 и присваивает
при этом errno значение EAGAIN.
При записи на терминал о блокировке волноваться почти не приходится. Даже если
вызов write блокируется из-за того, что буфер драйвера полон, буфер, вероятно, быстро
очистится, Проблема блокировки вызовов write чаще возникает при работе с
другими механизмами вывода, такими как каналы или сокеты. Я поговорю о них в
следующих главах. В частности, в разделе 8.1.3 приведен пример типичного
использования вызова select с сокетом, к которому подключаются несколько клиентов.
Если бит установлен в наборе, служащем для ожидания чтения, вы читаете; если
он установлен в наборе, служащем для ожидания записи, вы пишете. Что делать, если
бит установлен в наборе, служащем для ожидания ошибок? Это зависит от того, чему
соответствует открытый дескриптор файла. В случае сокетов «ошибку», возможно,
лучше называть «исключительной ситуацией», подразумевающей наличие срочных
данных (см. раздел 8.7). Что касается терминалов, то для них не определены ника-
212 ГЛАВА 4 Терминальный ввод-вывод
кие стандартные исключительные или ошибочные ситуации, хотя в той или иной
системе может быть реализовано что-то нестандартное, и тогда вам нужно
обратиться к документации.
Есть более замысловатый вариант вызова select, называемый pselect; различия между
ними заключаются в следующем:
• в случае pselect тайм-аут представляется структурой timespec, измеряется в
наносекундах и не изменяется;
• вызов pselect принимает шестой аргумент — маску сигналов (см. раздел 9.1.5).
Когда pselect возвращает управление, старая маска восстанавливается. По
причинам, которые будут рассмотрены в главе 9, вам следует использовать pselect
вместо select, если вы ожидаете, что вызов будет прерван сигналом (если
аргумент sigmask имеет значение NULL, вызовы select и pselect ведут себя одинаково
по отношению к сигналам).
Вызов pselect появился в SUS версии 3, поэтому он поддерживается далеко не
всеми системами.
4.2.4 Системный вызов poll
poll — ожидает готовности ввода-вывода
«Include <pcll.h>
int pcll(
struct pcllfd fdinfc[]
nfds_t nfds,
lnt timeout
v
/* информация с тестируемых дескрипторах файлов
/» числе элементов в
/* длительность тайм
1* Возвращает числе готовых дескрипторов файлов
(устанавливает еггпс) */
массиве
-аута (в
или -1 е
fdinfc */
миллисекундах)
случае сшибки
•/
•/
struct pollfd — структура,
struct pcllfd {
lnt fd;
shcrt events;
shcrt revents;
};
используемая вызовом poll
/* дескриптор файла
/* флаги событий (см.
*/
таблицу,
/* возвращаемые флаги ссбытт
приведенную ниже
i (см. таблицу)
) •/
•/
Вызов pell изначально был разработан для использования вместе с механизмом
ввода-вывода STREAMS, реализованным в AT&T System V (см. раздел 4.9), но, как и
select, его можно использовать с открытыми дескрипторами файлов любого типа.
Несмотря на его имя («опрос»), вызов pell, как и select, можно использовать или
для опроса, или для ожидания. Большинство систем поддерживают poll, но Darwin
6.6 является исключением.
В то время как select принимает битовые маски, вызову реп вы передаете
структуры pcllfd — по одной для каждого дескриптора файла, о котором хотите узнать.
Для каждого дескриптора файла вы устанавливаете флаги поля events, определяю-
ГЛАВА 4 Терминальный ввод-вывод 213
щие отслеживаемые события (например чтение, запись). Когда poll возвращает
управление, установленные биты поля revents каждой структуры говорят о том, какие
события произошли. Таким образом, в отличие от seleot, вызов poll не изменяет
полученные значения, и вы можете использовать их повторно.
Если вы хотите протестировать дескриптор файла, имеющий огромное значение,
вызов poll может оказаться более эффективным, чем seleot. Почему? Ну, допустим,
вам нужно протестировать дескриптор файла 1000. Вызов seleot должен был бы
протестировать все биты от 0 до 1000, тогда как для рои вы можете создать
массив, содержащий только один элемент. Другая проблема с вызовом seleot состоит
в том, что размеры наборов определяются в соответствии с потенциальным
числом дескрипторов файлов, а некоторые ядра допускают по-настоящему большие
значения. Битовые маски, содержащие тысячи битов, весьма неэффективны.
Аргумент timeout задается в миллисекундах, а не является структурой timeval или
timespeo, что отличает вызов poll от seleot и pseleot. Если timeout равен -1, вызов
poll блокируется, пока в контексте какого-то из указанных дескрипторов файлов
не произойдет хотя бы одно из запрошенных событий. Если он равен 0, вызов рои,
как seleot и pseleot, просто тестирует дескрипторы файлов и возвращает
управление. Если при этом он возвращает 0, это означает, что событие не произошло. Если
значение timeout положительно, poll блокируется на время, длительность
которого не превышает заданное значение (таким образом, как и в случае seleot и pselect,
0 и положительные значения на самом деле означают одно и то же).
В табл. 4.1 приведены флаги событий, поддерживаемые вызовом poll. Чтобы понять
их смысл, вам нужно знать, что вызов poll различает «нормальные» данные,
«приоритетные» данные и «высокоприоритетные» данные. Суть этих терминов зависит
от того, чему соответствует открытый дескриптор файла.
Таблица 4.1. Флаги событий, поддерживаемые системным вызовом poll
Флаг Смысл флага
'0LLRDN0RH Нормальные данные готовы к чтению
pollrdband Приоритетные данные готовы к чтению
'ollin' То же самое, что и pollrdnorm | pollrobano
'OLLPRI' Высокоприоритетные данные готовы к чтению
pollwrnorm Нормальные данные готовы к записи
pollout* То же самое, что и pollwrnorm
POLLWRBAND Приоритетные данные могут быть записаны
pollerr" Произошла ошибка ввода-вывода; устанавливается только в поле
revents (т. е. не на входе)
°0LLHUP* Устройство отсоединено (запись уже невозможна, но чтение может
быть осуществимым); устанавливается только в поле revents
pollnval* Неверный дескриптор файла; устанавливается только в поле revents
Самые распространенные флаги, используемые не с STREAMS; флаг pollpri обычно
используется только с сокетами.
214 ГЛАВА 4 Терминальный ввод-вывод
Если оставить в стороне необычные ситуации, вы можете считать флаги POLLIN |
P0LLPRI эквивалентом битов чтения, используемых вызовом select, a P0LL0UT |
POLLWRBAND эквивалентом битов записи. Имейте в виду, что при использовании pell
вам не нужно явно просить систему информировать вас об исключительных
ситуациях: три последних флага из таблицы устанавливаются при возникновении
соответствующих условий независимо от входных флагов.
Если у вас уже есть массив структур pcllfd и вы просто хотите отключить
проверку для какого-то дескриптора файла, можете присвоить его полю fd значение -1.
Вот версия функции readany (из раздела 4.2.2), реализованная с использованием pell
(в разделе 4.2.3 была приведена версия с вызовом select):
«define MAXFDS 100
int readany3(lnt fds[], int nfds, int *whichp)
{
struct pcllfd fdinfc[MAXFDS] = { { 0 } };
int i;
unsigned char c;
if (nfds > MAXFDS) {
errnc = E2BIG;
EC_FAIL
>
fcr (i = 0; i < nfds; i++) {
fdinfc[i].fd = fds[i];
fdinfc[i].events = POLLIN | POLLPRI;
>
ec_neg1( pcll(fdinfc, nfds, -1) )
fcr (i = 0; i < nfds; i++) {
if (fdinfc[i].revents & (POLLIN | POLLPRI)) {
с = 0; /* признак кенца файла •/
ec_neg1( read(fdinfc[i].fd, &c, 1) )
•whichp = i;
return c;
}
}
/* здесь сказаться "невезмежне" */
errnc = 0;
EC_FAIL
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_END
>
Комментарии по поводу функции readany3.
ГЛАВА 4 Терминальный ввод-вывод 215
• Структуры целесообразно инициализировать нулями на тот случай, когда в
конкретной системе определены дополнительные поля, о которых мы не знаем, и
именно это я делаю, объявляя массив fdinfo. Кроме того, если нам когда-нибудь
придется изучать структуру в отладчике, нули сделают дамп более понятным.
• Наша программа поддерживает только 100 дескрипторов файлов. Ее можно легко
улучшить, выделяя память для массива динамически.
• Строго говоря, при тестировании поля revents предполагается, что в качестве
флагов используются отдельные биты. В SUS это вроде бы не сказано, но в
случае вызова poll такое предположение вполне безопасно.
• Мы не проверяем флаги POUERR, P0UHUP и P0UNVAL, но делать это, пожалуй, следует.
• В своем приложении вы, наверное, не захотите формировать массив структур
pollfd для каждого вызова, так как релевантные дескрипторы файлов обычно
изменяются не так часто.
• Блокируемый вызов read может привести к проблемам, если другой поток
читает тот же дескриптор файла. В этой программе можно было бы использовать
решение, представленное в примере с вызовом seleot (см. раздел 4.2.3).
•
4.2.5 Проверка и чтение данных при наличии одного
источника ввода
3 разделе 4.2.2 мы мотивировали использование вызовов seleot и poll, приведя
пример с несколькими источниками вводимых данных. Однако иногда источник только
один, но вы хотите знать, готов ли символ, не считывая его, т. е. вы хотите
отделить проверку от чтения. Вы можете использовать для этого seleot или poll, задав
нулевой тайм-аут. Но если есть только один источник ввода, это можно с
легкостью реализовать при помощи только лишь вызова read с установленным флагом
3_N0NBL0CK. Мы создадим для этого две функции: oready, определяющую готовность
символа, и oget, читающую символ.
Проблема с oready в том, что если символ готов, его прочитает вызов read, но мы
хотели, чтобы символ был прочитан функцией oget. Решить эту проблему легко:
достаточно ее/„ранить преждевременно прочитанный символ в буфере. Далее
функция oget может проверить содержимое буфера. Если символ там, мы возвращаем
его, если нет, мы включаем блокировку и вызываем read. Эта схема используется в
следующих реализациях cready и cget:
«define EMPTY '\0'
statio unsigned ohar obuf = EMPTY;
typedef enum {CR.READY, CR_NOTREADY, CR.EOF} CR.STATUS;
bool oready(CR_STATUS *statusp)
{
if (cbuf != EMPTY) {
•statusp = CR.READY;
return true;
S Зак 4030
216 ГЛАВА 4 Терминальный ввод-вывод
}
setblook(STDlN_FlLENO, false);
switoh (read(STDlN_FlLENO, &obuf, 1)) {
oase -1:
if (errno = EAGA1N) {
*statusp = 0R_N0TREADY;
return true;
}
EO.FAIL
oase 0;
•statusp = 0R_EOF;
return true;
oase 1:
return true;
default: /* "невозможный" олучай */
errno = 0;
EO.FAIL
}
E0_0LEANUP_BGN
return false;
E0_0LEANUP_END
}
bool oget(0R_STATUS *statusp, int *op)
{
if (obuf != EMPTY) {
*op = obuf;
obuf = EMPTY;
•Statusp = OR.READY;
return true;
}
setblook(0, true);
switoh (read(STDlN_FlLENO, op, 1)) {
oase -1:
E0.FA1L
oase 0:
*op = 0;
*statusp = 0R_E0F;
return true;
oase 1:
*statusp = 0R_READY;
return true;
default: /* "невозможный" случай */
errno = 0;
EO.FAIL.
}
ГЛАВА 4 Терминальный ввод-вывод 217
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
Обе функции возвращают true при успехе и false в случае ошибки. Аргумент statusp
представляет результат работы cready и может иметь значения CR.READY, CR.NOTREADY
и CR_E0F. Функция cget может присвоить этому аргументу только значения CR.READY
И CR_E0F.
Мы приняли байт NUL за индикатор пустого буфера (EMPTY), а это означает, что байты
NUL, читаемые функцией cready, будут игнорироваться. Если это неприемлемо, в
качестве флага пустого буфера можно использовать отдельную булеву переменную.
Обратите внимание на то, как cready и cget переключаются между блокируемым и
неблокируемым режимами ввода. Так как мы читаем только один поток вводимых
данных, нам не нужно оставаться в неблокируемом состоянии при проверке
доступности символа, что нужно было делать в первой неэффективной версии
функции readany в разделе 4.2.2. Мы возвращаемся в состояние блокировки и
вызываем read. Как-никак, ожидание символа — это и есть суть блокировки.
Функции cready и cget наиболее полезны тогда, когда у программы есть какая-то
работа, которую можно отложить на потом, если символ готов. В качестве
примера можно привести программу, использующую пакет Curses (см. раздел 4.8). Curses
работает так, что весь вывод на экран содержится в буфере, пока не будет вызвана
функция refresh, отправляющая вывод на физический экран. Это требует времени,
так как refresh для минимизации числа передаваемых символов сравнивает то, что
в настоящее время выведено на экран, с новым образом экрана. При быстром
наборе текста частота обновлений может оказаться недостаточной, особенно если
экран должен обновляться при каждом нажатии клавиши, что требуется при
работе с текстовыми редакторами. Элегантным решением этой проблемы является
вызов refresh из функции ввода данных, но только если нет введенных данных,
ожидающих обработки. Если они есть, это означает, что пользователь вырвался
вперед, не дожидаясь обновления экрана, поэтому обновление экрана пропускается.
Экран будет обновлен при следующем выполнении функции ввода данных, если
при этом уже не будет символов, ожидающих обработки. Когда пользователь
прекратит печатать, экран станет текущим. С другой стороны, если программа может
обрабатывать символы быстрее, чем их вводит пользователь, экран будет
обновляться при каждом нажатии клавиши.
Все это может показаться сложным, но благодаря функциям, которые мы уже
создали, для реализации этого механизма нужно написать всего лишь несколько строк:
ec_false( cready(istatus) )
if (status == CR_NOTREADY)
refreshQ;
ec_false( cget(4status, &c) )
218 ГЛАВА 4 Терминальный ввод-вывод
4.3 Сеансы и группы процессов (задания)
В этом разделе обсуждаются сеансы и группы процессов (также называемые
заданиями), которые используются преимущественно командными оболочками.
4.3.1 Терминология
Когда пользователь входит в систему, создается новый сеанс, состоящий из новой
группы процессов; эта группа включает один процесс, выполняющий командную
оболочку входа в систему. Этот процесс называется лидером группы процессов, а
его идентификатор (например 73056) — идентификатором группы процессов. Этот
же процесс является еще и лидером сеанса, а его идентификатор —
идентификатором сеанса.' Терминал, при помощи которого пользователь вошел в систему,
становится управляющим терминалом сеанса. Лидер сеанса является также
управляющим процессом. Все это показано в правой части рисунка 4.1, где группа
процессов обозначена FG — это сокращение слова «foreground» (передний план).
* лидер группы процессов t лидер сеанса
Рис. 4.1. Сеанс, группы процессов, процессы и управляющий терминал
Если командной оболочкой включено управление заданиями, после чего в
фоновом режиме выполняется команда или конвейер команд, например:
$ du -a | grep tmp >out.trap&
то формируется новая группа процессов, на этот раз содержащая два процесса. Это
изображено в левой части рисунка 4.1, где группа процессов обозначена BG — от
«background» (фон). Один из двух новых процессов является лидером этой новой
группы процессов.
Обе группы процессов (73056 и 73381) относятся к одному сеансу и имеют один
управляющий терминал. При управлении заданиями новая группа процессов со-
В некоторых стандартах используется фраза «идентификатор группы процессов,
принадлежащий лидеру сеанса (process-group ID of the session leader)», но я предпочитаю
называть этот идентификатор просто идентификатором сеанса.
ГЛАВА 4 Терминальный ввод-вывод
219
сдается для каждой командной строки, и каждая такая группа процессов также
называется заданием.
То. что группа процессов работает в фоновом режиме, не означает, что ее
стандартные дескрипторы файлов не связаны с терминалом. В предыдущем примере
стандартный вывод команды du соединен посредством конвейера со стандартным
вводом grep, а стандартный вывод grep перенаправлен в файл, но это не связано с тем,
что процессы выполняются в фоновом режиме — просто так захотел пользователь.
Если при управлении заданиями с управляющего терминала поступают
определенные сигналы, такие как сигналы прерывания (SIGINT), выхода (SIGQUIT) или
приостановки (SIGTSTP), они отправляются только группе процессов переднего плана, но не
группам фоновых процессов. Под «управлением» понимается то, что пользователь
может выполнять команды оболочки для перевода групп процессов (заданий) с
переднего плана в фоновый режим и наоборот. Как правило, если группа процессов
работает на переднем плане, при нажатии комбинации Ctrl-z ей отправляется
сигнал SIGTSTP, который заставляет оболочку перевести эту группу в фоновый режим и
вывести одну из групп фоновых процессов на передний план. С другой стороны,
команда f g может перевести специфическую группу фоновых процессов на передний
план, а группа процессов переднего плана при этом переводится в фоновый режим.
Если управление заданиями не включено, два новых процесса в примере с
конвейером (du и grep) выполняются не в собственной группе процессов, а в группе
процессов 73056 вместе с первым процессом — командной оболочкой (см. рис. 4.2). Они
также выполняются в фоновом режиме, но это означает лишь то, что командная
оболочка не ожидает их завершения. Так как имеется только одна группа
процессов, перевод групп процессов с переднего плана в фоновый режим и наоборот
невозможен, а любые сигналы, поступившие с управляющего терминала,
отправляются всем процессам, входящим в единственную группу процессов сеанса. При
нажатии на терминале комбинации Ctrl-c сигнал SIGINT будет отправлен всем трем
процессам, в том числе двум фоновым, и если эти процессы не перехватывают и не
игнорируют сигнал, он приведет их к завершению, что в случае этого конкретного
сигнала выполняется по умолчанию (сигналы подробно обсуждаются в главе 9)-
Управляющий
терминал
" лидер группы процессов t лидер сеанса
Рис. 4.2. Управление заданиями не выполняется; есть только одна группа
процессов на сеанс
220 ГЛАВА 4 Терминальный ввод-вывод
Независимо от того, включено управление заданиями или нет, при зависании или
отсоединении управляющего терминала управляющему процессу отправляется
сигнал SIGHUP, который по умолчанию приводит к завершению процесса. При
типичном сеансе результатом этого является завершение командной оболочки и выход
пользователя из системы. Управляющий терминал больше не является
управляющим терминалом сеанса, который, таким образом, больше не имеет управляющего
терминала. Смогут ли другие процессы сеанса обращаться к терминалу? Ни в
каких стандартах это не определено, но большинство систем, назерное, будут
предотвращать это, возвращая, например, из вызовов read и write ошибку.
Однако, как только управляющий процесс завершается по какой угодно причине (не
только получив сигнал SIGHUP), каждый (или единственный) процесс из группы
процессов переднего плана получает сигнал SIGHUP, который по умолчанию вызывает его
завершение. Если процессы не перехватывают или не игнорируют этот сигнал, они
никогда не смогут получить доступ к прежнему управляющему терминалу.
Что происходит, если к управляющему терминалу пытается обратиться процесс,
входящий в группу фоновых процессов? Независимо от того, выполняется ли
управляющий процесс, имеет место следующее базовое правило: при любой
попытке фонового процесса прочитать управляющий терминал ему отправляется сигнал
SIGTTIN, а любая попытка выполнить запись приводит к генерированию сигнала
SIGTT0U. Здесь под «записью» также понимается использование стандартных
системных вызовов управления терминалами tcsetattr, tcdrain, tcf low, tcf lush и tcsendbreak
(см. разделы 4.5 и 4.6).
По умолчанию эти сигналы приостанавливают процесс, что очень разумно: фоновые
процессы, которым нужен доступ к управляющему терминалу, приостанавливаются, а
когда вы готовы с ними взаимодействовать, вы переводите их на передний план.
Исключения из указанного базового правила таковы.
• Если фоновый процесс пытается выполнить чтение, когда сигналы SIGTTIN
игнорируются или блокируются, вызов read возвращает ему ошибку ЕЮ.
• «Осиротевшему» фоновому процессу (лидер группы процессов завершил
работу) при вызове read также возвращается ошибка ЕЮ.
• Если атрибут терминала T0ST0P (см. раздел 4.5.6) не установлен, процессу
дозволяется выполнять запись.
• Если атрибут T0ST0P установлен, но сигналы SIGTTOU игнорируются или
блокируются, процессу дозволяется выполнять запись.
• Если атрибут T0ST0P установлен, осиротевшему фоновому процессу при вызове
write возвращается ошибка ЕЮ.
Итак, обобщим правила: фоновый процесс не может читать управляющий
терминал вообще; он получает ошибку или останавливается. Осиротевший фоновый
процесс при попытке записи на терминал получает ошибку. Фоновые процессы,
имеющие родителей, могут выполнять запись, если не установлен атрибут T0ST0P
или если игнорируются или блокируются сигналы SIGTT0U; в противном случае эти
процессы останавливаются.
ГЛАВА 4 Терминальный ввод-вывод 221
Несоответствия между чтением и записью объясняются тем, что символы никогда не
должны считываться сразу двумя процессами — это слишком опасно (представьте себе,
что будет, если вы введете rm *.о, а командная оболочка получит только гш *). Если
два процесса одновременно выполняют запись, это может лишь запутать, но не более
того. Если пользователь этого и добивается, что ж, да будет так.
4.3.2 Системные вызовы, служащие для работы с сеансами
setsid — создает сеанс и группу процессов
ffinoiude <unistd.h>
pid_t setsid(void);
/« Возвращает идентификатор группы процеооов или -1 в олучае ошибки
(устанавливает еггпо) */
getsid — получает идентификатор сеанса
«inolude <unistd.h>
pid_t getsid(
pid_t pid /* ID процеооа или 0 (ооответотвует вызывающему процеооу) */
);
/. возвращает идентификатор оеаноа или -1 при ошибке (устанавливает еггпо)
Я сказал, что каждая оболочка входа в систему запускается в новом сеансе, но откуда
берутся эти сеансы? На самом деле любой процесс, который еще не является
лидером сеанса, может вызвать setsid, чтобы стать лидером нового сеанса и
единственной группы процессов в этом сеансе. Важно сказать, что новый сеанс не имеет
управляющего терминала. Это прекрасно подходит для демонов, не нуждающихся в
управляющем терминале, а также для сеансов, которым нужен другой управляющий
терминал — не тот, что был задан во время входа в систему. Первое терминальное
устройство, открытое новым сеансом, становится его управляющим терминалом.
4.3.3 Системные вызовы, служащие для работы
с группами процессов
setpgid — задает или создает группу процессов
«inolude <unistd
int setpgid(
pid_t pid,
pid_t pgid
);
/. Возвращает О
.h>
при
/« ID процеооа или 0 (ооответотвует вызывающему
процеооу) */
/* идентификатор
успехе или -1 в
группы процеооов */
случае ошибки (устанавливает
еггпо) */
222 ГЛАВА 4 Терминальный ввод-вывод
getpgid — получает идентификатор группы процессов
«include <unistd.h>
pid_t getpgid(
pid_t pid /* ID процесса или 0 (соответствует вызывающему процессу) */
);
/• Возвращает идентификатор группы процессов или -1 в случае ошибки
(устанавливает еггпо) */
При помощи вызова setpgid процесс, не являющийся лидером группы процессов,
можно сделать лидером новой группы процессов. Это происходит, если два
аргумента равны и не существует группы процессов с указанным идентификатором. Или
же, если вторым аргументом является идентификатор существующей группы
процессов, изменяется группа процессов, определяемая аргументом pid. Однако при
этом действуют некоторые ограничения.
• Аргументом pid должен быть идентификатор вызывающего процесса или его
дочернего процесса, не выполнявшего системный вызов «exec» (который
обсуждается в разделе 53).
• pid должен относиться к тому же сеансу, что и вызывающий процесс.
• Если pgid существует, он должен относиться к тому же сеансу, что и
вызывающий процесс.
На практике эти ограничения не особо существенны. Обычно командная
оболочка создает дочерние процессы для каждой команды конвейера, выбирает один
процесс в качестве лидера группы процессов, создает новую группу процессов,
вызывая setpgid, после чего помещает другие команды в эту группу при помощи
дополнительных вызовов setpgid. Как только все это сделано, состав группы
процессов не изменяется, хотя теоретически это возможно.
Есть еще два вызова, позволяющих задать и получить идентификатор группы
процессов — setpgrp и getpgrp — но они менее функциональны, чем setpgid и getpgid,
и устарели.
4.3.4 Системные вызовы, работающие с управляющими
терминалами
tcsetpgrp — задает
dinciude <unistd
int tcsetpgrp(
int fd,
pid_t pgid
);
/* Возвращает О
идентификатор группы
h>
ipn
/•
/*
успехе
дескриптор файла
процессов переднего
•/
идентификатор группы процессов •/
или -1 в случае
ошибки (устанавливает
плана
еггпо)
•/
ГЛАВА 4 Терминальный ввод-вывод
223
tcgetpgrp — получает идентификатор группы процессов переднего плана
«include <unistd.h>
pid_t tcgetpgrp(
! int fd /» дескриптор файла */
I );
| /* Возвращает идентификатор группы процессов или -1 в случае сшибки
I (устанавливает еггпс) */
1
tcgetsid — получает идентификатор сеанса
«include <termics.h>
i
i
pid_t tcgetsid(
| int fd /* дескриптор файла */
! ):
J /* Возвращает идентификтср сеанса или -1 в случае ошибки (устанавливает
еггпс) »/
i
Системный вызов tcsetpgrp переводит группу процессов на передний план; это
означает, что она будет получать сигналы, поступающие с управляющего
терминала. Что работало на переднем плане, переводится в фоновый режим. Группа
процессов должна относиться к тому же сеансу, что и вызывающий процесс.
Команда fg использует вызов tcsetpgrp для перевода указанного процесса на
передний план. При вводе комбинации Ctrl-z процесс переднего плана не переводится
в фоновый режим непосредственно; на самом деле при этом процессу
отправляется сигнал SIGTSTP, останавливающий процесс, а затем родитель процесса —
командная оболочка — получает значение, возвращаемое вызовом waitpid, которое
говорит ей, что произошло. После этого командная оболочка выполняет вызов
tcsetpgrp, переводя себя на передний план.
Вызов tcgetsid реализован только в системах SUS, так что FreeBSD его не
поддерживает. Однако вы можете получить идентификатор сеанса по дескриптору
файла, открытому для управляющего терминала, выполнив сначала вызов tcgetpgrp для
получения идентификатора группы процессов переднего плана, а так как он
является идентификатором процесса, относящегося к интересующему нас сеансу,
далее вы можете выполнить для получения идентификатора сеанса вызов getsid.
4.3.5 Использование системных вызовов, служащих
для работы с сеансами
Вот код функции, которая с помощью вышеописанных системных вызовов
выводит подробную информацию о сеансах и группах процессов:
«include <termios.h>
224 ГЛАВА 4 Терминальный ввод-вывод
static veld showpginfc(const char *msg)
{
int fd;
printf("Xs\n", msg);
printf("\tprccess ID = Xid; parent = Xid\n",
(icng)getpidO, (icng)getppidO);
printf("\tsessicn 10 = Xid; prccess-grcup ID = Xid\n",
(icng)getsid(D), (icng)getpgid(D));
ec.negK fd = cpen("/dev/tty", O.RDWR) )
printf("\tccntrciiing terminal's foreground prccess-grcup ID = Xid\n",
(icng)tcgetpgrp(fd));
#if _X0PEN_VERS10N >= 4
printf("\tccntrciiing-terininai's session ID = Xid\n",
(icng)tcgetsid(fd));
#eise
printf("\tccntrcliing-terminal's session ID = Xid\n",
(icng)getsid(tcgetpgrp(fd)));
#endif
ec_neg1( cicse(fd) )
return;
EC_CLEANUP_BGN
EC_FLUSH("shcwpginfc")
EC.CLEANUP.END
}
Мы хотим вызывать shcwpginfc при запуске процесса и при получении им сигнала
SI6CDNT, чтобы можно было узнать, что происходит, когда процесс выполняется в
фоновом режиме. Перехват сигналов мы подробно обсудим в главе 9, а пока вам
достаточно знать, что в результате инициализации структуры и вызова sigactlon
функция catchsig будет выполняться при поступлении сигнала SI6CDNT.' После
этого главная программа «засыпает». Если она пробуждается из-за поступления
сигнала, она снова возвращается в сон.
int main(void)
{
struct sigacticn act;
memset(&act, 0, sizecf(act));
act.sa.handier = catchsig;
ec_neg1( sigaction(SIGCONT, &act, NULL) )
shcwpginfc("initial call");
В разделе 9.1.7 я скажу, что есть принципы, ограничивающие диапазон системных
вызовов, которые можно использовать в обработчиках сигналов. Кое-что из того, что делает
showpginfo, технически некорректно, однако в данном примере это не играет особой роли,
к тому же мы еще не добрались до главы 9.
ГЛАВА 4 Терминальный ввод-вывод 225
while (true)
sleep(10000);
£C_CIEANUP_BGN
exlt(EXlT_FAllURE);
=C_CIEANUP_END
static veld catchsigdnt slgnc)
If (slgnc == S1GC0NT)
shcwpglnfc("gct S1GC0NT");
Результаты выполнения pglnfc весьма показательны:
i echc $$
5140
i pglnfc
initial call
prccess 10 = 6262; parent = 5140
sesslcn 10 = 5140; prccess-grcup 10 = 6262
controlling terminal's foreground prccess-grcup 10 = 6262
controlling-terminal's sesslcn 10 = 5140
"Z[1]+ Stepped pginfc
t bg X1
[1]+ pginfc &
get S1GC0NT
prccess 10 = 6262; parent = 5140
session ID = 5140; prccess-grcup 10 = 6262
controlling terminal's foreground prccess-grcup 10 = 5140
controlling-terminal's sesslcn ID = 5140
Прежде всего, мы видим, что идентификатор процесса командной оболочки равен
5140. Первоначально pginfc выполняется на переднем плане. Этот процесс
выполняется в собственной группе процессов с идентификатором б2б2, который равен
идентификатору процесса pginfc. Это означает, что данный процесс является
лидером группы. Идентификатор сеанса равен идентификатору процесса командной
оболочки — следовательно, pginfc относится к тому же сеансу, что и оболочка. Когда
я ввел Ctrl-z, оболочка получила статус своего дочернего процесса pginfc, про
остановку которого сказано в выводе, и перевела себя на передний план, позволив
нам ввести команду bg, которая перезапустила pginfc в фоновом режиме. В
результате процессу pginfc был отправлен сигнал SI6C0NT, который привел к выводу
информации во второй раз. Во многом она осталась прежней, только в этот раз группа
процессов переднего плана имела идентификатор 5140, такой же, как и у
командной оболочки.
226 ГЛАВА 4 Терминальный ввод-вывод
Большинство системных вызовов, относящихся к сеансам и группам процессов,
используются командными оболочками, а не другими приложениями. Однако один
из них — setsid — очень важен для приложений. Вы убедитесь в этом в разделе 4.10.1,
где мы используем этот вызов, чтобы сделать управляющим терминалом процесса
псевдотерминал.
4.4 Системный вызов ioctl
Напомню, что в UNIX есть два типа устройств: блочные и символьные. Блочные
устройства мы рассмотрели в главе 3, а один важный тип символьных устройств —
терминалы — мы обсуждаем в этой главе. Есть системный вызов общего назначения,
служащий для управления символьными устройствами всех типов — это вызов iootl:
ioctl — позволяет
#inolude <...>
int iootl(
int fd,
int req,
);
/* Возвращает -
управлять
1 при
символьным устройством
/* дескриптор файла */
/* запрос •/
/• аргументы, зависящие от запроса •/
ошибке (устанавливает еггпс) и некоторое другое
значение в случае успеха *
/
В стандартах POSIX и SUS не определены устройства, поэтому, если не считать два
исключения, включаемый файл, различные запросы и варианты третьего
аргумента вызова icctl зависят от версии системы, а подробности обычно можно найти в
документации к драйверу, которым вы хотите управлять.
Два исключения таковы.
• Почти каждому действию, которое вы можете выполнить с терминалом при
помощи ioctl, соответствует стандартизированная функция. Это сделано в первую
очередь для того, чтобы типы аргументов можно было проверять во время
компиляции. Эти функции называются tcgetattr, tcsetattr, ted rain, tcflow, tcflush и
tcsendbreak и описываются в следующих разделах.
• SUS определяет, как ioctl можно использовать вместе с пакетом STREAMS,
который я вкратце опишу в разделе 4.9-
Кроме раздела 4.9 вызов ioctl в этой книге больше нигде обсуждаться не будет.
4.5 Установка атрибутов терминала
Двумя важными системными вызовами, служащими для управления терминалами,
являются вызов tcgetattr, который получает текущие атрибуты, и tcsetattr,
задающий новые атрибуты. Четыре других функции — tcdrain, tcflow, tcflush и tcsendbreak
— объясняются в разделе 4.6.
ГЛАВА 4 Терминальный ввод-вывод 227
4.5.1 Основы использования вызовов tcgetattr и tcsetattr
tcgetattr — получает атрибуты терминала
«include <termics.h>
int tcgetattr(
int fd, /* дескриптор файла */
struct termics *tp /* атрибуты */
);
/* Возвращает 0 при успехе или -1 в случае сшибки (устанавливает еггпс) »/
tcsetattr — задает атрибуты терминала
«include <termios.h>
int tcsetattr(
int fd, /* дескриптор файла */
int actions, /* действия, выполняемые при задании
атрибутов */
const struct termics *tp /* атрибуты */
);
/* Возвращает 0 при успехе или -1 в случае сшибки (устанавливает еггпс) */
struct termios — структура, используемая функциями управления терминалом
struct termics {
tcflag_t c_iflag;
tcflag_t c_cflag;
tcflag_t c_cflag;
tcflagjt c_lflag;
cc_t c_cc[NCCS];
};
/* флаги ввсда «/
/* флаги вывода */
/* управляющие флаги */
/* лекальные флаги */
/* управляющие символы «/
Структура, характеризующая терминал (termios), содержит около 50 флаговых
битов, которые говорят драйверу, как обрабатывать входящие и исходящие символы,
как задавать параметры коммуникационной линии, такие как скорость передачи
данных в бодах и т. д. Кроме того, эта структура определяет несколько
управляющих символов, таких как erase (обычно это Del или Backspace), kill (обычно Ctrl-u)
и EOF (обычно Ctrl-d).
Перед изменением атрибутов вы должны выполнять с целью инициализации
структуры вызов tcgetattr. Сама структура сложна и может включать флаги,
зависящие от версии системы, поэтому инициализировать ее с нуля непрактично. После
получения структуры вы настраиваете флаги и другие поля, а затем вызываете
tosetatt, чтобы изменения возымели действие. Второй аргумент этого вызова — action
— определяет, как и когда выполняются изменения, при этом используется один
из следующих символов:
228 ГЛАВА 4 Терминальный ввод-вывод
Задает атрибуты терминала немедленно, в соответствии с
информацией, указанной в структуре.
Похож на tcsanow, но при использовании этого параметра изменения
вступают в силу только после отправления всех имеющихся выводимых
символов. Этот параметр следует использовать при выполнении
изменений, влияющих на вывод, чтобы гарантировать, что символы будут
обработаны по правилам, действовашим, когда они были записаны.
Похож на tcsadrain, но при использовании этого параметра кроме
ожидания вывода всех имеющихся символов очищается входная очередь
(символы отбрасываются). При запуске нового приложения,
работающего в интерактивном режиме — например экранного редактора —
этот параметр использовать безопаснее всего, потому что он
предотвращает проблемы, связанные со слишком быстрым вводом символов.
В следующих подразделах я опишу большинство часто используемых флагов и их
типичные комбинации (например простой режим). Полный список
стандартизированных флагов можно найти в [SUS2002]. Или, чтобы узнать флаги,
поддерживаемые вашей системой, вы можете выполнить команду i»an termios или man termio.
4.5.2 Размер символа и паритет
Флаги поля c.cflag представляют размер символа (CS7 — 7 битов, CS8 — 8 битов),
число стоп-битов (CST0PB — 2, неустановленный флаг— 1), признак проверки
паритета (PARENB, если паритет проверять нужно) и флаг паритета (PAR0DD —
нечетность, неустановленный флаг — четность). Никакого творчества здесь нет: как
правило, программист обычно остается вполне удовлетворен, найдя комбинацию,
которая работает.
Вы также можете получить более подробную информацию о символах, которые не
проходят проверку паритета. Если флаг PARMRK поля c.iflag установлен,
некорректные символы предваряются символами 0377 и 0. В противном случае символы с
некорректным паритетом вводятся как байты NUL Вы также можете установить флаг
10NPAR поля c_if lag, тогда символы с некорректным паритетом будут игнорироваться.
4.5.3 Быстродействие терминалов
Большинство современных терминалов работают достаточно быстро, но список
символов (не целых чисел), определяющих разные стандартные скорости, в том
числе очень низкие, все еще существует. Имена этих символов имеют форму Вп,
где П может равняться 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, 9600, 19200
или 38400. Единицей измерения быстродействия терминалов является число битов,
передаваемых в секунду, которое в документации и стандартах UNIX называется
«бод».
В некоторых версиях скорость задается в поле c_cf lag, но ради портируемости кода
вам следует использовать для получения и задания скоростей ввода и вывода
четыре стандартных функции, а не саму структуру termios. Эти функции только
манипулируют структурой, поэтому сначала вы должны заполнить ее, вызвав tcgetattr,
TCSANOW
TCSADRAIN
TCSAFLUSH
ГЛАВА 4 Терминальный ввод-вывод 229
а после этого вызвать tcsetettr, чтобы изменение скорости возымело действие.
Помните, что стандартизированными значениями speed.t являются только
символы В п. Код, задающий скорость при помощи целого числа, не будет портируемым,
хотя некоторые версии могут это допускать.
cfgetispeed — получает скорость ввода из структуры termios
«include <termics.h>
speed.t cfgetispeed(
const struct ternics *tp /* атрибуты */
);
/* Всзврвщает сксрссть (инфсрмвция сб сшибквх не возвращается)
*/
cfgetospeed — получает скорость вывода из структуры termios
«include <terules.h>
speed.t cfgetcspeed(
censt struct ternics *tp /* атрибуты */
);
/* Возвращает сксрссть (информация сб сшибках не возвращается)
Ч
cfsetispeed — задает скорость ввода в структуре termios
«Include <termics.h>
lnt cfsetlspeed(
struct ternics *tp,
speed.t speed
);
/* Возвращает О при успехе или
Л
Л
-1 в
атрибуты */
скорость */
случае ошибки (может устанавливать
еггпс)
»/
cfsetospeed — задает скорость вывода в структуре termios
«include <ternics.h>
lnt cfsetcspeed(
struct ternics *tp,
speed.t speed
);
/* Возвращает О при успехе или
Л
Л
-1 в
атрибуты */
сксрссть */
случае ошибки (может устанавливать
еггпо)
*/
Скорость ВО имеет особый смысл: если она указана в качестве скорости вывода, вызов
tcsetattr отсоединит терминал. Если она задана как скорость ввода, это означает,
что скорости вывода и ввода одинаковы.
230 ГЛАВА 4 Терминальный ввод-вывод
4.5.4 Соответствие символов
Отображение символов перевода строки на символы ввода (return) и наоборот при
вводе данных контролируется флагами INLCR и ICRNL поля c_of lag; обычно первый
очищен, а второй установлен. В случае терминалов, вводящих при нажатии
клавиши ввода и символ ввода, и символ перевода на новую строку, символ ввода может
быть проигнорирован (IGNCR).
При выводе мы обычно хотим преобразовывать символ перевода строки в пару
«символ ввода/символ перевода строки»; это делает флаг 0NLCR поля c_of lag. Другие
флаги преобразуют символ ввода в символ перевода строки (0CRNL) и блокируют
символ ввода в столбце 0 (0NOCR). Если символ перевода строки сопровождается
символом ввода, флаг 0NLRET позволяет сообщить об этом драйверу терминала, чтобы
он мог следить за столбцом (при использовании табуляции и нажатиях Backspace).
Драйвер также может работать с терминалами, поддерживающими только верхний
регистр, которые уже гораздо менее распространены, чем когда-то." Если в поле
c_lf lag установлен флаг XCASE, в поле c_if lag — флаг IUCLC, а в поле c_of lag — флаг
0LCUC, то при вводе данных буквы верхнего регистра отображаются на буквы
нижнего регистра, а при выводе наоборот. Это разумно, так как в UNIX нижний регистр
используется чаще, чем верхний. Для ввода или вывода буквы в верхнем регистре
перед ней нужно указать обратную косую черту (\).
Если в поле c_if lag установлен флаг ISTRIP, вводимые символы обрезаются до семи
битов (после проверки паритета). В противном случае вводятся все восемь битов.
Некоторые терминалы используют семибитный код ASCII, поэтому для них
обрезать символы обычно желательно. Однако современные устройства и программы,
такие как xterm, telnet и ssh, могут передавать все восемь битов. При выводе
терминалу отправляются все биты, записанные процессом. Если терминалу
отправляются восемь битов данных, генерирование паритета нужно отключить, очистив флаг
PARENB поля c_cflag.
4.5.5 Задержки и табуляция
Терминалы долго были механическими и не имели больших буферов, поэтому для
выполнения различных движений, таких как возврат каретки, им требовалось
время. В поле c_of lag можно установить флаги, определяющие задержки для символов
перевода строки, ввода, символов Backspace, горизонтальной и вертикальной
табуляции и перевода страницы, но едва ли они кому-нибудь еще понадобятся.
Другой флаг — ТАВЗ — заменяет выводимые символы табуляции соответствующим
числом пробелов. Это полезно, когда терминал не поддерживает символы
табуляции, когда настраивать табуляцию слишком трудно или когда терминалом на
самом деле является другой компьютер, которому в выводе нужны пробелы.
' Фраза «гораздо менее распространены» была и в издании 1985 года. Теперь такие
терминалы, наверное, можно увидеть только в музеях.
ГЛАВА 4 Терминальный ввод-вывод 231
4.5.6 Управление потоком данных
Вывод на терминал мгновенно останавливается, если пользователь нажимает Ctrl-
s или если процесс вызывает tcflow (см. раздел 4.6). При вводе Ctrl-q или вызове
tcflow поток данных возобновляется.
Если установлен флаг IXANY поля c_iflag, для возобновления потока пользователь
может ввести любой символ, а не только Ctrl-q. Если очищен флаг IX0N,
пользователь вообще не может управлять потоком выводимых данных, при этом
комбинации Ctrl-s и Ctrl-q не имеют специального значения.
Драйвер терминала поддерживает также управление потоком вводимых данных. Если
установлен флаг IX0FF, то при заполнении входной очереди драйвер отправляет
терминалу комбинацию Ctrl-s, приостанавливая ввод. Когда длина очереди
уменьшается в результате чтения символов процессом, терминалу отправляется
комбинация Ctrl-q, возобновляющая ввод. Конечно, этот подход можно использовать только
с теми терминалами, которые его поддерживают.
Если в поле c_lf lag установлен флаг T0ST0P и процесс из группы фоновых
процессов пытается выполнить запись на управляющий терминал, ему отправляется
сигнал SIGTT0U (см. раздел 4.3-1).
4.5.7 Управляющие символы
Значения некоторых управляющих символов, имеющие место по умолчанию, можно
изменить, задав элементы массива с_сс в структуре termios. В табл. 4.2 для каждого
такого символа указаны индекс в массиве с_сс и значение по умолчанию.
В массиве символ представляется своим внутренним значением.
Управляющие символы ASCII таковы: символам Ctrl-a — Ctrl-z соответствуют
значения 1 — 26; символам Ctrl-[, Ctrl-\, Ctrl-], Ctrl-* и Ctrl-_ — значения от 27 до 31;
Ctrl-? (Del) имеет значение 127, a Ctrl-@ — значение 0. Значения 32 — 126
соответствуют 95 печатаемым символам ASCII и не используются в качестве управляющих
символов. Значения, превышающие 127, не могут быть сгенерированы при
помощи стандартной клавиатуры US English, но могут быть доступны на других
клавиатурах. Стандартного для UNIX способа использования функциональных клавиш,
генерирующих многосимвольные последовательности, нет.
Таблица 4.2. Индексы в массиве с_сс
Индекс
Смысл
Типичное значение
по умолчанию
VE0F
VE0L
VERASE
VINTR
VKILL
конец файла
альтернативный вариант конца
строки (используется редко)
символ erase
прерывание; генерирует SIGINT
аннулирование строки
Ctrl-d
неопределено
Ctrl-?t
Ctrl-c
Ctrl-u
(см. след. стр.)
232 ГЛАВА 4 Терминальный ввод-вывод
Таблица 4.2 {окончание)
Индекс Смысл Типичное значение по
умолчанию
VQUIT выход; генерирует SIGQUIT Ctrl-\
VSUSP остановка процесса; генерирует SIGTSTP' Ctrl-z
VSTART возобновление ввода или вывода Ctrl-q
VST0P приостановка ввода или вывода Ctrl-s
Возможно, имена VSUSP и VSTOP будут иметь больший смысл, если их поменять местами,
но в таблице все верно.
f Так как некоторые терминалы при нажатии клавиши Backspace генерируют Ctrl-? (ASCII DEL),
а не Ctrl-h (ASCII BS).
В большинстве версий систем определены дополнительные символы для таких
операций, как удаление слова, повторная печать и отбрасывание вывода, однако
стандартизированы только те, что приведены в таблице. Общее число элементов
массива равно NCCS.
Символы, определяемые индексами VINTR, VQUIT и VSUSP, генерируют сигналы, о чем
говорилось в разделе 4.3- По умолчанию сигнал SIGIHT завершает процесс, SIGQUIT
завершает его с созданием дампа ядра, a SIGTSTP останавливает процесс до
получения сигнала SIGC0NT. Подробно сигналы обсуждаются в главе 9.
Чтобы заблокировать управляющий символ, вы можете задать для него значение
_P0SIX_V0ISABLE. Или вы можете заблокировать прерывание, выход и остановку
(c_cc[VSUSP]), очистив в поле c.lflag флаг ISIG, что отключает генерирование
сигналов. Если определения _P0SIX_V0ISABLE нет в заголовочном файле unistd.h, это
значение можно получить при помощи вызова pathconf или fpathconf (см. раздел
1.5.6). Обычно оно определяется как ноль.
4.5.8 Эхо
Терминалы обычно работают в полнодуплексном режиме — это означает, что
данные могут передаваться по линии связи одновременно в двух направлениях.
Соответственно, для обеспечения проверки того, что напечатанные символы получены
корректно, их воспроизводит компьютер, а не терминал. При этом применяется
обычное отображение выводимых символов: так, например, символ ввода
отображается на символ перевода строки, а затем, при выводе эхо, символ перевода
строки отображается на символы ввода и перевода строки.
Чтобы отключить эхо, очистите флаг ECHO поля c_lflag. Это выполняется или для
защиты данных — например, при вводе пароля — или потому, что процесс сам
должен решать, нужно ли выводить эхо, когда его выводить и что именно
выводить. Примером такого процесса может служить экранный редактор.
Для символов erase и kill доступны два специальных типа эхо. Если установлен флаг
ЕСНОЕ, символ выводится в эхо как Васкврасе-пробел-Васкэрасе. Это удаляет невер-
ГЛАВА 4 Терминальный ввод-вывод 233
ный символ с экрана CRT-монитора (если терминал является механическим, это
лишь приводит к ерзанию печатающего элемента). Если установить флаг ЕСНОК, после
символа kill в эхо будет выведен символ перевода строки (возможно,
отображаемый на символы ввода и перевода строки), и пользователь сможет продолжить работу
с новой строки.
4.5.9 Неканонический и канонический ввод
Обычно вводимые символы вносятся в очередь вплоть до завершения строки, что
определяется символом перевода строки или EOF. Только тогда символы
становятся доступны вызову read, которому, возможно, нужен только один символ. Во
многих приложениях, таких как экранные редакторы и системы ввода форм, процесс-
читатель работал бы более эффективно, если бы он получал символы по мере их
ввода, не дожидаясь составления строки. Идея «строк» на самом деле порой не имеет
никакого смысла.
Если флаг ICAN0N («канонический») поля c.lflag очищен, вводимые символы не
составляются в строки вплоть до чтения, поэтому редактирование с помощью
символов erase и kill невозможно. Символы erase и kill утрачивают свое специальное
значение. Условия, необходимые для удовлетворения вызова read, определяются двумя
параметрами — MIN и TIME. Когда число символов в очереди достигает MIN или когда
после получения байта проходит столько десятых долей секунды, сколько задано в
параметре TIME, символы в очереди становятся доступными. Параметр TIME
использует таймер, который обнуляется при получении байта и не начинает работать, пока
не будет получен первый байт, так что время не сможет истечь, если не будет
получен хотя бы один байт (ниже описывается исключение, когда MIN равно нулю).
MIN и TIME содержатся в индексах vmin и VTIME массива с_сс. Так как эти позиции
могут быть теми же, которые используются VEOF и VEOL, убедитесь в том, что вы
устанавливаете MIN и TIME явно, в противном случае вы можете получить текущие
значения, соответствующие символам EOF и EOL, что приведет к очень странным
результатам. Если вы когда-нибудь обнаруживали, что процесс получает вводимые
данные при вводе каждого четвертого символа, происходило, наверное, именно это
(Ctrl-d, обычный символ EOF, имеет значение 4).
Идея параметров MIN и TIME состоит в том, чтобы позволить процессу получать
символы по мере их ввода или с небольшой задержкой, не утрачивая преимуществ
чтения нескольких символов при помощи одного вызова read. Однако если вы не
реализуете в своем методе ввода данных буферизацию ввода, вы можете также
присвоить MIN значение 1 (параметр TIME при этом не играет роли, если он больше
нуля), так как ваши системные вызовы read в любом случае будут считывать только
один символ.
То, что я только что сказал, относится к значениям MIN и TIME, превышающим ноль.
Если одно или оба значения равны нулю, имеет место один из следующих
специальных случаев.
234 ГЛАВА 4 Терминальный ввод-вывод
• Если значение TIME равно нулю, таймер отключается и используется параметр
MIN при условии, что он больше нуля.
• Если значение MIN равно нулю, параметр TIME больше не рассматривается как
таймер, отсчитывающий интервал между получением двух байтов, и отсчет
времени начинается, как только выполняется вызов read. Вызов read возвращает
управление после получения одного байта или после истечения времени TIME
— в зависимости от того, что наступает раньше. Таким образом, в этом случае
вызов read может возвратить ноль.
• Если оба значения равны нулю, вызов read возвращает управление, когда
запрошено минимальное число байтов (третий аргумент read) и это число байтов
доступно во входной очереди. Если никакие байты недоступны, read
возвращается с нулевым счетчиком, что похоже на неблокируемый вызов read.
В стандартах не сказано, что именно происходит, если установлен флаг 0_N0NBL0CK,
а значения MIN и/или TIME не равны нулю. Приоритет может принадлежать как
флагу 0_N0NBL0CK (что приводит к незамедлительному возврату, если никакие
символы недоступны), так и значениям MIN/TIME. Во избежание этой неопределенности
не устанавливайте 0_N0NBL0CK, если флаг ICANON очищен.
Ниже приведен код функции tc_keystroke, выполняющей буферизуемый, но
неканонический ввод. Так как она изменяет флаги терминала, я также создал функцию
tc_restcre, восстанавливающую то состояние терминала, в котором он пребывал до
первого вызова tc_keystroke. Вызывающая программа должна перед своим
завершением вызвать tc_restore.
static struct termics tbufsave;
static bccl have_attr = false;
int tc_keystrcke(vcid)
<
static unsigned char buf[10];
static ssize_t tctal = 0, next = 0;
static bccl first = true;
struct termics tbuf;
if (first) {
first = false;
ec_neg1( tcgetattr(STDIN_FILENO, itbuf) )
have_attr = true;
tbufsave = tbuf;
tbuf.c_lflag i= "ICANON;
tbuf.c_cc[VMIN] = sizecf(buf);
tbuf.c_cc[VTIME] = 2;
ec_neg1( tcsetattr(STDIN_FILENO, TCSAFLUSH, itbuf) )
}
if (next >= total)
switch (total = read(0, buf, sizeof(buf))) {
ГЛАВА 4 Терминальный ввод-вывод 235
case -1:
syserr("read");
case 0:
fprintf(stderr, "Mysterious E0F\n");
exit(EXIT_FAILURE);
default:
next = 0;
>
return buf[next++];
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_EN0
>
bccl tc_restore(vcid)
{
if (have_attr)
ec_neg1( tcsetattr(STDIN_FILENO, TCSAFLUSH, itbufsave) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_EN0
>
Обратите внимание, что мы очистили только флаг ICAN0N (и, конечно, задали MIN
и TIME). Обычно мы очищали бы больше флагов для отключения эхо и
отображения выводимых символов и т. д. Эта тема будет затронута в следующем разделе. При
разработке конкретного приложения вам следует поэкспериментировать со
значениями MIN и TIME. Я указал 10 символов и 0,2 секунды, что, похоже, работает
хорошо.
Вот тестовый код, который вызывает tc_keystroke в цикле:
setbuf(stdcut, NULL);
while (true) {
с = tc_keystrcke();
putchar(c);
if (c == 5) { h "E V
ec_faise( tc_restcre() )
printf("\nXs\n", "Exiting...");
exit(EXIT.SUCCESS);
}
}
Говоря об этом коде, важно отметить, что эхо выводится сначала драйвером
терминала при вводе символов, а затем еще раз функцией putchar, когда символы воз-
236 ГЛАВА 4 Терминальный ввод-вывод
вращает функция tc.keystrcke. Это позволяет нам оценить влияние значений MIN
и TIME: так, в следующем примере буквы «slow» были введены слишком медленно,
а буквы «fast» слишком быстро. Введенные буквы подчеркнуты.
ss!locwwfastfast~E
Exiting...
Таким образом, значения MIN и TIME не влияют на работу tc.keystcke в том
смысле, что ввод остается неканоническим (с интервалом 0,2 секунды при нашем
значении TIME), однако при этом вызов read выполняется в 10 раз реже (значение MIN).
4.5.10 Простой терминальный ввод-вывод
Форма неканонического ввода, описанная в предыдущем разделе, полезна, но иногда
мы хотим, чтобы при этом было отключено эхо и почти все остальное. Такой
режим обычно называется простым (raw), потому что никакая специальная обработка
ввода или вывода не выполняется, а символы можно читать немедленно, не
дожидаясь составления строки. Задать простой режим при помощи какого-то одного флага
нельзя; вместо этого нужно задать разные атрибуты по одному (команда stty
имеет параметр raw).
Мы хотим, чтобы простой терминальный ввод-вывод имел следующие
характеристики, хотя вы можете изменить те или иные аспекты в собственных целях:
1. Ввод должен быть неканоническим. Очистите флаг 1CAN0N и задайте значения
MIN и TIME.
2. Отображение символов отключено. Очистите флаг 0P0ST, чтобы отключить
обработку вывода. В случае ввода очистите 1NLCR и 1CRNL. Установите в качестве
размера символов значение CS8. Очистите флаг 1STR1P, чтобы получать все
восемь битов, и флаги 1NPCK и PARENB для отключения проверки паритета.
Очистите флаг 1EXTEN для отключения обработки расширенных символов.
3. Управление потоком данных отключено. Очистите флаг 1X0N.
4. Управляющие символы отключены. Очистите флаги BRK1NT и 1S1G и
заблокируйте все управляющие символы — даже те, которые специфичны для конкретной
системы.
5. Эхо отключено. Очистите флаг ECHO. -
Эти операции инкапсулированы в функцию tc_setraw. Обратите внимание, что она
сохраняет старую структуру termios, чтобы ею могла воспользоваться функция
tc_restcre (см. предыдущий раздел).
bccl tc_setraw(vcid)
{
struct termics tbuf;
long disable;
int i;
ГЛАВА 4 Терминальный ввод-вывод 237
«ifdef _P0SIX_V0ISABLE
disable = _POSIX_VDISABLE;
«else
/• если значение не определено, это обрабатывается как сшибка с еггпс = 0 */
ec_neg1( (еггпс = 0, disable = fpathccnf(STDIN_FILENO, _PO_VDISABLE)) )
(tendif
ec_neg1( tcgetattr(STDIN_FILENO, &tbuf) )
have.attr = true;
tbufsave = tbuf;
tbuf.c_cfiag &= "(OSIZE | PARENB);
tbuf.c_cfiag |= 0S8;
tbuf.c_ifiag &= "(INLOR | IORNL | ISTRIP I INPOK | IXON | BRKINT);
tbuf.c.cfiag &= "OPOST;
tbuf.c.ifiag &= "(IOANON | ISIG | IEXTEN | EOHO);
fcr (i = 0; i < NOOS; i++)
tbuf.c_cc[i] = (cc_t)disabie;
tbuf.c_cc[VMIN] = 5;
tbuf.c_cc[VTIME] = 2;
ec.negK tCsetattr(STOIN_FILENO, TOSAFLUSH, &tbuf) )
return true;
EO_OLEANUP_BGN
return false;
EOJ)LEANUP_ENO
>
Вот код, который тестирует функцию tc.setraw, используя команду stty для
отображения параметров терминала:
setbuf(stdcut, NULL);
printf("Initial attributes;\п");
system("stty | fcid -s -w 60");
printf("\r\nRaw attributes:\n");
tc_setraw();
systemC'stty | fcid -s -w 60");
tc_restcre();
printf("\r\nRestcred attributes:\n");
system("stty | fcid -s -w 60");
Ниже приведены результаты, которые я получил, выполнив этот код на системе
Solaris. Заметьте, что функция tc.setraw отключила все символы с_сс, а не только
стандартные символы, о которых мы знаем. Но она отключила не все флаги,
которые могут заставлять терминал работать не в простом режиме, такие как imaxbel.*
Возможно, вам придется адаптировать функцию tc.setraw к каждой системе, на кото-
* Нестандартный флаг, управляющий звуковым сигналом, который подается в том случае, если
введенная строка слишком длинна.
238 ГЛАВА 4 Терминальный ввод-вывод
рую вы будете портировать свой код, или же использовать библиотеку Curses (см.
раздел 4.8), имеющую свой способ перевода терминала в простой режим.
Initial attributes:
speed 38400 baud; evenp
rows = 40; oolumns = 110; yplxels = 0; xpixels = 0;
swtoh = <undef>;
brklnt -lnpok iornl -ixany imaxbel onlor
eoho eohoe eohok eohootl eohoke lexten
Raw attributes:
speed 38400 baud; -parity
rows = 40; oolumns = 110; yplxels = 0; xpixels = 0;
mln = 5; time = 2;
intr = <undef>; quit = <undef>; erase = <undef>; kill =
<undef>; eof = "e; eol = "b; swtoh = <undef>; start =
<undef>; stop = <undef>; susp = <undef>; dsusp = <undef>;
rprnt = <undef>; flush = <undef>; werase = <undef>; lnext =
<undef>;
-inpok -lstrlp -lxon Imaxbel -opost
-islg -loanon -eoho eohoe eohok eohootl eohoke
Restored attributes:
speed 38400 baud; evenp
rows = 40; oolumns = 110; ypixels = 0; xpixels = 0;
swtoh = <undef>;
brkint -inpok lornl -ixany imaxbel onlcr
eoho eohoe eohok eohootl eohoke iexten
Иногда во время (или после!) отладки программа, которая перевела терминал в
простой режим, завершается до того, как получает возможность восстановить
первоначальные значения. Пользователи иногда при этом думают, что компьютер
завис или что терминал «заблокировался»: они даже не могут использовать EOF для
выхода из системы. Однако все можно восстановить, находясь в простом режиме.
Во-первых, вспомните, что флаг ICRNL очищен; это означает, что вы должны будете
завершать вводимые строки комбинацией Ctrl-j, а не клавишей ввода." Во-вторых,
вы не будете видеть, что вы печатаете, потому что эхо также отключено. Начните с
ввода нескольких символов смещения на одну строку (line feed); вы должны
увидеть ряд приглашений командной оболочки, но не с левого края экрана, так как
обработка вывода (0P0ST) отключена. Затем напечатайте stty sane, введите символ
смещения на одну строку, и все должно быть в порядке.
Было бы неплохо, если бы в качестве символа, завершающего строку, можно было
использовать и символ перевода строки, и символ ввода (эта сноска впервые появилась в издании
1985 года, но воз и ныне там).
ГЛАВА 4 Терминальный ввод-вывод
239
4.6 Дополнительные системные вызовы,
служащие для управления терминалом
Как я показал в разделе 4.5.1, когда вы устанавливаете атрибуты терминала при
помощи tcsetattr, вы можете перед установкой атрибутов дождаться очистки
выходной очереди при помощи действия TCSADRAIN, а также отбросить любые
символы, имеющиеся во входной очереди, с использованием действия TCSAFLUSH. Есть два
системных вызова, позволяющих дожидаться очистки очереди и отбрасывать
символы без установки атрибутов:
tcdrain — ожидает вывод на терминал
«include <termics.h>
int tcdrain(
int fd А дескриптор файла */
);
I* Возвращает 0 при успехе или -1 в случае сшибки (устанавливает еггпс) */
tcflush — отбрасывает ввод с
«include <termics.h>
int
);
/•
tcflush(
int fd, A
int queue /*
Возвращает О при успехе
терминала, вывод на терминал
дескриптср файла */
релевантная счередь */
или -1 в случае сшибки
или и то и другое
(устанавливает
еггпс)
*/
Эти функции работают с очередью, т. е. символами, полученными от терминала,
но еще не прочитанными никаким процессом, или символами, записанными
процессом, но еще не переданными терминалу.
Вторым аргументом (queue) вызова tcflush является одно из следующих значений:
TCIFLUSH Очистка входной очереди.
TC0FLUSH Очистка выходной очереди.
tcioflush Очистка обеих очередей.
Таким образом, выполнение вызова tcsetattr с аргументом TCSADRAIN эквивалентно
вызову tcdrain перед установкой атрибутов, а выполнение вызова tcsetattr с
аргументом TCSAFLUSH эквивалентно вызову tcdrain и tcflush со значением TCIFLUSH в
качестве аргумента queue.
Вызов tcflow позволяет приложению приостановить или возобновить
терминальный ВВОД ИЛИ ВЫВОД:
240
ГЛАВА 4 Терминальный ввод-вывод
tcflow
— приостанавливает или возобновляет терминальный ввод
«include <terules.h>
lnt
•/
);
/•
tcficw(
lnt fd,
lnt action
Всзеращеет О
/•
/•
при успехе
дескриптор фейла
•/
или вывод
напрееление и признак присстансеки/есзсбнселения
или -1 е случее
сшибки (устенеелиеает
еггпс) */
Аргумент action может иметь одно из следующих значений:
TC00FF Приостановка вывода.
TC00N Возобновление приостановленного вывода.
TCI0FF Отправление символа STOP, говорящего терминалу приостановить ввод.
TCI0N Отправление символа START, говорящего терминалу возобновить
приостановленный ввод.
Значение TC00FF может использоваться приложением, выводящим страницы на
терминал, для задержки вывода после каждой страницы, чтобы пользователь мог
прочитать ее и продолжить работу, введя с клавиатуры символ START, которому по
умолчанию соответствует комбинация Ctrl-q (см. раздел 4.5.7). После вызова tcf lew
с аргументом TCO0F приложение может продолжать записывать выводимые страницы;
когда очередь терминала станет полной, следующий вызов write заблокируется, пока
очередь немного не освободится.
Однако пользователи обычно рассчитывают не на это, и приложения, выводящие
по одной странице, обычно работают не так. Команды вроде mere и man выводят
страницу, после чего предлагают пользователю ввести обычный символ (например
пробел или символ ввода) для вывода следующей страницы. Такие приложения
выводят одну страницу, выводят приглашение и блокируются при вызове read (как
правило, при отключенном каноническом вводе и эхо), дожидаясь, пока
пользователь введет символ, продолжающий вывод.
Что касается ввода, то значения TCI0FF и TCI0N ранее использовались для
предотвращения переполнения входной очереди, но сегодня за этим следит драйвер и/или
оборудование. В настоящее время вызов tcf lew, наверное, используется только при
программировании специализированных устройств, а не терминалов.
К той же категории относится вызов tcsendbreak, отправляющий терминалу
команду перерыва:
tcsendbreak — посылает
«include <termics.h>
int tcsendbreek(
lnt fd,
int duration
);
/* Возвращает О при ус г
терминалу команду перерыва
/•
/•
iexe
дескриптор фейле */
длительность перерыва */
или -1 в случае ошибки (устанавливеет
errno)
•/
ГЛАВА 4 Терминальный ввод-вывод
241
♦Команда перерыва» — это поток нулевых битов, отправляемых терминалу на
протяжении некоторого периода времени, что часто используется для перевода
терминала в специальный режим «внимания».
Если аргумент duration равен нулю, период составляет от четверти до половины
секунды. Если он не равен нулю, его суть зависит от версии системы. Но волноваться
по поводу портируемости кода не стоит: скорее всего, вам никогда не придется
использовать вызов tcsendbreak.
4.7 Системные вызовы, служащие
для идентификации терминала
Как я сказал в разделе 4.2.1, при помощи универсального пути /dev/tty вы можете
получить доступ к управляющему терминалу процесса, не зная его имя. Однако до
спецификации SUS2 это не было требованием, поэтому есть системный вызов,
позволяющий получить такой синоним:
ctermid — получает путь
«include <stdic.h>
char *cternid(
char *buf
);
/* Возвращает путь или
к управляющему терминалу
/* буфер размера
пустую строку при
L_ctermid или
сшибке (еггпс
NULL
не
•/
определено)
•/
Вы можете вызвать ctermid с аргументом NULL, в случае чего используется
статический буфер соответствующего размера. Или вы можете предоставить буфер
размера L.ctermid (включающий место для байта NUL) во избежание каких бы то ни было
конфликтов, если эту функцию могут вызывать несколько потоков. Имейте в виду,
что при ошибке ctermid возвращает пустую строку, а не NULL. Вызов ctermid не
обязан возвращать что-либо более полезное, чем строка /dev/tty, и в большинстве систем
(если не во всех) он именно это и делает.
Системный вызов, возвращающий фактическое имя терминала, был бы более
полезным, и для этого предназначены вызовы ttyname и ttyname.r, только вы должны
предоставить им открытый дескриптор файла:
ttyname — определяет путь к терминалу
«include <unistd.h>
char *ttyname(
int fd /* дескриптор файла •/
);
/* Возвращает строку или NULL в случае сшибки (устанавливает еггпс) */
242 ГЛАВА 4 Терминальный ввод-вывод
ttyname_r — определяет путь к терминалу
«include <unistd.h>
int ttyname_r(
int fd,
char *buf,
size_t bufsize
);
/• Возвращает о при
устанавливает) •/
/* дескриптор файла */
/• буфер для пути */
/* размер буфера •/
успехе или нсмер сшибки в случае сшибки (errnc не
Как вы узнали, изучая другие функции, суффикс _г означает, что вызов ttyname_r
реентерабелен: он использует не статическое хранилище, а буфер, который вы ему
передаете. Но вы также узнали, что определить нужный размер буфера непросто:
вы можете использовать макрос TTY_name_max, если он определен (в файле limits.h),
а если он не определен, вы должны вызвать sysccnf (см. раздел 1.5.5) с аргументом
_SC_TTY_name_max. Или вы можете просто во всех ситуациях вызывать sysccnf. Если
вы знаете, что только один поток вызывает ttyname, все значительно упрощается.
Вот моя версия команды tty, которая печатает имя терминала, подключенного к
стандартному источнику ввода, или «not a tty», если это не терминал:
int main(vcid)
{
char «ctty;
if ((ctty = ttyname(STDIN_FILENO)) == NULL) {
printf("not a tty\n");
exitd);
}
printf("Xs\n", ctty);
exit(O);
}
Я использовал числа 1 и 0 вместо символов EXIT.FAILURE и EXIT_SUCCESS потому, что
в SUS явно сказано, что возвращать нужно 1 или 0 (в стандарте POSIX говорится
лишь то, что код EXIT.FAILURE не должен быть нулем, но не утверждается, что он должен
равняться 1).
Если вам на самом деле нужно имя управляющего терминала, даже если
стандартный источник ввода каким-то образом открыт для другого терминального или
нетерминального устройства ввода, вы могли бы подумать, что вы можете открыть
/dev/tty и передать этот дескриптор файла в ttyname. Однако системы, на которых
я это испытывал, работают не так: ttyname просто возвращает /dev/tty вместо
фактического имени. Похоже, портируемого способа получения имени
управляющего терминала нет, хотя если стандартным источником ввода является терминал,
скорее всего это управляющий терминал.
ГЛАВА 4 Терминальный ввод-вывод 243
Чтобы проверить, для терминала ли открыт дескриптор файла, вы можете
использовать системный вызов isatty:
I isatty — проверяет, открыт ли дескриптор файла для терминала
! «include <unistd.h>
I
| int isatty(
int fd /* дескриптор файла */
);
I /* Возвращает 1, если дескриптор открыт для терминала, и 0, еоли нет
! (возвращая 0, может устанавливать errno) */
I
Если isatty возвращает 0, вы не можете быть уверены в том, что errno будет
установлена, но, как правило, если этот вызов возвратит что-то отличное от 1,
причина этого вас волновать не будет.
4.8 Полноэкранные приложения
Некоторые традиционные команды UNIX интерактивны (например dc, more), но все
же они читают и записывают текстовые строки. Есть, по меньшей мере, три
других интересных категории приложений, которые в более полной мере
используют возможности мониторов и устройств ввода (таких как мышь).
• Символьные полноэкранные приложения, которые выполняются дешевыми
терминалами (скорее всего, уже не производимыми) и эмуляторами терминалов,
такими как telnet и xterm. Наверное, самым известным таким приложением
является текстовый редактор vi.
• Приложения с графическим пользовательским интерфейсом (Graphical-User-
Interface, GUI), написанные для системы X Window, возможно, с
использованием высокоуровневого инструментария, такого как Motif, Gnome, KDE или даже
Tcl/Tk (кроме X есть и некоторые другие системы с GUI).
• Web-приложения, пользовательский интерфейс которых создается с
использованием таких технологий, как HTML, JavaScript и Java.
Функциональность терминалов, которой посвящена эта глава, используется
только приложениями первой группы. Х-приложения взаимодействуют по сети с так
называемым Х-сервером, и обработка GUI на самом деле выполняется на сервере,
который может работать под управлением UNIX или другой ОС. Если это система
UNIX, Х-сервер взаимодействует непосредственно с дисплеем и устройствами ввода
при помощи их драйверов, а не драйвера символьного терминала. Аналогично, Web-
приложения взаимодействуют по сети с браузером (см. раздел 8.4-3), а браузер
отвечает за взаимодействие с пользователем. Если браузер работает под
управлением UNIX, он часто является Х-приложением.
Я собираюсь привести пример очень простого символьного полноэкранного
приложения, которое очищает экран и отображает меню, выглядящее следующим образом:
244
ГЛАВА 4 Терминальный ввод-вывод
Wfta); во you want to-do?
1.' Cheek' out tasw/BVO
2. Reserve tape/WD .
3. Register new member
4. Search for title/actor
5. Quit
continue)
Рис. 4.З. Меню
Когда пользователь вводит число, эхо не выводится, а после ввода числа Enter
нажимать не нужно. Введя, например, 3, вы увидите экран, изображенный на рис. 4.4.
Рис. 4.4. Ответ приложения
Я приведу два варианта реализации этого приложения. Первый предназначен для
терминалов ANSI и прекрасно работает со всем, что может эмулировать терминал
VT100. Терминалы ANSI (и VT100) имеют десятки управляющих
последовательностей, но я буду использовать только две.
• Последовательность е[г;сН позиционирует курсор в строку г, столбец с. (е — это
escape-символ.)
• Последовательность e[2J очищает экран.
Эти управляющие последовательности используются двумя функциями; обратите
внимание, что функция clear не только очищает экран, но и переводит курсор в
левый верхний угол экрана:
«define ESC "\033"
bcol mvaddstr(int у, int x, ccnst char *str)
{
return printf(ESC "[1d;XdHXs", y, x, str) >= 0;
}
bcol clear(void)
{
return printf(ESC "[2J") >= 0 &&
mvaddstr(0, 0, "");
}
ГЛАВА 4 Терминальный ввод-вывод 245
Мы хотим, чтобы при вводе неверного символа терминалом подавался звуковой
сигнал, и можем использовать для этого код ASCII:
«define BEL "\007"
int beep(void)
{
return printf(BEL) >= 0;
}
Предполагая, что терминал переведен в простой режим путем вызова to_setraw, мы
вызываем для чтения символа функцию getoh:
int getoh(void)
{
ohar о;
switoh(read(STDIN_FILENO, &o, 1)) {
default:
errno = 0;
/* переход к оледуощему олучаю */
oase -1:
return -1;
oase 1:
break;
}
return o;
>
Это все сервисные функции пользовательского интерфейса, которые нам нужны.
Приложение полностью реализовано в функции main. Конечно, если бы оно что-
то делало, код был бы гораздо объемнее.
int main(void)
{
int о;
ohar s[100];
booi ok = faise;
eo_faise( to_setraw() )
setbuf(stdout, NULL);
whiie (true) {
eo_faise( oiearO )
eo_faise( ravaddstr(2, 9, "What do you want to do?") )
eo_faise( mvaddstr(3, 9, "1. Cheok out tape/DVD") )
eo_faise( «vaddstr(4, 9, "2. Reserve tape/DVD") )
eo_faise( nvaddstr(5, 9, "3. Register new яеяЬег") )
eo_faise( nvaddstr(6, 9, "4. Searoh for titie/aotor") )
246 ГЛАВА 4 Терминальный ввод-вывод
ec_false( mvaddstr(7, 9, "5. Quit") )
ec_false( mvaddstr(9, 9, "(Type iten number tc continue)") )
ec_neg1( с = getch() )
switch (c) {
case 'V:
case '2':
case '3':
case '4':
ec_false( clear() )
snprintf(s, sizecf(s), "Ycu typed Xc", c);
ec_false( mvaddstr(4, 9, s) )
ec_false( mvaddstr(9, 9, "(Press any key tc continue)") )
ec_neg1( getch() )
break;
case '5':
ck = true;
EC.CLEANUP
default:
ec_false( beep() )
>
}
EC_CLEANUP_BGN
(vcid)tc_restcre();
(vcid)clear();
exit(ck ? EXIT_SUCCESS : EXIT_FAILURE);
EC_CLEANUP_END
}
Наверное, вам не хочется ограничиваться поддержкой только одного типа
терминалов, каким бы распространенным он ни был. Тогда вам следует использовать
стандартизированную библиотеку Curses, которая была представлена в разделе 41.
Она включает БД управляющих последовательностей для каждого терминала,
который когда-либо был создан, и может выбирать подходящую управляющую
последовательность во время выполнения.
Я поступил умно, назвав функции mvaddstr, clear, beep и getch в соответствии с
одноименными функциями Curses, которые делают то же самое. Следовательно,
чтобы вы узнали все необходимое о версии приложения, использующей Curses, я
должен привести только код функции main:
«include <curses.h>
/• макрос "ее" для ERR (используется библиотекой Curses) */
«define ec_ERR(x) ec_cmp(x, ERR)
int main(void)
{
ГЛАВА 4 Терминальный ввод-вывод 247
int с;
char s[100];
bccl ck = false;
(vcid)initscr();
ec_ERR( raw() )
while (true) {
ec_ERR( clear() )
ec_ERR( mvaddstr(
ec_ERR( mvaddstr(
ec_ERR( mvaddstr(
ec_ERR( mvaddstr(
ec_ERR( mvaddstr(
ec_ERR( mvaddstr( 7,
ec_ERR( mvaddstr( 9,
ec_ERR( с = getchQ )
switch (c) {
case '1"
9, "What dc ycu want tc dc?") )
9, "1. Check cut tape/DVD") )
9, "2. Reserve tape/DVD") )
9, "3. Register new member") )
9, "4. Search fcr title/actcr") )
9, "5. Quit") )
9, "(Type item number tc continue)") )
case
case
case
ec_ERR( clear() )
snprintf(s, sizecf(s),
ec_ERR( mvaddstr( 4, 9,
ec_ERR( mvaddstr( 9, 9,
ec_ERR( getch() )
break;
case '5':
ok = true;
EC.CLEANUP
default:
ec_ERR( beep() )
}
"Ycu typed Xc", c);
s) )
"(Press any key tc continue)") )
}
EC_CLEANUP_BGN
(void)clear();
(vcid)refresh();
(vcid)endwin();
exit(ck ? EXIT_SUCCESS
EC_CLEANUP_END
>
EXIT_FAILURE);
Некоторые комментарии по поводу этой программы.
• Большинство функций Curses возвращают при ошибке ERR, так что я определил
для них макрос ec_ERR. В документации к библиотеке Curses не говорится, что
9 Зак 4030
248 ГЛАВА 4 Терминальный ввод-вывод
она использует errno, но я оставил все как есть; тем не менее, я помню, что если
я все же получу сообщение, оно может быть бессмысленным.
• Библиотека Curses требует, чтобы работа программы начиналась с вызова initscr
и заканчивалась вызовом endwin.
• В Curses функции tc_setraw соответствует функция raw.
Я больше не буду обсуждать Curses в этой книге, так как это не сервис ядра.*
Библиотека Curses входит в SUS2 и SUS3, так что вы можете найти ее спецификацию в
Интернете на сайте SUS2 по адресу wunv.unix-systems.org/version2/online.html.
Кроме того, большинство систем позволяют получить информацию о Curses при
помощи команд man curses и man ncurses (свободно распространяемая версия Curses).
4.9 Ввод-вывод с использованием STREAMS
STREAMS — это реализованный в некоторых системах UNIX механизм,
обеспечивающий возможность модульной реализации драйверов символьных устройств.
Приложения могут объединять разные модули STREAMS для получения нужных им
возможностей. В некотором смысле это похоже на работу фильтров UNIX на
уровне командной оболочки: например, команда who может не поддерживать сортировку
вывода, но вы можете связать ее с sort.
Я приведу пример псевдотерминалов, основанных на использовании STREAMS, в
следующем разделе. Что я сделаю? Я открою файл устройства для получения
псевдотерминала, после чего вызову ioctl, «поместив» в псевдотерминал два модуля
STREAMS (ldterm и ptem), чтобы он вел себя как терминал. В отличие от конвейеров
командных оболочек, поддерживающих десятки фильтров, которые можно
комбинировать сотнями творческих способов, использование модулей STREAMS обычно
напоминает приготовление пищи: вы используете модули, которые указаны в
документации, но если этим вы ограничитесь, то не получите ничего вкусного (не
путайте STREAMS со стандартной для ввода-вывода концепцией потоков,
используемых функциями fopen, fwrite и т. д. Это совершенно разные вещи).
Механизм STREAMS был обязательным в SUS1 и SUS2, но не в SUS3- Системы Linux,
которые присваивают символу _X0PEN_VERSI0N значение 500 (заявляя о соответствии
SUS2), не поддерживают STREAMS и потому не соответствуют SUS2 в этом смысле.
Я использую STREAMS в следующем разделе и упомяну несколько раз в других
местах книги, но не буду приводить полное описание этого механизма. Для
получения информации о STREAMS обратитесь к SUS или посетите сайт Sun, расположенный
по адресу www.sun.com, где можно найти «Руководство по программированию
STREAMS (STREAMS Programming Guide)».
' Кроме того, если бы я решил обсуждать Curses, мне пришлось бы рассмотреть и систему X,
а это оказалось бы для нас кошмаром (под «нами» я понимаю и вас. и себя).
ГЛАВА 4 Терминальный ввод-вывод 249
4.10 Псевдотерминалы
Драйвер обычного терминала связывает процесс с действительным терминалом так,
как показано на рис. 4.5а, где пользователь, работающий с терминалом, выполняет vi.
Что касается драйвера псевдотерминала, то по отношению к интерактивному
подчиненному (slave) процессу (vi) он работает как терминал, но с другой стороны он
связан с главным (master) процессом, а не с действительным устройством. Это позволяет
главному процессу передавать ввод подчиненному процессу, как если бы ввод
исходил от действительного терминала, и получать вывод подчиненного процесса, как если
бы он отправлялся действительному терминалу. Подчиненный процесс может
использовать tcgetattr, tcsetattr и другие системные вызовы, работающие только с
терминалами, как обычно, и все они будут работать точно так, как он ожидает.
( \
vi 1
0
—*
—►
•4.—
Драйвер
терминала
]
vi 1
0
J
Главный
процесс
—►
<—
-
TZ 1
Драйвер
. псевдотерминала:
сторона подчиненного
процесса
/драйвер
/ псевдотерминала:
сторона главного
процесса
I сторон
\процес
а) Драйвер терминала связан с терминалом б) Драйвер псевдотерминала связан с процессом
Рис. 4.5. Драйвер терминала и драйвер псевдотерминала
С концептуальной точки зрения соединение псевдотерминала с процессом
можно рассматривать как перенаправление его ввода и вывода при помощи
командной оболочки, но с использованием терминала, а не конвейера. В
действительности vi не будет работать с конвейером:
$ vi >trop
ex/vi: Vi's standard input and output must be a terminal
Наверное, чаще всего псевдотерминалы используются сервером telnetd (см. рис. 4.6).
Он взаимодействует с клиентом telnet по сети и с пользователем telnet посредством
псевдотерминала. На практике сеансы telnet обычно начинаются с командной
оболочки, как и сеансы, при которых используется действительный терминал; на рисунке
vi выполняется потому, что пользователь ввел в командной оболочке команду vi.
В наше время действительный терминал используется гораздо реже, чем telnet или
xterm, так как большинство пользователей подключаются к компьютеру UNIX по сети,
а не по коммутируемой линии (возможно, с интернет-провайдером они
связываются по коммутируемой линии, но только для того, чтобы подключиться к
Интернету). Обсуждать реализацию сервера telnetd мы не будем (см. упражнение 4.6); нас
интересует только то, что касается псевдотерминала.
250 ГЛАВА 4 Терминальный ввод-вывод
vi 1
1 °)
Драйвер
терминала
]
г. :ы i
Главный
процесс
Драйвер
ч псевдотерминала:
сторона подчиненного
процесса
/'Драйвер
*у псевдотерминала:
А сторона главного
^процессе
а) Драйвер терминала связан с терминалом б) Драйвер псевдотерминала связан с процессом
Рис. 4.6. Псевдотерминал, используемый сервером telnet (telnetd).
На главной стороне псевдотерминала главный процесс получает все, что
записывает подчиненный процесс. В случае vi и других программ, работающих с
экраном, эти данные включают escape-последовательности очистки экрана и т. п. (см.
раздел 4.8). И драйверу терминала, и драйверу псевдотерминала безразлично, что
это за последовательности. С другой стороны, управляющие операции —
например, те, что выполняются при помощи tcsetattr — полностью обрабатываются
драйверами и не достигают главного процесса. Таким образом, на рис. 4.6
escape-последовательности, сгенерированные vi, достигают процесса telnet, который
выполняется на левом компьютере. Если он выполняется в полноэкранном режиме, он
может просто отправить их действительному терминалу. Еще чаще процесс telnet
вообще не соединен с драйвером терминала, а выполняется под управлением
оконной системы. В этом случае он должен знать, как преобразовать
escape-последовательности в то, что нужно оконной системе для отображения символов. Такие
программы называются эмуляторами терминала. Так что запомните: эмулятор
терминала и псевдотерминал — это совершенно разные вещи: первый обрабатывает escape-
последовательности, а второй заменяет драйвер терминала, перенаправляя ввод-
вывод главному процессу.
4.10.1 Библиотека для работы с псевдотерминалами
Так как в этом разделе используются некоторые системные вызовы (например fork),
описанные в главе 5, вы можете вернуться к этому разделу после того, как
прочитаете ее.
Как и все другие устройства, псевдотерминалы представляются специальными
файлами, имена которых в разных системах различаются. К сожалению, связать
процесс с псевдотерминалом далеко не так просто, как показано здесь:
$ vi </dev/pty01 >/dev/pty01
На самом деле все сложнее: мы должны получить имя псевдотерминала, выполнить
кое-какие системные вызовы для его подготовки и сделать его управляющим
терминалом. Некоторые из нужных системных вызовов стандартизированы
спецификацией SUS1 (см. раздел 1.5.1) и более поздними стандартами, но системы, предшество-
ГЛАВА 4 Терминальный ввод-вывод 251
вавшие SUS, такие как FreeBSD, работают совершенно иначе. Хуже того: системы,
использующие для реализации псевдотерминалов STREAMS, требуют выполнения
некоторых дополнительных этапов, a SUS2 и SUS3 имеют несколько отличий.
Я собираюсь инкапсулировать различия между системами в небольшую
библиотеку функций, имя каждой из которых будет начинаться с символов pt_. Затем я
использую эту pt-библиотеку для реализации примера — приложения
записи/воспроизведения вывода.
Вот общая схема использования псевдотерминалов, представленная в форме
девяти этапов:
1. Откройте главную сторону псевдотерминала для чтения и записи.
2. Получите доступ к псевдотерминалу (объясняется ниже).
3. Из имени или дескриптора файла главной стороны получите имя подчиненной
стороны. Пока не открывайте ее.
4. Выполните системный вызов fork (см. раздел 5.5) для создания дочернего
процесса.
5. В дочернем процессе выполните вызов setsid (см. раздел 4.3-2), чтобы процесс
оставил свой управляющий терминал.
6. В дочернем процессе откройте подчиненную сторону псевдотерминала. Он
станет новым управляющим терминалом. В случае систем, поддерживающих
STREAMS (например Solaris), вы должны использовать STREAM. Если вы имеете
дело с системой BSD, вы должны явно сделать псевдотерминал управляющим
терминалом при помощи следующей команды:
ec_neg1( ioctKfd, TIOCSCTTY) )
7. Перенаправьте относящиеся к дочернему процессу дескрипторы стандартных
файлов ввода, вывода и ошибок на псевдотерминал.
8. Выполните системный вызов execvp (см. раздел 5.3), чтобы дочерний процесс
выполнил нужную вам программу (например vi). (Можно использовать любой
из шести вариантов системного вызова «exec»; в примере я использую execvp).
9. Теперь родительский процесс может выполнять чтение и запись на главной
стороне псевдотерминала, используя дескриптор файла, полученный на этапе 1.
Дочерний процесс будет читать данные из стандартного источника ввода и
записывать в стандартный поток вывода и стандартный поток вывода
информации об ошибках, как обычно (он ничего не знает о контексте, в котором он
выполняется), а их дескрипторы будут работать так, как если бы они были
открыты для терминального устройства.
Схема всего этого изображена на рис. 4.56.
Есть три метода открытия главной стороны псевдотерминала (этап 1):
A. Если система соответствует SUS3, вы просто вызываете posix_openpt (см. ниже).
B. В случае большинства систем SUS1 и SUS2 (включая Solaris и Linux) вы
открываете файл-клон /dev/ptmx и получаете уникальный псевдотерминал, хотя не бу-
252 ГЛАВА 4 Терминальный ввод-вывод
дете знать имя фактического файла, если оно вообще есть. Это нормально, так
как вам нужен лишь открытый дескриптор файла.
С. Системы BSD включают набор специальных файлов, имена которых имеют форму
/dev/ptyXY, где X и Y — цифра или буква. Вы должны перебирать эти файлы,
пока не найдете файл, который сможете открыть (я не шучу!).
Мы можем использовать макрос _X0PEN_VERSI0N для проведения различия между
методами А и В, а для систем, использующих метод С, мы определим макрос
MASTER_NAME_SEAROH. На этапе б мы определим макрос NEED_STREAM_SETUP для систем,
которые требуют использования STREAM, и макрос NEED_Ti00S0TTY для систем,
нуждающихся в вызвове iootl. Вот фрагмент кода pi-библиотеки, определяющий эти
макросы для Solaris и FreeBSD; системы Linux в этих макросах не нуждаются:
«if defined(SOLARIS) /* воли нужно, укажите здвоь дополнительные варианты «/
#define NEED_STREAM_SETUP
«endif
«if defined(FREEBSD) /* еоли нужно, укажите здеоь дополнительные варианты •/
#define NEED.TIOOSOTTY
«endif
«ifndef _XOPEN_UNIX
«define HASTER_NAHE_SEAROH
«endif
Если нужно, дополните этот код, чтобы адаптировать его к своей системе.
Вот краткое описание вызова posix.openpt, поддерживаемого только системами SUS3:
posixopenpt — открывает псевдотерминал
«inolude <stdlib.h>
«inolude <fontl.h>
int posix_openpt(
int oflag
v
/« флаг 0_RDWR,
операции ИЛИ
1* Возвращает деокриптор файла
(устанавливает еггпо) */
в олучае
возможно
о флагом
объединенный
0_N0CTTY «/
уопеха или -1 при
при
ошибке
помощи
Флаг 0.NOCTTY не позволяет главной стороне стать управляющим терминалом, если
процесс не имеет управляющего терминала (напомню, что первое терминальное
устройство, открываемое для процесса, который не имеет управляющего
терминала, становится управляющим терминалом). Как правило, нам не нужно, чтобы главная
сторона была управляющим терминалом, хотя именно это нам нужно от
подчиненной стороны (этап 5), так что мы воспользуемся флагом 0_NOCTTY.
Код pt-библиотеки нуждается в некоторых директивах «include, отсутствующих в
файле defs.h:
ГЛАВА 4 Терминальный евод-еывод 253
«ifdef _XOPEN_UNIX
«include <strcpts.h> /• для STREAMS */
«endif
«ifdef NEEDJIOCSCTTY
«include <sys/ttyccm.h> /* для TIOCSCTTY •/
«endif
Все функции pt-библиотеки работают со структурой PTINF0, которая содержит
дескрипторы файлов для главной и подчиненной сторон и полные пути к ним, если
они известны:
«define PT_MAX_NAME 20
typedef struct {
int pt_fd_m; /* дескриптор файла главней стерсны */
int pt_fd_s; /* дескриптор файла подчиненней стерсны */
char pt_name_m[PT_MAX_NAME]; /* имя файла главней стерсны */
char pt_name_s[PT_MAX_NAME]; /* имя файла подчиненней стерсны •/
> PTINF0;
«define PT_GET_MASTER_FD(p) ((p)->pt_fd_m)
«define PT_GET_SLAVE_FD(p) ((p)->pt_fd_s)
Два макроса нужны приложениям для получения дескрипторов файлов.
Функция pt_cpen_master, код которой вы скоро увидите, выделяет в памяти место
для структуры PTINF0 и возвращает указатель на нее, чтобы ее можно было
использовать с другими библиотечными функциями. В этом нет ничего необычного:
подобным образом стандартная библиотека ввода-вывода использует структуру FILE.
Внутри pt_cpen_master для выполнения этапа 1 вызывается функция find_and_cpen_niaster,
при этом используется один из вышеописанных методов:
«if defined(MASTER_NAME_SEARCH)
«define PTY_RANGE \
"0123456789ABCDEFGHIJKLMN0P0RSTUVWXYZabcdefghijklnincpqrstuvwxyz"
«define PTY_PR0T0 "/dev/ptyXY"
«define PTY_X 8
«define PTY_Y 9
«define PTY_MS 5 /• для получения имени подчиненней стерсны
измените букву на 'f */
«endif /» MASTER_NAME_SEARCH */
static beel find_and_cpen_master(PTINFO *p)
{
«if defined(_XOPEN_UNIX)
«if _X0PEN_VERSI0N >= 600
p->pt_name_m[0] = '\0'; /* неизвестно или требуется имя */
ec_neg1( p->pt_fd_m = pcsix_cpenpt(0_RDWR | 0_N0CTTY) )
«else
254 ГЛАВА 4 Терминальный ввод-вывод
strcpy(p->pt_namejn, "/dev/ptmx"); /* клснирсвание устрсйства */
ec_neg1( p->pt_fd_m = cpen(p->pt_nanie_ni, 0_R0WR) )
#endif
#elif defined(MASTER_NAHE_SEARCH)
int i, j;
char prctc[] = PTY_PR0T0;
if (p->pt_fd_m != -1) {
(vcid)clcse(p->pt_fd_m);
p->pt_fd_m = -1;
}
fcr (i = 0; i < sizecf(PTY_RANGE) - 1; i++) {
prctc[PTY_X] = PTY_RANGE[i];
prctc[PTY_Y] = PTY_RANGE[0];
if (access(prctc, F_0K) == -1) {
if (errnc == ENOENT)
continue;
EC.FAIL
}
fcr (j = 0; J < sizecf(PTY_RANGE) - 1; J++) {
prctc[PTY_Y] = PTY_RANGE[J];
if ((p->pt_fd_ra = cpen(prctc, 0_RDWR)) == -1) {
if (errnc == ENOENT)
break;
}
else {
strcpy(p->pt_namejii, prctc);
break;
}
}
if (p->pt_fd_m != -1)
break;
}
if (p->pt_fd_m == -1) {
errno = EAGAIN;
EC_FAIL
}
«else
errno = ENOSYS;
EC_FAIL
#endif
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
ГЛАВА 4 Терминальный ввод-вывод
255
Основная часть кода find_and_open_master нужна для поиска нужного имени среди
3844 имен! Чтобы немного ускорить поиск, мы используем системный вызов aooess
(см. раздел 3-8.1), проверяя существует ли каждое из имен вида /dev/ptyXO. Если нет,
мы забываем про /dev/ptyXl, /dev/ptyX2 и другие 59 имен из этой серии и просто
переходим к следующему X.
Этап 2 — получение доступа к псевдотерминалу. Работая с системами SUS, вы
должны вызвать grantpt для получения доступа к подчиненной стороне и unlookpt, если
псевдотерминал был заблокирован:
grantpt — предоставляет доступ к подчиненной стороне псевдотерминала
«inolude <stdlib.h>
int grantpt(
int fd /* деокриптор файла */
);
/* Возвращает 0 в олучае уопеха или -1 при ошибке (устанавливает еггпо) */
unlockpt — разблокирует псевдотерминал
«inolude <stdlib.h>
int unlookpt(
int fd
);
/* Возвращает О в
/* дескриптор
олучае уопеха или
файла
-1 при
•/
ошибке
(устанавливает
еггпо)
•/
Этап 3 состоит в получении имени подчиненной стороны. Системы SUS1
предоставляют для этого системный вызов:
ptsname — получает имя подчиненной стороны псевдотерминала
«inolude <stdlib.h>
ohar *ptsnaroe(
int fd
);
/» Возвращает имя или
/* деокриптор
NULL в олучае
файла
ошибки
*/
(еггпо
не определено) */
Работая с системами BSD (макрос master_name_search определен), вы получаете имя
из имени главной стороны, изменяя «р» в выражении /dev/ptyXY на «t». To есть, если
вы обнаружили, что в качестве главной стороны может быть открыт файл /dev/ptyK4,
соответствующим именем подчиненной стороны будет /dev/ttyK4.
Ниже приведен код функции pt_open_master, которая вызывает функцию find_and_
open_master для выполнения этапа 1, после чего выполняет этапы 2 и 3- В итоге мы
получаем имя подчиненной стороны, но она еще не открыта.
PTINFO •pt_open_raaster(void)
{
256 ГЛАВА 4 Терминальный ввод-вывод
PTINF0 »р = NULL;
char *s;
ec_null( p = callccd, slzeof(PTINFO)) )
p->pt_fd_m = -1;
p->pt_fd_s = -1;
ec_false( find_and_cpen_niaster(p) )
«ifdef _XOPEN_UNIX
ec_neg1( grantpt(p->pt_fd_m) )
ec_neg1( unlcckpt(p->pt_fd_m) )
ec_null( s = ptsname(p->pt_fd_ni) )
if (strlen(s) >= PT_MAX_NAME) {
errnc = ENAMETOOLONG;
EC.FAIL
}
strcpy(p->pt_nanie_s, s);
#elif defined(MASTER_NAME_SEARCH)
st rcpy(p->pt_name_s, p->pt_nanie_ni);
p->pt_name_s[PTY_MS] = 't';
«else
errnc = ENOSYS;
EC.FAIL
#endif
return p;
EC_CLEANUP_BGN
if (p != NULL) {
(vcid)clcse(p->pt_fd_ui);
(vcid)clcse(p->pt_fd_s);
free(p);
}
return NULL;
EC_CLEANUP_END
}
Этап 4 — создание дочернего процесса при помощи вызова fork — выполняется
программой, использующей библиотеку: библиотечной функции, позволяющей
сделать это, нет.
Этап 5 заключается в выполнении в дочернем процессе вызова setsid (см. раздел
4.3-2), чтобы процесс оставил свой управляющий терминал, потому что мы хотим
сделать управляющим терминалом псевдотерминал. Этапом 6, который также
выполняется в дочернем процессе, является открытие подчиненной стороны, имя
которой у нас уже есть. Два этих этапа выполняет функция pt_cpen_slave:
bccl pt_cpen_slave(PTINFO *p)
{
ec_neg1( setsidO )
if (P->pt_fd_s != -1)
ГЛАВА 4 Терминальный ввод-вывод 257
ec_neg1( clcse(p->pt_fd_s) )
ec_neg1( p->pt_fd_s = cpen(p->pt_name_s, 0_RDWR) )
#if defined(NEED_TIOCSCTTY)
ec_neg1( icctl(p->pt_fd_s, TIOCSCTTY, 0) )
#endif
#if defined(NEED_STREAM_SETUP)
ec_neg1( icctl(p->pt_fd_s, I.PUSH, "ptem") )
ec_neg1( icctl(p->pt_fd_s, I_PUSH, "ldterm") )
#endif
h
Изменение режима не так уж и важно, так чтс ничего страшнсгс, если зтс
не сработает из-за тсгс, чтс мы не обладаем правами суперпсльзсвателя.
*/
if (fchmcd(p->pt_fd_s, PERM_FILE) == -1 && errnc != EPERM)
EC_FAIL
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Опять-таки, функция pt_open_slave должна быть вызвана в дочернем, а не в
родительском процессе (пример вы скоро увидите). В случае систем, для которых мы
определили NEEO_STREAM_SETUP, таких как Solaris, нужно поместить в поток два
модуля: ptem, эмулятор псевдотерминала, и ldterm, модуль нормального терминала.*
(Системные вызовы ioctl и команда I.PUSH стандартизированы, но имена модулей,
которые используются для настройки псевдотерминала, не стандартизированы).
В конце функции pt_cpen_slave мы изменяем режим работы псевдотерминала, так
как в некоторых системах он может не иметь подходящих прав. Однако, так как
он может не принадлежать идентификатору пользователя процесса (это зависит
от системы), вызов fchmod (см. раздел 3-7.1) может завершиться неудачей, но мы все
равно продолжаем начатое, надеясь на лучшее.
Этапы 7, 8 и 9 выполняются не при помощи pt-библиотеки, а с использованием
системных вызовов, представленных в главах 5 и 6. Как бы то ни было, в этом
разделе я приведу соответствующий код.
Так как дочерний (подчиненный) и родительский (главный) процессы
выполняются независимо друг от друга, родительский процесс перед началом чтения
данных с псевдотерминала и их записи на этапе 9 должен убедиться в том, что
дочерний процесс полностью настроен. В pt-библиотеке реализован предназначенный
для родительского процесса вызов, который не возвращает управление, пока не
станет ясно, что мы можем безопасно продолжить работу:
' Если вы имеете дело с системой Solaris (или другой системой, в которой реализован
механизм STREAMS), используйте для получения соответствующей информации команды man ptem
и man ldterm.
258 ГЛАВА 4 Терминальный ввод-вывод
bool pt_wait_mastBr(PTINFO *p)
{
fd_SBt fd_SBt_writB;
FD_ZER0(4fd_SBt_writB);
FD_SET(PT_GET_MASTER_FD(p), 4fd_SBt_writB);
Bc_neg1( SBlBCt(PT_GET_MASTER_FD(p) + 1, NULL, 4fd_SBt_writB, NULL,
NULL) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
В этой функции мы просто вызываем select (см. раздел 4.2.3), чтобы подождать, пока
не станет возможной запись на псевдотерминал.
Следующей функцией является функция pt_close_master, вызываемая в
родительском процессе для закрытия дескрипторов файлов, когда псевдотерминал
становится ненужным, и для освобождения структуры PTINF0:
bool pt_close_mastBr(PTINFO *p)
{
вс_пвд1( closB(p->pt_fd_m) )
ггвв(р);
return truB;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Есть также функция pt_closB_slave, но она обычно не вызывается, потому что
дочерний процесс «оккупирован» другой программой на этапе 8. Кроме того, так как
мы перенаправили дескрипторы файлов, сторона псевдотерминала,
соответствующая дочернему процессу, к этому времени представляется стандартными
дескрипторами файлов (0, 1 и 2), а они почти всегда при завершении процесса
закрываются автоматически. Тем не менее, вот эта функция:
bool pt_closB_slavB(PTINFO *p)
{
(void)olosB(p->pt_fd_s); /» вероятно, дескриптор ужв закрыт */
frBB(p);
return truB;
>
Если вы вызываете в своем коде pt_close_slave, это может свидетельствовать об
ошибке.
ГЛАВА 4 Терминальный ввод-вывод 259
Обратите внимание, что структура PTINF0 уничтожается и в pt_clcse_master, и в
pt_close_slave. Так и должно быть, потому что структура создается и в
родительском, и в дочернем процессах. Это станет более понятным при обсуждении
вызова fork в разделе 5.5.
Итак, вот общая схема использования вызовов pt-библиотеки:
PTINFO »p = NULL;
bccl ck = false;
ec_null( p = pt_cpen_master() ) /* Этапы 1, 2 и 3 */
switch (fcrk()) { /* Этап 4 */
case 0:
ec_false( pt_cpen_slave(p) ) /* Этапы 5 и 6 */
/*
Перенаправление дескрипторов файлов и выполнение
вызсва exec (не показано) - этапы 7 и 8
*/
break;
case -1:
EC_FAIL
>
ec_false( pt_wait_master(p) ) /* Синхронизация перед этапсм 9 */
/»
Теперь родительский (главный) прсцесс по мере надсбнссти
читает данные с псевдстерминала и записывает их - этап 9
*/
ck = true;
EC_CLEANUP
EC_CLEANUP_BGN
if (p != NULL)
(vcid)pt_clcse_raaster(p);
/*
Освободите здесь другие ресурсы
»/
exlt(ok ? EXIT_SUCCESS : EXIT_FAILURE);
EC_CLEANUP_END
Именно такую структуру будет иметь приложение, которое я собираюсь представить.
4.10.2 Программа, записывающая и воспроизводящая вывод
Я собираюсь использовать псевдотерминал для создания системы
записи/воспроизведения, позволяющей записать вывод команды и воспроизвести его, как если бы
команда была выполнена. Это означает не только то, что данные должны быть
выведены в первоначальном виде, но и что воспроизведение должно быть выполнено с
той же скоростью. Чтобы понять, что я имею в виду, взгляните на следующий
сценарий, который отображает время, ждет 5 секунд и снова отображает время:
260 ГЛАВА 4 Терминальный ввод-вывод
$ cat >script1
date
sleep 5
date
$ chmcd +x scriptl
$ scriptl
Wed Ncv 6 10:46:31 MST 2002 [пятисекундная пауза]
Wed Ncv 6 10:46:36 MST 2002
Как видите, между первым и вторым вызовами date прошло 5 секунд
(комментарии, приведенные в квадратных скобках, в выведенные данные не входят).
Если мы просто запишем вывод в файл и выведем этот файл на экран, текст будет
отображен слишком быстро. Конечно, временные показатели будут такими же, какие
записала команда date, но при выводе данных с помощью команды cat паузы не будет:
$ scriptl >tmp [пятисекундная пауза]
$ cat tmp
Wed Ncv 6 10:55:24 MST 2002 [паузы нет]
Wed Ncv 6 10:55:29 MST 2002
Это не то, что нам нужно. Мы хотим, чтобы записанные данные воспроизводились
с той же скоростью, с которой они были записаны, и создадим для этого команду
record (при вызове с параметром -р она будет воспроизводить записанное):
$ record scriptl
Wed Ncv 6 10:57:18 MST 2002 [пятисекундная пауза]
Wed Ncv 6 10:57:23 MST 2002
$ record -p
Wed Ncv 6 10:57:18 MST 2002 [пятисекундная пауза]
Wed Ncv 6 10:57:23 MST 2002
В книге я это продемонстрировать не могу, но можете мне поверить, что после
вывода первой строки перед выводом второй прошло 5 секунд, т. е. данные были
выведены именно так, как и были записаны,
Итак, команда record должна, как минимум, сохранять информацию о событиях —
время вместе с символами — чтобы модуль воспроизведения знал, насколько
быстро данные следует записывать в стандартный поток вывода. Мы также хотим, чтобы
наше приложение работало с интерактивными программами, поэтому оно не
может просто перехватывать вывод при помощи канала — мы должны использовать
псевдотерминал, так как многие интересные команды, такие как vi и emacs, а также
другие полноэкранные приложения, будут работать только на терминалах.
Отношения между модулем записи и командой, вывод которой записывается, показаны
на рис. 4.7. Все, что команда record читает из своего стандартного источника
ввода, записывается на псевдотерминал, а все, что она читает с псевдотерминала,
записывается в стандартный поток вывода и в файл событий.
ГЛАВА 4 Терминальный ввод-вывод 261
Сначала я приведу код структур данных и функций, записывающих вывод команд
в форме последовательности событий. Затем я приведу код функций
воспроизведения, которые читают
Рис. 4.7. Запись результатов выполнения Сценария1
события и выводят их данные в соответствующие моменты времени.
Воспроизведение осуществляется без использования псевдотерминалов, так как на самом деле
команда не выполняется. Наконец, я приведу код модуля записи, созданного в
соответствии со схемой использования pt-библиотеки, показанной в предыдущем
разделе.
Каждый раз, когда интересующий нас процесс записывает некоторые данные, главная
сторона псевдотерминала читает их и обрабатывает как одно событие. Она
сохраняет время, прошедшее с момента начала записи, и размер данных в структуре event.
Затем она записывает структуру и сами данные в файл recording.tmp.
Расположение трех событий в файле показано на рис. 4.8.
recording.tmp
I
событие 0 |
[время, данные,| данные
размер) .
1
событие 1 |
(время, данные! данные
размер) |
1
событие 2 |
(время, данные,|Данные
размер) .
-
Рис. 4.8. Схема файла recording.tmp
Вот структура event вместе с глобальным дескриптором файла и макросами,
определяющими имя файла и размер буфера, используемого при записи и чтении данных:
«define EVFILE "recording.trap"
«define EVBUFSIZE 512
struct event {
struct timeval e_tinie;
unsigned e_datalen;
>;
static int fd_ev = -1;
Структура timeval была описана в разделе 1.7.1.
262 ГЛАВА 4 Терминальный ввод-вывод
Модуль записи использует для открытия записываемого файла (создавая его, если
это нужно) функцию ev_creat, для записи события в файл — ev_write, а для
закрытия файла — ev_close:
static bool ev_creat(void)
{
ec_neg1( fd_ev = open(EVFILE, 0_WR0NLY | 0_CREAT | 0_TRUNC,
PERM_FILE) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
static bool ev_write(char «data, unsigned datalen)
{
struct event ev = { { 0 } };
get_rel_time(&ev.e_tirae);
ev.e_datalen = datalen;
ec_neg1( writeall(fd_ev, &ev, sizeof(ev)) )
ec_neg1( writeall(fd_ev, data, datalen) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
static void ev_close(void)
{
(void)close(fd_ev);
fd_ev = -1;
}
Функция writeall обеспечивает простой способ записи полного буфера, даже если
для этого нужно выполнить несколько вызовов write; она обсуждалась в разделе 2.9.
Функция get_rel_time получает при помощи функции gettiraeofday (см. раздел 1.7.1)
текущее время и вычитает из него начальное время (сохраненное при первом
вызове), используя другую функцию, tiraeval_subtract:
static void get_rel_tirae(struct timeval *tv_rel)
{
static bool first = true;
static struct timeval starttime;
struct timeval tv;
ГЛАВА 4 Терминальный ввод-вывод 263
if (first) {
first = false;
(void)gettiraeofday(&starttirae, NULL);
}
(void)gettiraeofday(&tv, NULL);
tiraeval_subtraot(&tv, &starttirae, tv_rel);
statio void tiraeval_subtraot(oonst struot tiraeval *x,
oonst struot tiraeval *y, struot tiraeval *diff)
{
if (x->tv_seo == y->tv_seo || x->tv_useo >= y->tv_useo) {
diff->tv_seo = x->tv_seo - y->tv_seo;
diff->tv_useo = x->tv_useo - y->tv_useo;
}
else {
diff->tv_seo = x->tv_seo - 1 - y->tv_seo;
■ diff->tv_useo = 1000000 + x->tv_useo - y->tv_useo;
}
Обратите внимание, что timeval_subtraot предполагает, что х >= у.
Это все функции, нужные для записи данных. Для их воспроизведения нужна
функция, открывающая записанный файл, и функция, читающая структуру event и
следующие за ней данные:
statio bool ev_open(void)
{
eo_neg1( fd_ev = open(EVFILE, 0_RD0NLY) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_EN0
}
statio bool ev_read(struot event *ev, ohar «data, unsigned datalen)
{
ssize_t nread;
eo_neg1( nread = read(fd_ev, ev, sizeof(*ev)) )
if (nread != sizeof(»ev)) {
errno = ЕЮ;
EC.FAIL
}
ec_neg1( nread = read(fd_ev, data, ev->e_datalen) )
if (nread != ev->e_datalen) {
errno = ЕЮ;
264 ГЛАВА 4 Терминальный ввод-вывод
EC_FAIL
>
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Еще кое-что: как только модуль воспроизведения получил событие, он должен
перед выводом данных подождать необходимое время. Это принципиальное
различие между обычным воспроизведением данных и воспроизведением данных в
реальном времени. Функция ev_sleep извлекает время из структуры event,
подсчитывает, сколько нужно подождать, и приостанавливается на этот интервал времени.
Структура timeval хранит время в секундах и микросекундах, поэтому мы
используем для ожидания и функцию sleep стандартного С, и системный вызов usleep (см.
раздел 9-7.3):
static bccl ev_sleep(struct timeval *tv)
{
struct timeval tv_rel, tv_diff;
get_rel_time(&tv_rel);
if (tv->tv_sec > tv_rel.tv_sec ||
(tv->tv_sec = tv_rel.tv_sec && tv->tv_usec >= tv_rel.tv_usec)) {
timeval_subtract(tv, 4tv_rel, &tv_diff);
(vcid)sleep(tv_diff.tv_sec);
ec_neg1( usleep(tv_diff.tv_usec) )
>
/• иначе уже слишком псзднс */
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
Это все, что нужно для записи и воспроизведения событий. Вот функция,
воспроизводящая уже записанные данные:
static bccl playback(vcid)
<
bccl ck = false;
struct event ev;
char buf[EVBUFSIZE];
struct termics tbuf, tbufsave;
ec_neg1( tcgetattr(STDIN_FILENO, &tbuf) )
tbufsave = tbuf;
ГЛАВА 4 Терминальный ввод-вывод 265
tbuf.c.lflag &= 'ECHO;
ec_neg1( tcsetattr(STDIN_FILENC, TCSAFLUSH, itbuf) )
ec_false( ev_cpen() )
while (true) {
ec_false( ev_read(&ev, buf, sizecf(buf)) )
if (ev.e_datalen == C)
break;
ev_sleep(&ev.e_time);
ec_neg1( writeall(STDCUT_FILENC, buf, ev.e_datalen) )
>
ec_neg1( write(STDCUT_FILENC, "\n", 1) )
ck = true;
EC.CLEANUP
EC_CLEANUP_BGN
(vcid)tcdrain(STDCUT_FILENC);
(vcid)sleep(l); /• Предоставим терминалу всзмсжнссть стреагирсвать. */
(vcid)tcsetattr(STDIN_FILENC, TCSAFLUSH, itbufsave);
ev_clcse();
return ck;
EC.CLEANUP.END
}
Чтение событий и запись в STDDUT_FILEND реализованы в этой функции довольно просто.
Однако отправление определенных escape-последовательностей вызывает ответ
терминала, и хотя во время воспроизведения ответ нас не волнует (что бы он ни
означал, это уже отражено в записанных данных), мы должны отключить эхо, чтобы ответ
терминала не исказил вывод. В конце функции мы вызываем ted rain для вывода
оставшихся данных, ожидаем одну секунду, чтобы терминал мог их обработать, и
отбрасываем любой оставшийся ввод при восстановлении атрибутов терминала.
Функции tcgetattr, tcsetattr и tcdrain были описаны в разделах 4.5.1 и 4.6.
Теперь мы можем использовать pt-библиотеку вместе с методами «ev» для записи
данных (если нужно, вернитесь к схеме, описанной в конце предыдущего
раздела). Я приведу код функции main по частям; начинается она так:
int main(int argc, char »argv[])
{
bool ok = false;
PTINFD «p = NULL;
if (argc < 2) {
fprintf(stderr, "Usage: record end ...\n record -p\n");
exit(EXIT_FAILURE);
>
if (strcmp(argv[1], "-p") == D) {
playback();
ok = true;
26C ГЛАВА 4 Терминальный ввод-вывод
EC_CLEANUP
Если программа запущена с параметром -р, она просто вызывает функцию playback,
которую мы уже обсудили, и завершается. В противном случае она выполняет
запись в соответствии с уже описанной схемой:
ec_null( p = pt_open_master() )
switch (fork()) {
case 0:
ec_false( pt_cpen_slave(p) )
ec_false( exec_redirected(argv[1], iargv[1], PT_GET_SLAVE_FD(p),
PT_GET_SLAVE_FD(p), PT_GET_SLAVE_FD(p)) )
break;
case -1:
EC_FAIL
}
ec_false( ev_creat() )
ec_false( pt_wait_niaster(p) )
Все, что связано с перенаправлением дескрипторов файлов и выполнением команды,
я вынес в функцию ехес_redirected, потому что я собираюсь обсудить эти темы в
главах 5 и 6. Я приведу код ехес_redirected, не объясняя его. Прочитав главы 5 и 6,
вы можете вернуться к нему.
Пока что в функции main мы дошли до этапа 9 (см. предыдущий раздел), на
котором мы начинаем читать и записывать данные, используя дескриптор файла
главной стороны псевдотерминала. Имейте в виду, что команда (scripti, vi или другая),
вывод которой мы записываем, уже выполняется и, вероятно, заблокирована в
вызове read в ожидании некоторого ввода с того, что она считает терминалом. Вот
оставшаяся часть функции main:
tc_setraw();
while (true) {
fd_set fd_set_read;
ohar buf[EVBUFSIZE];
ssize_t nread;
FD_ZERO(ifd_set_read);
FD_SET(STDIN_FILENO, ifd_set_read);
FD_SET(PT_GET_MASTER_FD(p), ifd_set_read);
ec_neg1( select(FD_SETSIZE, ifd_set_read, NULL, NULL, NULL) )
if (FD_ISSET(STDIN_FILENO, ifd_set_read)) {
ec_neg1( nread = read(STDIN_FILENO, ibuf, sizecf(buf)) )
ec_neg1( writeall(PT_GET_MASTER_FD(p), buf, nread) )
}
if (FD_ISSET(PT_GET_MASTER_FD(p), ifd_set_read)) {
if ((nread = read(PT_GET_MASTER_FD(p), ibuf,
sizecf(buf))) > 0) {
ГЛАВА 4 Терминальный ввод-вывод 267
ec_false( ev_write(buf, nread) )
ec_neg1( writeall(STDOUT_FILENO, buf, nread) )
}
else if (nread == 0 || (nread == -1 && errno == EIO))
break;
else
EC.FAIL
}
}
ec_neg1( ev_write(NULL, 0) )
fprintf(stderr,
"EOF cr errcr reading stdin cr master pseudo-terminal; exiting\n");
ck = true;
EC_CLEANUP
EC_CLEANUP_BGN
if (p != NULL)
(vcid)pt_clcse_maste r(p);
tc_restcre();
ev_clcse();
prirrtf("\n");
exit(ck ? EXIT_SUCCESS : EXIT.FAILURE);
EC_CLEANUP_END
>
Комментарии по поводу этой части main.
• Так как record будет использоваться в основном с экранными командами, мы
переводим терминал в простой режим при помощи функции tc.setraw (см.
раздел 4.5Л0). Атрибуты восстанавливаются функцией tc_restcre в коде очистки. Мы
не использовали эти функции при создании функции playback, потому что
тогда мы хотели лишь отключить эхо.
• Программа record имеет два источника ввода: то, что печатает пользователь, и
то, что отсылает назад псевдотерминал (см. рис. 4.5). Она вызывает select для
ожидания того, пока один из этих двух источников не сможет предложить
некоторые данные, обрабатывает их, после чего возвращается в цикле к select.
Обратите внимание, что программа каждый раз настраивает переменную fd_set
_read, так как вызов select, сообщая результаты, изменяет ее биты.
• Мы выходим из цикла, когда достигаем конца файла или получаем сообщение
об ошибке ввода-вывода от псевдотерминала, а не от стандартного источника
ввода, используемого record. Как-никак, решать, когда все завершено, должны не
мы, а команда, с которой мы работаем. При достижении конца файла или ошибке
ввода-вывода все дескрипторы файлов подчиненной стороны
псевдотерминала закрыты, т. е. команда завершила работу. Указывающий на это признак
зависит от системы, так что мы обрабатываем оба варианта.
Наконец, как я и обещал, вот код функции exec.redirected, который вы сможете понять,
прочитав главу 6:
268 ГЛАВА 4 Терминальный ввод-вывод
bool exeo_redlreoted(oonst char *flle, ohar «const argv[], lnt fd_stdin,
lnt fd_stdout, lnt fd_stderr)
{
if (fd.Stdln != STDIN.FILENO)
ec.negK dup2(fd_stdln, STDIN_FILENO) )
If (fd.Stdout != STD0UT_FILEN0)
eo.negK dup2(fd_stdout, STDOUT.FILENO) )
If (fd.Stderr != STDERR_FILENO)
eo_neg1( dup2(fd_stderr, STDERR.FILENO) )
if (fd.stdin != STDIN_FILENO)
(void)olose(fd_stdln);
if (fd_Stdout != STD0UT_FILEN0)
(void)close(fd_stdout);
if (fd_stderr != STDERR_FILENO)
(void)close(fd_stderr);
ec_neg1( execvp(file, argv) )
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Без видеоролика трудно представить работу программы record. Так испытайте ее сами!
Вы улыбнетесь, когда увидите, как она воспроизводит записанные данные с теми же
временными интервалами — как будто на клавиатуре печатает привидение.
Упражнения
4.1. Измените функцию getln, чтобы она возвращала строки, заканчивающиеся EOF.
Мы обсуждали это в разделе 4.2.1.
4.2. Реализуйте функцию Bfdopen, о которой речь шла в разделе 4.2.1.
4.3- Реализуйте упрощенную версию команды stty; пусть она просто выводит
информацию о текущем статусе терминала.
4.4. Реализуйте версию команды stty, позволяющую пользователю указать только
10 операндов, используемых чаще всего. Вы должны будете решить, что это
за операнды.
4.5. Создайте простой экранный редактор. Реализуйте небольшой набор функций,
обеспечивающих базовые возможности редактирования без особых тонкостей.
Храните редактируемый текст во внутренней памяти. Используйте для
обновления экрана и чтения данных с клавиатуры библиотеку Curses (или эквивалент).
Если сможете, напишите документацию к программе, используя макросы man.
4.6. Реализуйте сервер telnetd. Полный список ожидаемых от него возможностей
можно найти в [RFC854]. Однако вы можете реализовать лишь те воможнос-
ти, которые нужны для подключения к серверу с другого компьютера с
использованием telnet и выполнения утилит командной строки, а также vi или emacs.
!!№■■■■ 5
Процессы и потоки
5.1 Введение
Теперь оставим в покое тему ввода-вывода и займемся исследованием
многозадачности в UNIX. В этой главе будут рассмотрены вопросы запуска программ и
процессов с помощью exec, fork, wait и родственных им системных вызовов. В
следующей главе мы перейдем к изучению простейших способов взаимодействия между
процессами с помощью каналов. Более сложные механизмы взаимодействия будут
обсуждаться в главах 7 и 8.
В процессе изучения этой темы мы напишем свой собственный командный
интерпретатор (shell), для начала реализовав довольно ограниченную его версию.
Затем будем постепенно расширять его возможности и в следующей главе закончим
разработку. В результате у нас получится командный интерпретатор, способный
выполнять перенаправление ввода-вывода, обслуживать конвейеры команд, запускать
фоновые процессы и имеющий переменные среды окружения.
5.2 Среда окружения
Начнем обсуждение со среды окружения, с которой большинство пользователей
UNIX уже знакомы на уровне командной оболочки. При запуске любая программа
UNIX получает от вызывающего процесса два набора данных: аргументы
командной строки и переменные среды окружения. В программах на языке С оба набора
представлены в виде массивов указателей, причем последний указатель в каждом
из массивов имеет значение NULL. Кроме того, программа получает счетчик,
содержащий количество элементов в массиве аргументов. В других языках
программирования эти данные передаются иначе, но мы рассматриваем только языки С и C++.
Исполнение программ на языках С и C++ начинается с функции main(), которая может
быть определена одним из двух способов":
Некоторые реализации допускают передачу функции main() третьего аргумента — массива
строк, который содержит переменные окружения. Но это отступление от стандартов и к
тому же совершенно излишнее.
270 ГЛАВА 5 Процессы и потоки
main — точка входа в программу на языке С или C++
int main(
int argc, /* количестве аргументов */
char *argv[] /* массив аргументов в виде стрск */
)
int main(void)
Параметр argc содержит количество аргументов командной строки без учета
последнего, пустого указателя, который завершает массив argv. Если программа не
предусматривает прием аргументов, то argc и argv могут быть опущены и
использована вторая форма объявления.
Кроме того, любая программа получает в свое распоряжение глобальную переменную
environ. Она указывает на массив с переменными окружения, который также
завершается пустым указателем (переменной, в которой хранилось бы количество
элементов массива, не предусмотрено):
environ -
extern
- переменные окружения
char
••environ;
/•
/•
массив переменных
(нет ни
среды
окружения •/
в одном из заголовочных файлов)
•/
Строки в массиве аргументов могут содержать любую комбинацию символов, но
каждая строка обязательно завершается символом «\0». Строки с переменными
окружения имеют более строгую форму. Каждая из них записывается в виде: «имя=
значение*, и также завершается символом «\0». Само собой разумеется, имя
переменной окружения не может содержать символ «=•>.
Процесс передачи переменных окружения в функцию main() мы рассмотрим в
разделе 5.3, здесь же я лишь хочу показать, как получать и изменять переменные
окружения из массива environ.
Самый простой способ — обратиться к массиву environ напрямую:
extern char ««environ;
int main(void)
{
int i;
for (i = 0; environ[i] != NULL; i++)
printf("Xs\n", environ[i]);
exit(EXIT_SUCCESS);
>
В ОС Solaris я получил следующее (ради экономии места приводится не весь список):
H0ME=/home/marc
HZ=100
ГЛАВА 5 Процессы и потоки 271
LC_COLLATE=en_US.IS08859-1
LC_CTYPE=en_US.IS08859-1
LC_MESSAGES=C
LC_MONETARY=en_US.IS08859-1
LOGNAME=marc
MAIL=/va r/mail/ma re
Как правило, в программах не требуется просматривать весь список, чаще всего
необходимо получить значение какой-нибудь определенной переменной. Для этой
цели используется стандартная функция getenv:
getenv — возвращает
«Include <stdllb.h>
char *getenv(
censt char *var
значение переме
/* имя переменней
/* Возвращает значение переменней или
(значение переменней
иной
•/
NULL,
еггпс не спределенс) <
окружения
если
-/
таксвая
не найдена
Функция getenv возвращает часть строки, которая стоит после символа «=»,
например, программа:
Int maln(vcld)
{
char *s;
s = getenv("LOGNAME");
if (s == NULL)
printf("nepeMeHHafl не найдена\п");
else
printf("значение переменней: \"Xs\"\n", s);
exit(EXIT_SUCCESS);
у меня вывела:
значение переменней: "marc"
Изменить значение переменной окружения не так просто, как прочитать. Хотя этот
массив и является исключительной собственностью приложения и может изменяться
без каких-либо ограничений, тем не менее, нет никаких гарантий, что в нем
предусмотрено дополнительное место под новые строки или что в строку можно
записать более длинное значение переменной. Таким образом, изменение среды
окружения есть задача нетривиальная — необходимо полностью пересоздать
массив строк. После этого можно будет изменить указатель envircn, записав туда адрес
нового массива. Измененная версия среды окружения может быть передана новым
программам, запускаемым из данного приложения (мы увидим это в разделе 53),
272 ГЛАВА 5 Процессы и потоки
к ней же будут обращаться и все последующие вызовы getenv. Изменения не
коснутся других приложений, включая командный интерпретатор, который запустил
процесс, выполнивший модификацию. Таким образом, если вы хотите изменить
переменные окружения командной оболочки, нужно делать это с помощью самой
командной оболочки.
Вместо того, чтобы работать с массивом envircn напрямую, можно
воспользоваться стандартными функциями:
putenv — изменяет или добавляет
«include <stdlib.h>
int putenv(
char «string /* стрска е
v
переменные в
еиде «имя=значение»
/» Всзеращает О е случае успеха,
сшибки - e переменней errnc)
•/
среду окружения
*/
ненулеесе значение -
е случае
сшибки
(кед
setenv — изменяет или добавляет переменные в среду окружения
«include <stdlib.h>
int setenv(
censt char «var,
censt char *val,
int overwrite
);
/* Всзеращает 0 e ел
e переменней errnc)
/*
/*
/*
учае
*/
переменная, кстсрая дсбаеляется или изменяется */
значение
затереть
успеха,
•/
старее значение? ♦/
-1 е случае сшибки (кед сшибки -
unsetenv — удаляет переменную среды окружения
«include <stdlib.h>
int unsetenv(
censt char *var /«
);
/* Всзеращает О е случа
e переменней errnc) */
удаляемая
e успеха,
переменная
-1 e случае
*/
сшибки
(кед
сшибки -
Все эти функции вносят изменения в массив, который хранит переменные среды
окружения и, возможно, изменяют сам указатель envircn, если в массиве обнаружится
нехватка места. Если вы самостоятельно изменяете указатель envircn или какую-либо
часть массивй, на который он указывает, то поведение этих функций может быть
непредсказуемым. Поэтому выбирайте всегда что-то одно — или вы пользуетесь
стандартными функциями, или взаимодействуете со средой окружения
самостоятельно; никогда не смешивайте эти два способа.
Функция putenv принимает строку в виде «имя=значение» и вставляет в массив envircn
указатель на нее, в результате строка становится частью среды окружения. По этой
ГЛАВА 5 Процессы и потоки 273
причине никогда не передавайте функции данные с автоматическим классом
размещения (локальные и нестатические переменные) и не изменяйте их
содержимое после вызова putenv.
Функция setenv более сложная. Она копирует имя переменной и ее значение в
специально отведенное для этого место. Если переменная уже существует, ее
значение будет изменено при условии, что аргумент overwrite не равен нулю, в
противном случае «старое» значение останется без изменений. Если переменной не
существует, она будет добавлена в среду окружения, в этом случае значение
аргумента overwrite игнорируется.
Функция unsetenv является парной к setenv. Она удаляет переменную среды
окружения. Если ваша система не поддерживает функцию unsetenv, то самый лучший
способ «удалить» переменную — присвоить ей в виде значения пустую строку. В
некоторых случаях это может оказаться неприемлемым, но это уже зависит от самого
приложения. (Некоторые системы, такие как Linux, FreeBSD и Darwin определяют
unsetenv как void и в этих системах она не имеет возвращаемого значения.)
Несмотря на то, что эти функции стандартизованы, их наличие в системе не
является обязательным. Операционные системы FreeBSD, Linux и Solaris имеют
функцию putenv, а первые две — все три функции. Спецификация SUS3 требует наличия
в системе функций setenv и unsetenv, но не все системы следуют ей. Таким образом,
самый простой способ определить наличие этих функций следующий: setenv и
unsetenv пришли из мира BSD, поэтому все производные системы, включая FreeBSD,
поддерживают их. Функция putenv пришла из System V и должна присутствовать во
всех системах, производных от System V, включая Solaris. Поскольку SUS3
рекомендует поддержку функций setenv и putenv, лучшим выходом будет все-таки
использовать их, а для систем, которые не поддерживают setenv и putenv — включать в
приложения собственные версии этих функций.
Функции setenv и putenv достаточно просты в реализации, если вас не волнует
возможная утечка памяти. Проблема состоит в том, что они могут выделять или (в случае
unsetenv) оставлять неиспользуемые блоки памяти, но они не могут освобождать
занятую память из-за того, что точно не известно, как эта память была
распределена в самый первый раз. Несмотря на этот дефект, приведу свою версию setenv:
int setenv(const char «var, const char *val, int overwrite)
{
int i;
size_t varlen;
char »*e;
if (var == NULL || val == NULL || var[0] == *\0" II
strchr(var, '=•) != NULL) {
errno = EINVAL;
return -1;
}
varlen = strlen(var);
274 ГЛАВА 5 Процессы и потоки
for (i = 0; environ[i] != NULL; i++)
if (strnomp(environ[i], var, varien) == 0 &&
environ[i][varlen] == '=')
break;
if (environ[i] == NULL) {
if ((e = maiioo((i + 2) * sizeof(ohar *))) == NULL)
return -1;
memopy(e, environ, i * sizeof(ohar »));
/* возможна утечка памяти из-за потери отарого маооива указателей */
environ = e;
environ[i + 1] = NULL;
return setnew(i, var, vai);
>
eise {
if (overwrite) {
if (strien(&environ[i][varien + 1]) >= strien(vai)) {
strcpy(&environ[i][varien + 1], vai);
return 0;
}
return setnew(i, var, vai);
}
return 0;
}
}
Комментарии к функции.
• Первое, что делает функция — проверяет входные аргументы на соответствие
стандарту.
• Затем выполняется поиск переменной, которая возможно уже существует в списке.
Обратите внимание: мы ищем строку, начинающуюся с заданной
последовательности символов и заканчивающуюся символом «=».
• Если переменная найдена и аргумент overwrite равен нулю, то на этом работа
функции завершается. В противном случае возможны две ситуации: если новое
значение короче старого, то новое значение просто копируется на место
старого, в противном случае вызывается функция setnew (показана ниже), которая
вставит новую строку в среду окружения.
• Если переменная не найдена, выполняется увеличение размеров массива. Мы не
можем использовать функцию realloo, потому что не знаем, как этот массив был
размещен ранее. Поэтому, с помощью malloo выделяется объем памяти под
новый массив, куда выполняется копирование содержимого старого массива.
Затем в конец нового массива вставляется «пустой» указатель и вызывается
функция setnew, которая вставит в массив новую строку.
Функция setnew выделяет память для новой строки с учетом длины имени,
значения и символа *=» и записывает в массив указатель на эту строку:
ГЛАВА 5 Процессы и потоки 275
static int setnew(int i, ccnst char *var, ccnst char *val)
{
char «s;
if ((s = niallcc(strlen(var) + 1 + strlen(val) + 1)) == NULL)
return -1;
strcpy(s, var);
strcat(s, "=");
strcat(s, val);
/* возможна утечка памяти из-за пстери указателя envircn[i] */
envircn[i] = s;
return 0;
>
А теперь напишем функцию unsetenv, которая также сначала проверяет корректность
входных аргументов и, двигаясь по массиву, находит нужную переменную. Если
переменная найдена, то выполняется сдвиг оставшейся части массива на один
элемент вверх, при котором затирается указатель на строку с удаляемой переменной.
Такой подход также чреват утечками памяти, поскольку мы не знаем, как
освободить память, занимаемую удаляемой переменной.
int unsetenv(ccnst char *var)
{
int i, fcund = -1;
size_t varlen;
if (var == NULL || var[0] == '\0' || strchr(var, '=') != NULL) {
errnc = EINVAL;
return -1;
}
varlen = strlen(var);
fcr (i = 0; envircn[i] != NULL; i++)
if (strncrap(envircn[i], var, varlen) == 0 &&
envircn[i][varlen] == '=')
fcund = i;
if (fcund != -1)
/• всзмсжна утечка памяти из-за пстери указателя envircn[fcund] */
memmcve(&envircn[found], &envircn[fcund +1],
(i - found) * sizecf(char *));
return 0;
>
Обратите внимание: вместо memcpy мы использовали memmcve, так как она
гарантирует копирование перекрывающихся областей памяти, что в данном случае
определенно имеет место.
276 ГЛАВА 5 Процессы и потоки
Вы можете задаться вопросом, почему в этих функциях не использовались
макроопределения проверки ошибок «ее». Причина в том, что мы хотели приблизить их
поведение к стандартным функциям как можно ближе.
Устранить утечки памяти не так сложно, как может показаться на первый взгляд,
это будет наше первое упражнение в данной главе (см. упражнение 5.1).
5.3 Системный вызов exec
Невозможно понять системные вызовы exec и fork без четкого понимания
различий между процессом и программой. Если вы встречаетесь с этими терминами
впервые, рекомендую вернуться к разделу 1.1.2. Если вы готовы продолжить
чтение, то в двух словах напомню суть различий: процесс — это среда исполнения,
которая включает в себя сегмент исполняемого кода, сегменты пользовательских
и системных данных, а также набор дополнительных ресурсов, полученных во время
исполнения. Программа — это файл, который содержит исполняемый код, сегменты
с данными для инициализации и с данными пользователя.
Системный вызов exec повторно инициализирует процесс, подменяя его указанной
программой — программа меняется, а процесс остается. Системный вызов fork (см.
раздел 55) наоборот запускает новый процесс, который является точной копией
существующего, простым копированием сегментов с исполняемым кодом и
данными. Вновь созданный процесс продолжает работу с того же места, что и
изначальный, таким образом, оба процесса продолжают исполнять один и тот же код.
По отдельности друг от друга exec и fork используются крайне редко. В разделе 5.4
мы будем использовать их вместе друг с другом, и вы убедитесь, насколько
мощной может быть эта пара.
Системный вызов exec — единственный способ запуска программ в UNIX. Мало того,
что командная оболочка использует exec для запуска наших программ, но и сама
она запускается именно таким образом. А системный вызов fork — единственный
способ запустить новый процесс.
На самом деле, системного вызова с именем exec не существует. Под этим именем
подразумевается целое семейство из шести системных вызовов, имена которых в
общем виде можно записать как ехесАВ. А — это один из символов, «1» или «v», они
определяют, как входные аргументы передаются вызову — в виде списка (от англ.
list) или в виде массива (от англ. vector). В (может отсутствовать) — это либо «р»,
указывающий, что поиск файла программы должен выполняться с помощью
переменной PATH, либо «е» — такому вызову передается специфичная среда
окружения (как это ни странно, но нет системного вызова exec, который совмещал бы в
себе характерные особенности «е» и «р»). Таким образом, мы получаем шесть
различных системных вызовов: execl, execv, execlp, execvp, execle и execve*. Обсуждение
мы начнем с execl, а затем рассмотрим и все остальные.
' На самом деле, та же самая функциональность достаточно просто может быть
реализована двумя системными вызовами вместо шести, но об этом чуть позже. См. упражнение 5.6.
ГЛАВА 5 Процессы и потоки 277
execl -
- запускает
программу; входные аргументы передаются в
«include <unistd.h>
int
);
л
execl(
ccnst char
ccnst char
ccnst char
• • • i
NULL
•path,
•argO,
*arg1,
/*
Л
Л
Л
Л
полный
первый
втсрсй
путь к программе */
аргумент (arg[0] - имя файла)
аргумент (если необходимо) */
остальные аргументы (если необходимы)
пустей
В случае сшибки возвращает -1
указатель, завершающий список
виде списка
*/
*/
*/
(кед сшибки - в переменней еггпс)
*/
Аргумент path должен содержать полное имя исполняемого файла с программой
для действующего идентификатора пользователя (например, с правами 755).
Исполняемый код процесса будет «затерт» исполняемым кодом новой программы,
сегмент данных также будет затерт сегментом данных новой программы, а стек будет
переинициализирован. Исполнение новой программы начнется с самого начала
(т. е. будет вызвана функция main()).
В случае успеха возврат из execl не предусмотрен, потому что точка возврата
будет утеряна. В случае ошибки execl вернет -1, не имеет смысла проверять это
значение, поскольку никакого другого вы не получите. Наиболее частые причины,
приводящие к невозможности запустить новую программу, это либо отсутствие
файла с программой, либо отсутствие права на ее запуск.
Все остальные аргументы вызова собираются в массив указателей на строки, а
последним всегда должен стоять пустой указатель, который определяет конец списка
входных аргументов. Первый аргумент, в соответствии с соглашениями — это имя
файла программы (только само имя файла, без пути к нему). Новая программа
получает доступ к этому списку через уже знакомые нам аргументы функции main()
— а где и argv. Среда окружения также передается новой программе и доступна ей
через указатель environ или посредством функции getenv, которая обсуждалась в
предыдущем разделе.
Поскольку процесс продолжает существовать и сегмент с системными данными
практически не изменяется, почти все атрибуты процесса также остаются
неизменными, включая идентификатор процесса, идентификатор процесса-предка,
идентификатор группы процесса, идентификатор сессии, управляющий терминал,
реальные идентификаторы пользователя и группы, текущий и корневой каталоги,
приоритет, статистические характеристики времени исполнения в пространстве
пользователя и в пространстве ядра и, как правило, дескрипторы открытых
файлов. Гораздо проще перечислить основные атрибуты, которые изменяются.
• Если процесс назначал свои обработчики сигналов, то все они сбрасываются в
исходное состояние, поскольку функции-обработчики после запуска новой
программы станут недоступны. (Более подробно о сигналах читайте в главе 9-)
278 ГЛАВА 5 Процессы и потоки
• Если в новой программе установлены биты set-user-ID или set-group-ID, то
действующие идентификаторы пользователя и группы переустанавливаются в
соответствии с идентификаторами владельца и группы файла программы. Нет
никакого способа вернуть прежние действующие идентификаторы, если они
отличаются от реальных.
• Регистрация всех функций, которая была выполнена с помощью atexit(),
отменяется, поскольку код этих функций будет затерт.
• Сегменты общей памяти (см. раздел 7.12) отсоединяются, поскольку точки
соединения будут утеряны.
• Именованные семафоры POSIX (см. раздел 7.10) закрываются. Семафоры System
V (см. раздел 7.9) остаются без изменений.
Полный список атрибутов вы найдете в приложении А, но суть состоит в
следующем: если сохранение атрибута или ресурса не имеет смысла, то он или
сбрасывается в состояние по умолчанию, или закрывается.
Для демонстрации системного вызова execl я написал следующий пример:
void exectest(void)
{
printf("Шустрая рыжая лисица перепрыгнула через ");
ec_neg1( execK"/bin/echo", "echo", "ленивую", "собаку.", NULL) )
return;
EC_CLEANUP_BGN
EC_FLUSH("exectest");
EC_CLEANUP_END
}
В результате работы этой функции мы получим:
ленивую собаку.
А что же произошло с лисицей? Получается, что она не такая уж и шустрая.
Стандартная библиотека ввода-вывода, в состав которой входит функция printf,
буферизует вывод и по завершении процесса все незаполненные буферы выталкиваются.
Но перед вызовом execl процесс не завершался и буфер вывода, находившийся в
сегменте данных пользователя, оказался затертым еще до того, как он мог бы быть
вытолкнут.
Эта проблема легко решается либо отказом от буферизации вывода:
setbuf(stdout, NULL);
либо принудительным выталкиванием буфера непосредственно перед вызовом execl:
fflush<stdout);
ГЛАВА 5 Процессы и потоки 279
Как я уже упоминал, дескрипторы файлов после вызова execl остаются открытыми. Если
такое положение вещей вас не устраивает, то необходимо явно закрыть дескрипторы
системным вызовом clcse. Например, предположим, что программа требует, чтобы
абсолютно все дескрипторы были закрыты (очень необычное требование). В этом случае
можно попробовать такой прием:
errnc = 0;
ec_neg1( cpenjnax = sysccnf(_SC_OPEN_HAX) )
fcr (fd =0; fd < cpenjnax - 1; fd++)
(vcid)clcse(fd); /* игнорируем возможные сшибки */
ec_neg1( execKpath, argO, argl, arg2, NULL) )
Для того, чтобы получить максимально возможное число открытых файлов,
вызывается функция sysccnf (см. раздел 1.5.5) Номер самого последнего дескриптора на
1 меньше этого числа. Мы так неосторожны при закрытии дескрипторов, что даже
не обременяем себя проверкой был ли он открыт на самом деле, мы просто
стремимся закрыть все подряд. Это один из редких случаев, когда нас не интересуют
возможные ошибки при обращении к системному вызову.
Все будет идти хорошо, пока execl не потерпит неудачу — мы даже не увидим
сообщения об ошибке, поскольку все дескрипторы, в том числе и дескриптор с
номером 2, будут закрыты! Мы как раз подошли к тому моменту, когда нам может
понадобиться флаг FD.CLOEXEC (закрыть при запуске), с которым мы уже
познакомились в разделе 3-8.3- Если этот флаг установлен (с помощью fcntl), в случае
успешного запуска программы дескриптор будет закрыт, иначе дескриптор
останется открытым. Мы можем установить этот флаг как на отдельные дескрипторы, так
и на все сразу:
for (fd = 0; fd < cpenjnax - 1; fd++) {
ec_neg1( flags = fcntKfd, F_SETFD) )
ec_neg1( fcntl(fd, F_SETF0, flags | FD_CL0EXEC) )
>
ec_neg1( execl(path, argO, argl, arg2, NULL) )
Проблема закрытия или установки флага на все дескрипторы заключается в том,
что их может быть несколько тысяч, в результате будет проделан огромный объем
совершенно ненужной работы. В то же время было бы неправильным перед
обращением к exec оставлять открытыми любые дескрипторы кроме стандартных, если
конечно вызываемая программа не предполагает наличия каких-либо открытых
дескрипторов, кроме первых трех, что встречается довольно редко. В большинстве
случаев вы должны постоянно следить за тем, какие файлы были открыты и
закрывать их явно перед обращением к exec.
Так как стандартные дескрипторы в большинстве случаев остаются открытыми, флаг
FD_CL0EXEC используется в отношении них довольно редко. В качестве примера, когда
желательно оставить дескриптор открытым, можно привести файл журнала.
В случае неудачного завершения вызова exec ваша программа сможет отметить этот
факт в журнале.
ЮЗак. 4030
280 ГЛАВА 5 Процессы и потоки
Другие пять вызовов семейства exec предоставляют три дополнительные
возможности, недоступные в execl.
• Передать аргументы в виде массива, а не в виде списка. Это совершенно
необходимо, когда число аргументов в момент написания программы (например
командного интерпретатора) заранее неизвестно.
• Поиск файла программы с использованием значения переменной окружения PATH,
как это делает командная оболочка.
• Передавать указатель на сформированную «вручную» среду окружения вместо
неявной передачи указателя environ'.
Ниже приводится краткое описание остальных системных вызовов семейства exec:
execv — запускает программу; аргументы передаются в виде массива
«include <unistd.h>
int execv(
const char «path, /* полный путь к файлу с программой */
char «const argv[] /* массиа аргументса */
);
/* В случае сшибки асзаращает -1 (кед сшибки - а переменней еггпс) */
exeelp — запускает программу; аргументы передаются в виде списка, поиск
файла ведется с использованием переменной PATH
«include <unistd.h>
int execlp(
const char «file, /* имя файла с программой */
censt char *argO, /* пераый аргумент (имя файла) */
const char *arg1, /* атерей аргумент (если необходим) */
..., /* остальные аргументы (если необходимы) */
NULL /• пустей указатель, зааершапций список аргументса •/
);
/* В случае сшибки асзаращает -1 (кед сшибки - а переменней еггпс) */
execvp — запускает программу; аргументы передаются в виде массива,
поиск файла ведется с использованием переменной PATH
«include <unistd.h>
int execvp(
const char «file, /• имя файла с программой •/
char «const argv[] /* массиа аргументов */
);
/• В случае ошибки возаращает -1 (код ошибки - а переменной errno) */
' См. упражнение 52.
ГЛАВА 5 Процессы и потоки 281
execle — запускает программу; аргументы передаются в виде списка, так же
передается среда окружения
«include <unistd.h>
int execle(
ccnst char «path, /* пслный путь к файлу с программой */
const char *argO, /* первый аргумент (имя файла) */
ccnst char *arg1, /* втсрсй аргумент (если необходим) »/
..., /* остальные аргументы (если необходимы) */
NULL /* пустей указатель, завершающий список аргументов */
char «ccnst enw[] /* массив сформированной среды окружения */
);
/* В случае сшибки возвращает -1 (кед сшибки - в переменней еггпс) */
execve — запускает программу; аргументы передаются в виде массива, так
же передается среда окружения
«include <unistd.h>
int execve(
ccnst char *path, /* полный путь к файлу с программой */
char *ccnst argv[] /* массив аргументов */
char «const enw[] /* массив сформированной среды окружения */);
/* В случае сшибки возвращает -1 (кед сшибки - в переменней еггпс) */
Обратите внимание: используемый здесь аргумент argv абсолютно идентичен
аргументу argv, передаваемому функции main. He забывайте: последним в этом
массиве всегда должен стоять NULL. Если аргумент file в вызовах execlp или execvp не
содержит символ «/», то выполняется последовательная подстановка имен
каталогов из переменной PATH и производится поиск файла с правом на исполнение. Если
такой файл найден и он является исполняемой программой (проверяется
сигнатура файла), то она запускается на исполнение. Если нет, то делается
предположение о том, что файл является сценарием и вызывается командный интерпретатор,
которому передается путь к файлу в первом аргументе.
Например, если file — это команда echo, и в результате поиска был обнаружен файл
/bin/echo, то этот файл будет запущен как программа, потому что он содержит
команды микропроцессора. А если в качестве аргумента передать, например, имя
myscript, и в результате будет найден файл /heme/marc/myscript, который не
является двоичным исполняемым файлом, то execvp и execlp выполнят эквивалент команды:
sh /hone/marc/nyscript argl arg2 _
Существует еще одно универсальное соглашение: если первая строка сценария
записана в форме:
#! pathname [arg]
282 ГЛАВА 5 Процессы и потоки
тогда для запуска сценария вместо интерпретатора командной оболочки
используется интерпретатор, указанный в строке pathname, которому в качестве первого
аргумента передается имя файла. Интерпретатор должен правильно воспринимать
строку, начинающуюся с «*!», потому что в большинстве языков сценариев с
символа «#» начинаются комментарии. Если в строке «#!» присутствует необязательный
аргумент arg, то он становится первым аргументом интерпретатора, а полное имя
файла-сценария — вторым.
Например, можно записать в файл такую последовательность строк:
#! /usr/bin/python2.2
print "Привет, Мир!"
сделать этот файл исполняемым и запустить:
$ mycmd
Привет, Мир!
$
Практически во всех системах обработка строки «*!•> выполняется системным
вызовом execvp, а не самим командным интерпретатором.
Если поиск файла по всем каталогам, перечисленным в переменной PATH, не
увенчался успехом, то ехео завершается с признаком ошибки. Если в аргументе file
имеется символ «/», считается, что задан полный путь к файлу и поиск в этом
случае не выполняется.
Вполне возможно реализовать exeovp и exeolp в виде библиотечных функций,
которые обращаются к exeov и exeol соответственно. Я покажу свою версию exeovp,
которая достаточно близка к оригиналу, но при этом использует макросы
проверки ошибок «ео».
Я надеюсь, вы еще помните, что exeovp сначала пытается выполнить найденный файл
как обычную скомпилированную программу, а потом как сценарий. Ниже
приводится функция, которая делает практически то же самое:
int exeo_path(oonst ohar «path, ohar «oonst argv[], ohar *newargv[])
{
int i;
exeov(path, argv);
if (errno == ENOEXEC) {
newargv[0] = argv[0];
newargv[1] = (ohar *)path;
i = 1;
do {
newargv[i + 1] = argv[i];
} while (argv[i++] != NULL);
return execv("/bin/sh", (char *oonst *)newargv);
ГЛАВА 5 Процессы и потоки 283
>
return -1;
}
В случае ошибки, функция exec.path возвращает значение -1 и код ошибки в
переменной errno.
Наша версия execvp называется execvp2. Она пытается найти требуемый файл
программы в каждом из каталогов, перечисленных в переменной окружения PATH и
запустить его на исполнение вызовом функции exec.path. Если имя файла
содержит символ <•/» или переменная PATH не существует, имя используется по принципу
«как есть»:
int execvp2(ccnst char «file, char *ccnst argv[])
<
char *s, *pathseq = NULL, «path = NULL, »*newargv = NULL;
--Jjit argc;
fcr (argc = 0; argv[argc] != NULL; argc++)
/» Если этс сценарий, тс несбхсдимс предусмотреть месте
еще пед един аргумент и NULL. ♦/
ec_null( newargv = mallcc((argc + 2) * sizecf(char «)) )
s = getenv("PATH");
if (strchr(file, '/') != NULL || s == NULL || s[0] == '\0')
ec_neg1( exec_path(file, argv, newargv) )
ec_null( pathseq = strdup(s) )
/* Следующая стрска выделяет памяти сбычне бсльше чем дсстатсчнс */
ec_null( path = mallcc(strlen(file) + strlen(pathseq) + 2) )
while ((s = strtck(pathseq, ":")) != NULL) {
pathseq = NULL; /* ссебщить strtck, чте работаем с той же стрскей */
if (s[0] == '\0')
s = ".";
strcpy(path, s);
strcat(path, "/");
strcat(path, file);
exec.path(path, argv, newargv);
>
errnc = ENOENT;
EC.FAIL
EC_CLEANUP_BGN
free(pathseq);
free(path);
free(newargv);
284 ГЛАВА 5 Процессы и потоки
return -1;
EC_CLEANUP_END
}
Некоторые комментарии по поводу распределения памяти.
• У нас нет ни возможности, ни необходимости освобождать выделенную память
в случае успешного запуска новой программы, так как весь сегмент данных
пользователя будет затерт.
• Взяв длину переменной PATH, прибавив к ней длину имени файла и еще два
байта под символы «/» и «\0», мы получим вполне достаточный объем для полного
имени файла.
• Если мы будем пытаться исполнить файл как сценарий, нам необходимо
добавить в массив аргументов два дополнительных элемента: один для указателя на
строку с именем исполняемого файла командного интерпретатора, второй для
полного имени файла-сценария. Поэтому в цикле for выполняется подсчет
количества аргументов в массиве argv и затем размещается массив newargv, размер
которого на два элемента больше, чем a rgv.
• Указатель на строку PATH мы получаем через обращение к функции getenv.
А поскольку мы не собираемся менять эту переменную среды окружения (на
случай если так и не удастся запустить программу), функцией strdup создается
дубликат, с которым потом и будет работать функция strtok. (Строка передается в
функцию strtok только при первом вызове, если затем в первом аргументе
передается пустой указатель, это означает, что она должна продолжить работу с
прежней строкой.)
Я уже говорил: как правило, exec используется в паре с системным вызовом fork,
но иногда он может использоваться отдельно. Например, иногда большую программу
можно разбить на стадии и с помощью exec переходить от одной стадии к другой.
Но поскольку при запуске новой программы происходит потеря сегмента данных,
стадии исполнения должны быть достаточно независимы друг от друга, хотя
данные могут быть переданы через аргументы, переменные среды окружения или
через файлы. Такое применение exec хотя и достаточно редкое, но известное.
Гораздо более типичный случай использования exec — выполнение некоторого объема
подготовительной работы перед вызовом основной программы. Например, команда
nchup, которая сначала подавляет сигналы прерывания и завершения, а затем с
помощью execvp запускает команду пользователя. Другой пример — команда nice,
которая задает приоритет запускаемой программы (см. пример в разделе 5.15).
5.4 Командная оболочка (версия 1)
Мы знаем уже достаточно много системных вызовов, чтобы написать свою
собственную версию командной оболочки, хотя и весьма ограниченную в своих
возможностях. Главный недостаток вызова exec в том, что программа должна совершить
«самоубийство» для запуска другой программы. Для начала мы попробуем реали-
ГЛАВА 5 Процессы и потоки 285
зовать возможность изменения и получения переменных среды окружения: оператор
присваивания (например B00K=/usr/mark/book) и команду set, которая выводит
список переменных окружения*.
В первую очередь нужно разбить командную строку на аргументы. Для простоты
реализации мы не будем разбирать аргументы в кавычках. Более того, поскольку
мы еще не умеем запускать фоновые процессы, последовательности и конвейеры
процессов, то не будем обращать внимание на специальные символы «4», «;» и «|».
Также мы пока оставим в стороне вопрос перенаправления ввода-вывода (« > », «
» » и « < »). Таким образом, в нашем представлении командная строка состоит из
отдельных слов, разделенных символами пробела или табуляции. Мы должны
собрать все аргументы и разместить в массиве argv:
«define MAXLINE 200
statio booi getargs(int «argop, ohar *argv[], int мах, booi *eofp)
{
statio ohar ond[MAXLiNE];
ohar *omdp;
int i;
«eofp = false;
if (fgets(omd, sizeof(omd), stdin) == NULL) {
if (ferror(stdin))
EC.FAIL
«eofp = true;
return false;
}
if (strohr(omd, '\n') == NULL) {
/* «проглотить» оотаток отроки */
while (true) {
switoh (getcharQ) {
oase '\n':
break;
case EOF:
if (ferror(stdin))
EC.FAIL
default:
oontinue;
>
break;
}
printfCKoxaHfla не выполнена -- слишком длинная строка\п");
' Здесь пока не предусмотрена команда export. Вызываемой программе передаются сразу все
переменные окружения.
286 ГЛАВА 5 Процессы и потоки
return false;
}
cmdp = and;
fcr (i = 0; i < max; i++) {
if ((argv[i] = strtck(cmdp, " \t\n")) == NULL)
break;
cmdp = NULL; /* для strtck - продолжаем работу о той же строкой */
}
if (i >= max) {
printf("Команда не выполнена - слишком много аргументов\п");
return false;
}
•argcp = i;
return true;
EC_CLEANUP_BGN
EC_FLUSH("getargs")
return false;
EC_CLEANUP_END
}
Комментарии.
• Функция getargs возвращает true, если разбор строки прошел без ошибок, в
противном случае возвращается false.
• Для чтения входной строки используется стандартная функция fgets. Она не
вызывает переполнение буфера, если строка окажется длиннее установленного
ограничения. Но в этом случае мы должны прочитать входную строку до
конца, поскольку непрочитанные символы могут стать потенциальным источником
ошибок нашего командного интерпретатора. Таким образом, если в
прочитанной строке не найден символ перевода строки, то перед тем как вывести
сообщение об ошибке, выполняется чтение остатка строки.
• Затем, с помощью функции strtck, извлекаются отдельные аргументы.
• С помощью EC_FLUSH, функция getargs выводит сообщения в случае появления
ошибок при вызове одной из библиотечных функций, а также свои
собственные, например, сообщение о том, что строка слишком длинная.
Теперь нам необходимо написать функции, обслуживающие встроенные команды:
операцию присваивания и set. Они достаточно просты в реализации:
extern char ««environ;
void set(int argc, char *argv[])
{
int i;
if (argc != 1)
printf("Слишком много аргументов\п");
else
ГЛАВА 5 Процессы и потоки 287
fcr (i = 0; envircn[i] 1= NULL; i++)
printf("Xs\n", envircn[i]);
}
vcid asg(int argc, char *argv[])
{
char «name, *val;
if (argc != 1)
printf("CflMuiKCM мнсгс аргументсв\п");
else {
name = strtck(argv[0], "=");
val = strtck(NULL, ""); /* пслучить правую часть выражения */
if (name == NULL || val == NULL)
printf("Неверная ксманда\п");
else
ec_neg1( setenv(name, val, true) )
}
return;
EC_CLEANUP_BGN
EC_FLUSH("asg")
EC_CLEANUP_END
}
И, наконец, завершаем программу функцией main, которая выводит символ
приглашения к вводу (в данном случае — <•©•>). Она принимает ввод от пользователя,
проверяет, является ли введенная строка встроенной командой, если нет, пытается
ее исполнить:
«define MAXARG 20
int main(vcid)
<
char *argv[MAXARG];
int argc;
bccl ecf;
while (true) {
printf("@ ");
if (getargs(&argc, argv, MAXARG, &ecf) && argc > 0) {
if (strchr(argv[0], '=') != NULL)
asg(argc, argv);
else if (strcmp(argv[0], "set") == 0)
set(argc, argv);
else
execute(argc, argv);
}
288 ГЛАВА 5 Процессы и потоки
if (eof)
exit(EXIT_SUCCESS);
}
}
statio void exeoute(int argo, ohar *argv[])
<
execvp(argv[0], argv);
printf("Невозможно выполнить команду\п");
}
Обратите внимание: возврат к началу цикла происходит только в том случае, если
exeovp не может запустить указанную программу. Именно по этой причине вряд ли
всерьез можно говорить о полезности нашей командной оболочки. Тем не менее,
ниже приводится пример типичного сеанса работы с интерпретатором (список
переменных окружения сильно сокращен ради экономии места):
$ shO
@ set
SSH_CLIENT=192.168.0.1 4971 22
USER=maro
НAIL=/va r/maiI/ma го
EDIT0R=vI
@ LASTNAME=Roohkind
в set
SSH_CLIENT=192.168.0.1 4971 22
USER=maro
MAIL=/var/maII/maro
EDIT0R=vi
LASTNAME=Roohkind
в eoho Hello World!
Hello World!
$
Обратите внимание на символ приглашения «$» в конце приведенного листинга. Он
свидетельствует о том, что наш интерпретатор завершил свою работу после
выполнения команды echo. На самом деле это завершила свою работу программа echo, которая
подменила наш интерпретатор в результате обращения к системному вызову execvp.
Естественно, было бы лучше, если бы наша командная оболочка умела запускать
команды в виде новых процессов. Об этом и пойдет речь в следующем разделе.
5.5 Системный вызов fork
Вызов fork до определенной степени является противоположностью вызову exec.
Он запускает новый процесс, но не новую программу, где новый процесс — это
точная копия старого со всеми его данными.
ГЛАВА 5 Процессы и потоки 289
fork — создает новый процесс
«include <unistd.h>
pid_t fork(void);
/* Возвращает идентификатор дочернего процесса или 0 в случае успеха и -1
в случае ошибки (код ошибки - в переменной еггпо) */
По завершении fork оба процесса (потомок и предок) получают от него
возвращаемое значение. В зависимости от него реакция потомка и предка может сильно
отличаться. Обычно процесс-потомок сразу же вызывает exec для запуска новой
программы, а предок может либо подождать завершения процесса-потомка, либо
заняться своими делами.
Процесс-потомок получает от вызова fork значение 0, родительский процесс —
идентификатор процесса-потомка. Возвращаемое значение -1 свидетельствует об
ошибке, но поскольку fork не имеет входных аргументов, то ошибочная ситуация
никак не связана с вызывающим процессом. Единственно возможная ошибка —
исчерпание системных ресурсов, то есть либо нехватка места в файле подкачки,
либо в системе исполняется слишком много процессов. В случае неудачи процесс-
предок может подождать некоторое время (обратившись, например, к системному
вызову sleep) и повторить попытку, но это немного не то, что обычно делают
командные интерпретаторы. Как правило, они незамедлительно выводят сообщение
об ошибке и переходят в ожидание новых команд от пользователя.
Программы, запущенные вызовом exec получают от запустившей их программы
большинство атрибутов в неизменном состоянии, поскольку сегмент системных
данных не изменяется. То же самое происходит и при создании нового процесса
вызовом fork: процесс-потомок наследует атрибуты от родителя как результат
копирования сегмента системных данных предка. Такого рода наследование
позволяет пользователю установить атрибуты в командной оболочке, например, текущий
каталог, действующий идентификатор пользователя и приоритет, которые затем
будут автоматически передаваться всем запускаемым командам. Можно
рассматривать эти атрибуты как «черты, характерные для близких родственников».
Для наследования недоступно лишь несколько атрибутов.
• Идентификаторы процессов у потомка и предка будут отличаться, это совершенно
очевидно.
• Если в рамках родительского процесса было запущено несколько потоков,
потомок унаследует только один, тот который вызвал fork.
• Потомок получает от родителя дубликаты открытых дескрипторов. Они
соответствуют одним и тем же файлам. Они совместно используют одни и те же
записи в системной таблице файлов, а значит и текущие позиции в файлах у
них будут одни и те же. Если потомок изменит ее с помощью lseek, то
следующая операция ввода-вывода предка будет производиться с новой текущей
позиции. Однако сами файловые дескрипторы у предка и потомка разные. Если
290 ГЛАВА 5 Процессы и потоки
потомок закроет свой дескриптор, то соответствующий дескриптор предка по-
прежнему останется открытым.
• Процесс-потомок сбрасывает накапливаемые значения времени исполнения в
пространстве пользователя и ядра в ноль, потому что был порожден новый
процесс.
(Полный список атрибутов вы найдете в приложении А.)
Простой пример, демонстрирующий работу системного вызова fork:
void forktest(void)
{
int pid;
printf("Начало теста \п");
pid = fork();
printf("Возвращаемое значение Xd\n", pid);
Результат:
$ forktest
Начало теста
Возвращаемое значение 98657
Возвращаемое значение О
$
В данной ситуации предок первым вывел свое сообщение, но это совершенно
необязательно. Если для вас очередность исполнения имеет значение, процессы
можно синхронизировать (см. раздел 9-2-3). Сделать это можно с помощью
каналов или, что немного сложнее, с помощью сигналов.
Перезапустим пример forktest, перенаправив вывод в файл:
$ forktest >tmp
$ cat trap
Начало теста
Возвращаемое значение 56807
Начало теста
Возвращаемое значение 0
$
На этот раз мы получили сообщение «Начало теста» дважды! Можете объяснить,
почему?
Это произошло из-за буферизации вывода — потомок, вместе со всем остальным,
унаследовал от предка и частично заполненный выходной буфер. Когда потомок
завершил свою работу, его буфер был вытолкнут, то же самое произошло и с
предком. В предыдущем испытании printf не буферизовала свой вывод, поскольку «знала»,
ГЛАВА 5 Процессы и потоки 291
что устройством стандартного вывода является терминал, который предполагает
более интерактивный режим работы. Проблему управления побочными
эффектами на выходе мы подробнее рассмотрим в разделе 57.
Вызовы fork и exec прекрасно дополняют друг друга. Дочерний процесс,
созданный вызовом fork, сам по себе не представляет особой ценности, так как является
точной копией родителя. Мы получим гораздо больше выгоды, если потомок
заменит себя новой программой — то есть как раз то, что делает вызов exec. Мы
увидим это в следующем разделе, когда продолжим работу над нашим командным
интерпретатором.
Процедура создания нового процесса обходится системе очень недешево. Прежде
всего это связано с необходимостью разрешения проблем клонирования,
копированием огромного количества данных (сегмент кода, как правило, доступен
только для чтения, поэтому допускает совместное использование) и все для того,
чтобы выполнить лишь несколько сотен команд, прежде чем будет запущена новая
программа. Выглядит слишком расточительным, впрочем, так оно и есть. Более
грамотный подход, который называется «копирование при записи», предпринят в
отношении виртуальной памяти UNIX, для которой операция копирования
обходится очень дорого. Суть подхода состоит в следующем: в момент клонирования
потомок и предок совместно используют сегмент данных, который состоит из
набора страниц. Пока их содержимое остается неизменным, никакого копирования не
выполняется. Затем, когда один из процессов попытается что-либо записать в
страницу, будет создана ее точная копия, в результате каждый из процессов получит свою
собственную версию страницы. Такой метод достаточно прозрачен и эффективен
для аппаратных конфигураций, которые поддерживают динамическое
преобразование адресов (практически во всех современных компьютерах). Поскольку
между fork и exec обычно выполняется очень маленький объем работ, то и объем
копирования измененных страниц получается не очень большим. Но даже если
необходимо будет скопировать все страницы, мы все равно остаемся в выигрыше. Не
забывайте, что механизм «копирования при записи» работает внутри ядра — он не
изменяет семантику вызова fork, и кроме улучшения производительности
пользователь ничего не замечает.
Еще большей эффективностью, чем «копирование при записи», обладает
разновидность системного вызова fork, которая называется vfork:
vfork — создает новый
«include <unistd.h>
процесс с
разделением
памяти
pid_t vfork(void);
/* Возвращает идентификатор дочернего процесса или
в случае ошибки (код ошибки - в переменной errno)
0 в
V
(устарел)
случае успеха, и -1
Системный вызов vfork ведет себя точно так же, как и fork, за одним исключением
— он не выполняет никакого «копирования при записи» — и потомок, и предок
совместно используют один и тот же сегмент данных. Таким образом, хаос неиз-
292 ГЛАВА 5 Процессы и потоки
бежен, если дочерний процесс немедленно не завершит свою работу или не
вызовет ехео (см. пример функции exeoute2 в следующем разделе).' В ранних версиях
BSD UNIX vfork занимал очень важное положение, но на сегодняшний день его
преимущество в производительности выглядит не таким существенным, к тому же
он потенциально опасен, поэтому я не рекомендую его использовать.
Самая недавняя попытка повысить эффективность связки fork/exec вылилась в
появление функции posix.spawn — часть модификаций POSIX от 1999 года. Учитывая,
что конечная цель — создать дочерний процесс, который запустит другую программу,
в ней удалось избежать проблем, свойственных vfork. Она не предполагает такой
же гибкости в использовании, как раздельное применение вызовов fork и ехео, но,
тем не менее, прекрасно подходит для большинства ситуаций. Основная цель
posix.spawn — предоставить эффективный способ запуска программ как дочерних
процессов в системах реального времени, когда работа с виртуальной памятью
выполняется слишком медленно или аппаратная конфигурация не поддерживает
динамическое преобразование адресов.
Вызов posix.spawn можно реализовать в виде библиотечной функции, используя все
туже связку fork/exec (упражнение 5.8), хотя разработчики систем реального
времени представляют себе ее не иначе как системный вызов.
На мой взгляд, самое время обратить свои взоры на стандартную функцию system:
system — запускает команду
«include <stdlib.h>
int systen(
const char *command /* команда */
);
/* Возвращает кед завершения команды или
в переменней еггпс) */
-1 в случае сшибки (кед сшибки -
Эта функция вызывает fork и вызовом exec запускает командный интерпретатор,
которому в качестве командной строки передается аргумент сооиапЬ. Это довольно
удобный способ запуска команд изнутри приложения, но он не обладает
эффективностью (всегда вызывается командный интерпретатор) и гибкостью связки fork/
exec или posix.spawn.
5.6 Командная оболочка (версия 2)
Теперь объединим вместе fork и exec и немного улучшим командную оболочку из
предыдущего раздела так, чтобы она уже не завершалась при вызове внешней ко-
* Два процесса, одновременно выполняющие запись и чтение одних и тех же переменных,
похожи на потоки и подвержены тем же проблемам. Более подробно об этом — в разделе
5.17.
ГЛАВА 5 Процессы и потоки 293
манды, а после ее исполнения была готова принять новый ввод от пользователя!
Для этого мы заменим функцию execute на execute2:
static void execute2(int argc, char *argv[])
{
pid_t pid;
switch (pid = fcrk()) {
case -1: /* предск (ошибка) */
EC_FAIL
case 0: /* потомок */
execvp(argv[0], argv);
EC.FAIL
default: /* предок */
eo_neg1( wait(NULL) )
}
return;
EC_CLEANUP_BGN
EC_FLUSH("execute2")
if (pid == 0)
_exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Более подробно системный вызов wait будет описан в разделе 5.8, а пока все что
вам нужно знать — это то, что он ожидает завершения дочернего процесса и
потом возвращает управление в вызывающую программу. Функция execute2
обрабатывает ошибки от fork и wait иначе, чем ошибки execvp. Если fork или wait
возвращают значение -1, управление находится в процессе-предке, поэтому просто
печатается сообщение об ошибке (EC_FLUSH) и функция завершается. После этого
вызывающая функция (main из предыдущего раздела) вновь выведет символ
приглашения к вводу, что нам и требуется, — командная оболочка не должна завершаться
из-за ошибок вызова команд. Но, если возврат происходит из execvp, управление
находится в дочернем процессе и лучшее, что мы можем сделать — это просто
завершить работу потомка. В противном случае мы получим две командные
оболочки, ожидающие ввода. Если бы в этой ситуации мы попробовали ввести строку
команды, то каждый из двух процессов в конкурентной борьбе получил бы только
часть символов, поскольку один символ может быть прочитан только один раз. Такое
развитие событий может иметь более чем серьезные последствия, например,
представьте себе, что мы вводим команду «rm t«» и один из процессов получил «гш ♦», а
другой — «t».
Причины, по которым вместо exit вызывается .exit, я объясню в следующем разделе.
294 ГЛАВА 5 Процессы и потоки
5.7 Завершение процесса и системные
вызовы exit
Процесс заканчивает работу в четырех случаях:
1. При обращении к системному вызову exit. Возврат значения из функции main
эквивалентен вызову exit с тем же самым значением. Выход из функции main без
возвращаемого значения равносилен возврату значения 0.
2. При обращении к _exit или _Exit — двум разновидностям системного вызова
exit, которые будут описаны чуть ниже.
3. При получении сигнала на завершение.
4. В случае краха системы, причиной которого может стать все что угодно,
начиная от перебоев в сети электропитания и заканчивая ошибкой в приложении
или в операционной системе.
В этом разделе рассматриваются первые два случая, сигналы обсуждаются в главе
9, а о последнем мне совершенно нечего сказать."
Между тремя разновидностями системного вызова exit существуют следующие
различия:
• _exit и _Exit ведут себя совершенно идентично, хотя чисто технически _exit
относится к UNIX, a _Exit — к стандарту языка С. Начиная с этого момента я буду
упоминать только _exit, но все, что я скажу, в равной степени относится и к _Exit;
• exit (без символа подчеркивания) также определен стандартами языка С. Он
делает все то же самое, что и _exit, но кроме этого выполняет дополнительные
операции по завершению процесса — вызывает функции, зарегистрированные
обращением к atexit (см. раздел 1.3-4) и выталкивает стандартные буферы ввода-
вывода, как если бы были вызваны f flush или fclose. (Выталкивает ли буферы .exit,
зависит от конкретной реализации.)
Поскольку exit выполняет все то же, что и _exit, то все, что будет сказано об _exit,
в равной степени относится и к exit.
Как правило, _exit вызывается вместо exit в процессах-потомках, которые не смогли
вызвать exec, как это было продемонстрировано в функции execute2 в предыдущем
разделе. Делается это потому, что код завершения процесса, унаследованный
потомком (занимающийся сборкой мусора, освобождением ресурсов и пр.), обычно
должен выполняться единожды. Но это справедливо не для всех случаев, поэтому в
каждой конкретной ситуации вы должны еще раз все оценить и принять решение
— какой из вариантов подходит вам больше.
Если для контроля над ошибками вы собираетесь использовать макросы «ее», как
это делается в данной книге, не забывайте, что автоматическое отображение со-
" Некоторые компьютеры предусматривают возможность передачи сигнала процессам при
падении напряжения, но это просто подмена четвертого случая третьим.
ГЛАВА 5 Процессы и потоки 295
общений выполняется в функции, зарегистрированной с помощью atexit, которая
не будет вызвана при завершении процесса вызовом .exit (см. раздел 1.4.2). В
функции execute2 мы побеспокоились об этом и добавили макрос EC.FLUSH, который
отрабатывает как в предке, так и в потомке, несмотря на то, что _exit вызывается
только в потомке.
Ниже приводится краткое описание семейства функций exit. Обратите внимание,
объявления функций находятся в двух различных заголовочных файлах:
_exit — завершает процесс без обращения к коду сборки мусора
«include <unistd.h>
void _exit(
int status /* код завершения */
);
/* В программу управление уже не возвращается */
_Exit — завершает процесс без обращения к коду сборки мусора
«include <stdlib.h>
void _Exit(
int status /* код завершения */
);
/* В программу управление уже не возвращается */
exit — завершает процесс с обращением к коду сборки мусора
«include <stdlib.h>
void exit(
int status /* код завершения */
);
/* В программу управление уже не возвращается */
.exit и родственные ему системные вызовы завершают работу процесса,
обратившегося к нему, с кодом завершения, равным младшему байту аргумента status. Эти
вызовы имеют ряд побочных эффектов, наиболее важные из которых
перечислены ниже, полный список вы найдете в [SUS2002]. Фактически, эти побочные
эффекты имеют место при любом варианте завершения процесса (исключая, разве
что, крах системы).
• Все открытые дескрипторы закрываются.
• Как уже говорилось в разделе 4.3, если процесс был управляющим процессом
(лидер сеанса), сеанс теряет свой управляющий терминал. Кроме того,
каждому процессу в сеансе передается сигнал SIGHUP, который приводит к завершению
процессов.
• Родительский процесс извещается о завершении процесса-потомка через один из
системных вызовов семейства wait, которые будут описаны в следующем разделе.
296 ГЛАВА 5 Процессы и потоки
• Это не оказывает непосредственного влияния на дочерние процессы, за
исключением того, что их новым предком становится специальный процесс в
системе, чей идентификатор (обычно — 1) потомок может получить, обратившись к
системному вызову getppid (см. раздел 5.13)-
Родительский процесс получает код завершения дочернего процесса через один
из системных вызовов wait. Код завершения — это число в диапазоне от 0 до 255.
В соответствии с соглашениями, значение 0 соответствует коду успешного
завершения, а ненулевое значение — какой-либо ошибке, код которой определяется
самим завершившим работу приложением. В этой книге вы уже наверняка
сталкивались с макросами exit.success и exit.fatlure, которые определяют число 0 и
ненулевое значение (обычно — 1) соответственно. Использование макросов вместо
числовых значений повышает удобочитаемость текстов программ, поэтому мы будем
использовать их и впредь.
После того как процесс завершится, он прекращает свое исполнение, но остается
в системе до тех пор, пока код завершения не будет передан процессу-предку, если
он, конечно же, заинтересован в этом. Если потомок не смог сообщить предку о
своем завершении, он переходит в состояние «зомби».
5.8 Системные вызовы wait, waitpid и waitid
Системные вызовы wait, waitpid и waitid ожидают, пока дочерний процесс не
изменит свое состояние (приостановка, возобновление или завершение) и
возвращают его вызывающей программе. Порядок завершения процесса обсуждался в
предыдущем разделе, а о приостановке и возобновлении работы процесса мы
говорили в разделе 4.3, поэтому сразу перейдем к основной теме раздела. Начнем
обсуждение с системного вызова waitpid, а затем перейдем к двум другим.
Некоторые из характеристик, описываемых в этом и следующем разделах,
относятся к системам, совместимым с Х/Open, и к моменту написания данной книги
еще отсутствовали в Linux и FreeBSD. Далее в тексте эти свойства помечены как [X/
Open]. (Х/Open обсуждалась в разделах 1.2 и 1.5.1; хотя Linux и претендует на
принадлежность к Х/Open, тем не менее, когда мы проверяли это с помощью
соответствующих макросов, наличие функциональности Х/Open не было подтверждено.)
waitpid — ожидает изменения состояния дочернего процесса
«include <sys/wait.h>
pid_t waitpid(
pid_t pid, /* идентификатор процесса или группы процессов */
int «statusp, /* указатель на статус или NULL •/
int cpticns /* флаги (см. ниже) »/
);
/« В случае успеха возвращает идентификатор процесса или 0, в случае ошибки
-1 (код ошибки в переменной еггпо) »/
ГЛАВА 5 Процессы и потоки 297
Аргумент pid может принимать следующие значения:
>0 Ожидать изменение состояния дочернего процесса с указанным
идентификатором.
-1 Ожидать изменение состояния любого дочернего процесса.
О Ожидать изменение состояния любого дочернего процесса,
принадлежащего к той же группе процессов, что и вызывающий.
< -1 Ожидать изменение состояния любого дочернего процесса,
принадлежащего к группе процессов с идентификатором -pid.
О группах процессов мы говорили в разделе 4.3. Как правило, командная оболочка
запускает каждый конвейер из команд под своим групповым идентификатором и
в ожидании завершения его работы будет передавать в waitpid отрицательное
значение группового идентификатора, благодаря чему системный вызов будет
возвращать управление по завершении каждой из команд.
На выходе из waitpid вызывающий процесс получает идентификатор процесса-
потомка в виде возвращаемого значения, чей идентификатор совпал с аргументом
pid. Ноль возвращается только в том случае, когда был установлен флаг WN0HANG
(будет описан ниже).
Допускается ожидать изменения состояния только прямых потомков, порожденных
системным вызовом fork. Ожидание процессов-«внуков» не допускается, даже если
их родители (прямые потомки вызывающего процесса) к моменту вызова уже
завершили свою работу. Как объяснялось в предыдущем разделе, «осиротевшие»
процессы передаются под опекунство специального системного процесса, а не их
«бабушкам-дедушкам».
Как правило, процессы-предки заинтересованы в получении информации о
состоянии своих потомков — в противном случае процессы-потомки по завершении
превращаются в «зомби» и пребывают в таком виде, пока не завершит работу
родительский процесс. Тогда системный процесс, ставший «приемным родителем»,
сможет обратиться к вызову wait и удалить «зомбированный» процесс. Учитывая,
что многие процессы могут исполняться в системе довольно длительное время (до
нескольких месяцев), такое «невнимание» к дочерним процессам может привести
к заполнению системных таблиц ненужным мусором. Если ожидание потомков
невозможно, по тем или иным причинам, процесс может использовать сигналы для
предотвращения «зомбирования» — подробнее об этом будет рассказано в
следующем разделе.
Системный вызов waitpid может вернуть состояние изменившего его дочернего
процесса только один раз (такой дочерний процесс называется ожидаемый). Или
другими только один раз словами: ожидаемый потомок перестает быть ожидаемым,
если отчет об изменении его состояния уже получен. Это означает следующее: если
в одной точке программы было получено состояние потомка и вдруг
обнаружилось, что это не тот потомок, которого ожидали, то нет никакого способа вернуть
298 ГЛАВА 5 Процессы и потоки
результат обратно в систему, чтобы другой waitpid мог получить его. (Хотя waitid
может сделать это; см. ниже.)
Если к моменту вызова waitpid имеется ожидаемый дочерний процесс,
соответствующий заданному аргументу pid, управление в вызывающую программу
возвращается немедленно. Если потомок найден, но еще не изменил свое состояние,
системный вызов waitpid блокируется до появления подходящего ожидаемого
потомка. Если потомок не был найден, вызывающей программе возвращается значение
-1 и код ошибки ECHILD. Это может произойти потому, что аргумент pid задан
неправильно или процесс-потомок перестал быть ожидаемым, т. е. его состояние уже
было получено.
Если в качестве аргумента statusp передан непустой указатель, то по заданному адресу
записывается код состояния потомка. Он представляет собой комбинацию аргумента
системного вызова _exit или exit (если речь идет о завершении потомка) и числа,
описывающего причину завершения или приостановки. Макросы, применяемые для
анализа комбинации:
WlFEXITED(status) true, если потомок завершил работу обычным
образом (обращением к _exit или exit").
WEXITSTATUS(status) Если WIFEXITED вернул true, младшие 8 бит
представляют собой аргумент вызова _exit или exit.
WlFSIGNALED(status) true, если потомок завершился аварийно (по сигналу).
WTERMSIG(status) Возвращает номер сигнала, вызвавшего аварийное
завершение потомка, при условии, что WIFSIGNALED
вернул true.
WlFSTOPPED(status) true, если потомок приостановлен. Возможно
только в случае установки флага wuntraced
(см. ниже).
WSTOPSIG(status) Номер сигнала, который вызвал приостановку
потомка, при условии, что WIFST0PPE0 вернул true.
WlFCONTINUED(status) true, если исполнение потомка было возобновлено.
Возможно только в случае установки флага
WC0NTINUE0. [Х/Ореп].
WCOREOUMP(status) true, если был создан файл с дампом памяти
(называемый в UNIX «core dump» — дамп ядра). Этот
файл может использоваться при поиске причин,
вызвавших «сваливание» процесса (макрос
нестандартный, но он присутствует в большинстве систем).
Последний аргумент options может содержать один или более флагов,
объединенных операцией ИЛИ:
WCDNTINUED Сообщать о возобновлении работы потомка.
wnohang Не ожидать потомка, если он еще не изменил свое
состояние, возвращать 0 вместо идентификатора
процесса.
Не забывайте, что _Exit и _exit по сути идентичны, а оператор возврата из функции main
равносилен обращению к системному вызову exit.
ГЛАВА 5 Процессы и потоки 299
WUNTRACED Сообщать о приостановке в работе потомка.'
Несколько примеров, использующих системный вызов waitpid:
/* Ожидать завершения потомка pid и получить код завершения. */
eo_neg1( waitpid(pid, &status, 0) )
/* Ожидать завершения любого из потомков, без получения кода завершения. */
eo_neg1( pid = waitpid(-1, NULL, 0) )
/* Получить от любого из потомков по групповому идентификатору pgid оообщение о
завершении или о приоотановке и получить код ооотояния.
Не ожидать, еоли потомок еще не изменил ооотояние. */
eo_neg1( pid = waitpid(-pgid, &status, WNOHANG | WUNTRACEO) )
Пример программы, которая создает три дочерних процесса, все они
завершаются разными способами. От каждого из потомков принимается код завершения.
int main(void)
{
pid_t pid;
/* Случай 1: Явный вызов _exit */
if (fork() == 0) /* потомок */
_exit(123);
/* предок */
eo_false( wait_and_display() )
/* Случай 2: Аварийное завершение */
if (fork() == 0) { /* потомок */
int a, b = 0;
a = 1 / b;
_exit(EXIT_SUCCESS);
}
/* предок */
eo_false( wait_and_display() )
/* Случай З: Внешний оигнал */
if ((pid = fork()) == 0) { /* потомок */
sleep(100);
_exit(EXIT_SUCCESS);
}
/* предок */
eo_neg1( kill(pid, SIGHUP) )
eo_false( wait_and_display() )
exit(EXIT_SUCCESS);
* Имя WUNTRACED сложилось исторически. На сегодняшний день больше подошло бы имя WST0PPED.
300 ГЛАВА 5 Процессы и потоки
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
static bcol wait_and_dispiay(vcid)
{
pid_t wpid;
int status;
ec_neg1( wpid = waitpid(-1, (status, D) )
dispiay_status(wpid, status);
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
vcid dispiay_status(pid_t pid, int status)
{
if (pid != D)
printf("ripcuecc Kid; ", (icng)pid);
if (WIFEXITED(status))
printf("Kcfl завершения Xd\n", WEXITSTATUS(status));
eise {
char «desc;
char «signame = get_macrcstr("signai", WTERMSIG(status), &desc);
if (desc[0] == '?')
desc = signame;
if (signame[D] == '?')
printf("Сигнал #Xd", WTERMSIG(status));
eise
printf("!Hs", desc);
if (WCOREDUMP(status))
printf(" - сбрсшен дамп памяти ");
if (WIFSTDPPED(status))
printf(" (присстансвлен)");
#if defined(_XOPEN_UNIX) && !defined(LINUX)
else if (WIFCONTINUED(status))
printf(" (всзсбнсвлен)");
tendif
printf("\n");
}
}
ГЛАВА 5 Процессы и потоки 301
Как мы уже упоминали, строка:
#if defined(_XOPEN_UNIX) && !defined(LINUX)
выполняет проверку специально для Linux, который ошибочно утверждает о
полной совместимости с Х/Ореп.
Функция get.mac rest г выводит имя сигнала по его номеру и работает точно так же,
как errsymbel, представленная в разделе 1.4.1. Исходный код функции вы найдете
на web-сайте [AUP2003].
В результате работы программы мы получили следующее:
Процесс 9585: Код завершения 123
Процесс 9586: Erroneous arithmetic operation - сброшен дамп памяти
Процесс 9587: Hangup
Как показывают результаты, в первом случае потомок завершил свою работу
самостоятельно, обратившись к системному вызову _exit и передав ему код завершения 123.
Во втором случае потомок завершил свою работу по сигналу SIGFPE (ошибка в
операции с плавающей точкой), как только попытался выполнить деление на ноль. В
третьем случае потомок, находящийся в состоянии ожидания, прекратил свою
работу по сигналу SIGHUP, переданному предком. (Системный вызов kill, который
передал сигнал, обсуждается в разделе 9.1.9)
Функцию display.status мы будем использовать в следующей версии нашей
командной оболочки.
Второй представитель группы системных вызовов wait представляет собой
упрощенный вариант waitpid и эквивалентен вызову последнего с аргументом pid,
равным -1, и с аргументом cptiens равным нулю:
wait — ожидает завершения дочернего процесса
«include <sys/wait.h>
pid_t wait(
int «statusp, /* указатель на статус
);
/• Возвращает идентификатор процесса или
в переменной еггпо) •/
или NULL
*/
-1 в случае
ошибки
(код
ошибки -
Системный вызов wait довольно редко используется в больших приложениях,
поскольку ожидает завершения любого потомка. Дело в том, что когда некоторая
функция, являющаяся частью большого приложения, создает дочерний процесс и
пытается его подождать, она может случайно «дождаться» завершения совершенно
другого потомка, внося беспорядок и сумятицу в ход исполнения всего
приложения в целом. Для таких случаев waitpid подходит гораздо лучше, так как он
позволяет ожидать конкретный процесс или, по крайней мере, члена группы процессов.
Никогда не используйте wait при написании библиотечных функций, которые
создают дочерние процессы.
302 ГЛАВА 5 Процессы и потоки
Предположим, что процесс имеет двух потомков и заранее не известно, какой из
них завершит работу первым. Если нам нужно только дождаться завершения
обоих, можно сначала подождать завершения любого из потомков, а потом вызвать
waitpid для ожидания завершения другого. Или родительский процесс может
выполнять какую-либо работу и периодически вызывать waitpid с флагом WN0HANG для
каждого из потомков. Но, как мы уже говорили, никогда не используйте waitpid с
аргументом pid, равным -1, если приложением не гарантируется отсутствие
других потомков. Вообще, в больших и сложных проектах, где над отдельными
частями работают целые группы разработчиков (например библиотеки для работы с
изображениями или библиотеки для взаимодействия с базами данных) таких гарантий
дать никто не может. Было бы просто здорово, если бы существовала такая
разновидность системного вызова wait, которому можно было бы передать целый
массив идентификаторов, но, увы, такого вызова нет.
Третий представитель группы системных вызовов wait — waitid, был введен в UNIX
спецификацией SUS1 и к моменту написания книги также отсутствует в Linux и
FreeBSD.' Он имеет одну новую и очень важную особенность: вы можете получить
код состояния процесса и оставить его ожидаемым, не причиняя никакого вреда,
если случайно «дождались» не того потомка. Кроме того, он более информативен
по сравнению с waitpid или wait.
waitid — ожидает изменения состояния дочернего процесса [Х/Ореп]
«include <sys/wait.h>
int waitid(
idtype_t idtype, /* тип идентификатора */
id_t id, /* идентификатор */
siginfc_t «infcp, /• возвращаемые сведения */
int options /* флаги */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки
в переменней еггпс) */
siginfot -
typedef
int
pid.
uid
•/
int
- структура для waitid*
struct {
si_ccde;
_t si_pid;
_t si_uid;
si_status;
} siginfc_t;
/•
/•
/•
/•
/•
показаны
кед (см.
•
тслькс пеля имеющие стнешение
ниже) */
идентификатор прсцесса-
реальный
идентификатор
потомка
•/
пользователя
кед завершения или сигнал */
к wait
прсцесса-
•/
потомка
Похоже на то, что в моей версии Linux имеется частичная реализация этого вызова
(отсутствует в страницах справочного руководства, но работает, хотя набор флагов включает в
себя только те, что определены для waitpid). Вы должны будете проверить собственную версию
Linux.
Эта структура была увеличена за счет Realtime Signals Extension (расширение сигналов
реального времени), см. раздел 9.5.
ГЛАВА 5 Процессы и потоки
303
Первый аргумент idtype может принимать одно из следующих значений:
P.PID Аргумент id является идентификатором ожидаемого дочернего
процесса — ожидается изменение состояния потомка с
идентификатором процесса id (аналогично waitpid с pid>0).
P_PGID Аргумент id является групповым идентификатором —
ожидается изменение состояния потомка из группы с
идентификатором id (аналогично waitpid с pid<-l).
P.ALL Аргумент id игнорируется — ожидается изменение состояния
любого потомка (аналогично waitpid с pid == -1).
Системный вызов waitid не имеет прямого эквивалента waitpid с pid==0, но то же
самое можно сделать, если в первом аргументе передать значение P.PGID, а во
втором — групповой идентификатор вызывающего процесса.
Аргумент options может содержать один или более флагов, объединенных
операцией ИЛИ:
WEXITED Сообщать о завершении потомка (всегда подразумевается
в waitpid, который не имеет такого флага).
WSTDPPED Сообщать о приостановке потомка (подобно waitpid с флагом
WUNTRACED).
WCDNTINUED Сообщать о возобновлении работы потомком (аналогичный
флаг предусмотрен и в waitpid).
WN0HANG Не ожидать изменения состояния потомка. Если оно еще
не изменилось — возвращать значение 0 (аналогичный флаг
предусмотрен и в waitpid).
WN0WAIT Оставить потомка ожидаемым. Таким образом, отследить
изменение состояния потомка можно будет несколькими
вызовами waitpid.
Через аргумент infop системный вызов waitpid может вернуть значительно больше
информации, чем те же waitpid или wait. Этот аргумент является указателем на
структуру типа siginf o_t. Выше показаны только те поля структуры, которые имеют
непосредственное отношение к системному вызову waitpid. По возвращении из
вызова, идентификатор процесса-потомка находится в поле si_pid. Поле si_code
содержит информацию о причине завершения потомка. Наиболее часто
встречающиеся коды:
CLD_EXITED Потомок завершил работу7 посредством обращения к вызову
_exlt или exit.
CLD_KILLED Аварийное завершение потомка (по сигналу).
CLD_DUMPED Аварийное завершение потомка, создан файл с дампом памяти.
CLD_STDPPED Потомок приостановлен.
CLD.CONTINUED Потомок возобновил работу.
Если код CLD.EXITED, то поле si_status содержит число, переданное системному
вызову .exit или exit. В любом другом случае изменение состояния процесса может быть
вызвано только сигналом, поэтому поле si_status будет содержать номер сигнала.
Ниже приводится исходный код функций wait_and_display и display_status из
предыдущего примера, переделанных под системный вызов waitid:
304 ГЛАВА 5 Процессы и потоки
static bcci wait_and_display(vcid)
{
siginfc_t infc;
ec_neg1( waitid(P_ALL, 0, iinfc, WEXiTEO) )
dispiay_status(&infc);
return true;
EC_CLEANUP_BGN
return faise;
EC_CLEANUP_ENO
}
vcid dispiay_status(siginfc_t *infcp)
{
printf("Процесс Xid завершен:\n", (icng)infcp->si_pid);
printf("\tKCfl = Xs\n",
get_macrcstr("sigchld-ccde", infcp->si_ccde, NULL));
if (infcp->si_ccde == CL0_EXiTE0)
printf("\tKCfl завершения = Xd\n", infcp->si_status);
eise
printf("\tcMrHan = Xs\n",
get_macrcstr("signai", infcp->si_status, NULL));
Результат работы программы:
Прсцесс 9580 завершен:
код = CLO.EXiTEO
кед завершения = 123
Прсцесс 9581 завершен:
кед = CL0_0UMPE0
сигнал = SiGFPE
Прсцесс 9582 завершен:
кед = CL0JULLE0
сигнал = SiQHUP
При использовании waitid, проблема, связанная со случайным получением отчета
о завершении работы «не того» потомка, может быть предотвращена, если
построить работу по такому алгоритму
1. Запустить waitid с аргументами P.ALL и WNOWAiT.
2. Если полученный идентификатор процесса-потомка не представляет никакого
интереса — вернуться к шагу 1.
3. Если получен идентификатор нужного потомка — перезапустить вызов waitid с
данным идентификатором и без флага WNOWAiT (или просто waitpid).
4. Если есть еще потомки, которых нужно «дождаться» — перейти к шагу 1.
ГЛАВА 5 Процессы и потоки 305
Единственная проблема в том, что системный вызов waitid не доступен в системах, не
соответствующих спецификации SUS1, включая Linux, Free BSD и Darwin. Поэтому в них
придется использовать более неуклюжие методы работы с waitpid, описанные выше.
5.9 Сигналы, завершение и ожидание
Детальному обсуждению сигналов посвящена глава 9, но, на мой взгляд, настало
самое время поговорить о сигнале SIGCHLD. Краткое введение в сигналы было дано
в разделе 1.1.3, его должно хватить для понимания материала, излагаемого здесь,
если это не так — прочитайте сначала главу 9, а потом вернетесь сюда.
Как я уже говорил в предыдущем разделе, родительский процесс может получить
информацию об изменении состояния потомка (завершение, приостановка или
возобновление) с помощью одного из системных вызовов семейства wait. Если
потомка вообще никто не ожидает, он превращается в «зомби» и пребывает в
таком состоянии до тех пор, пока предок не завершит работу.
Я не упоминал об этом раньше, но дочерний процесс может сообщить предку об
изменении состояния с помощью сигнала SIGCHLD. По умолчанию этот сигнал
игнорируется, если предок не предпринял дополнительных мер по изменению
своей реакции на него.
Родительский процесс может получить сигнал, если его интересуют изменения
состояния потомка. К сожалению, он не может узнать от какого потомка пришел
сигнал.' Если для получения идентификатора процесса-потомка он воспользуется
вызовом wait, существует опасность получить состояние не того процесса, потому
что между передачей сигнала и обращением к системному вызову wait любой
другой потомок мог изменить свое состояние. Наиболее безопасный способ —
воспользоваться возможностями системного вызова waitid или с помощью waitpid
проверять всех возможных потомков одного за другим.
Вместо ожидания завершения работы дочерних процессов, предок может вообще
предотвратить превращение потомков в «зомби», обратившись к системному
вызову sigaction с флагом SA.NOCLDWAIT для сигнала SIGCHLD. В этом случае сообщение
об изменении состояния потомка будет просто «выброшено» и предок его не
получит. Если в таком случае родительским процессом будет сделан системный
вызов wait или waitpid, независимо от входных аргументов (даже WN0HANG) предок
будет заблокирован до тех пор, пока все потомки не завершат свою работу, после чего
ему будет возвращено значение -1 и код ошибки ECHILD. Родитель по-прежнему может
узнавать об изменении состояния потомков через перехват сигнала SIGCHLD.
Если предок явно заявил об игнорировании сигнала SIGCHLD (например, назначив
действие SIG.IGN через вызов sigaction), заменив действие по умолчанию, это будет
равносильно установке флага SA.NOCLDWAIT [X/Open]. Но так как не все системы под-
' Если не используются дополнительные возможности сигналов реального времени, но это
расширение доступно не везде.
306 ГЛАВА 5 Процессы и потоки
держивают эту возможность, наилучшим выходом будет также выполнять установку
флага SA.NOCLDWAIT, который поддерживается большим количеством систем.
5.10 Командная оболочка (версия 3)
Улучшим функцию execute2 из раздела 5.6 за счет использования waitpicl-версии
функции display.status из раздела 5.8:
static vcid execute3(int argo, char *argv[])
{
pid_t pid;
int status;
switch (pid = fcrkO) {
case -1: /* предск (сшибка) */
EC_FAIL
case 0: /* пстсмск */
execvp(argv[0], argv);
EC.FAIL
default: /* предск */
ec_neg1( waitpid(pid, &status, 0) )
dispiay_status(pid, status);
}
return;
EC_CLEANUP_BGN
EC_FLUSH("execute3")
if (pid == 0)
_exit(EXIT_FAILURE);
EC_CLEANUP_EN0
}
Ниже приводится пример сеанса обновленной версии командной оболочки. Команда
f ре просто пытается выполнить деление на ноль, она была специально написана
для этого примера:
$ sh3
@ date
Tue Feb 25 12:49:52 MST 2003
Прсцесс 9954: Кед завершения 0
@ echc Командная сбелечка стала немнеге лучше!
Командная сбелечка стала немнеге лучше!
Прсцесс 9955: Кед завершения 0
@ fpe
Прсцесс 9956: Errcnecus arithmetic cperaticn - сброшен дамп памяти
9 EOT $
Комбинация символов EOT указывает место, где я нажал клавиши Ctn+D.
ГЛАВА 5 Процессы и потоки 307
5.11 Получение идентификаторов
пользователя и группы
Здесь приводится группа системных вызовов, с помощью которых можно узнать
реальные и действующие идентификаторы пользователя и группы (основные
понятия были даны в разделе 1.1.5):
getuid
— возвращает реальный идентифи
«include <unistd.h>
uid.
/•
_t getuid(vcid);
Возвращает идентификатор пользователя
катор
(кеды
пользователя
сшибок
не
предусмотрены)
•/
geteuid — возвращает действующий идентификатор пользователя
«include <unistd.h>
uid_t geteuid(vcid);
/* Возвращает идентификатор пользователя (кеды сшибок не предусмотрены) */
getgid — возвращает реальный идентификатор группы
«include <unistd.h>
gid_t getgid(vcid);
/» Возвращает идентификатор
группы(ксды
сшибок
не предусмотрены)
•/
getegid — возвращает действующий идентификатор группы
«include <unistd.h>
gid_t getegid(void);
/* Возвращает идентификатор группы (кеды сшибок не предусмотрены) */
Идентификатор пользователя или группы — это просто некоторое число. Если вы
хотите получить имя, то вам нужно будет обратиться к системным вызовам getpwuid
и getgrgid, которые были описаны в разделе 3.5.3- Ниже приводится пример программы,
которая выводит реальные и действующие идентификаторы пользователя и группы:
int main(vcid)
{
uid_t uid;
gid_t gid;
struct passwd *pwd;
struct group *grp;
uid = getuid();
ec_null( pwd = getpwuid(uid) )
printf("Реальный пользователь = Xld (Xs)\n", (lcng)uid, pwd->pw_name);
308 ГЛАВА 5 Процессы и потоки
uid = geteuidO;
ec_nuii( pwd = getpwuid(uid) )
printf("Действующий псльзсввтель = Xid (Xs)\n", (icng)uid, pwd->pw_name);
gid = getgidO;
ec_nuii( grp = getgrgid(gid) )
printf("PeaflbHBfl группе = Xid (Xs)\n", (icng)gid, grp->gr_nane);
gid = getegidO;
ec_nuii( grp = getgrgid(gid) )
printfC'fleucTByixuafl группа = Xid (Xs)\n", (icng)gid, grp->gr_neme);
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Чтобы вывод программы получился интереснее, я изменил пользователя и группу
файла программы (uidgrp) и включил биты изменения пользователя и группы при
исполнении:
$ su
Pesswcrd:
# chcwn Bdncsys uidgrp
# chined +s uidgrp
$ uidgrp
Реальный псльзсввтель = 100 (mere)
Действующий пользователь = 4 (edm)
Ревльнвя группе = 14 (sysadmin)
Действующей группе = 3 (sys)
$
5.12 Изменение идентификаторов
пользователя и группы
Правила изменения реального и действующего идентификаторов пользователя и
группы достаточно сложны и различаются для обычных процессов и процессов,
работающих с правами суперпользователя. Прежде чем приступить к обсуждению,
должен заметить, что помимо текущих реальных и действующих идентификаторов,
для каждого процесса ядро хранит оригинальные действующие идентификаторы,
которые были установлены последним вызовом exec. Они называются сохраненные
идентификаторы.
ГЛАВА 5 Процессы и потоки 309
Ниже приводятся правила изменения идентификатора пользователя (эти же
правила применимы и для идентификатора группы):
1. Обычный процесс (не обладающий правами суперпользователя) не может
изменить реальный или сохраненный идентификаторы иначе как через вызов exec
(который может изменить сохраненный идентификатор).
2. Обычный процесс может изменить действующий идентификатор на реальный
или на сохраненный.
3. Процесс, обладающий правами суперпользователя, может изменить реальный
и действующий идентификаторы на любой другой.
4. Когда суперпользовательский процесс изменяет реальный идентификатор,
сохраненный идентификатор получает то же значение.
С суперпользовательскими процессами все понятно — допускается все, что
угодно. Два правила, затрагивающие обычные процессы означают следующее: если вызов
exec изменил реальный и действующий идентификаторы, то процесс имеет
возможность переключаться между ними.
Приведу пример, где такое переключение могло бы оказаться уместным: представьте
себе утилиту putfile, которая передает файлы между компьютерами по сети. Эта
утилита должна иметь возможность писать в файл журнала, доступ к которому имеет
только администратор (назовем его pfadm). Владельцем файла программы putfile
является pfadm, и у него установлен бит set-user-ID. После запуска утилита имеет
право доступа к файлу журнала, поскольку действующий пользователь — pfadm.
Затем, для передачи пользовательских файлов, действующий идентификатор
устанавливается равным реальному, чтобы доступ к файлам регулировался реальными
правами пользователя. По завершении работы с пользовательскими файлами
действующий идентификатор переустанавливается обратно в pfadm, чтобы получить
доступ к файлу журнала. (Если бы изначальный действующий идентификатор
пользователя не сохранялся, то обратный переход был бы невозможен.) Обратите
внимание: несмотря на то, что имя пользователя pfadm звучит как-то по-особенному,
тем не менее, это самый обычный пользователь, а не суперпользователь.
А теперь — сами системные вызовы. Единственно полезные вызовы для процесса с
правами обычного пользователя — seteuid и setegid.
seteuid — устанавливает действующий идентификатор пользователя
«include <unistd.h>
int seteuid(
uid_t uid /» действующий идентификатор пользователя */
/• Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) «/
31 О ГЛАВА 5 Процессы и потоки
setegid — устанавливает действующий идентификатор группы
«include <unistd.h>
int setegid(
gid_t gid /* действующий идентификатор группы */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
Отталкиваясь от правил выше, можно утверждать, что в случае процесса с правами
обычного пользователя, аргумент должен быть равен реальному или
сохраненному идентификатору. Для процесса с правами суперпользователя, аргумент может
соответствовать идентификатору любого пользователя.
В распоряжении суперпользователя имеются два дополнительных системных вызова:
setuid — устанавливает реальный идентификатор пользователя
«include <unistd.h>
int setuid(
uid_t uid /» реальный и сохраненный идентификатор пользователя */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
setgid — устанавливает реальный идентификатор группы
«include <unistd.h>
int setgid(
gid_t gid /* реальный и есхраненный идентификатор группы */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменной еггпо) */
В случае суперпользователя, в качестве аргумента допускается передавать
идентификатор любого пользователя, который станет сразу и реальным и сохраненным.
В случае обычного пользователя эти вызовы ведут себя аналогично двум
предыдущим, что может порой вводить в заблуждение, поэтому следует избегать
применения этих вызовов.
Два других вызова, setreuid и setregid, обладают той же функциональностью, что и
четыре предыдущих вызова и привносят дополнительную неразбериху, поэтому их
также следует избегать. (Эти системные вызовы играли очень важную роль еще до
появления setreuid и setegid.)
ГЛАВА 5 Процессы и потоки
311
5.13 Получение идентификатора процесса
Процесс может получить свой идентификатор и идентификатор предка с помощью
системных вызовов:
getpid — возвращает идентификатор процесса
«include <unistd.h>
pid_t getpid(void);
/* Возвращает идентификатор процесса */
getppid
— возвращает идентификатор родительского процесса
«include <unistd.h>
pid_t
getppid(void);
/* Возвращает идентификатор родительского
процесса
•/
Пример использования вызова getpid я уже приводил в разделе 2.4.3-
5.14 Системный вызов ch root
chroot — изменяет корневой каталог
«include <unistd.h>
int chroot(
const char *path /* путь к
);
/* Возвращает 0 в случае успеха
в переменной еггпо) */
каталогу */
, -1 в
случае
ошибки
(код
ошибки -
Этот системный вызов, предназначенный специально для суперпользовательских
процессов, изменяет корневой каталог процесса, т. е. каталог с именем — «/». Этот
вызов не оговаривается стандартами, но реализуется практически во всех UNIX-
подобных системах.
Две области применения chroot, которые первыми приходят на ум:
• когда необходимо выполнить команду с жестко зашитыми именами файлов для
обработки файлов, расположенных в необычном месте (например /etc/passwd).
В любом удобном для вас месте может быть сконструировано новое дерево
каталогов, произведена смена корня и запущена требуемая команда. В первую
очередь этот прием используется для отладки новых утилит;
• из соображений безопасности. Например, новый корень может быть назначен
для процесса Web-сервера, который не сможет получить доступ к файлам,
находящимся за пределами установленного корневого каталога. Даже применение
имени «..» не даст никакого результата, поскольку для корневых каталогов оно
трактуется как ссылка на сам корневой каталог.
11 Зак. 4030
312 ГЛАВА 5 Процессы и потоки
После изменения корневого каталога не существует реальной возможности возврата
к предыдущему корню с помощью chrcct. Однако некоторые системы реализуют
системный вызов f chrcct, который устанавливает корневой каталог по его
дескриптору (аналогично fchdir, см. раздел 3-6.2). Суть его работы проста — открывается
дескриптор корневого каталога на чтение, вызовом chrcot изменяется корневой
каталог, а затем производится возврат к прежнему корню вызовом fchrcct,
которому передается ранее открытый дескриптор.
5.15 Получение и изменение приоритета
Как я уже говорил в разделе 1.1.6, каждому процессу назначается значение
параметра nice (в переводе с английского — тактичный, внимательный, дружелюбный),
посредством которого процесс может влиять на уровень своего приоритета. Чем выше
параметр nice, тем более «вежлив» и «тактичен» процесс по отношению к другим и
тем ниже его приоритет. Меньшее значение nice означает более «грубое» и
«беспардонное» поведение процесса и более высокий его приоритет. Параметр nice
представляет собой положительное значение, обычно — число 20 (довольно странно, но
в документации к UNIX это число называется как NZER0) и является смещением от
некоторого числа, которое зависит от системы. Процесс стартует с параметром nice
равным 20 и может стать как очень «дружелюбным», задав параметр nice равным 39,
так и совершенно «беспардонным», задав параметр nice равным 0.
Для того, чтобы изменить свой приоритет, процесс обращается к системному
вызову nice:
nice — изменяет значение параметра nice
«include <unistd.h>
int nice(
int incr /* приращение */
);
/* Возвращает нсвсе значение nice - NZER0 или
(кед сшибки - в переменней еггпс) */
-1 в случае сшибки
Системный вызов nice добавляет приращение incr к текущему значению
параметра nice. Результат должен получиться в диапазоне от 0 до 39 включительно. Если
он вышел за пределы указанного диапазона, используется ближайшее допустимое
значение. Только суперпользователь может уменьшить значение параметра nice,
повысив тем самым приоритет обслуживания.
Фактически, системный вызов nice возвращает новое значение nice, уменьшенное
на 20, таким образом, возвращаемое значение лежит в диапазоне от -20 до 19, при
условии, что NZER0==20. Однако, возвращаемое значение редко используется в
программах. Особенно если учесть, что новое значение nice, равное 19, неотличимо
от признака ошибки (19 - 20 = -1). Эта ошибка остается неисправленной, если она
вообще была замечена, так много лет, что может служить показателем того, как мало
ГЛАВА 5 Процессы и потоки 313
обеспокоены UNIX-программисты обслуживанием ошибочных ситуаций,
связанных с второстепенными системными вызовами.'
Многие пользователи знакомы с командой nice, которая позволяет запускать
программы с пониженным (или повышенным, если запуск производит
суперпользователь) приоритетом. Представляю нашу версию этой команды:
«define USAGE "псрядск использования: aupnice [-nura] ксианда\п"
int main(int argc, char *argv[])
{
int incr, cmdarg;
char «cmdname, *cmdpath;
if (argc < 2) {
fprintf(stderr, USAGE);
exit(EXIT_FAILURE);
}
if (argv[1][0] =='-'){
incr = atci(&argv[1][1]);
cmdarg = 2;
}
else {
incr = 10;
cmdarg = 1;
}
if (cmdarg >= argc) {
fprintf(stderr, USAGE);
exit(EXIT_FAILURE);
}
(vcid)nice(incr);
cmdname = strrchr(argv[cmdarg], '/');
if (cmdname == NULL)
cmdname = argv[cmdarg];
else
cmdname++;
cmdpath = argv[cmdarg];
argv[cmdarg] = cmdname;
execvp(cmdpath, &argv[cmdarg]);
EC.FAIL
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
Это замечание перекочевало из первой редакции книги 1985 года. Таким образом, за
последние^ лет данная ошибка так и осталась неисправленной. За это время мы получили
потоки и сигналы с улучшенными характеристиками и тысячи других улучшений, так что на
самом деле все не так плохо.
314 ГЛАВА 5 Процессы и потоки
EC_CLEANUP_END
>
Обратите внимание на то, как повторно был использован массив входных аргументов
argv для передачи системному вызову exevp. Переменная cmdarg хранит индекс
полного имени команды (либо 1, либо 2, в зависимости от того, было ли задано
приращение параметра nice). Вызову execvp передается часть массива аргументов,
которая начинается с индекса cmdarg.
Первая строка в массиве, как предполагается, является простым именем команды
— не полным именем, которое включает в себя маршрут к исполняемому файлу.
Таким образом, мы должны были убрать все компоненты полного имени кроме
последнего — имени файла, в случае, если имя исполняемой команды содержит
символы «/•>. В то время как первым аргументом execvp передается то, что было
набрано в командной строке aupnice.
Заметьте также, что мы использовали execvp без fcrk. Простое изменение
приоритета не требует наличия родительского процесса, который бы ждал завершения
работы потомка.
Ниже приводится результат тестирования aupnice в ОС Solaris. В данном случае была
использована команда ps. Обратите внимание: приоритет изменился с 58 до 28, в то время
как параметр nice был увеличен на 10 (значение по умолчанию в программе aupnice):
$ ps -ас
PID CLS PRI TTY TIME CMD
1D46D TS 58 pts/2 D:DD ps
$ aupnice ps -ac
PID CLS PRI TTY TIME CMD
1D461 TS 28 pts/2 D:DD ps
Существуют два более новых системных вызова — getpricrity и setpricrity,
которые также могут использоваться для управления приоритетом приложения. За
дополнительной информацией обращайтесь к [SUS2002] или к документации по
вашей системе.
5.16 Ограничения для процессов
Ядро устанавливает для процессов ряд ограничений на такие ресурсы, как
максимальный размер файла и максимальный размер стека. Как правило, по достижении
предельного значения либо функции (например malice или write) возвращают
признак ошибки, либо процессу посылается сигнал, например SIGSEGV, который,
например, генерируется в случае переполнения стека (см. главу 9).' Существует семь стан-
' Как же тогда обработчик сигнала SIGSEGV сможет работать, если исчерпано пространство,
отведенное под стек? Вы можете создать альтернативный стек, как это описано в разделе
93. Если этого не сделать, то будет выполнено действие по умолчанию, что означает
завершение процесса.
ГЛАВА 5 Процессы и потоки 315
дартных ресурсов, перечисленных ниже, ограничения на которые могут быть
получены или изменены. Некоторые реализации могут определять ряд
дополнительных ресурсов, ограничения на которые могут быть доступны приложению.
Для каждого из ресурсов существуют максимальный (строгий) предел и текущий
(мягкий) предел. Обычный процесс может изменить текущий предел, присвоив ему
достаточно разумное значение, вплоть до максимального. Кроме того, обычный
процесс может понизить (но не повысить) максимальный предел до величины
текущего предела. Суперпользовательский процесс в состоянии устанавливать
пределам любое значение, которое поддерживается ядром.
Системные вызовы, с помощью которых можно узнать или изменить значения
пределов:
getrlimit — возвращает значение ограничения ресурса
«include <sys/rescurce.h>
int getrlintit(
int resource, /* ресурс */
struct rlintit *rlp /* всэвращаемсе значение ограничения */
);
/* Всзвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
setrlimit — изменяет значение ограничения ресурса
«include <sys/rescurce.h)
int setrlintit(
int rescurce,
censt struct rlintit
);
/* Всзвращает О в случае
в переменней еггпс) */
>
/*
«rip /*
успеха,
ресурс
невсе
-1 в
•/
значение ограничения */
случае
ошибки (кед
ошибки -
struct rlimit
— структура для системных
struct rlintit {
rlint_t
rlim_t
>;
rlint_cur;
rlimjnax;
/*
/«
текущий (мягкий)
вызовов getrlimit
предел
максимальный (строгий)
•/
предел
•/
и
setrlimit
За одно обращение к системному вызову можно получить или изменить предел
только для одного ресурса. Стандартные ресурсы:
RLIHIT_C0RE Максимальный размер файла дампа памяти в байтах. Ноль
означает, что файл вообще не будет создаваться. По
достижении ограничения не генерируется никакой ошибки, запись
в файл просто останавливается.
316 ГЛАВА 5 Процессы и потоки
RLIMIT DATA
RLIMIT.FSIZE
RLIMIT NOFILE
RLIMIT STACK
RLIMIT.AS
RLIMIT.CPU Максимальный объем процессорного времени в секундах.
По достижении ограничения посылается сигнал SIGXCPU,
который по умолчанию завершает процесс. Стандарты не
определяют, что может произойти, если процесс перехватывает этот
сигнал, игнорирует или блокирует его — процесс все равно
может быть завершен, так как нет никакой возможности
продолжать исполнение процесса, не потребляя процессорного
времени.
Максимальный размер сегмента данных в байтах. По
достижении ограничения функции распределения памяти (такие как
«alloc) будут терпеть неудачу.
Максимальный размер файла в байтах. По достижении
ограничения посылается сигнал SIGXFSZ. Если сигнал игнорируется
или блокируется, соответствующие функции завершаются
с признаком ошибки.
Максимальное число файловых дескрипторов, которые могут
быть использованы приложением. По достижении
ограничения соответствующие функции завершаются с признаком ошибки.
Максимальный размер стека в байтах. По достижении
ограничения посылается сигнал SIGSEGV.
Максимальный общий размер памяти, который включает
в себя размер сегмента данных, стека и проекций в память,
выполненных с помощью пиар (см. раздел 7.14). По
достижении ограничения соответствующие функции завершаются
с признаком ошибки. Если это стек, посылается сигнал SIGSEGV.
Действующие ограничения передаются через системный вызов exec и
наследуются потомками через системный вызов fork. Таким образом, если после входа в
систему для командной оболочки были установлены какие-то ограничения, они
будут относиться ко всем запускаемым из этой оболочки процессам. Однако
команда изменения ограничений должна быть встроена в саму командную оболочку,
подобно команде cd. Изменение ограничений внешней командой (фактически —
дочерним процессом) никак не скажется на самой командной оболочке.
Большинство командных интерпретаторов поддерживают команду ulimit, которая может
устанавливать ограничение на размер файла. Некоторые оболочки имеют в своем
распоряжении аналогичные команды с именами, например, limit, limits или plimit.*
В дополнение к фактическим значениям для элементов структуры rlimit существует
ряд специальных макроопределений: RLIH_SAVED_MAX, RLIM_SAVED_CUR И RLIH.INFINITY.
Системный вызов get rlimit возвращает фактические значения, если они умещаются
в беззнаковый тип rlim.t, ширина которого стандартами не оговаривается. В
противном случае, в поле структуры записывается rlih_saved_max, если
соответствующему пределу присвоено максимально возможное (строгое) значение или RLIH_SAVED_CUR,
Не путайте ограничения процесса с ограничениями для пользователя, такими как
дисковые квоты. Пользовательские ограничения могут быть получены или изменены
нестандартными командами, например, quota, реализованными на базе нестандартных системных
вызовов, таких как quotactl или ioctl.
ГЛАВА 5 Процессы и потоки 317
которое имеет смысл: «пределу присвоено то значение, которое присвоено».
Значение rlim_infinity означает, что для данного ресурса ограничение отсутствует.
Если системному вызову setrlimit в поле структуры передается значение RLIM_
SAVED_CUR, то устанавливается текущее ограничение для ресурса, RLIM_SAVED_MAX —
максимально возможное ограничение, RLIM.INFINITY — снимает любые
ограничения, иначе для ресурса передается фактическое значение ограничения в виде
числа. Однако RLIM_SAVED_MAX или RLIM_SAVED_CUR могут использоваться только в том случае,
если они были возвращены вызовом getrllmit; в противном случае должны
использоваться фактические значения. Или другими словами: вы можете использовать эти
макросы только в том случае, когда фактические значения не умещаются в тип riim.t.
Макросы RLIM_SAVED_CUR и RLIM_SAVED_MAX позволяют в некоторой степени управлять
ограничениями даже тогда, когда невозможно получить или передать фактические
значения.
Если реализация не предусматривает наличия таких значений ограничений,
которые не уместились бы в тип rlim_t, то макросы RLIM_SAVED_CUR и RLIM_SAVED_MAX
оказываются невостребованными, поэтому в некоторых реализациях им
присваивается значение RLIM.IMFINITY. Мы увидим это на следующем примере, который
выводит значения ограничений, устанавливает ограничение на размер файла в
смехотворно низкое значение и затем пытается превысить его, что приводит к
аварийному завершению приложения по сигналу SIGXFSZ:
Int maln(vcld)
{
struct rllmlt r;
Int fd;
char buf[500] = { 0 };
if (slzecf(rllm_t) > slzecf(lcng Icng))
prlntf("Внимание: rllm_t > Icng Icng; "
"результаты мсгут быть искажены\п");
ec_false( sbcwIImit(RLIMIT_CORE, "RLIMIT.CORE") )
ec_false( sbcwIImit(RLIMIT_CPU, "RLIMIT.CPU") )
ec_false( shcwIimit(RLIMIT_DATA, "RLIMIT.DATA") )
ec_false( sbcwlimit(RLIMIT_FSIZE, "RLIMIT.FSIZE") )
ec_false( sbcwIImit(RLIMIT_NOFlLE, "RLIMIT.NOFILE") )
ec_false( sbcwIImIt(RLIMIT_STACK, "RLIMIT.STACK") )
Kifndef FREEBSD
ec_false( showIIniIt(RLIMIT_AS, "RLIMIT.AS") )
«endif
ec_neg1( getrIImit(RLIMIT_FSIZE, &r) )
r.rllm.cur = 500;
ec_neg1( setrIi«it(RLIMIT_FSIZE, &r) )
ec_neg1( fd = openC'tmp", OJfflONLY | O.CREAT | 0_TRUNC, PERM.FILE) )
ec_negl( wrlte(fd, buf, slzeof(buf)) )
ec_neg1( write(fd, buf, slzeof(buf)) )
318 ГЛАВА 5 Процессы и потоки
printf("Bbmo запиоано два буфера! (?)\п");
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
>
statio bool showIimit(int resouroe, oonst ohar «name)
{
struot rlimit r;
eo_neg1( getrliniit(resouroe, &r) )
printfC'Xs: ", name);
printf("rIim_our = ");
showvalue(r. rIini_our);
printf("; rliin_niax = ");
showvaIue(r. rlini_niax);
printf("\n")l
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
statio void showvalue( rlii»_t Iim)
{
/*
Вое макрооы могут иметь значение, эквивалентное RLIH_INFINITY;
поэтому он должен быть проверен первым;
не используйте для этого оператор выбора switch.
*/
if (lira == RLIH.INFINITY)
printf("RLIH_INFINITY");
«ifndef BSD_DERIVED
else if (lira == RLIH_SAVED_CUR)
printf("RLIM_SAVED_CUR");
else if (lira == RLIM_SAVED_MAX)
printf("RLIH_SAVED_HAX");
#endif
else
printfC'Xllu", (unsigned long long)lira);
>
ГЛАВА 5 Процессы и потоки 319
Как видно из текста программы, FreeBSD и Darwin не полностью реализуют все, о
чем говорилось.*
Ниже приводятся результаты, полученные на четырех различных системах.
Solaris:
RLIMIT.CCRE: rllm.cur = RLIM_INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.CPU: rllm.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.CATA: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.FSIZE: rllm.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.NCFILE: rlim.cur = 256; rlim.max = 1C24
RLIMIT.STACK: rllm.cur = 868352C; rlim.max = 133464C64
RLIMIT.AS: rllm.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
File Size Limit Exceeded - core dumped
Linux:
RLIMIT.CCRE; rllm.cur = C; rlim.max = RLIM.INFINITY
RLIMIT.CPU: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.CATA: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.FSIZE: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.NCFILE: rlim.cur = 1C24; rlim.max = 1C24
RLIMIT.STACK: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.AS: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
File size limit exceeded
FreeBSD:
RLIMIT.CCRE: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.CPU: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.DATA: rlim.cur = 53687C912; rlim.max = 53687C912
RLIMIT.FSIZE: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.NCFILE: rllm.cur = 957; rlim.max = 957
RLIMIT.STACK: rlim.cur = 671C8864; rlim.max = 671C8864
Filesize limit exceeded
Darwin:
RLIMIT.CCRE: rlim.cur = C; rlim.max = RLIM.INFINITY
RLIMIT.CPU: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.DATA: rlim.cur = 6291456; rlim.max = RLIM.INFINITY
RLIMIT.FSIZE: rlim.cur = RLIM.INFINITY; rlim.max = RLIM.INFINITY
RLIMIT.NCFILE: rlim.cur = 256; rlim.max = RLIM.INFINITY
RLIMIT.STACK: rlim.cur = 524288; rlim.max = 671C8864
Filesize limit exceeded
* Фактически, эти вызовы изначально появились в BSD4.2. Но в BSD-системы не вошли
некоторые из последних расширений POSIX.
320 ГЛАВА 5 Процессы и потоки
Следует упомянуть еще об одном устаревшем системном вызове, который менее
функционален и имеет некоторые проблемы с передачей возвращаемого значения:
ulimit — возвращает и изменяет значения ограничений
#inciude <uiimit.h>
icng uiimit(
int cmd, /* команда */
/* необязательный аргумент */
);
/* Возвращает значение ограничения или -1 в случае сшибки, при условии, что
изменилось значение переменней errnc (кед сшибки - в переменней errnc) */
Аргумент cmd может быть одним из следующих:
UL_GETFSIZE Получить текущее ограничение на максимальный размер
файла. Эквивалентно вызову getrlinit для ресурса RLIMIT_FSIZE.
UL.SETFSIZE Установить ограничение на максимальный размер файла.
Значение ограничения передается во втором аргументе типа
long. Увеличить ограничение может только процесс,
исполняющийся с правами суперпользователя. Эквивалентно вызову
setriinit для ресурса RLIMIT_FSIZE, когда значения полей
rlin.cur и г11я_яах совпадают.
Получаемые и устанавливаемые значения измеряются в блоках по 512 байт.
Учитывая, что размеры могут быть очень большими, возвращаемое значение может
получиться отрицательным и даже равным -1. Таким образом, единственный
способ проверить наличие ошибки — это записать ноль в переменную errnc перед
обращением к системному вызову и проверять ее значение, когда возвращаемое
значение равно -1. Ошибкой можно считать только тот случай, когда изменилось
значение переменной errnc.
Вызов uiimit имеет еще ряд особенностей, но нет никакого смысла в его
использовании. Он только вносит беспорядок, поэтому старайтесь избегать его и
используйте setriimit и getrlimit.
Для получения информации об использовании ресурсов как самим процессом, так
и его потомками, предназначен системный вызов get ru sage:
getrusage — возвращает использованный объем ресурсов
«include <sys/rescurce.h>
int getrusage(
int whe,
struct rusage *r_usage
);
/* Возвращает О в случае ус
в переменней errnc) */
/*
/*
RUSA6E_SELF или RUSAGE_CHILDREN */
везвращаемые сведения
;пеха, -1 в случае сшибки
*/
(код сшибки -
ГЛАВА 5 Процессы и потоки 321
struct rusage — структура для
struct rusage {
struct timeval ru.
struct timeval ru.
/* следующие псля
lcng
long
lcng
lcng
lcng
lcng
lcng
lcng
lcng
lcng
lcng
lcng
lcng
lcng
};
ru_maxrss;
ru_ixrss;
ruJLdrss;
ruJLsrss;
rujilnflt;
rujnajflt;
ru_nswap;
ru_inblcck;
ru_cublcck;
rujnsgsnd;
ru_msgrcv;
ru_nsignals;
ru_nvcsw;
ru_nivcsw;
.utime;
.stime;
системного вызова get rusage
/•
/•
время исполнения инструкций в процессе */
время исполнения инструкций в ядре */
стандартами не сговариваются */
/•
/•
/•
/•
/•
/•
/•
Л
Л
/•
Л
Л
Л
Л
максимальный сбъем занимаемой памяти */
общий сбъем разделяемсй памяти */
общий объем неразделяемого сегмента данных */
общий сбъем неразделяемого стека */
утилизированных страниц */
сшибок при обращении к странице */
обращений к файлу псдкачки */
операций блокового ввсда */
операций блокового вывсда */
сообщений переданс */
сообщений принятс */
сигналсв принятс */
переключений кснтекста прсцесссм */
переключений кснтекста ядрсм */
Спецификация [SUS2002] определяет только первые два поля структуры.
Современные BSD-системы, включая FreeBSD, поддерживают их все. ОС Solaris
поддерживает только стандартные поля. ОС Linux 2.4 — стандартные поля и дополнительно
rujninflt, rujnajflt и ru_nswap. За дополнительной информацией о назначении
отдельных полей структуры обращайтесь к страницам справочного руководства (man)
системного вызова get rusage. Интервалы времени (два первых поля структуры)
соответствуют тому, что возвращает системный вызов times (см. раздел 1.7.2), но с
более высоким разрешением, поскольку в timeval (см. раздел 1.7.1) время
измеряется в микросекундах.
5.17 Введение в потоки
Этот раздел представляет собой введение в потоки, причем настолько краткое, что
его будет достаточно лишь для того, чтобы продемонстрировать некоторые
интересные примеры программ. Я объясню, что такое потоки, покажу, какие
преимущества они дают и расскажу об опасностях, которые вас подстерегают при
неосторожном обращении с ними.
Чтобы получить полное представление о потоках (для управления ими существует
порядка сотни системных вызовов), вам придется прочитать отдельную книгу,
посвященную данной теме, например [Norl997] или [But 1997].
322 ГЛАВА 5 Процессы и потоки
5.17.1 Создание потока
В примерах, встречавшихся до сих пор, создаваемые нами процессы (с помощью
вызова fork) имели единственную последовательность исполнения, которая
называется потоком. Процесс двигался вперед от инструкции к инструкции, пользуясь
единственным стеком, изменяя глобальные данные и используя системные
ресурсы, порой в результате обращения к системным вызовам.
Благодаря наличию в UNIX поддержки потоков POSIX, процесс может иметь
несколько последовательностей исполнения, каждая из которых исполняет свою
собственную последовательность команд и обладает своим собственным стеком. Все
остальное, чем владеет процесс, включая глобальные данные и ресурсы, например
открытые файлы или текущий каталог, находятся в совместном использовании у
потоков.* Следующий пример наглядно показывает, что я имею ввиду:
static long x = 0;
statio void *thread_func(void *arg)
{
while (true) {
printf("Поток 2, значение счетчика Xld\n", ++x);
sleep(1);
}
}
int main(void)
{
pthread_t tid;
ec_rv( pthread_create(&tid, NULL, thread_func, NULL) )
while (x < 10) {
printf("noTOK 1, значение счетчика Xld\n", ++x);
sleep(2);
}
return EX1T_SUCCESS;
EC_CLEANUP_BGN
return EX1T_FA1LURE;
EC_CLEANUP_END
}
Последовательность, в которой потоки выводят число, непредсказуема. Вот что
получилось у нас:
Потоки могут располагать своими собственными данными. Более подробное описание вы
найдете в [SUS2002] или в одной из уже упоминавшихся книг.
ГЛАВА 5 Процессы и потоки 323
Пстск
Пстск
Пстск
Пстск
Пстск
Пстск
Пстск
Пстск
Пстск
Пстск
Пстск
Пстск
2,
1.
2,
1,
2,
2,
1.
2,
2,
1,
2,
2,
значение
значение
значение
значение
значение
значение
значение
значение
значение
значение
значение
значение
счетчика
счетчика
счетчика
счетчика
счетчика
счетчика
счетчика
счетчика
счетчика
счетчика
счетчика
счетчика
1
2
3
4
5
6
7
8
9
10
11
12
Как видите, оба потока, один из которых представлен функцией main, другой —
функцией thread_func, изменяют одну и ту же глобальную переменную х. В таком
подходе есть ряд неприятных моментов, но об этом мы поговорим в разделе 5.17.3-
Первичный поток, который представлен функцией main, запускает второй поток с
помощью системного вызова pthread_create:
pthread_create — создает новый поток
«include <pthread.h>
int pthread_create(
pthread_t *thread_id,
ccnst pthread_attr_t «attr,
vcid *(*start_fcn)(vcid *),
vcid *arg
);
/* Возвращает О в случае успеха
/•
/*
/•
/*
в
идентификатор нсвсгс пстска */
атрибуты (или NULL) */
функция запуска */
аргумент функции запуска */
противном случае - кед: сшибки */
Исполнение нового потока начинается с функции запуска, адрес которой
передается системному вызову. Прототип функции запуска:
Функция запуска потока
vcid «start_fcn(
vcid «arg
);
/* Возвращает код завершения •/
Четвертый аргумент системного вызова pthread_create напрямую передается функции
запуска. Это указатель на тип void и зачастую он действительно используется как
указатель на некоторые данные. Поскольку потоки находятся в одном и том же адресном
пространстве, они оба могут пользоваться этим указателем. При желании вы можете
передать в этом аргументе простое целое число, но при этом необходимо выполнить
приведение типа передаваемого значения к типу void*. А чтобы полностью
обезопасить себя, вы должны проверить (например с помощью функции assert) совместимость
типа передаваемого аргумента с типом void*, примерно таким образом:
324 ГЛАВА 5 Процессы и потоки
assert(sizeofdong) <= slzeof(vold *));
Атрибут — это не просто набор флагов, это скорее объект типа pthread_attr_t,
который вы должны инициализировать такими системными вызовами, как pthread.
attr.setsoope и pthread_attr_setstaoksize, которые я не собираюсь обсуждать здесь.
(Полагаю, вы уже начинаете понимать, откуда могла взяться сотня системных
вызовов для работы с потоками.) В наших примерах мы всегда будем использовать
атрибуты по умолчанию, то есть второй аргумент pthread.oreate всегда будет NULL.
Системные вызовы семейства «pthread» в случае успешного выполнения возвращают
значение 0, а в случае неудачи — код ошибки. Они не изменяют глобальную
переменную errno. На этот случай в нашем распоряжении имеется специальный
макрос — eo.rv, который принимает код ошибки и интерпретирует его так, как будто
он был получен из переменной errno."
5.17.2 Ожидание завершения потока
Поток может дождаться окончания работы другого потока и получить код его
завершения с помощью pthread_join (аналог системного вызова wait).
pthreadjoin — ожидает завершения потока
#inolude <pthread
h>
int pthread_join(
pthreadjt thread.id,
void **status_ptr
1* Возвращает О в
олучае
/•
/•
идентификатор потока
код завершения (или
yonexa,
в противном
•/
NULL,
олучае -
еоли
код
не требуетоя)
ошибки
•/
•/
Ниже приводится немного измененный пример работы двух потоков. На этот раз
Поток 1 передает Потоку 2 значение счетчика, на котором он должен завершить
свою работу и вернуть значение переменной х обратно Потоку 1:
statio long x = 0;
statio void *thread_funo(vold *arg)
{
while (x < (long)arg) {
prlntf("flOTOK 2, значение счетчика Xld\n", ++x);
sleep(1);
}
return (void *)x;
}
* Некоторые функции контроля над ошибками были изменены с целью сделать их
безопасными при работе с потоками, но тексты измененных функций в книге не приводятся.
Однако вы можете взять их на Web-сайте.
ГЛАВА 5 Процессы и потоки 325
int main(vcid)
{
pthread_t tid;
vcid «status;
assert(sizecf(icng) <= sizecf(vcid »));
ec_rv( pthread_create(&tid, NULL, thread.func, (vcid *)6) )
whiie (x < 10) {
printf("ncTCK 1, значение счетчика Xid\n", ++x);
sieep(2);
}
ec_rv( pthread_jcin(tid, istatus) )
printf("Kcfl завершения Пстска 2: Xid\n", (icng)status);
return EXIT_SUCCESS;
EC_CLEANUP_BGN
return EXIT_FAILURE;
EC_CLEANUP_END
}
Результат работы примера:
Пстск 1, значение счетчика 1
Пстск 2, значение счетчика 2
Пстск 2, значение счетчика 3
Пстск 1, значение счетчика 4
Пстск 2, значение счетчика 5
Пстск 2, значение счетчика 6
Пстск 1, значение счетчика 7
Пстск 1, значение счетчика 8
Пстск 1, значение счетчика 9
Пстск 1, значение счетчика 10
Кед завершения Пстска 2: 7
5.17.3 Синхронизация потоков (мьютексы)
Как мы увидим в следующих главах, многозадачная архитектура UNIX позволяет
нескольким процессам одновременно использовать одни и те же данные. В случае с
потоками ситуация намного сложнее — необходимо избегать совместного
использования данных несколькими потоками. На самом деле, пример, приведенный выше,
имеет дефект — два потока могут одновременно обратиться к одним и тем же
данным — к переменной х. На первый взгляд кажется, что простой оператор
инкремента является атомарным (неделимым), однако никто не даст вам гарантий, что так оно
и есть. Фактически, вполне может сложиться ситуация, когда Поток 1 успел записать
только 16 бит в 32-битное число, после чего был прерван и управление перешло
326 ГЛАВА 5 Процессы и потоки
Потоку 2, который полностью прочитает все 32 бита еще недописанного числа.'
В более реалистичных ситуациях, когда потоки имеют дело с целыми структурами
данных, эта проблема становится еще острее. Чтобы избежать проблем,
необходимо сделать операции доступа к данным атомарными, а для этого:
• если структура данных временно не готова, она не должна быть доступна
другим потокам;
• если поток выполняет чтение данных, перерасчет результата и запись данных
обратно, то ни один другой поток не должен иметь возможность изменять
данные до тех пор, пока не завершится вся последовательность операций. В
противном случае изменения, сделанные одним из потоков будут безвозвратно
утеряны.
Для соблюдения данных условий предусмотрено несколько простых системных
вызовов, которые реализуют объекты, ограничивающие совместный доступ к
ресурсам. Эти объекты называются мыотексы (mutex, от англ. mutual exclusion —
взаимоисключение). Потоки могут использовать их для защиты критических
участков кода от возможности одновременного исполнения несколькими потоками.
Основные системные вызовы для работы с мьютексами — pthread_mutex_lock и
pthread_mutex_unlock:
pthreadmutexlock — захватить
«include <pthread.h>
int pthread_mutex_lock(
pthread_mutex_t «mutex /* бл
);
/* Возвращает 0 в случае успеха,
мьютекс
окируемый мьютекс */
в противном
случае -
код
ошибки
Ч
pthreadmutexunlock —
«include <pthread
int pthread_mutex.
pthreadjnutex.
);
/* Возвращает О в
h>
отпустить мьютекс
_unlock(
_t *mutex /* разблокируемый
случае
успеха, в противном
мьютекс
случае -
*/
код
ошибки
ч
Механизм взаимного исключения работает примерно таким образом: если мьютекс
уже захвачен, системный вызов pthread_mutex_lock блокирует работу потока до тех
пор, пока он не будет освобожден.
Самый простой способ объявить и инициализировать мьютекс
static pthread_mutex_t mtx = PTHREAO_MUTEX_INITIALIZER;
И это лишь одна из проблем. Другая проблема заключается в оптимизации, выполняемой
компилятором, в результате которой целое число могло бы существовать только в
регистре микропроцессора. По этой причине вы не должны предоставлять одновременный
доступ к данным из потоков, не предусмотрев дополнительной защиты.
ГЛАВА 5 Процессы и потоки 327
Эта переменная может быть как статической для данного программного модуля
(static), так и внешней, объявленной в другом модуле (extern). Допускается
создавать мьютексы и с автоматическим классом размещения (локальные переменные
функций) или размещать их в динамической памяти, но в этом случае они
должны быть инициализированы системным вызовом pthread_mutex_init.'
Теперь немного изменим наш пример, чтобы защитить совместно используемые
данные от одновременного доступа к ним:
static pthread_mutex_t rntx = PTHREAD_MUTEX_INITIALIZER;
static Icng x = 0;
static vcid *thread_func(vcid *arg)
{
bed dene;
while (true) {
ec_rv( pthread_mutex_Icck(&mtx) )
dene = x >= (Icng)arg;
ec_rv( pthread_mutex_unlcck(&mtx) )
if (dene)
break;
ec_rv( pthread_mutex_lcck(&mtx) )
printf("ncTCK 2, значение счетчика Xld\n", ++x);
ec_rv( pthread_mutex_unlcck(&mtx) )
sleep(1);
}
return (vcid *)x;
EC_CLEANUP_BGN
EC_FLUSH("thread_func")
return NULL;
EC_CLEANUP_END
>
int raain(vcid)
{
pthread_t tid;
vcid «status;
beel dene;
' Вы не должны инициализировать мьютексы с автоматическим классом размещения
инициализатором PTHREAD_HUTEX_INITIALIZER, даже если компилятор позволит вам сделать это,
потому что в некоторых системах инициализация может производиться функцией, не
обеспечивающей безопасность при работе с потоками. В языке C++ вы можете получить подобную
проблему со статическими локальными переменными функций, поскольку компилятор может
отложить инициализацию мьютекса до первого фактического вызова функции.
328 ГЛАВА 5 Процессы и потоки
assert(sizecf(icng) <= sizecf(vcid *));
ec_rv( pthread_create(&tid, NULL, thread_func, (vcid *)6) )
while (true) {
ec_rv( pthread_mutex_icck(&ratx) )
dene = x >= 10;
ec_rv( pthread_mutex_uniock(&mtx) )
if (dene)
break;
ec_rv( pthreadjnutex_icck(&mtx) )
printf("IlCTCK 1, значение счетчика Xid\n", ++x);
ec_rv( pthreadjnutex_unicck(&mtx) )
sieep(2);
}
ec_rv( pthread_jcin(tid, &status) )
printf("Kcfl завершения Пстска 2: Xid\n", (icng)status);
return EXIT_SUCCESS;
EC_CLEANUP_BGN
return EXIT_FAILURE;
EC_CLEANUP_EN0
}
Обратите внимание: мы вынесли проверку условия завершения цикла из
оператора while, так как любой доступ к разделяемым данным должен быть окружен
системными вызовами, захватывающими и отпускающими мьютекс. Мы не можем
выполнять весь цикл под защитой мьютекса, так как это исключит возможность
параллельного исполнения потоков. Степень используемой защиты должна быть
достаточно высока и при этом она не должна сильно сказываться на
производительности. К сожалению, если проверить производительность достаточно просто,
то убедиться в полном отсутствии ситуаций, приводящих к «гонке за ресурсами»,
довольно сложно.
В нашем примере все еще есть один недостаток: предположим, что Поток 2
потерпел неудачу при обращении к системному вызову pthread_mutex_uniook, что
приведет к его аварийному завершению, тогда Поток 1 окажется навсегда
заблокированным внутри системного вызова pthread_mutex_lcok. Одно из решений этой
проблемы — в блоке завершения функции pthread.fипс предусмотреть освобождение
захваченного мьютекса, например, так:
EC_CLEANUP_BGN
(vcid)pthread_imjtex_unlcck(&mtx);
EC_FLUSH("thread_func")
return NULL;
EC_CLEANUP_EN0
Наше предположение о том, что, если первый вызов pthread_mutex_uniook потерпел
неудачу, второй завершится успешно, выглядит довольно натянутым. С другой сто-
ГЛАВА 5 Процессы и потоки 329
роны, трудно представить себе ситуацию, когда вызовы pthread_mutex_lcck и
pthread_mutex_unlcck могли бы потерпеть неудачу, при условии, что мьютекс
содержит правильное значение. Возможно, самый простой и понятный способ
заключается в добавлении кода, выполняющего проверку ошибок на этапе разработки и
отладки, и исключении его из программы при передаче ее в эксплуатацию. Хотя
этот код можно оставить, чтобы впоследствии иметь возможность получать
отчеты об ошибках и не терять время впустую, пытаясь понять причину зависания
приложения. Это еще один пример сложности разработки многопоточных
приложений. Вы можете потратить 5% всего времени на написание кода и 95% — на то, чтобы
убедиться, что все работает правильно.
Наиболее правильный подход заключается в том, чтобы представить все операции
с разделяемыми данными в виде набора функций (или классов, если вы
используете объектно-ориентированный язык программирования, например C++), которые
будут работать с мьютексами. Не разбрасывайте операции по захвату и
освобождению мьютексов по всему тексту программы, как это было сделано в моих
примерах.
Теперь я продемонстрирую пример, в котором все обращения к совместно
используемой переменной осуществляются всего одной функцией — get_and_incr_x. В этом
примере и сама переменная х, и мьютекс перекочевали внутрь функции. Обратите
внимание: насколько проще стало читать исходный код. Более простой практически
всегда означает более надежный!
static lcng get_and_incr_x(lcng incr)
{
static lcng x = 0;
static pthread_mutex_t intx = PTHREA0_MUTEX_1N1TIALIZER;
lcng rtn;
ec_rv( pthread_mutex_lcck(&mtx) )
rtn = x += incr;
ec_rv( pthread_mutex_unlcck(&i»tx) )
return rtn;
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUi>_END
}
static vcid *thread_func(vcid *arg)
{
while (get_and_incr_x(0) < (lcng)arg) {
prlntf("ncTCK 2, значение счетчика Xld\n", get_and_incr_x(1));
sleep(1);
}
return (vcid *)get_and_incr_x(0);
}
330 ГЛАВА 5 Процессы и потоки
int main(vcid)
{
pthread_t tid;
vcid *status;
assert(sizecf(long) <= sizecf(vcid *));
ec_rv( pthread_create(&tid, NULL, thread_func, (vcid *)6) )
while (get_and_incr_x(0) < 10) {
printfCTIcTCK 1, значение счетчика Xld\n", get_and_incr_x(1));
sleep(2);
}
ec_rv( pthread_jcin(tid, &status) )
printfC'Kcfl завершения Пстска 2: Xld\n", (lcng)status);
return EXIT.SUCCESS;
EC_CLEANUP_BGN
return EXIT_FAILURE;
EC_CLEANUP_END
}
На этот раз я решил сделать возможные ошибки при захвате и освобождении мью-
текса фатальными. Это еще один из возможных вариантов. Я не собираюсь
утверждать, что это единственно правильный путь. Я лишь хочу донести до вас, что
существует огромное число способов обработки ошибок.
Очень важно понять, почему в функции get_and_incr_x под защиту ставится только
операция доступа к переменной х:
ec_rv( pthread_mutex_lcck(&mtx) )
rtn = х += incr;
ec_rv( pthread_mutex_unlcck(&mtx) )
Дело в том, что переменная х объявлена статической, то есть почти глобальной,
поэтому операции с ней должны быть поставлены под защиту. Переменные rtn и
incr — локальные переменные, они находятся на стеке и являются уникальными
для каждого из потоков (каждый поток имеет свой экземпляр каждой из этих
переменных), поэтому они не нуждаются в защите.
У меня есть некоторые замечания по этому примеру, которые я выскажу в
упражнении 5.11.
Существуют еще три типа объектов синхронизации потоков, о которых вы
сможете прочитать в [SUS2002] или в одной из книг, упомянутых в начале раздела 5.17.
• Блокировки чтения-записи — похожи на мьютексы, но они различают
операции чтения и записи, обеспечивая дополнительные возможности
параллельного исполнения потоков. За дополнительной информацией о блокировках
чтения-записи обращайтесь к разделу 7.11.4.
ГЛАВА 5 Процессы и потоки 331
Спин-блокировки (или циклические блокировки) — также напоминают мьютексы,
но они работают быстрее и предназначены для установки блокировок на
непродолжительное время. Названы они так потому, что вместо блокирования
исполнения, поток вынужден в цикле ожидать снятия блокировки.
Барьеры — точки синхронизации, в которых один или более потоков ожидают
завершения некоторой операции, прежде чем одному из них будет позволено
двигаться дальше.
5.17.4 Переменные состояния
Представим себе такую ситуацию: Поток «А» выполняет некоторые действия
(например, принимает данные через сетевое соединение) и помещает их в очередь.
Поток «Б» вынимает данные из очереди и выполняет дополнительные действия над
ними (например записывает в базу данных). С использованием мьютекса «М»
работу потоков можно организовать следующим образом:
Поток «Л» Поток «£»
1. Прочитать данные. [В] 1. Захватить мьютекс «М». [Ь]
2. Захватить мьютекс «М». [Ь] 2. Если в очереди есть данные — извлечь их
3. Поместить очередную оттуда и записать в базу,
порцию данных в очередь. 3. Освободить мьютекс *М».
4. Освободить мьютекс «М». 4. Перейти к шагу 1.
5. Перейти к шагу 1.
(Метка [В] означает, что операция может быть достаточно продолжительной,
метка [Ь] — операция выполняется очень быстро.)
Этот вариант работает достаточно надежно, но Поток «Б» расходует слишком много
процессорного времени во время ожидания поступления данных в очередь.
Можно попробовать улучшить ситуацию:
Поток *А» Поток «£>»
1. Прочитать данные. [В] 1. Захватить мьютекс «М».
2. Захватить мьютекс «М». [Ь] 2. Если в очереди есть данные — извлечь их
3. Поместить очередную оттуда и записать в базу,
порцию данных в очередь. 3. Освободить мьютекс «М». [Ь]
4. Освободить мьютекс «М». 4. Перейти в режим ожидания на 1 секунду.
5. Перейти к шагу 1. 5. Перейти к шагу 1.
Но и это далеко не лучшее решение, потому что в этом случае ухудшается время
отклика Потока «Б». В момент поступления данных в очередь он может все еще
«спать», вместо того, чтобы «заняться делом». Кроме того, процессор по-прежнему
будет использоваться Потоком «Б» вхолостую, если очередь пуста. Нам нужно
«научить» Поток «А» сообщать Потоку «Б» о поступлении новой порции данных.
Можно было бы попробовать реализовать это с помощью еще одного мьютекса «О»,
который будет сообщать о том, что очередь не пуста:
332 ГЛАВА 5 Процессы и потоки
Поток «А» Поток «Б»
1. Прочитать данные. [В] 1. Захватить мьютекс «М». [Ь]
2. Захватить мьютекс «М». [Ь] 2. Если в очереди есть данные — извлечь их
3. Поместить очередную оттуда и записать в базу,
порцию данных в очередь. 3. Освободить мьютекс «М».
4. Освободить мьютекс «М». 4. Захватить мьютекс «О». [В]
5. Освободить мьютекс «О». 5. Перейти к шагу 1.
6. Перейти к шагу 1.
Теперь Поток «Б» пытается захватить мьютекс «О» (на шаге 4) и блокируется,
поскольку этот мьютекс будет освобожден Потоком «А» только в том случае, когда
поместит данные в очередь.
Теперь проблема состоит в том, что Поток «А» может попытаться «освободить» не
захваченный мьютекс «О». В результате Поток «Б» не получит сигнал о
поступлении данных в очередь и на очередном цикле может оказаться заблокированным
на мьютексе «О», несмотря на то, что в очереди есть данные.
Более серьезная проблема заключается в том, что освободить мьютекс может только
тот поток, который его захватил. Поэтому обращение к системному вызову на 5-м
шаге потока «А» закончится с признаком ошибки.
Мы могли бы заменить мьютексы семафорами с подсчетом (см. раздел 7.8), но
наилучшее решение предлагает механизм переменных состояния. С их помощью
алгоритм работы потоков будет выглядеть следующим образом:
Поток «А» Поток «6»
1. Прочитать данные. [В] 1. Захватить мьютекс «М». [Ь]
2. Захватить мьютекс «М». [Ь] 2. while (queue_is_e»pty){
3. Поместить очередную cond_wait(C, И); [В],
порцию данных в очередь. )
4. condsignal(C). 3- Извлечь данные из очереди и записать
5. Освободить мьютекс «М». их в базу.
6. Перейти к шагу 1. 4. Освободить мьютекс «М».
5. Перейти к шагу 1.
Теперь мьютекс «О» стал ненужным, а вместо него появилась переменная
состояния — «С». В Потоке «Б» функция ccnd_wait ожидает наступления состояния «С»,
которым управляет Поток «А» на шаге 4. Но ccnd_wait особым образом
взаимодействует с мьютексом «М» — он должен быть заблокирован в момент вызова
функции. Мьютекс освобождается на входе в функцию и потом опять автоматически
запирается, когда ccnd_wait возвращает управление. Таким образом, нам удалось
избежать проблем потери сигнала от Потока «А» и взаимоблокировки потоков,
которые были присущи предыдущим версиям алгоритмов. Кроме того, шаги 2 и 3
Потока «Б» выполняются под защитой мьютекса «М», что нам и требовалось.
Почему вызов функции ccnd_wait производится в цикле, ведь она вернет
управление только тогда, когда наступит состояние «С», то есть когда Поток «А» поместит
данные в очередь? Дело в том, что функция cond_wait, подобно любому
блокирующему системному вызову UNIX, может быть прервана (например, обычным
сигналом, таким как SIGINT). В этом случае она вернет управление, несмотря на то, что
ГЛАВА 5 Процессы и потоки 333
состояние еще не наступило и очередь по-прежнему пуста. Цикл проверки
условия гарантирует возврат в ccnd_wait, если в очереди пока еще нет данных. Поскольку
прерывания системных вызовов случаются достаточно редко, процессорным
временем, потраченным на дополнительную проверку, можно пренебречь.
Еще одна причина проверки наличия данных в очереди, перед обращением к ccnd
.wait, заключается в том, что состояние могло наступить «задолго» до того, как Поток
«Б» подойдет к началу цикла. В этом случае, если Поток «Б» сразу вызовет ccnd_wait,
без проверки условия, то он будет заблокирован до тех пор, пока Поток «А» повторно
не сообщит о наступлении состояния, несмотря на то, что фактически в очереди
есть данные. Если повторного сигнала не поступит, ожидание может затянуться
«навечно». Поэтому крайне важно не обращаться к ccnd.wait без явной на то
необходимости.
Таким образом, чтобы организовать работу через переменные состояния, вам
необходимы три вещи: переменная состояния, мъютекс и предикат (условие). Если
первые две — это данные определенного типа, то предикат (или условие) — это
обычный код, проверяющий некоторое условие — конкретизация того, что
представляет переменная состояния.
Ниже приводится краткое описание системных вызовов pthread_ccnd_signai и pthread.
ccnd_wait:
pthreadcondsignal — сигнализирует о наступлении
#include <pthread.h>
int pthread_ccnd_signal(
pthread_ccnd_t *ccnd
);
1* Возвращает О в случае
/♦ переменная ссстсяния */
успеха, в прстивнсм случае -
состояния
кед
сшибки
*/
pthread_cond_wait — ожидает наступления состояни*
«include <pthread.h>
int pthread_ccnd_wait(
pthread_ccnd_t *ccnd,
/» переменная ссстсяния »/
pthread_nuitex_t *mutex /» мыотекс */
v
1* Возвращает О в случае
успеха, в прстивнсм случае -
кед
сшибки
*/
Как и в случае с мьютексами, вы можете объявить и инициализировать
переменную состояния статически:
static pthread_ccnd_t ccnd = PTHREAO_CONO_INITIALIZER;
Переменная состояния с автоматическим классом размещения (локальная
переменная на стеке) или в динамической памяти должна быть инициализирована
системным вызовом pthread_cond_init.
334 ГЛАВА 5 Процессы и потоки
Ниже приводится пример программы, первичный поток которой помещает
данные в очередь и сигнализирует о наступлении состояния. Второй поток ожидает
наступления состояния, вынимает данные из очереди и выводит их на экран:
static pthread.mutex.t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_ccnd_t ccnd = PTHREAD_COND_INITIALIZER;
struct ncde {
int n_number;
struct ncde *n_next;
} *head = NULL;
static vcid *thread_func(vcid *arg)
{
struct ncde *p;
while (true) {
ec_rv( pthread_mutex_lcck(&mtx) )
while (head == NULL)
ec_rv( pthread_ccnd_wait(&ccnd, &mtx) )
p = head;
head = head->n_next;
printf("llcfly4eHC числе Xd из счереди\п", p->n_number);
free(p);
ec_rv( pthread_mutex_unlcck(&mtx) )
}
return (vcid *)true;
EC_CLEANUP_BGN
(vcid)pthread_mutex_unlcck(&mtx);
EC_FLUSH("thread.func")
return (vcid »)false;
EC.CLEANUP.END
}
int main(vcid)
{
pthread_t tid;
int i;
struct ncde *p;
ec_rv( pthread_create(&tid, NULL, thread_func, NULL) )
fcr (i = D; i < 1D; i++) {
ec_null( p = mallcc(sizecf(struct ncde)) )
p->n_number = i;
ec_rv( pthread_mutex_lcck(&mtx) )
p->n_next = head;
head = p;
ГЛАВА 5 Процессы и потоки 335
ec_rv( pthread_ccnd_signal(4ccnd) )
ec_rv( pthread_mutex_unlcck(&mtx) )
sleep(1);
>
ec_rv( pthread_jcin(tid, NULL) )
printf("KcHeu передачи - выхсд\п");
return EXIT_SUCCESS;
EC_CLEANUP_BGN
return EXIT_FAILURE;
EC_CLEANUP_END
>
Первичный поток, следуя алгоритму, показанному выше, захватывает мьютекс',
помещает данные в очередь и вызывает pthread_ccnd_signal:
ec_rv( pthread_mutex_lcck(&mtx) )
p->n_next = head;
head = p;
ec_rv( pthread_ccnd_signal(4ccnd) )
ec_rv( pthread_niutex_unlcck(&nitx) )
Второй поток, так же следуя нашему алгоритму, захватывает мьютекс, вызывает
pthread_ccnd_wait и извлекает данные из очереди:
ec_rv( pthread_mutex_lcck(&mtx) )
while (head = NULL)
ec_rv( pthread_ccnd_wait(4ccnd, &mtx) )
p = head;
head = head->n_next;
printf("Пслученс числе Xd из счереди\п", p->n_number);
free(p);
ec_rv( pthread_mutex_unlcck(&mtx) )
Параллельная работа потоков возможна благодаря тому, что pthread_ccnd_wait
освобождает мьютекс на время ожидания, а перед возвратом в вызывающую программу
опять захватывает его. Таким образом, гарантируется следующее.
• Обе критические секции кода, которые работают с очередью, исполняются под
защитой мьютекса mtx.
• Когда второй поток подходит к выполнению оператора присваивания р = head;
указатель head уже не является «пустым».
В результате программа вывела:
Пслученс числе 0 из счереди
Пслученс числе 1 из счереди
* Вполне допустимо вызывать pthread_cond_signal с незапертым мьютексом. Это может несколько
увеличить пропускную способность.
336 ГЛАВА 5 Процессы и потоки
Пслученс
Пслученс
Пелучено
Пслученс
Пслученс
Пслученс
Пслученс
Пслученс
числе 2
числе 3
числе 4
числе 5
числе 6
числе 7
числе 8
числе 9
из
из
из
из
из
из
из
из
спереди
спереди
спереди
спереди
счереди
счереди
счереди
очереди
А куда же делось сообщение «Конец передачи — выход»? Мы никогда его не
получим, потому что после вывода строки: «Получено число 9-» программа зависает, и
мы вынуждены будем прервать ее нажатием клавиш Ctrl+C. Взглянув на программу
еще раз, мы без труда обнаружим причину такого поведения: второй поток входит в
бесконечный цикл ожидания поступления новых данных в очередь (которые уже
никогда не поступят), а первичный поток блокируется в системном вызове pthread_jcin.
Эта проблема легко разрешается за счет передачи второму потоку данных, которые
служат своего рода признаком конца передачи. Или первичный поток может
принудительно завершить работу второго потока. Мы попробуем пойти по этому пути, но
для начала поговорим о том, как можно принудительно завершить работу потока.
5.17.5 Принудительное завершение потока
Любой поток может принудительно завершить работу другого потока с помощью
системного вызова pthread_cancel:
pthread_cancel — принудительно завершает работу потока
«include <pthread.h>
int pthread_cancel(
pthread_t thread_id /* идентификатор пстска */
);
I* Возвращает 0 в случае успеха, в претивнем случае - кед сшибки */
Как правило, принудительное завершение работы потока может произойти
только в точке завершения, в качестве которой может служить один из двухсот с
лишним системных вызовов и стандартных функций, которые могут быть
заблокированы, например read, waitpid или pthread_wait_ccnditicn." Если поток вызывает функцию,
определенную в любом месте программы, то ее можно рассматривать как
потенциальную точку завершения, потому что вызываемая функция может обращаться
к одному из системных вызовов или функций, которые в свою очередь могут
выполнять роль точки завершения.
Назначение точек завершения состоит в том, чтобы дать возможность выполнить
обычный код, не беспокоясь о том, что работа потока может быть прервана в са-
" Спецификация SUS определяет 65, или около того, функций, которые всегда служат
точками отмены, и еще 150 функций, которые могут выполнять эту роль.
ГЛАВА 5 Процессы и потоки 337
мый неподходящий момент. Например, вы можете вносить изменения в связанный
список (возможно под защитой мьютекса), будучи уверенными, что
последовательность необходимых действий будет выполнена до конца.
Ни один из системных вызовов «pthread», работающий с мьютексами, не может быть
точкой завершения, так же как и функции free, malloc, oalloo или realloo. Если бы
такое было возможно, работа с мьютексами стала бы весьма обременительной,
поскольку каждый раз при обращении к pthread_mutex_look вам пришлось бы
предусматривать обработку ситуации с принудительным завершением.
С другой стороны, если поток вообще не обращается к функциям, которые могут
исполнять роль точки завершения и не предусмотрено варианта
самостоятельного завершения его работы, то такой поток может «жить» очень долго. В такой
ситуации можно предусмотреть одно или несколько обращений к системному вызову
pthread.testoanoel в подходящих для этого местах, чтобы обозначить возможные
точки завершения. Эти вызовы не делают абсолютно ничего, если не было
команды на принудительное завершение.
pthreadtestcancel —
завершение
«inolude <pthread.h>
проверяет
void pthread_testoanoel(void);
наличие
команды
на
принудительное
Я сказал: «как правило, принудительное завершение работы потока может
произойти только в точке завершения'', и это действительно так, если выбран тип
принудительного завершения PTHREAD_CANCEL_DEFERRED. Это значение по умолчанию,
которое может быть изменено на PTHREAD_CANCEL_ASYNCHRONOUS при обращении к
вызову pthread_setoanoeltype (см. [SUS2002]), в этом случае поток будет завершен
немедленно, со всеми вытекающими отсюда последствиями. Я надеюсь, вы знаете что
делаете, если соберетесь обрывать работу потока таким способом.
Теперь изменим функцию main из предыдущего примера так, чтобы она
принудительно завершала работу второго потока, когда заканчивается передача данных:
for (i = 0; i < 10; i++) {
eo_null( p = malloo(sizeof(struot node)) )
p->n_number = i;
eo_rv( pthread_mutex_look(&nitx) )
p->n_next = head;
head = p;
eo_rv( pthread_oond_signal(&oond) )
eo_rv( pthread_ffiutex_unlook(&ratx) )
sleep(1);
}
eo_rv( pthread_oanoel(tid) )
eo_rv( pthread_join(tid, NULL) )
printf("Конец передачи - выход\п");
return EXIT_SUCCESS;
338 ГЛАВА 5 Процессы и потоки
На этот раз программа завершается без внешнего вмешательства:
Получено чиоло 0 из очереди
Получено чиоло 1 из очереди
Получено чиоло 2 из очереди
Получено чиоло 3 из очереди
Получено чиоло 4 из очереди
Получено чиоло 5 из очереди
Получено чиоло 6 из очереди
Получено чиоло 7 из очереди
Получено чиоло 8 из очереди
Получено чиоло 9 из очереди
Конец передачи - выход
Но и это еще не все. Теперь нам необходимо вернуться ко второму потоку и
тщательно просмотреть весь код, чтобы убедиться, что в случае принудительного
завершения он оставит очередь в непротиворечивом состоянии:
statio void *thread_funo(void *arg)
{
struot node *p;
while (true) {
eo_rv( pthread_mutex_look(&mtx) )
while (head == NULL)
eo_rv( pthread_oond_wait(&oond, &mtx) )
p = head;
head = head->n_next;
printf("Получено чиоло Xd из очереди\п", p->n_number);
free(p);
eo_rv( pthread_mutex_unlook(&mtx) )
}
return (void *)true;
EC_CLEANUP_BGN
(void)pthread_mutex_unlook(&mtx);
EC_FLUSH("thread_funo")
return (void Ofalse;
EC.CLEANUP.END
}
Сразу же обнаруживаются две проблемы:
• точкой завершения является обращение к системному вызову pthread_cond_wait.
Если в этом месте поток завершит свою работу, то с очередью будет все в
порядке, поскольку никаких операций с ней еще не производилось, однако мью-
текс останется заперт. Мы не должны обрывать работу потока при запертом
мьютексе, потому что его никто не сможет отпереть;
ГЛАВА 5 Процессы и потоки 339
• точкой завершения может быть функция printf. Если поток завершит работу в
этом месте, данные уже будут извлечены из очереди, но занимаемая ими память
не будет освобождена, что приведет к утечке памяти.
Для этой программы перечисленные недостатки не играют особой роли,
поскольку она прекращает работу сразу же после принудительного завершения потока. Но
так бывает далеко не всегда, так что определенно имеет смысл отыскать все
возможные проблемные места. Важно предусмотреть все, что может произойти, а не
только то, что обычно происходит.
Второй недостаток устраняется очень просто — надо записать число в локальную
переменную, освободить память и потом вызвать printf.
Но первая проблема не имеет очевидного решения. Чтобы ее устранить, мы
должны установить обработчик принудительного завершения — функцию, которая
вызывается непосредственно перед завершением потока. Внутри функции мы
поместим код, устраняющий первый недостаток.
Заголовок обработчика принудительного завершения должен соответствовать
следующему прототипу (имя функции не имеет значения):
void cleanup_handler(vcid *arg);
Установка обработчика производится системным вызовом pthread_cleanup_push,
изъятие — pthread_cleanup_pop:
pthread_cleanup_push
шения
«include <pthread.h>
vcid pthread_cleanup_
— устанавливает обработчик принудительного завер-
push(
vcid (*handler)(vcid«),
vcid *arg
);
/*
Л
указатель на функцию-сбрабстчик */
входной аргумент функции */
pthread_cleanup_pop — изымает обработчик принудительного завершения
«include <pthread.h>
vcid pthread_cleanup_pcp(
int execute /* вызвать
);
функцию-сбрабстчик?
•/
Когда производится принудительное завершение потока, вызывается
функция-обработчик. Если было установлено несколько обработчиков, они вызываются в
порядке обратном их установки. После вызова каждый из обработчиков изымается
из стека обработчиков.
Иногда имеет смысл вызывать обработчик и при нормальном завершении потока.
Для этого нужно передать значение true в аргументе execute вызова pthread_cleanup_pop.
340 ГЛАВА 5 Процессы и потоки
Ниже приводится улучшенная версия thread.func. Обратите внимание: я
инициализировал локальную переменную р значением NULL, чтобы при любом исходе она
содержала верное значение для функции free.
static void cieanup_handier(vcid *arg)
{
free(arg);
(vcid)pthread_fflutex_unicck(&ntx);
}
static void *thread_func(vcid *arg)
{
struct node *p = NULL;
pthread_cieanup_push(cieanup_handier, p);
whiie (true) {
ec_rv( pthread_mutex_icck(&mtx) )
whiie (head = NULL)
ec_rv( pthread_ccnd_wait(&ccnd, &i»tx) )
p = head;
head = head->n_next;
printfC'ncflyneHc числе Xd из счереди\п", p->n_number);
free(p);
ec_rv( pthreadj»utex_unicck(&mtx) )
}
pth read_cieanup_pcp(faise);
return (vcid *)true;
EC_CLEANUP_BGN
(vcid)pthread_rautex_unicck(&mtx);
EC_FLUSH("thread_func")
return (vcid *)faise;
EC_CLEANUP_END
}
Теперь все должно быть в порядке. В принципе, всех этих сложностей с
обработчиком можно было бы избежать, если бы я предусмотрел окончание работы по
завершении передачи данных. Вам определенно стоит рассмотреть и такую
возможность при написании своих программ. С другой стороны, если поток, который
должен быть завершен, окажется заблокированным (скажем, вызовом read), то
принудительное завершение может оказаться единственно подходящим способом
остановить его. Это определенно лучше, чем использовать сигналы, которые
разблокируют системный вызов, потому что они имеют ряд неприятных побочных
эффектов (см. главу 9).
ГЛАВА 5 Процессы и потоки 341
5.17.6 Потоки и процессы
Вероятно, вы уже задались вопросом — когда должны применяться процессы, а когда
.потоки? Как правило, потоки используются для параллельной обработки одних и
тех же структур данных. Наиболее типичные ситуации:
• когда необходимо сохранить малое время отклика интерфейса программы на
действия пользователя и одновременно выполнять обработку некоторых
данных. Например, производить печать документа или форматирование текста в
текстовом процессоре и при этом позволить пользователю просматривать и
редактировать документ;
• когда необходимо организовать обработку данных так, чтобы получить
максимальную выгоду от многопроцессорной системы. Планировщик задач в UNIX
может автоматически запускать различные потоки на разных процессорах;
• когда необходимо иметь дело с разного рода событиями, которые могут
заблокировать системные вызовы (например read, waitpid или msgrcv). Это будет
темой обсуждения следующего раздела.
Процессы используются тогда, когда нет такой тесной привязки к структуре и
размещению данных. (Впрочем, структуры данных можно довольно просто передавать
через общую память, но об этом мы поговорим в главе 7.) В отличие от потоков,
каждый процесс обладает своим собственным действующим идентификатором
пользователя, дескрипторами файлов, глобальными переменными и пр. Процессы
Золее просты в разработке и отладке. В них реже возникают ситуации, требующие
Злокировки процесса, и проще решаются проблемы с взаимоблокировкой и
«гонкой за ресурсами». Кроме того, процессы предназначены для решения крупных
частей задачи или даже задачи целиком, в то время как потоки призваны
обеспечить параллельное решение более мелких частей задачи.
Внимание-, в момент написания книги некоторые реализации потоков, обычно
распространяемые с Linux и FreeBSD, не полностью соответствовали стандартам
POSIX — это либо реализация потоков на пользовательском уровне, либо в виде
отдельных процессов. Основная проблема первой из них заключается в том, что
злокировка системных вызовов, таких как msgrcv, приводит к блокировке всех
потоков, а не только того, который обратился к системному вызову. Проблема со второй
реализацией заключается в неправильной работе отдельных системных вызовов,
таких как waitpid, потому что они вызываются не из того процесса (только
родительский процесс может выполнять ожидание изменения состояния дочернего
процесса). Другой неприятный момент — если в вашем дистрибутиве включена
реализация потоков, не отвечающая вашим требованиям, придется найти, собрать
л установить нужный пакет.
Лля ОС Linux вам следует искать свежую версию реализации потоков, которая
называется «Native POSIX Thread Library for Linux» (NPTL). В ней исправлены самые
существенные проблемы совместимости с POSIX.
342 ГЛАВА 5 Процессы и потоки
5.18 Проблема блокирования
Мы уже сталкивались с рядом системных вызовов, которые могут быть
заблокированы — read, write, pthread_cond_wait и waitpid, в этой книге вы встретитесь с еще
большим числом подобных вызовов, особенно в главах 7 и 8. Одна из основных
сложностей программирования под UNIX заключается в том, что процесс может
оказаться заблокированным более чем в одном системном вызове, потому что
заранее предсказать эту ситуацию вы не можете. Я называю это проблемой
блокирования.
Во избежание блокирования системных вызовов, работающих с дескрипторами
файлов, таких как read или write, вы можете использовать всего один системный
вызов select или poll (см. раздел 4.2), чтобы приостановить работу до тех пор, пока
не будет готов тот или иной дескриптор. Но эти вызовы бесполезны в случаях, когда
нужно дождаться завершения процесса, поступления сигнала или сообщения,
освобождения семафора или изменения переменной состояния, которые никак не
связаны с дескрипторами файлов. Разумеется, можно известить поток,
заблокированный на select или poll, о наступлении события, записав сообщение в канал,
созданный специально для этой цели, но это не поможет в случае с системными
вызовами msgrcv и mq_receive, которые сами блокируют работу в ожидании
сообщения. Нет никакой возможности связать системные вызовы ожидания сообщений
с дескрипторами файлов. И это не проблема обмена сообщениями, это проблема
блокирования.
5.18.1 Решения с использованием потоков и процессов
Исторически, решение проблемы блокирования заключалось в создании
дочернего процесса, который и должен был быть заблокирован." Когда дочерний процесс
будет разблокирован по той или иной причине, он выполняет запись в канал,
чтобы сообщить предку о наступлении события. Каналы — отличный выбор, так как
они довольно просты в применении (мы будем рассматривать их в следующей главе),
а поскольку работа с ними осуществляется через файловые дескрипторы, предок
может воспользоваться системными вызовами select и poll. Фактически, этот
прием преобразует блокировку любого типа процесса-предка в блокировку на
файловом дескрипторе.
Но процессы слишком тяжеловесные объекты, слишком высока цена их создания,
планирования и содержания. К тому же, есть определенные проблемы с
разделением одних и тех же структур данных между двумя процессами. Было бы здорово, если
бы события могли помещаться в некоторую очередь по мере их появления. Она могла
бы просматриваться процессом в наиболее удобное для него время. Организовать
Эта методика была изобретена еще моей матушкой, примерно в 1953 году. В магазине она
оставляла одного ребенка в одной очереди, второго — в другой, а сама отправлялась в
отделы, где не было очередей. Тем не менее, иногда даже она бывала «заблокирована» в
очереди, потому и произвела на свет еще троих.
ГЛАВА 5 Процессы и потоки 343
очередь событий между двумя процессами можно с помощью общей памяти и
семафоров, но межпроцессные семафоры чересчур медлительны, и этот фактор
приобретает особое значение, если речь идет о частой смене событий.
Другая проблема с раздельными процессами состоит в том, что некоторые
объекты, подобно дескрипторам, нельзя передать существующему процессу (по крайней
мере, достаточно переносимым способом). Дескрипторы могут передаваться только
через механизм наследования, таким образом, если процесс «А» должен быть
заблокирован перед выполнением операции ввода-вывода, и уже запущен процесс «Б»,
который только что открыл сетевое соединение, он не сможет заставить процесс
«А» ждать на недавно открытом дескрипторе.
Другое решение заключается в использовании потоков POSIX. В самом простом
случае создаются отдельные потоки для каждого объекта, на котором нужно
выполнить блокировку (например: семафор, очередь сообщений, набор
дескрипторов). Каждый поток просто запускает соответствующий блокирующий системный
вызов (со сброшенным флагом 0.N0NBL0CK). Когда управление возвращается в
поток, он помещает событие в общую очередь (под защитой мьютекса) и опять
вызывает блокирующий системный вызов. Главному потоку останется только ждать
поступления событий в очередь. Блокируемые потоки извещают основной поток
о поступлении событий через переменную состояния, с которой мы уже
встречались в разделе 5.17.4.
5.18.2 Прототип унифицированного диспетчера событий
Я продемонстрирую обобщенный подход к решению проблемы блокирования с
помощью потоков, который я назвал «Унифицированный диспетчер событий». Это
набор функций, которые могут использоваться в любом приложении. При таком
подходе приложение регистрирует необходимые события, создавая для каждого из
них свой блокируемый поток, и переходит в цикл обработки единой очереди. По
мере поступления событий, приложение извлекает их из очереди, обрабатывает и
опять становится на ожидание поступления новых событий.
Я не буду здесь приводить полные исходные тексты функций — вы найдете их на
Web-сайте. Не забывайте, я говорю лишь о прототипе, опытном образце, который
недостаточно эффективен для критических применений и к тому же использует
макросы контроля ошибок «ее», как и все примеры в этой книге.
Начнем с создания перечисления всех возможных событий:
enum UEM.TYPE {
UEM_SVMSG, /* сообщение System V */
UEM_PXMSG, /* сообщение POSIX */
UEM_SVSEM, /* семафор System V */
UEM.PXSEM, /* семафор POSIX */
UEM_FD_READ, /* набор дескрипторов - чтение */
UEM_FD_WRITE, /* набор дескрипторов - запись */
UEM_FD_ERROR, /* набор дескрипторов - ошибка */
!2 3ак. 4030
344
ГЛАВА 5 Процессы и потоки
};
UEM_SI6, /* поступил сигнал */
UEM_PROCESS, /* процесс изменил свое состояние
UEM_HEARTBEAT, /* периодическое событие */
UEM_N0NE /* пустое событие */
Последние два хочется отметить особо. Элемент UEM.HEARTBEAT представляет
события, которые должны следовать с определенным интервалом. Элемент UEH.NONE —
просто потому, что всегда желательно иметь нечто, обозначающее что-то пустое.
Чтобы зарегистрировать событие, нужно предусмотреть некоторую структуру
данных, которая будет хранить сведения, необходимые для передачи в блокирующий
системный вызов. Например, системному вызову select необходимо передать
набор файловых дескрипторов, таким образом, наша структура будет хранить
данные для любого типа события:
struct uem_reg {
enum UEMJTYPE ur_type; /* тип события */
pthread_t ur_tid; /* идентификатор потока */
union {
int urjnqid; /* идентификатор очереди сообщений System V */
struct {
int s_semid; /* идентификатор набора семафоров System V */
struct sembuf *s_scps; /* действия над семафорами */
} ur_svsem;
Kifdef POSIX.IPC
mqd_t urjnqd; /* дескриптор счереди сообщений POSIX */
sem_t *ur_sem; /* семафор POSIX */
#endif
int ur_signum; /* номер сигнала */
pid_t ur_pid; /* идентификатор процесса */
lcng ur_usecs; /* микросекунды (для периодических событий) */
fd_set ur_fdset; /* набор дескрипторов */
} ur_rescurce;
void *ur_data; /* данные, передаваемые вместе с событием */
size_t ur_size; /* размер (используется для различных целей) */
};
Макрос P0SIX_IPC не является стандартным, но мы будем пользоваться им. Он
нужен нам потому, что Linux и FreeBSD еще не в полной мере поддерживают
механизмы межпроцессных взаимодействий POSIX.
Для регистрации события приложение вызывает одну из функций uem_register_£,
где Е определяет тип события. Например, вместо прямого обращения к
системному вызову waitpid, который может заблокировать вызывающий процесс, будет
вызвана функция:
ec_false( uem_register_prccess(pid, NULL) )
ГЛАВА 5 Процессы и потоки 345
которая зарегистрирует событие ожидания завершения дочернего процесса.
Когда дочерний процесс завершит работу, в очередь будет помещено
соответствующее событие вместе с кодом завершения, а приложение, зарегистрировавшее
событие, сможет получить его.
Ниже приводится исходный код функции uem_register_prccess:
bccl uem_register_process(pid_t pid, vcid *data)
<
struct uem_reg *p;
ec_null( p = new_reg() )
p->ur_type = UEM.PROCESS;
p->ur_rescurce.ur_pid = pid;
p->ur_size = 0;
p->ur_data = data;
ec_rv( pthread_create(&p->ur_tid, NULL, thread_prccess, p) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Функция new_reg создает новую структуру. Мы оформили эту операцию в виде
отдельной функции, чтобы локализовать начальную инициализацию данных в одном месте.
static struct uem_reg *new_reg(vcid)
<
struct uem_reg *p;
ec_null( p = callccd, sizecf(struct uem_reg)) )
return p;
EC_CLEANUP_BGN
return NULL;
EC_CLEANUP_END
}
Регистрационная информация просто передается функции потока, которая может
выглядеть примерно так:
static vcid *thread_prccess(vcid *arg)
<
struct uera_event *e = NULL;
pthread_cleanup_push(cleanup_handler, e);
ec_null( e = calloc(1, sizecf(struct uem_event)) )
e->ue_reg = (struct uem_reg *)arg;
346 ГЛАВА 5 Процессы и потоки
if (waitpid(e->ue_reg->ur_resource.ur_pid, &e->ue_result, 0) == -1)
e->ue_errno = errno;
ec_false( queue_event(e) )
pthread_cleanup_pop(false);
return NULL;
EC.CLEANUP.BGN
uem_free(e);
EC_FLUSH("thread_process")
return NULL;
EC_CLEANUP_END
}
Первым делом она устанавливает обработчик принудительного завершения (с
которым мы уже встречались в разделе 5.17.5). Затем создает структуру данных
события, которая будет помещена в очередь после наступления ожидаемого события
(когда waitpid вернет управление). Структура события, которая используется
всеми потоками:
struct uem.event {
struct uem_reg *ue_reg;
void *ue_buf;
ssize_t ue_result;
int ue.errno;
struct uent_event »ue_next;
};
Обратите внимание: она содержит указатель на структуру с регистрационной
информацией. Поле ue_buf хранит указатель на возвращаемые данные (например
сообщение), которых в данном конкретном случае нет. Код завершения процесса
помещается в поле ue.result. Если возникла ошибка, код ее переписывается в поле
ue_errno. Вызывающее приложение может проверить это поле, чтобы убедиться в
благополучном завершении системного вызова. Поле ue.next служит для ссылки на
следующий элемент очереди, таким образом, очередь представляет собой односвяз-
ный список.
Обработчик принудительного завершения потока освобождает память, занятую
структурой с событием, с помощью uem_f гее (исходный код этой функции здесь не
ПРИВОДИТСЯ):
static void cleanup_handler(void »arg)
{
(void)uem_free((struct uem.event *)arg);
}
Приложение, извлекшее событие из очереди, также вызывает uem_f ree для
освобождения памяти.
ГЛАВА 5 Процессы и потоки 347
Действия по постановке события в очередь выполняются функцией queue.event,
которая вызывается из потока, после возврата из waitpid. Обратите внимание:
событие ставится в очередь даже в том случае, когда waitpid завершился с признаком
ошибки, благодаря чему основное приложение «узнает» о возникших проблемах.
static pthread_mutex_t uem_mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_ccnd_t uem_ccnd_event = PTHREAD_COND_INITIALIZER;
static struct uem_event *event_head;
static bed queue_event(struct uem_event *e)
{
struct uem_event *cur;
ec_rv( pthread_mutex_Icck(&uem_nitx) )
if (eventjiead == NULL)
event_head = e;
else {
fcr (cur = eventjiead; cur->ue_next 1= NULL; cur = cur->ue_next)
/* одна и та же сшибка ставится в счередь тслькс един раз */
if (e->ue_errnc 1= 0 &&
cur->ue_reg->ur_type == e->ue_reg->ur_type &&
cur->ue_errnc == e->ue_errnc) {
ec_rv( pthread_mutex_unlcck(&uem_nitx) )
uem_free(e);
return true;
}
cur->ue_next = e;
}
ec_rv( pthread_ccnd_signaI(&uem_cond_event) )
ec_rv( pthread_mutex_unIock(&uem_mtx) )
return true;
EC_CLEANUP_BGN
(void)pthread_mutex_unIock(&uem_mtx);
return false;
EC_CLEANUP_END
>
Эта функция написана в соответствии с шаблоном из раздела 5.17.4. Она
использует переменную состояния для оповещения приложения о наступившем событии.
Однако сообщение об ошибке (ue_errno ! = 0) может поступить в queue.event
много раз, если системный вызов в потоке продолжает терпеть неудачу. Например, если
в waitpid будет передан неверный идентификатор процесса, поток будет пытаться
передать в queue.event одно и то же сообщение до завершения потока. Это
довольно плохо с точки зрения расходования времени процессора, но мы определенно
должны постараться избежать заполнения очереди тысячами событий,
сообщающих об одном и том же. Поэтому перед постановкой события в очередь, мы прове-
348 ГЛАВА 5 Процессы и потоки
ряем наличие такого события. (Применен далеко не самый лучший способ проверки,
но это же всего лишь прототип.)
Если событие уникально, то строка:
cur->ue_next = e;
помещает его в очередь, затем подается сигнал через переменную состояния,
освобождается мьютекс и на этом работа функции завершается.
Я не буду приводить здесь исходный код функций регистрации других событий,
поскольку они отличаются друг от друга только системным вызовом. Так, функция
uem.register.svmsg запускает поток, который содержит обращение к системному
вызову msg rev, функция uem_register_pxmsg запускает поток, который обращается к mq.receive
и так далее. Все они используют queue.event для постановки события в очередь.
Приложение, которое использует эту библиотеку, могло бы выглядеть примерно так:
struct uem.event »e;
ec_false( uem_reglster_prccess(pld, NULL) )
ec_false( uem_register_pxmsg(mqd, NULL) )
while (true) {
ec_null( e = uem_walt() )
If (e->ue_errnc != 0)
. . . I* вывед сообщения сб сшибке •/
else
switch (e->ue_reg->ur_type) {
case UEM.PXMSG:
... /* обработка принятого сообщения */
break;
case UEM.PROCESS:
... /* обработка кеда завершения процесса */
break;
}
}
Самое главное, чего мы добились: приложение теперь может быть заблокировано
только в одном месте — при обращении к uem.wait, независимо от количества и типов
зарегистрированных событий. Ниже приводится исходный код функции uem.wait,
обратите внимание: она, как и queue.event, следует шаблону работы с
переменными состояния, которые были предметом обсуждения раздела 5.17.4:
ГЛАВА 5 Процессы и потоки 349
struct uem_event *uem_wait(vcid)
{
struct uem_event *e = NULL;
ec_rv( pthread_mutex_icck(4uei»_mtx) )
whiie (event_head == NULL)
ec_rv( pthread_ccnd_wait(&uem_ccnd_event, Suemjntx) )
e = event_head;
event_head = event_head->ue_next;
ec_rv( pthread_mutex_unicck(&uem_i«tx) )
return e;
EC_CLEANUP_BGN
(vcid)pthread_mutex_unicck(&uem_i«tx);
return NULL;
EC_CLEANUP_END
}
При выполнении условия (в очереди есть событие), событие извлекается из
очереди и возвращается указатель на него. Ответственность за освобождение
занимаемой событием памяти возлагается на вызывающую программу.
Итак, мы получили решение проблемы блокирования приложения!
Поскольку описанная методика основана на потоках, она целиком и полностью
зависит от конкретной реализации потоков в системе. Потоки должны быть
быстрыми, легкими и строго следовать стандарту POSIX. В противном случае,
накладные расходы будут слишком велики, системные ресурсы будут расходоваться
впустую, а множество мелких несоответствий затруднит поиск ошибок из-за
многопоточной природы приложения.
Если эта тема представляет для вас интерес, попробуйте переписать пакет иея,
основываясь на процессах. Вы убедитесь, что реализовать такой подход будет
достаточно сложно, но еще сложнее будет поднять производительность до необходимого
уровня. С другой стороны, это поможет вам понять, почему потоки так важны.
Упражнения
5.1. Устраните проблему утечки памяти в функциях setenv и unsetenv, как это было
предложено в конце раздела 5.2.
5.2. Перепишите функции управления средой окружения из раздела 5.2 так,
чтобы имелась возможность экспорта переменных, как это делает стандартная
командная оболочка. То есть измененное значение переменной
экспортируется только при условии вызова соответствующей функции, например env.expcrt.
Подумайте о том, надо ли изменять указатель envi ron и когда лучше это
сделать, а так же о том, можно ли будет использовать существующую функцию
getenv или ее тоже придется заменить.
350 ГЛАВА 5 Процессы и потоки
5.3. Напишите программу, которая просматривает аргументы командной строки.
Отберите те, которые следуют форме «переменная=значение», внесите
соответствующие изменения в среду окружения и вызовите программу, имя
которой должно идти первым аргументом. Остальные аргументы должны стать
аргументами вызываемой программы. Не используйте fork.
5.4. Напишите функцию exeolp2 по аналогии с exeovp из раздела 5.3-
5.5. Добавьте поддержку комбинации символов «#1» в функцию exeo_path из
раздела 5.3.
5.6. Разработайте и напишите функции exeovx и exeolx, которые смогут заменить
все шесть системных вызовов ехео, как это было предложено в примечании к
разделу 53- Вы можете использовать любые системные вызовы ехео, какие
только сочтете необходимыми.
5.7. Объясните своими словами основные принципы реализации системных
вызовов fork и ехео. (Рекомендую к прочтению [Вас198б], [МсК1996], [Маи2001]
или [Bov2001].) Затем покажите, каким образом может быть улучшена
функция posix_spawn. He надо писать действующий код, просто распишите
алгоритм на бумаге простым человеческим языком.
5.8. Изучите назначение posix_spawn (см. [SUS2002]) и реализуйте ее с
применением fork и ехео, как это было предложено в конце раздела 5.5. Для начала
опустите атрибуты и действия. Затем добавьте действия, а затем — атрибуты. Чтобы
выполнить эту работу, вам потребуется реализовать ряд связанных с posix_spawn
системных вызовов (например posix_spawn_file_aotions_init). Это очень
полезное упражнение, даже если вы не собираетесь иметь дело с системами
реального времени.
5.9- Продумайте и поставьте эксперимент по измерению времени,
потребляемого системным вызовом fork. (Допускается использовать функции timestart и
timestop из раздела 1.7.2.) Если у вас есть такая возможность — проведите
эксперимент на разных версиях UNIX и на различных аппаратных платформах.
Сравните время, затраченное вызовом fork со временем, затрачиваемым
вызовом vfork, если ваша система поддерживает их оба. Проделайте тот же
эксперимент с posix_spawn.
5.10. Исследуйте особенности системных вызовов, эквивалентных ехео и fork, в
других операционных системах, таких как VMS, OS/390, Windows и MacOS.
Сравните их характеристики, опишите достоинства и недостатки.
5.11. В последнем примере раздела 5.17.3 до сих пор сохраняется вероятность
изменения значения статической переменной х между вызовами get_and_inor_x(0)
и get_and_incr_x(D, конкурирующим потоком, что может привести к
неправильным результатам. Исправьте эту ошибку так, чтобы потоки выполняли
только одно обращение к функции для проверки и увеличения значения переменной.
5.12. Напишите полноэкранное приложение fileview (см. раздел 4.8), которое
делит экран по горизонтали на две части. Команда «s» запрашивает строку и
пытается найти ее в стандартных заголовочных файлах. В верхней части экрана
должны выводиться полные имена файлов, в которых найдено хотя бы одно
ГЛАВА 5 Процессы и потоки 351
совпадение с искомой строкой. В процессе поиска, по команде «v»,
программа должна выводить содержимое выбранного файла в нижней части экрана,
подсвечивая найденные совпадения. Выберите по своему усмотрению две
клавиши, с помощью которых можно было бы перемещаться по строкам в
верхней части экрана, и две клавиши — в нижней части экрана. Для простоты
реализации пронумеруйте файлы в верхней части экрана и обрабатывайте
номер соответствующего файла по команде «v». Обратите внимание слова «в
процессе поиска» означают, что вы должны реализовать работу команд в виде
отдельных потоков, а так как библиотека Curses наверняка не безопасна в
многопоточной среде, организуйте обращение к ней через мьютексы.
Предусмотрите также команду «q», по которой приложение будет завершать свою работу.
513- Объясните, как можно реализовать приложение fileview (из предыдущего
упражнения), используя процессы вместо потоков. Возможно, перед этим вам
следует прочитать главу 7. Если считаете, что реализация приложения без
использования потоков невозможна, объясните почему? (учитывая, что в этом
случае вы будете доказывать отрицательное утверждение, доказательство
должно выглядеть очень убедительным.)
514. Напишите программу отображения атрибутов процесса, перечисленных в
приложении А. На данном этапе можете ограничиться атрибутами, которые
упоминались в первых пяти главах. Позднее вы сможете расширить этот
список. Если вам встретятся атрибуты, которые вы не сможете отобразить,
объясните почему? Чтобы результат работы программы выглядел интереснее,
выполните несколько системных вызовов при запуске, откройте пару файлов,
переназначьте действия по некоторым сигналам и тому подобное.
Базовые механизмы
межпроцессного
взаимодействия
6.1 Введение
Итак, теперь мы умеем создавать процессы, но этого мало: нам хотелось бы
связывать их друг с другом, чтобы они могли взаимодействовать. В этой главе мы будем
соединять процессы при помощи каналов, используя базовые методики, которые
всегда поддерживались всеми версиями UNIX. В следующей главе мы начнем
изучать более эффективные и надежные, но в то же время менее универсальные и более
сложные методики реализации межпроцессного взаимодействия.
Каналы известны большинству пользователей UNIX как возможность командной
оболочки. Например, для получения отсортированного списка пользователей,
вошедших в систему, можно напечатать следующее:
$ who | sort | more
Здесь мы имеем дело с тремя процессами, соединенными посредством двух
каналов. Данные перемещаются только в одном направлении: от who к sort и далее к more.
Используя системные вызовы, можно также создать конвейеры, поддерживающие
двунаправленную коммуникацию (от процесса А к В и наоборот), и кольцевые
конвейеры (от А к В к С к А). Однако большинство командных оболочек не
поддерживают нотацию, нужную для реализации этих более сложных вариантов,
поэтому большинству пользователей UNIX они неизвестны."
В начале главы я приведу несколько простых примеров однонаправленного
взаимодействия процессов. Затем мы улучшим примитивную оболочку, разработанную
в главе 5. Наша новая оболочка будет достаточно полной, чтобы ее можно было
' Эти варианты можно реализовать при помощи именованных каналов (FIFO), но
командная оболочка при этом играет роль постороннего наблюдателя: она думает, что это
обычные файлы.
354 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
назвать «настоящей»: она будет поддерживать конвейеры, фоновые процессы,
перенаправление ввода-вывода и аргументы, заключенные в кавычки. Однако она не
будет поддерживать генерирование имен файлов (т. е. такие команды, как is t*. ?)
и конструкции программирования (например операторы if). В конце главы я
объясню, как реализовать двунаправленное взаимодействие процессов, и опишу
возможные проблемы, связанные с взаимоблокировкой.
6.2 Каналы
Этот раздел посвящен неименованным каналам, хотя многое из нижесказанного
относится и к именованным каналам (FIFO), которые подробно обсуждаются в
разделе 7.2.
6.2.1 Системный вызов pipe
pipe-
- создает канал
#include <unistd.h>
int
);
/*
pipe(
int pfd[2]
Возвращает О при
успехе
/•
или
дескрипторы файлсв
-1 в случае
ошибки
'/
(устанавливает
еггпо)
*/
Системный вызов pipe создает канал взаимодействия между процессами, который
представляется двумя дескрипторами файлов, возвращаемыми в массиве pfd. При
записи данных в элемент pfd[l ] они помещаются в канал; при чтении из pfd[0] данные
извлекаются.
В документации UNIX указан параметр PIPE_BUF, который можно считать размером
буфера канала. Если несколько процессов или потоков записывают данные в один
канал, операция записи блока, размер которого не превышает pipe_buf байтов,
непременно будет атомарной, т. е. данные, записанные в канал разными процессами
или потоками, не будут чередоваться. Это очень важно, если несколько процессов
записывают в канал структурированные данные, так как в противном случае не было
бы никакой гарантии, что читатель получит корректные данные. Значение PIPE_BUF
не бывает меньше 512, но если во время выполнения вам нужно узнать
действительное значение PIPE_BUF для конкретного канала, вы можете воспользоваться
вызовом fpathconf (см. раздел 1.5.6):
int pfd[2];
long v;
ec_neg1( pipe(pfd) )
errno = 0;
v = fpathconf(pfd[0], _PC_PIPE_BUF);
if (errno != 0)
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 355
EC_FAIL
else if (v == -1)
printf("No limit for PIPE_BUF\n");
else
printf("PIPE_BUF = Xld\n", v);
При выполнении этого кода Solaris вывела мне 5120, FreeBSD — 512, a Linux — 4096.
Первоначально флаг O.NONBLOOK (см. раздел 4.2.2) для канала не установлен, т. е.
операции чтения и записи могут заблокироваться. Как вы, наверное, догадались,
этот флаг можно установить, выполнив вызов fcntl (см. раздел 3.8.3). Влияние
этого флага на вызовы read и write объясняется ниже.
6.2.2 Работа неименованных (и именованных) каналов
В данном разделе под «каналами» понимаются как неименованные каналы, так и
именованные (которые подробнее обсуждаются в разделе 7.2).
Некоторые системные вызовы, служащие для ввода-вывода, работают с каналами
не так, как с обычными файлами, а некоторые вообще ничего не делают. Эти
особенности отражены в следующем списке (здесь описаны только основные вызовы;
все подробности вы можете найти в [SUS2002]).
• write — данные записываются в канал в порядке поступления. В нормальных
условиях (флаг 0_N0NBL00K не установлен) при полном канале вызов write
блокируется, пока вызов read не освободит достаточно места; частичная запись
невозможна. Размер канала зависит от версии UNIX, но, судя по всему, он не
может быть меньше, чем PIPE.BUF байтов. Если флаг 0.N0NBL00K установлен, а объем
записываемых данных не превышает PIPE.BUF байтов, вызов write или запишет
данные немедленно, или возвратит -1, присвоив при этом errno значение EAGAIN;
частичная запись не выполняется. Но если размер данных превышает PIPE.BUF
байтов, частичная запись возможна.
• read — данные считываются из канала в порядке поступления, как и были
записаны. После прочтения данные могут быть прочитаны повторно или
возвращены в канал. В нормальных условиях (флаг 0_N0NBL00K не установлен) при пустом
канале вызов read блокируется, пока не станет доступным хотя бы один байт
данных, если при этом закрыты не все дескрипторы файлов, используемые для
записи. Если все дескрипторы файлов, используемые для записи, закрыты,
вызов read возвратит нулевой счетчик (обычный признак конца файла). Однако
число байтов, указанное при вызове read в качестве третьего аргумента, может
быть прочитано не всегда.- будет прочитано столько байтов, сколько имеется в
наличии на текущий момент, и будет возвращено соответствующее число.
Конечно, число байтов, которые нужно прочитать, никогда не будет превышено,
а непрочитанные байты останутся в канале, по крайней мере, до следующей
операции чтения. Если флаг 0.N0NBL00K установлен, вызов read, выполненный для
пустого канала, возвратит -1, присвоив при этом errno значение EAGAIN.
356 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
• close — в случае каналов функциональность этого вызова более широка, чем при
работе с файлами. Он не только освобождает дескриптор файла для
повторного использования, но и играет для читателя роль признака конца файла, если
все дескрипторы файлов, используемые для записи, закрыты. Если все
дескрипторы файлов, используемые для чтения, закрыты, выполнение вызова write для
дескриптора файла, используемого для записи, приведет к ошибке. При этом
также обычно генерируется сигнал, свидетельствующий о фатальной ошибке;
см. раздел 9.1.3.
• f stat — при работе с каналами этот вызов не очень полезен и используется разве
что для определения того, что дескриптор файла открыт для канала.
Возвращаемое число обычно представляет количество байтов в канале, но никакой
стандарт UNIX этого не требует.
• dup — этот системный вызов и вызов dup2 объясняются в разделе 6.3-
• lseek — с каналами не используется. Если канал содержит последовательность
сообщений, вы не можете просто просмотреть их в поисках сообщения,
которое нужно прочитать следующим. Все это похоже на зубную пасту: чтобы
изучить ее, вы должны выдавить ее из тюбика, но после этого затолкнуть ее назад
невозможно. Это одна из причин, по которой каналы плохо подходят для
реализации приложений, передающих сообщения между процессами.
Системные вызовы, отсутствующие в списке (например select, poll), работают с
каналами так, как вы и предполагаете. Например, если дескриптор файла,
проверяемый при помощи select, открыт для канала, вызов select проверяет, заблокиру-
ется ли вызов read или write.
Отношения между атомарной и неатомарной, блокируемой и неблокируемой, а
также полной, частичной и отсроченной (возвращается -1, a errno присваивается
значение EAGAIM) записью сравнительно сложны. Чтобы лучше разобраться с этим,
изучите таблицу 6.1 (которая основана на таблицах POSIX1990). Два первых
столбца определяют состояние флага O.NONBLOCK и объем записываемых данных, а в трех
последних столбцах указано, что при этом происходит в случае полного канала, а
также канала, который может принять часть данных немедленно, и канала,
который может принять все данные.
Под «полной записью» в таблице понимается то, что вызов write не возвращает
управление, пока не будут записаны все данные. Смысл «неатомарной операции» в
том, что все данные помещаются в канал, но не обязательно последовательно (при
этом даже не гарантируется, что первые PIPE.BUF байтов будут расположены
последовательно). «EAGAIN» означает, что вызов write возвращает -1, присваивая при
этом errno значение EAGAIN. Под «частичной записью» понимается то, что в канал
записываются не все данные, запись которых запрошена.
Во второй строке последнего столбца написано «может заблокироваться» по той
причине, что в начале выполнения write канал может принять все данные, но так как
запись не является атомарной, другой процесс или поток может заполнить
некоторую часть канала до завершения вызова write, что приведет к его блокировке.
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 357
Таблица 6.1. Запись в канал
CLNONBLOCK? Объем Канал полон
записываемых
данных
Канал может
немедленно принять часть
данных (хотя бы 1 байт,
но меньше объема за-
писываемых данных)
Канал может
немедленно
принять все
данные
не установлен <=PIPE_BUF
не установлен >pipe_buf
установлен <=pipe_buf
установлен
>PIPE_BUF
блокируется;
полная запись;
атомарная
операция
блокируется;
полная запись;
неатомарная
операция
EAGAIN
блокируется; полная
запись; атомарная
операция
блокируется; полная
запись; неатомарная
операция
EAGAIN
EAGAIN не блокируется;
частичная запись или
EAGAIN; неатомарная
операция
не блокируется;
полная запись;
атомарная
операция
может заблоки-
роваться; полная
запись;
неатомарная операция
не блокируется;
полная запись;
атомарная
операция
не блокируется;
полная или
частичная запись
или EAGAIN;
неатомарная
операция
Давайте резюмируем вышесказанное.
• Если запрошенный объем записываемых данных не превышает pipe_BUF байтов,
запись всегда атомарна (т. е. никогда не выполняется частично).
• Если флаг 0.N0NBL0CK не установлен (нормальный случай) запись никогда не
выполняется частично, даже если она неатомарна.
• Частично запись выполняется только тогда, когда установлен флаг O.NONBLOCK, a
запрошенный объем превышает pipe_BUF байтов.
Табл. 6.2 характеризует поведение вызова read.
Таблица 6.2. Чтение из канала
0_NONBLOCK? Данные, которые можно немедленно
прочитать, отсутствуют
Данные можно сразу же прочитать
полностью или частично (от 1 байта
до запрошенного объема)
не установлен блокируется, если нет писателей
(возвращается 0)
установлен EAGAIN, если нет писателей
(возвращается 0)
не блокируется; возможно
частичное чтение
не блокируется; возможно
частичное чтение
Обратите внимание, что вторая таблица гораздо проще, чем первая, потому что
атомарность или полнота чтения никогда не гарантируется. Вызов read работает
следующим образом.
358 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
• Если все дескрипторы файлов, используемые для записи, закрыты, вызов read при
пустом канале всегда незамедлительно возвращает 0, что большинством программ
рассматривается как конец файла.
• Если дескриптор файла, используемый для записи, открыт, вызов read при
пустом канале блокируется или не блокируется — это зависит от того, установлен
ли флаг 0_N0NBL0CK.
• При непустом канале вызов read всегда возвращает управление немедленно, каким
бы ни было отношение между запрошенным объемом и объемом данных,
имеющихся в канале. Возвращается число байтов, прочитанных на самом деле.
• Так как гарантии атомарности нет, вы не должны допускать чтение данных из
канала несколькими читателями, если только вы не используете другой
механизм контроля совместного доступа (например межпроцессный семафор),
предотвращающий одновременное чтение (на практике вы редко будете делать это
— скорее, вы будете использовать что-то вроде очередей сообщений).
Вот дополнительные принципы, о которых следует помнить.
• Если у вас есть один писатель и один читатель (например, конвейер командной
оболочки) и читатель подготовлен к частичному чтению (например, он
использует функции ввода-вывода стандартного С), вы можете записывать любые
объемы данных, хотя лучше всего использовать величины, кратные размеру блока
(см. раздел 2.12).
• Если у вас есть несколько писателей (и один читатель), всегда записывайте PIPE_BUF
байтов или менее. Нет никакого практического способа заставить каналы
корректно работать с более крупными объемами данных, если только вы не
используете другой механизм синхронизации, такой как семафор (см. раздел 7.8).
• О значении PIPE_BUF с уверенностью можно сказать только то, что оно не
может быть меньше 512. Если вам на самом деле нужно узнать его, используйте
вызов fpathconf.
• В стандартах не требуется, чтобы чтение было атомарным, но если
запрошенный объем не превышает pipe.BUF байтов, оно во всех версиях UNIX атомарно.
Используйте эти сведения на свой страх и риск.
• Помните, что признак конца файла (возврат 0 из вызова read) не будет
получен, если не будут закрыты все дескрипторы файлов, используемые для записи,
в том числе относящиеся к процессу/который выполняет чтение. Когда я
покажу, как соединить два процесса посредством канала, это станет более понятным.
6.2.3 Примеры использования каналов
Теперь я снова говорю только о неименованных каналах; примеры с
именованными каналами будут приведены в следующей главе.
Если процесс один, какая польза от канала? Никакой, но такой пример очень
информативен:
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 359
void pipetest(void)
{
int pfd[2];
ssize_t nread;
char s[100];
ec_neg1( pipe(pfd) )
ec_neg1( write(pfd[1], "hello", 6) )
ec_neg1( nread = read(pfd[0], s, sizeof(s)) )
if (nread == 0)
printf("E0F\n");
else
printf("read Xld bytes: Xs\n", (long)nread, s);
return;
EC_CLEANUP_BGN
EC_FLUSH("pipetest");
EC_CLEANUP_END
Результаты выполнения этого кода таковы:
read 6 bytes: hello
Мы смогли безопасно записать в канал б байтов, не беспокоясь о его заполнении,
так как 6 меньше, 512 (минимальное значение PIPE_BUF). Но если бы мы записали
больше данных и заполнили канал, вызов write заблокировался бы, пока канал не
был бы частично освобожден вызовом read. Однако этого бы не случилось, потому
что read так и не был бы выполнен. Мы попали бы в ситуацию,.называемую
взаимоблокировкой (deadlock). Чтобы избегать взаимоблокировок при использовании
каналов, нужно быть внимательным, но слишком напрягаться не следует: при
реализации однонаправленного взаимодействия процессов с помощью одного
канала (как делает командная оболочка) взаимоблокировка (из-за каналов)
невозможна. Она возможна только в более экзотических ситуациях.
Если у нас есть два процесса, как связать их, чтобы один мог читать из канала то,
что записывает другой? Никак. Как только процессы созданы, связать их нельзя,
потому что процесс, создающий канал, не может передать дескриптор файла
другому процессу.* Конечно, он может передать номер дескриптора файла, однако в
другом процессе этот номер не будет корректным. Но если мы сформируем канал
в одном процессе до создания другого процесса, второй процесс унаследует
дескрипторы файлов канала, и они будут корректны в обоих процессах. Таким
образом, два процесса, взаимодействующих посредством канала, могут быть
родительским и дочерним процессами, двумя потомками одного процесса, «дедом» и
«внуком» и т. д. Однако они должны быть связаны родственными отношениями, а ка-
' Некоторые системы поддерживают непортируемый способ выполнения этой задачи.
360 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
нал должен быть назначен процессу при его создании. На практике это может быть
серьезным ограничением, потому что после завершения процесса его никак нельзя
воссоздать и заново связать с теми же каналами — необходимо завершить и
оставшиеся процессы, а затем нужно воссоздать все семейство процессов.
В следующем примере один процесс (выполняющий функцию pipewrite)
формирует канал, создает дочерний процесс (выполняющий piperead), который
наследует канал, после чего записывает в канал некоторые данные, чтобы их прочитал
дочерний процесс. Хотя дочерний процесс наследует нужный дескриптор файла,
используемый для чтения, номер дескриптора ему неизвестен, поэтому этот номер
передается в качестве аргумента.
void pipewrite(vcid)
{
int pfd[2];
char fdstr[10];
ec_neg1( pipe(pfd) )
switch (fcrk()) {
case -1:
EC_FAIL
case 0: /* дсчерний процесс */
ec_neg1( clcse(pfd[1]))
snprintf(fdstr, sizecf(fdstr), "Xd", pfd[0]);
execlp("./piperead", "piperead", fdstr, NULL);
EC.FAIL
default: /* родительский процесс */
ec_neg1( clcse(pfd[0]) )
ec_neg1( write(pfd[1], "hellc", 6) )
}
return;
EC_CLEANUP_BGN
EC_FLUSH("pipewrite");
EC_CLEANUP_END
}
Вот код дочернего процесса:
int main(int argc, char *argv[])
{
int fd;
ssize_t nread;
char s[100];
fd = atci(argv[1]);
printf("reading file descriptor )!d\n", fd);
ec_neg1( nread = read(fd, s, sizecf(s)) )
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 361
if (nread == 0)
printf("E0F\n");
else
printf("read Xld bytes: Xs\n", (long)nread, s);
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exlt(EXIT_FAILURE);
EC_CLEANUP_END
}
А вот результат выполнения этого кода:
reading file descriptor 3
read 6 bytes: hello
Думаю, этот пример нуждается в комментариях.
• Так как дочерний процесс будет только читать данные из канала, но не
записывать их, мы сразу же после создания этого процесса закрываем сторону записи
канала (выполняя в родительском процессе вызов close pfd[l]) ради экономии
дескрипторов файлов (если бы дочерний процесс собирался читать больше
данных, мы просто обязаны были бы закрыть сторону записи, иначе дочерний
процесс никогда не получил бы признак конца файла).
• Родительский процесс не читает данные из канала, поэтому он закрывает pdf [0].
• Функция snprintf преобразует дескриптор файла, используемый для чтения, из
целого числа в строку, чтобы его можно было использовать в качестве
аргумента программы.
• Так как мы знаем, что дочерняя программа хранится в текущем каталоге, мы
указали путь к ней как . /piperead, чтобы вызов execlp ее не искал. Это не только
экономит время, но и предотвращает случайное выполнение вместо нее какой-
то другой программы piperead (мало ли каким может оказаться путь во время
работы пользователя). Мы могли бы достичь этого же результата с помощью
вызова execl, который не выполняет поиск.
• Ожидать дочерний процесс ни к чему, так как в этом простом примере
родительский процесс сразу же выполняет выход.
• Программа piperead — дочерний процесс — просто преобразует свой аргумент
обратно в целое число и читает данные с использованием этого дескриптора
файла (запомните: целое число не означает, что дескриптор файла корректен
— он корректен потому, что был унаследован).
Итак, чтобы при помощи канала соединить два процесса для однонаправленной
коммуникации, нужно сделать следующее:
1. Создать канал.
2. Вызвать fork для создания дочернего процесса, который будет читать данные.
362 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
3. Закрыть в дочернем процессе сторону записи канала и выполнить другие
подготовительные действия (что это за действия, я расскажу при обсуждении
последующих примеров).
4. Выполнить в дочернем процессе дочернюю программу.
5. Закрыть в родительском процессе сторону чтения канала.
6. Если в канал должен записывать данные еще один дочерний процесс, нужно
создать его, провести необходимую подготовку и выполнить программу. Если
записывать данные должен родительский процесс, просто записывайте их.
Все наши примеры однонаправленного взаимодействия посредством каналов
будут соответствовать этой схеме.
Теперь вам должно быть ясно, почему fork и exec не объединены в один
системный вызов. Они разделены, чтобы можно было выполнить вышеуказанный этап 3-
Мы уже поняли, что кое-какую работу (закрытие pfd[1]) нужно выполнить между
вызовами fork и exec, а позднее в этой главе вы узнаете и про другую такую работу.
Мы не можем выполнить ее до вызова fork, потому что не хотим, чтобы это
повлияло на родительский процесс (в родительском процессе сторона записи канала
должна быть открытой). В то же время мы не хотим, чтобы эта работа
выполнялась в дочерней программе, потому что ей не следует знать, как она была вызвана
и как связаны ее ввод и вывод (это краеугольный камень философии UNIX). Так
что мы поступаем мудро: мы делаем это в дочернем процессе, выполняя код,
взятый из родительского процесса. Кроме того, локализация кода соединения процессов
облегчает его отладку и изменение.
И все же, если есть всего лишь несколько типичных способов соединения
процессов, почему нет одного специального системного вызова fork-exec,
принимающего различные параметры межпроцессного взаимодействия? Как-никак, некоторые
системные вызовы принимают много параметров, так почему же здесь сделано
иначе? Дело в том, что первоначальные разработчики UNIX, Томпсон и Ричи,
хотели минимизировать число системных вызовов. Вызовы fork и exec просты, но, тем
не менее, позволяют вызывающему формировать самые разнообразные
соединения. Зачем же выдумывать еще один системный вызов?*
Программа piperead читает данные с использованием дескриптора файла, номер
которого в нее передан. Это странная программа: никакая стандартная команда UNIX
так не работает. На самом деле многие программы читают данные из файла с
конкретным дескриптором, полагая при этом, что он уже открыт, но этот дескриптор
равен нулю (стандартный ввод, STDIN_FILENO) и не передается в качестве аргумента.
Аналогично, многие программы записывают данные в файл с дескриптором 1
(стандартный вывод, STDOUT.FILENO). Чтобы связать команды так, как это делает
командная оболочка, мы должны как-то заставить вызов pipe возвратить в массиве pfd
дескриптор 0 для стороны чтения и дескриптор 1 для стороны записи. Увы, pipe
не поддерживает такой возможности. Мы могли бы попытаться закрыть дескрип-
' Но теперь он у нас есть: это вызов posix_spawn, который был упомянут в разделе 5.5.
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 363
торы 0 и 1 до выполнения pipe, чтобы сделать их доступными, но это небезопасно,
потому что в стандартах ничего не сказано о том, какие дескрипторы файлов
будет использовать вызов pipe, к тому же обычно мы не можем позволить себе
принести в жертву стандартные ввод и вывод лишь для создания канала. Что же делать?
Использовать вызов dup или dup2.
6.3 Системные вызовы dup и dup2
dup — дублирует дескриптор файла
«include <unistd.h>
int dup(
int fd
);
1* Возвращает новый
errnc) */
/* дублируемый
дескриптор файла
дескриптор файла или
•/
-1 в случае сшибки
(устанавливает
dup2
- дублирует дескриптор файла
«include <unistd.h>
int
);
/*
dup2(
int fd,
int fd2
Возвращает новый
errnc) */
/* дублируемый
/* используемый
дескриптор
дескриптор
файла
файла
•/
•/
дескриптор файла или -1 в случае сшибки
(устанавливает
Вызов dup дублирует существующий дескриптор файла, возвращая новый
дескриптор, открытый для того же файла (или канала и т. д.) Оба дескриптора разделяют
одно описание файла (см. раздел 2.2) так же, как унаследованный дескриптор файла
разделяет описание файла с соответствующим дескриптором, принадлежащим
родительскому процессу. Вызов завершается неудачей, если указан некорректный
аргумент (дескриптор не открыт) или если нет доступных дескрипторов файлов.
Вызов dup возвращает наименьший из доступных дескрипторов, так что если вы
знаете, какие дескрипторы открыты, вы можете узнать, что будет возвращено.
Однако легче использовать вызов dup2, который позволяет указать при помощи
аргумента fd2, какой дескриптор файла следует возвратить. Если нужно, вызов dup2
закрывает дескриптор fd2, чтобы сделать его доступным.
Вызов dup(fd) эквивалентен вызову:
fcntl(fd, FJHIPFD, 0)
а вызов dup2(fd, fd2) — коду:
close(fd2);
fcntKfd, F_DUPFD, fd2);
364 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
за исключением того, что dup2 атомарен.- если при дублировании возникает
проблема, дескриптор fd2 не закрывается. Во всех примерах я буду использовать dup2,
а не dup, потому что так проще.
Так как описание файла является общим, в использовании второго дескриптора
файла есть только одно преимущество: он имеет другой номер, который,
возможно, лучше соответствует вашим целям. Предположим, что мы создаем канал и
затем вызываем dup2, делая STDIN_FILEND (дескриптор файла 0) дубликатом стороны
чтения канала (дескриптор которой может иметь значение 3, 4, 27 или любое
другое). Если мы затем выполним при помощи вызова exec программу, читающую
STDIN_FILEND (таких программ много!), мы обманем ее, и она будет читать данные
из канала. Используя аналогичный алгоритм, можно сделать STDOUT.FILEND
стороной записи канала.
Если аргументы вызова dup2 равны, он просто возвращает этот дескриптор файла,
ничего не закрывая и не дублируя. Это полезно, если, скажем, дескриптор стороны
чтения канала по той или иной причине уже равен STDIN_FILENO.
Давайте рассмотрим использование dup2 на примере. Следующая функция
формирует канал, создает дочерний процесс, который будет читать данные из канала, делает
дескриптор STDIN_FILENO дочернего процесса дескриптором стороны чтения
канала и вызывает для чтения данных из канала команду cat. Теперь, когда мы можем
использовать STDIN_FILENO, нам не нужна специальная программа вроде pipe read,
которую мы использовали в примере из предыдущего раздела.
vcid pipewrite2(vcid) /* содержит сшибку */
{
int pfd[2];
pid_t pid;
ec_neg1( pipe(pfd) )
switch (pid = fcrkQ) {
case -1:
EC_FAIL
case 0: /* дочерний процесс */
ec_neg1( dup2(pfd[0], ST0IN_FILEN0) )
ec_neg1( clcse(pfd[0]))
ec_neg1( clcse(pfd[1]))
execlpC'cat", "cat", NULL);
EC.FAIL
default: /• родительский процесс */
ec_neg1( clcse(pfd[0]) )
ec_neg1( write(pfd[1], "hellc", 6) )
ec_neg1( waitpid(pid, NULL, 0) )
}
return;
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 365
EC_CLEANUP_BGN
EC_FLUSH("pipewrite2");
EC_CLEANUP_END
}
Результат выполнения этого кода вполне предсказуем:
hellc
Обратите внимание, что в дочернем процессе после дублирования одного из двух
дескрипторов, возвращенных вызовом pipe, мы закрываем оба дескриптора. После
этого сторона чтения канала все еще открыта, но теперь ее дескриптором
является STDIN.FILENO. Родительский процесс закрывает дескриптор pfd[D] (сторону
чтения), потому что он ему не нужен. Вызов waitpid ожидает завершения cat; в
примере pi pe read из предыдущего раздела этот вызов отсутствовал.
К сожалению, после вывода «hello» программа зависает, и мы не увидим
приглашение командной оболочки, пока не завершим программу, нажав Ctrl-c. Попробуйте
догадаться, почему это так. Не знаете? Читайте дальше.
Программа зависает потому, что команда cat, которая читает данные до получения
признака конца файла, не получает его и, следовательно, не завершается. Если
помните, read возвращает 0 только тогда, когда все открытые для канала
дескрипторы файлов, используемые для записи, закрыты, а во время вызова waitpid
дескриптор pf d[ 1 ] все еще открыт. Родительский и дочерний процессы блокируют друг друга.
Что делать? Закрыть сторону записи канала после записи данных:
default: /* родительский процесс */
ec_neg1( clcse(pfd[D]) )
ec_neg1( write(pfd[1], "hellc", 6) )
ec_neg1( clcse(pfd[1]) )
ec_neg1( waitpid(pid, NULL, D) )
Канал может быть организован не только от родительского процесса к дочернему.
В следующем примере реализован эквивалент команды
$ whc | wc
позволяющей узнать число пользователей, вошедших в систему:
vcid whc_wc(vcid)
{
int pfd[2];
pid_t pidl, pid2;
ec_neg1( pipe(pfd) )
switch (pidl = fork()) {
case -1:
EC_FAIL
366 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
case 0: /• первый дсчерний процесс */
ec_neg1( dup2(pfd[1], STDOUT.FILENO) )
ec_neg1( clcse(pfd[0]))
ec_neg1( clcse(pfd[1]))
execlp("whc", "whc", NULL);
EC_FAIL
}
/* рсдительский прсцесс »/
switch (pid2 = fcrk()) {
case -1:
EC.FAIL
case 0: /* втсрсй дсчерний прсцесс */
ec_neg1( dup2(pfd[0], STDIN_FILEN0) )
ec_neg1( cicse(pfd[0]))
ec_neg1( cicse(pfd[1]))
execip("wc", "wc", "-i", NULL);
EC.FAIL
}
/» и спять рсдительский прсцесс */
ec_neg1( cicse(pfd[0]) )
ec_neg1( cicse(pfd[1]) )
ec_neg1( waitpid(pid1, NULL, 0) )
ec_neg1( waitpid(pid2, NULL, 0) )
return;
EC_CLEANUP_BGN
EC_FLUSH("whc_wc");
EC_CLEANUP_END
}
Результат выполнения этого кода таков:
В приведенном примере процессы who и wc являются потомками одного
родительского процесса, но ничто не мешает сделать wc потомком whc:
void whc_wc2(void)
{
int pfd[2];
pid_t pidl, pid2;
ec_neg1( pipe(pfd) )
switch (pidl = fork()) {
case -1:
EC.FAIL
case 0: /* дсчерний прсцесс */
switch (pid2 = fcrk()) {
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 367
case -1:
EC_FAIL
case 0: /* прсцесс-внук */
ec_neg1( dup2(pfd[0], STDIN_FILEN0) )
ec_neg1( clcse(pfd[0]))
ec_neg1( clcse(pfd[1]))
execlp("wc", "wc", "-1", NULL);
EC.FAIL
}
/* снсва дсчерний процесс */
ec_neg1( dup2(pfd[1], STDOUT.FILENO) )
ec_neg1( clcse(pfd[0]))
ec_neg1( clcse(pfd[1]))
execlp("whc", "whc", NULL);
EC.FAIL
}
/* рсдительский прсцесс */
ec_neg1( close(pfd[0]) )
ec_neg1( clcse(pfd[1]) )
ec.negK waltpld(pld1, NULL, 0) )
return;
EC_CLEANUP_BGN
EC_FLUSH("whc_wc2");
EC_CLEANUP_END
>
Два замечания по поводу этого примера.
• Процесс-внук должен быть создан дочерним процессом до закрытия
дескриптора, используемого для чтения, чтобы внук его унаследовал.
• Родительский процесс ожидает завершения только процесса whc (pidl). Он не
может ожидать завершения wc, так как wc не является его дочерним процессом.
Дочерний процесс, выполняющий whc, также не может ожидать завершения wc,
потому что программа whc это не поддерживает. Выполнение waitpid после
вызова, выполняющего wc, не поможет, потому что при успешном выполнении exec
весь код перезаписывается. После завершения whc (родителя wc) системный
процесс унаследует wc и будет ожидать его (см. объяснение в разделе 5.8).
Мы вполне могли бы поступить наоборот и сделать wc родителем whc (см.
упражнение 6.11).
Конечно, использовать командную оболочку для выполнения такой команды
намного проще, чем писать собственную программу. В следующем разделе мы
напишем такую оболочку.
368 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
6.4 Настоящая командная оболочка
Наша командная оболочка будет поддерживать подмножество возможностей
типичной оболочки UNIX. Предлагаю все это обсудить.
■ Простая команда состоит из имени команды, за которым следует
необязательная последовательность разделенных пробелами или символами табуляции
аргументов, каждый из которых является одним словом или строкой,
заключенной в двойные кавычки ("). Если аргумент заключен в кавычки, он может
включать любые другие специальные символы (|, ;,&,>,<, пробелы, символы
табуляции и символы перевода строки). Входящие в состав аргумента кавычки и
обратные косые черты должны быть предварены обратными косыми чертами (\"
или \\). Команда может иметь до 50 аргументов, каждый из которых может
содержать до 500 символов.
■ Стандартным источником ввода простой команды может быть сделан файл; для
этого перед именем файла нужно указать символ <. Аналогично, стандартный
вывод может быть перенаправлен при помощи символа >, при этом файл
вывода перезаписывается. Если для перенаправления вывода используется пара >>,
вывод присоединяется к файлу. Если файл вывода не существует, он создается.
■ Конвейер представляет собой последовательность, включающую одну или
несколько простых команд, разделенных вертикальными чертами (I). В конвейере
стандартный вывод каждой простой команды кроме последней соединяется
посредством канала со стандартным вводом ближайшей к ней правой команды.
■ Конвейер завершается символом перевода строки, точкой с запятой (;) или ам-
персандом (&). В двух первых случаях командная оболочка перед
продолжением работы ожидает завершения самой правой простой команды. Если указан
амперсанд, оболочка ничего не ждет. Она сообщает номер процесса каждой
простой команды, входящей в конвейер, и выполняет каждую простую
команду, игнорируя сигналы прерывания и выхода.
■ Встроенными командами являются присваивание, set и cd. Первые две из них
были описаны в разделе 5.4. Команда cd работает так, как вы думаете.
Прежде всего, мы должны разбить введенную строку на лексемы (tokens) — группы
символов, формирующие синтаксические единицы; в качестве примеров лексем можно
привести слова, заключенные в кавычки-строки и специальные символы, такие как &
и ». Каждая лексема представляется одной из следующих символических констант:
.WORD
.BAR
.AMP
.SEMI
.GT
.GTGT
.LT
NL
Аргумент или имя файла. Если аргумент заключен в кавычки,
они после распознания лексемы удаляются.
Символ |.
Символ &.
Символ
Символ >.
Символ ».
Символ <.
Символ перевода строки.
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 369
T_E0F Специальная лексема, указывающая на достижение конца
файла. Если стандартным источником ввода является терминал,
получение этой лексемы говорит о том, что пользователь ввел
символ EOT (Ctrl-d).
T_ERR0R Специальная лексема, свидетельствующая об ошибке.
Работа лексического анализатора сводится к чтению ввода и составлению лексем
из символов. При каждом вызове анализатора возвращается одна лексема. В
случае лексемы T.W0RD возвращается еще и строка, состоящая из введенных символов
(введенные символы, соответствующие другим лексемам, очевидны).
Нерелевантные символы — например, пробелы, разделяющие аргументы, — лексический
анализатор должен пропускать, ничего не возвращая.'
Наш лексический анализатор является машиной с конечным числом состояний: при
чтении символов они или сразу же распознаются как лексемы, или накапливаются
(например символы, образующие слово). После прочтения любого символа
лексический анализатор может перейти в новое состояние — так анализатор
запоминает, что он делает и как следует интерпретировать символы. Например, пробелы
внутри строки, заключенной в кавычки, нужно обрабатывать не так, как пробелы,
отделяющие аргументы друг от друга. Наша оболочка должна поддерживать
четыре СОСТОЯНИЯ:
NEUTRAL Начальное состояние лексического анализатора при каждом
вызове. Пробелы и символы табуляции пропускаются. Символы
1, 4, ;, < и символы перевода строки немедленно распознаются
как лексемы. Обнаружив символ >, анализатор переходит в
состояние GTGT, в котором он проверяет, следует ли за > второй
символ >, так как > и » являются двумя разными лексемами.
Прочитав символ двойных кавычек, анализатор переходит
в состояние INQU0TE и начинает составлять строку. Любой
другой символ воспринимается как начальный символ слова,
не заключенного в кавычки; он сохраняется в буфере, и
анализатор переходит в состояние INW0RD.
GTGT В это состояние анализатор переходит после прочтения
символа >. Если следующим символом также является >,
возвращается лексема t_gtgt. В противном случае возвращается
лексема T.GT, но перед этим второй (лишний) прочитанный
символ возвращается в очередь вводимых символов при помощи
функции ungetc стандартного С.
IN0U0TE Анализатор переходит в это состояние после прочтения
начального символа двойных кавычек, после чего символы
накапливаются в буфере до прочтения завершающего символа
двойных кавычек. Затем анализатор возвращает лексему
T_W0RD и собранную строку. Для обработки escape-символа
\ нужно предпринять специальные действия.
Большинство систем UNIX поддерживают команду lex, которая может автоматически
сгенерировать лексический анализатор по описанию распознаваемых лексем. Но ее сложно
использовать, а генерируемые ей лексические анализаторы иногда оказываются
крупными и медленными, поэтому их обычно лучше кодировать вручную. Это несложно.
370 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
INW0RD Это состояние говорит о том, что был прочитан и помещен
в буфер первый символ слова. Мы продолжаем накапливать
символы, пока не будет прочитан символ, который не может
входить в состав слова (скажем, |). Лишний прочитанный
символ помещается обратно во входную очередь, после чего
возвращается лексема t_word.
Думаю, теперь вы легко поймете код нашего лексического анализатора gettoken:
typedef enum {T_W0RD, T.BAR, T_AMP, T_SEMI, T_0T, T.GT0T, T_LT,
T_NL, T_E0F, T.ERROR} TOKEN;
static TOKEN gettcken(char »wcrd, size_t maxwcrd)
{
enum {NEUTRAL, 0Т0Т, IN0U0TE, INW0RD} state = NEUTRAL;
int c;
size_t wcrdn = 0;
while ((c = getchar()) != EOF) {
switch (state) {
case NEUTRAL:
switch (c) {
case ';':
return T_SEHI;
case '&':
return T_AMP;
case ' |':
return T_BAR;
case '<':
return T_LT;
case '\n' :
return T_NL;
case ' ':
case '\f:
continue;
case '>':
state = 0Т0Т;
continue;
case "":
state = IN0U0TE;
continue;
default:
state = INWORD;
ec_false( stcre_char(wcrd, maxwcrd, c, &wcrdn) )
continue;
}
case 0Т0Т:
if (c == '>■)
return T_0T0T;
/
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 371
ungetc(c, stdin);
return T_GT;
case iNQUOTE:
switch (c) {
case '\Y:
if ((c = getchar()) == EOF)
с = '\Y;
ec_faise( stcre_char(wcrd, maxwcrd, c, iwcrdn) );
continue;
case '"':
ec_faise( stcre_char(wcrd, maxwcrd, '\0', &wcrdn) )
return T_W0R0;
default:
ec_faise( stcre_char(wcrd, maxwcrd, c, &wcrdn) )
continue;
}
case iNWORD:
switch (c) {
case ';
case '&
case '|
case '<
case '>
case '\n':
case ' ':
case '\t':
ungetc(c, stdin);
ec_faise( stcre_char(wcrd, maxwcrd, '\0', iwcrdn) )
return T.W0RD;
default:
ec_faise( stcre_char(wcrd, maxwcrd, c, &wcrdn) )
continue;
}
}
}
ec_faise( Iferrcr(stdin) )
return T_E0F;
EC_CLEANUP_BGN
return T_ERR0R;
EC_CLEANUP_ENO
}
static bcci stcre_char(char *wcrd, size_t maxwcrd, int c, size_t *np)
{
errnc = E2BIG;
ec_faise( *np < maxword )
372 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
word[(*np)++] = с;
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
При написании большой программы ее удобно отлаживать по частям. Это не только
предоставляет раннюю обратную связь, но и облегчает поиск ошибок, так как их
приходится искать в меньших фрагментах кода. Вот программа, позволяющая
протестировать функцию gettoken:
int main(void)
{
char word[2D0];
while (1)
switch (gettoken(word, sizeof(word))) {
case T_W0RD:
printf("T_WORD <Xs>\n", word);
break;
case T_BAR:
printf("T_BAR\n");
break;
case T_AMP:
printf("T_AMP\n");
break;
case T_SEM1:
printf("T_SEHl\n");
break;
case T_GT:
printf("T_GT\n");
break;
case T_GTGT:
printf("T_GTGT\n");
break;
case T_LT:
printf("T_LT\n");
break;
case T_NL:
printf("T_NL\n");
break;
case T_EDF:
printf("T_EDF\n");
exit(EXIT_SUCCESS);
case T_ERR0R:
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 373
printf("T_ERROR\nn);
exit(EXIT_SUCCESS);
}
}
Запустив эту программу и введя такую строку (заканчивающуюся символом EOT)
sort <inf | pr -h "Sept. Results" »outf&
я увидел следующее:
T_W0RD <sort>
T_LT
T_W0RD <inf>
T_BAR
T_W0RD <pr>
T_W0RD <-h>
T_W0RD <Sept. Results>
T_GTGT
T_W0RD <OUtf>
T_AMP
T_NL
T_E0F
Конечно, это не доказывает, что функция gettoken корректна, но все же внушает
оптимизм, и мы можем продолжить работу над командной оболочкой.
Далее нужно написать функцию, обрабатывающую простые команды, которые могут
завершаться символом |, &, ; или символом перевода строки. Мы назовем эту
функцию command. Она просто заносит аргументы в массив argv, который позднее будет
передан в вызов execvp. Возможны три варианта стандартного источника ввода:
источник по умолчанию (STDIN_FILENO), файл (если указан символ <) или сторона
чтения канала (если простой команде предшествует символ |). Вариантов
стандартного вывода четыре: вариант по умолчанию (STD0UT_FILEN0), создаваемый или
перезаписываемый файл (если указан символ >), создаваемый или дополняемый файл
(в случае ») или сторона записи канала (если за простой командой следует
символ |). Используемые варианты определяются лексемами, возвращаемыми функцией
gettoken.
Обрабатывая лексемы, command использует следующие переменные для
отслеживания конфигурации стандартного ввода и вывода простой команды:
srcfd Дескриптор исходного файла, первоначально STDIN_FILEN0. Если ввод
перенаправлен при помощи <, srcfd присваивается значение -1, а в
переменную srcfile записывается имя файла. Если источником ввода
является канал, переменной srcfd будет присвоен номер дескриптора
файла, ОТЛИЧНЫЙ ОТ STDIN_FILENO.
srcfile Исходный файл, используемый только тогда, когда простая команда
включает <.
dstfd Дескриптор файла вывода, первоначально STDOUT.FILENO. Если вывод
перенаправлен при помощи > или », dstfd присваивается значение -1,
а в dstfile записывается имя файла. Как и в случае srcfd, если выводом
является канал, dstfd присваивается номер дескриптора, отличный
ОТ STDOUT.FILENO.
dstfile Файл вывода, используемый только тогда, когда простая команда
включает > или ».
append Эта булева переменная устанавливается в true только в том случае, если
вывод был перенаправлен с помощью ».
makepipe Аргумент функции comand. Если он установлен в true, вызывающий
просит, чтобы connand создала канал, использовала сторону чтения
в качестве стандартного источника ввода и передала дескриптор
стороны записи вызывающему, который будет использовать его в качестве
своего стандартного вывода. Эта схема будет объяснена чуть ниже.
Обрабатывая лексемы, command ищет логические ошибки: наличие двух < или >,
использование >, когда простая команда завершается символом |, использование <,
когда простой команде предшествует символ |, и т. д. Интересно отметить, что
типичная командная оболочка UNIX не проверяет некоторые такие аномалии:
например, вы можете выполнить следующую команду:
$ who >outf | wc
Функция command возвращает завершающую конвейер лексему потому, что от
завершающего символа зависят дальнейшие действия: если им является 4, оболочка не
ожидает завершения самой правой простой команды, а при выполнении простых
команд конвейера сигналы прерывания и выхода игнорируются. Если же мы
ожидаем самую правую простую команду, нужно возвратить и ее идентификатор
процесса при помощи аргумента wpid. Если завершающим символом является символ
перевода строки, оболочка предлагает пользователю ввести новую команду.
Самый тонкий нюанс command в том, что если за простой командой следует символ |,
эта функция вызывает себя рекурсивно. Рекурсивные вызовы выполняются, пока
не будет достигнут какой-то другой завершающий символ (;, & или символ
перевода строки). При каждом рекурсивном вызове нужно сформировать канал (с
помощью системного вызова pipe), поэтому аргумент makepipe устанавливается в true.
Дескриптор канала, используемый для записи, передается обратно (при помощи
другого аргумента, pipefdp). Простая команда не вызывается (с использованием
invoke), пока рекурсивный вызов не возвратит управление, потому что она не
может быть вызвана, пока не станет доступной сторона записи канала. Кроме того,
как уже было сказано, завершающий символ конвейера должен быть известен до
вызова какой-либо из составляющих конвейер простых команд — это нужно для
того, чтобы вызов invoke знал, что делать с сигналами. Таким образом, конвейер
обрабатывается слева направо, но простые команды вызываются в обратном
порядке. Мы выполняем один вызов command с аргументом makepipe, установленным в
false, для самой левой простой команды, а затем по одному рекурсивному вызову
с makepipe, установленным в true, для каждой дополнительной простой команды.
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 375
Простые команды вызываются по мере развертывания стека вызовов команд — вся
необходимая информация тогда уже доступна.
Вот код ccitiinand. Изучите его тщательно — он того заслуживает.
«define MAXARG 50 /* максимальнее числе аргументов команды */
«define HAXFNAHE 500 /• максимальная длина имени файла */
«define MAXW0R0 500 /* максимальная длина аргумента «/
static TOKEN ccmmand(pid_t *wpid, bcoi makepipe, int «pipefdp)
{
TOKEN tcken, term;
int argc, srefd, dstfd, pid, pfd[2] = {-1, -1};
char *argv[MAXARG], srcfiie[MAXFNAME] = "", dstfiie[MAXFNAME] = "";
char wcrd[MAXW0RD];
bcci append;
argc = 0;
srefd = STDIN.FILENO;
dstfd = STD0UT_FILEN0;
whiie (true) {
switch (tcken = gettcken(wcrd, sizecf(wcrd))) {
case T_W0R0:
if (argc >= MAXARG - 1) {
fprintf(stderr, "Tec many args\n");
continue;
}
if ((argv[argc] = maiicc(strien(wcrd) + 1)) == NULL) {
fprintf(stderr, "Out cf arg memcry\n");
continue;
}
strcpy(argv[argc], wcrd);
argc++;
continue;
case T_LT;
if (makepipe) {
fprintf(stderr, "Extra <\n");
break;
}
if (gettoken(srcfiie, sizecf(srcfiie)) != T_W0RD) {
fprintf(stderr, "Iiiegai <\n");
break;
}
srefd = -1;
continue;
case T_GT:
case T_GTGT:
if (dstfd l= ST00UT_FILEN0) {
fprintf(stderr, "Extra > or »\n");
!3 3ак. 40.10
376 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
break;
}
if (gettcken(dstfiie, sizecf(dstfiie)) 1= T_W0RD) {
fprintf(stderr, "liiegai > cr »\n");
break;
}
dstfd = -1;
append = tcken == T_GTGT;
continue;
case T_BAR;
case T_AMP:
case T_SEMI:
case T_NL:
argv[argc] = NULL;
if (tcken == T_BAR) {
if (dstfd l= STDDUT_FILEND) {
fprintf(stderr, "> or » conflicts with |\n");
break;
}
terra = ccraraand(wpid, true, idstfd);
if (terra == T_ERRDR)
return T_ERRDR;
}
eise
terra = tcken;
if (makepipe) {
ec_neg1( pipe(pfd) )
•pipefdp = pfd[i];
srcfd = pfd[D];
}
ec_neg1( pid = invcke(argc, argv, srcfd, srcfiie, dstfd,
dstfiie, append, terra == T_AMP, pfd[1]) )
if (tcken != T_BAR)
♦wpid = pid;
if (argc = D && (tcken l= T_NL || srcfd > 1))
fprintf(stderr, "Missing comi»and\n");
whiie (-argc >= D)
free(argv[argc]);
return term;
case T_EDF:
exit(EXIT_SUDDESS);
case T_ERRDR:
return T_ERRDR;
}
}
ED_DLEANUP_BGN
return T_ERRDR;
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 377
EC_CLEANUP_END
}
Почему, встретив лексему T_BAR, указывающую на необходимость создания канала,
мы создаем его в следующем вызове command вместо того, чтобы создать канал в
текущем вызове command и просто передать дескриптор чтения следующему
вызову? Ради экономии дескрипторов файлов. Если бы вызов pipe выполнялся до
рекурсивного вызова command, а не после, каждый уровень рекурсии — каждая простая
команда — связывал бы два файловых дескриптора канала, которые не закрывались
бы до возврата рекурсивного вызова. На некоторых системах, предоставляющих мало
дескрипторов файлов, конвейеры были бы тогда ограничены и могли включать
примерно в два раза меньше простых команд. Если вызывающему коду нужно
сформировать канал, то, прося сделать это читателя, мы можем вызвать pipe как раз перед
вызовом invoke (скоро мы до него доберемся), благодаря чему конвейеры могут иметь
любую длину.
Дочернему процессу, который будет создан вызовом invoke, не нужна сторона
записи канала (pfd[l]), поэтому она передается вызову invoke в качестве последнего
аргумента, чтобы дочерний процесс мог закрыть ее; мы увидим это чуть ниже.
Вот программа main, выполняющая первый вызов command. Она создана на основе
функции main, которую мы написали для оболочки из раздела 54.
int main(void)
{
pid_t pid;
TOKEN term = T_NL;
ignore_sig();
while (true) {
if (term == T_NL)
printf("Xs\ PROMPT);
term = commandUpid, false, NULL);
if (term == T_ERR0R) {
fprintf(stderr, "Bad command\n");
EC_FLUSH("main-bad command")
term = T_NL;
}
if (term != T_AMP 8A pid > 0)
wa it_and_d isplay(pid);
fd_check();
>
}
Вызов ignore.sig нужен для игнорирования сигналов прерывания и выхода — мы
не хотим, чтобы нашу оболочку можно было уничтожить, нажав клавишу
прерывания или выхода. Код ignore_sig будет приведен в разделе 9.1.6. Завершающий символ
конвейера, возвращенный функцией command, говорит нам, нужно ли дожидаться
378 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
завершения самой правой простой команды (скоро вы увидите код этой версии
функции wait_and_display) и следует ли выводить приглашение.
Перенаправляя ввод-вывод, создавая каналы и т. д., мы не хотим забыть о закрытии
каких-либо дескрипторов файлов, поэтому, обрабатывая команду, мы каждый раз
вызываем fd.check, чтобы убедиться в том, что открыты только стандартные
дескрипторы (из итоговой версии этой оболочки вызов f d.check вполне можно было бы удалить).
Мы проверяем только первые 20 дескрипторов файлов, так как этого достаточно для
обнаружения любых дескрипторов, по недосмотру оставленных открытыми:
static vcid fd_check(vcid)
{
int fd;
bcci ck = true;
fcr (fd =3; fd < 20; fd++)
if (fcntKfd, FJ3ETFL) != -1 || errnc != EBADF) {
ck = false;
fprintf(stderr, "*•* fd Xd is cpen ***\n", fd);
}
if (!ck)
_exit(EXIT_FAILURE);
}
Для выполнения простой команды функция ccmmand вызывает invcke, передавая
аргументы команды (а где и argv) и описанные выше переменные источника и
назначения, (srcfd, srcfile, dstfd, dstfile и append). Предпоследний аргумент говорит invcke,
нужно ли простую команду выполнить в фоновом режиме, а последним аргументом
является дескриптор файла, закрываемый в дочернем процессе, о чем я уже говорил.
Используемая вызовом invcke схема выполнения fork и exec вам уже знакома:
static pid_t invcke(int argc, char *argv[], int srcfd, censt char «srcfile,
int dstfd, censt char «dstfile, beel append, beel bckgrnd, int closefd)
{
pid_t pid;
char «cmdname, «cmdpath;
if (argc == 0 || builtin(argc, argv, Srcfd, dstfd))
return 0;
switch (pid = fcrkQ) {
case -1;
fprintf(stderr, "Can't create new prccess\n");
return 0;
case 0;
if (clcsefd != -1)
ec_neg1( clcse(clcsefd) )
if (Ibckgrnd)
ec_false( entry_sig() )
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 379
redirect(srcfd, srcfile, dstfd, dstfile, append, bckgrnd);
cmdname = strchr(argv[0], '/');
if (cnidnanie == NULL)
cmdname = argv[0];
else
cmdname++;
cnidpath = argv[0];
argv[0] = cmdname;
execvp(cmdpath, argv);
fprintf(stderr, "Can't execute Xs\n", cmdpath);
_exit(EXIT_FAILURE);
}
/* родительский процесс */
if (srcfd > STOOUT.FILENO)
ec_neg1( clcse(srcfd) )
if (dstfd > STDOUT.FILENO)
ec_neg1( clcse(dstfd) )
if (bckgrnd)
printf("Xld\n", (lcng)pid);
return pid;
EC.CLEANUP.BGN
if (pid == 0)
_exit(EXIT_FAILURE);
return -1;
EC_CLEANUP_ENO
}
Возня с cmdname и cmdpath обусловлена тем, что нам нужно хранить весь путь,
который мог быть введен в качестве первого аргумента execvp, но при этом указатель
argv[0] должен указывать только на имя файла.
Вызываемая команда может оказаться встроенной. Если это так, функция builtin
(см. ниже) возвращает true, в противном случае для выполнения простой команды
создается дочерний процесс. Напомню, что сигналы прерывания и выхода уже
игнорируются. Если команда должна быть выполнена не в фоновом режиме, мы
вызываем entry.sig (см. раздел 9-1-6), восстанавливая тот способ обработки сигналов,
какой имел место при запуске оболочки.
Вызов invcke выполняет функцию redirect для перенаправления ввода-вывода и
дублирования источника и назначения с использованием STDIN_FILEN0 и STDOUT.FILENO,
если это необходимо. Вот ее код:
static vcid redirect(int srcfd, ccnst char «srcfile, int dstfd,
ccnst char «dstfile, bccl append, bcol bckgrnd)
{
int flags;
if (srcfd == STDIN_FILENC && bckgrnd) {
380 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
srcfile = "/dev/null";
srcfd = -1;
}
If (srcflle[0] l= '\0')
ec_neg1( srcfd = cpen(srcfile, 0_R00NLY, 0) )
ec_neg1( dup2(srcfd, STDIN.FILENO) )
If (srcfd l= STDIN_FILENO)
ec.negK close(srcfd) )
If (dstflle[0] != '\0') (
flags = OJ/RONLY | O.CREAT;
If (append)
flags |= 0_APPEN0;
else
flags |= O.TRUNC;
ec.negK dstfd = open(dstfile, flags, PERM.FILE) )
}
ec.negK dup2(dstfd, STOOUT.FILENO) )
if (dstfd l= STDOUT.FILENO)
ec.negK clcse(dstfd) )
fd_check();
return;
EC.CLEANUP.BGN
_exlt(EXIT_FAILURE); /* мы в дочернем процессе */
EC_CLEANUP_END
>
Если стандартный ввод фоновой команды не перенаправлен на файл или канал,
мы предусмотрительно перенаправляем его на специальный файл /dev/null, при
прочтении которого сразу же будет получен признак конца файла. Остальные
части redirect пояснений не требует.
Функция wait_and_display (вызываемая из main) основана на коде из раздела 5.8. Ее
аргумент говорит о том, завершения какого процесса она должна ожидать, но во
время ожидания она может узнать о завершении других процессов. Если это
происходит, она вызывает display.status, чтобы вывести идентификаторы этих
процессов и описание причины их завершения. Когда завершается указанный ей
процесс, wait_and_display отображает его статус и возвращает управление:
static bool walt_and_display(pid_t pid)
<
pid_t wpid;
int status;
dc {
ec.negK wpid = waitpid(-1, &status, 0) )
display_status(wpid, status);
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 381
} while (wpid != pid);
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
Наконец, мы добрались до функции builtin. Большая ее часть взята из раздела 5.4;
я добавил код обработки команды cd. Если она вызвана без аргументов,
выполняется переход к каталогу, путь к которому задан переменной среды НОМЕ.
static bool builtin(int argc, char *argv[], int srcfd, int dstfd)
{
char «path;
if (strchr(argv[0], '=') != NULL)
asg(argc, argv);
else if (strcrap(argv[0], "set") == 0)
set(argc, argv);
else If (strcrap(argv[0], "cd") == 0) {
if (argc > 1)
path = argv[1];
else if ((path = getenvC'HOME")) == NULL)
path = ".";
if (chdir(path) == -1)
fprintf(stderr, "Xs: bad directory\n", path);
>
else
return false;
if (srcfd != STDIN.FILENO || dstfd != STD0UT_FILEN0)
fprintf(stderr, "Illegal redirection or pipeline\n");
return true;
>
Как вы заметили, некоторые ошибки, обнаруживаемые нашей оболочкой,
обрабатываются со всем вниманием — с помощью макросов «ее» — тогда как при других
ошибках оболочка просто выводит сообщение и продолжает работу. Мы считаем
серьезными «невозможные» ошибки, потому что если они происходят, это
означает, что ОС нанесена смертельная рана. Это компромисс: определенно,
игнорировать эти ошибки не следует, потому что невозможное, как известно, иногда
происходит, но мы не хотим писать код восстановления от ошибок, которые крайне
маловероятны. Иногда мы угадываем неверно: серьезная ошибка происходит раз
за разом, потому что она оказывается совсем не невозможной. Если это так, нужно
переписать код обработки ошибок, чтобы она выполнялась иначе.
Я не буду приводить примеры использования этой оболочки, потому что она
работает так же, как и оболочка, к которой вы привыкли.
382 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
6.5 Двунаправленное взаимодействие
с использованием однонаправленных каналов
Теперь мы перейдем от однонаправленного взаимодействия, используемого
командной оболочкой, к двунаправленному. Типичная командная оболочка не
позволяет организовать двунаправленное взаимодействие процессов. Двунаправленные
конвейеры создаются в программах, написанных на С.
Мы начнем с довольно простого примера. Допустим, мы хотим вызвать в
программе команду sort для сортировки кое-каких данных. Конечно, это можно было бы
сделать подобным образом:
system("sort <файл_с_данными >файл_вывода");
Затем мы могли бы прочитать отсортированные данные из файла_вывода. Однако
нас это не устраивает: мы хотим, чтобы команда sort получила данные по каналу и
отправила нам отсортированный вывод тоже по каналу. Так как sort может читать
свой стандартный источник ввода и записывать данные в стандартный вывод (она
является фильтром), все должно у нас получиться. Мы уже знаем, как сделать
произвольный дескриптор файла стандартным вводом или выводом процесса.
Чтобы вы лучше представили всю ситуацию, сначала я решу поставленную задачу
неверно — думаю, это принесет больше пользы, чем простое обсуждение уже
готового решения.
Так как каждый канал имеет сторону чтения и сторону записи, а оба
соответствующих дескриптора наследуются дочерним процессом, мы воспользуемся только одним
каналом. Команда sort будет читать из него данные для получения своего ввода и
записывать в него отсортированный вывод. Родительский процесс также будет
обращаться к каналу. Он будет записывать в канал неотсортированные данные и
читать из него отсортированный результат. Вот программа, читающая данные из
файла datafile и сортирующая их с помощью sort; отсортированные данные
печатаются:
void fsortO(void) /* неверный вариант »/
{
int pfd[2], fd;
ssize_t nread;
pid_t pid;
char buf[512];
ec_neg1( pipe(pfd) )
ec_neg1( pid = fork() )
if (pid == 0) { /* дочерний процесс */
ec_neg1( dup2(pfd[0], STDIN_FILEN0) )
ec_neg1( close(pfd[0]) )
ec_neg1( dup2(pfd[1], STD0UT_FILEN0) )
ec_neg1( close(pfd[1]) )
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 383
execlpC'scrt", "scrt", NULL);
EC.FAIL
}
/* родительский процесс */
ec_neg1( fd = cpen("datafile", 0_R00NLY) )
while (true) {
ec_neg1( nread = read(fd, buf, sizecf(buf)) )
if (nread == 0)
break;
ec_neg1( write(pfd[1], buf, nread) )
>
ec_negl( clcse(fd) )
ec_neg1( clcse(pfd[1]) )
while (true) {
ec_neg1( nread = read(pfd[0], buf, sizecf(buf)) )
If (nread == 0)
break;
ec_negl( write(ST00UT_FILEN0, buf, nread) )
>
ec_neg1( clcse(pfd[0]) )
ec_neg1( waitpid(pid, NULL, 0) )
return;
EC_CLEANUP_BGN
EC_FLUSH("fscrtO");
EC_CLEANUP_EN0
}
Содержимое файла dataf ile таково:
peach
apple
orange
strawberry
plum
pear
cherry
banana
apricot
tcmatc
pineapple
mange
Когда я запустил эту программу, она вывела названия фруктов в таком же порядке,
а затем зависла. Мне пришлось нажать клавишу прерывания, чтобы завершить ее.
Что пошло не так?
С использованием только одного канала в данном случае связаны две проблемы. Во-
первых, после записи неотсортированных данных в канал родительский процесс
384 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
немедленно начинает читать канал, предполагая, что он читает вывод sort. Однако
он обгоняет sort и читает свой же собственный вывод! Поэтому данные и были
выведены неотсортированными. Это напоминает первый пример из раздела 6.2.3.
Вторая проблема приводит к взаимоблокировке. Дочерний процесс, выполняющий
sort, начинает читать свой стандартный источник ввода, но он оказывается
пустым, так как родительский процесс уже опустошил его. Как бы то ни было,
команда sort блокируется в системном вызове read в ожидании признака конца файла,
который может быть получен только после закрытия стороны записи. Конечно,
родительский процесс уже закрыл сторону записи, но у дочернего процесса она все
еще открыта — все-таки, предполагалось, что дочерний процесс будет записывать
в нее свой вывод. Так что дочерний процесс попадает в тупик. Вообще говоря,
взаимоблокировка происходит с любым фильтром, который обманным путем
заставляют выполнять чтение и запись с использованием одного канала.
Первую проблему можно попробовать решить, заставив родительский процесс перед
чтением канала дождаться завершения дочернего процесса. На первый взгляд, это
разумно. Однако это не сработает, если при сортировке большого объема данных
канал будет заполнен выводом дочернего процесса, потому что дочерний процесс
заблокируется в системном вызове write. Так как родительский процесс будет
заблокирован в вызове waitpid, снова произойдет взаимоблокировка.
Можно также попробовать использовать семафоры для синхронизации операций
и предотвращения взаимоблокировки, но это все равно, что палить из пушки по
воробьям. Проблема будет устранена, если мы просто воспользуемся двумя
каналами, каждый из которых будет обрабатывать однонаправленный трафик. Один канал
будет обрабатывать данные, поступающие к sort, а второй —данные, передаваемые
обратно. Вот исправленный вариант функции fsortO:
void fsort(void)
{
int pfdout[2], pfdin[2], fd;
ssize_t nread;
pid_t pid;
ohar buf[512];
eo_neg1( pipe(pfdout) )
eo_neg1( pipe(pfdin) )
eo_neg1( pid = fork() )
if (pid == 0) { /* дочерний процеоо */
eo_neg1( dup2(pfdout[0], STDIN_FILEN0) )
eo_neg1( olose(pfdout[0]) )
eo_neg1( olose(pfdout[1]) )
eo_neg1( dup2(pfdin[1], STD0UT_FILEN0) )
eo_neg1( olose(pfdin[0]) )
eo_neg1( olose(pfdin[1]) )
exeolpCsort", "sort", NULL);
EC_FAIL
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 385
}
/* родительокий процеоо */
eo_neg1( olose(pfdout[0]) )
eo_neg1( olose(pfdin[1]) )
eo_neg1( fd = open("dataflle", 0_RD0NLY) )
while (true) {
eo_neg1( nread = read(fd, buf, slzeof(buf)) )
If (nread == 0)
break;
eo_neg1( wrlte(pfdout[1], buf, nread) )
}
eo_neg1( olose(fd) )
eo_neg1( olose(pfdout[1]) )
while (true) {
eo_neg1( nread = read(pfdin[0], buf, sizeof(buf)) )
if (nread == 0)
break;
eo_neg1( wrlte(STD0UT_FILEN0, buf, nread) )
}
eojiegK olose(pfdin[0]) )
eo_neg1( waitpld(pid, NULL, 0) )
return;
EC_CLEANUP_BGN
EC_FLUSH("fsort");
EC_CLEANUP_ENO
}
Теперь мы получаем следующее:
apple
apricot
banana
cherry
mango
orange
peach
pear
pineapple
plum
strawberry
tomato
He только список отсортирован, но и программа сама завершилась! В корректной
версии интересно то, что хотя она использует на один канал больше, никакие
дополнительные дескрипторы файлов не используются. В обеих версиях
родительский и дочерний процессы должны читать данные друг друга и писать друг другу,
386 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
так что каждому нужны два файловых дескриптора каналов. Во второй версии
родительский процесс может закрыть сторону чтения pfdout и сторону записи pfdin.
Вообще, при использовании двух каналов взаимоблокировка все же возможна, хотя
и не в нашем примере. Взаимоблокировка произошла бы, если бы родительский
процесс, записывая данные в свой канал вывода, заблокировался по причине его
гдполнения, а дочерний процесс вместо очистки канала записал родительскому
процессу много данных и заблокировался на втором канале. Чтобы исключить
взаимоблокировку, каждую ситуацию нужно тщательно исследовать, и не только при
небольших объемах тестовых данных.
Есть и другие пути, ведущие к взаимоблокировке. Давайте обсудим один из них,
изучив более сложный пример двунаправленного межпроцессного взаимодействия.
Дочерним процессом в этом примере является стандартный редактор ed.
Родительский процесс использует редактор как сервер, отправляя ему команды и получая
обратно вывод. Такой подход может быть реализован в визуальном редакторе:
например, родительский процесс может отвечать за работу с клавиатурой и экраном,
a ed — выполнять фактическое редактирование. Объем книги не позволяет мне
подробно обсуждать визуальный редактор, поэтому за основу своего примера я взял
совсем простую интерактивную программу поиска, очень похожую на дгер. Вот
пример сеанса взаимодействия с ней (то, что вводил я, подчеркнуто):
$ search
File? datafile
Search pattern? la
apple
apricot
Search pattern? apple
apple
pineapple
Search pattern? o$
tomato
mango
Search pattern? EOT
$
Файл с названиями фруктов взят из уже изученных нами примеров сортировки.
Когда я использовал команду sort, я знал, когда нужно прекратить читать ее вывод:
при достижении конца файла. Все было просто, потому что sort читает весь свой
ввод, записывает весь свой вывод и завершается. Однако редактор интерактивен.
Он читает некоторый ввод, возможно, записывает в поток вывода некоторые
данные неизвестного заранее объема, после чего возвращается и читает другие вве-
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 387
денные данные. Мы можем перехватить выводимые им данные, сделав его
стандартным выводом канал, но как мы узнаем, сколько данных читать за один раз? Мы не
можем дожидаться конца файла, так как редактор не будет закрывать свой
дескриптор файла, используемый для вывода, вплоть до завершения. Если прочитать слишком
много данных, произойдет взаимоблокировка, а если прочитать слишком мало,
утратится синхронизация между командой и ее результатами.
Если бы редактор можно было изменить, мы заставили бы его отправлять
недвусмысленную строку данных, когда он был бы готов принять больше ввода. На
самом деле редактор может предлагать пользователю ввести данные (эта функция
включается командой Р), но символом приглашения является символ «, который
трудно назвать недвусмысленным (хотя вы можете изменить приглашение; см.
упражнение 6.12). Нам нужно, чтобы редактор сообщал нам о завершении вывода
результатов выполнения каждой команды.
Мы прибегнем к хитрости: после выполнения каждой команды мы будем подавать
редактору команду г (читать файл), указывая имя несуществующего файла, и
ожидать сообщение об ошибке, генерируемое ed. Появление этого сообщения говорит
о том, что редактор отреагировал на некорректную команду г и готов принять новую
команду. Имя несуществующего файла должно быть таким, чтобы сообщение об
ошибке было недвусмысленным. Наверное, лучше всего было бы использовать имя,
содержащее управляющие символы, потому что они редко встречаются в
текстовых файлах, но ради ясности мы используем имя «end-of-file». Чтобы точно узнать,
какое именно сообщение генерируется, запустим редактор:
$ ed
г end-of-file
?end-of-file
3
$
Итак, мы должны ожидать «?end-of-file».
Чтобы облегчить работу, давайте напишем функции взаимодействия с редактором.
В самом начале мы вызываем edinvoke для запуска редактора и формирования
каналов. Вместо непосредственного использования дескрипторов файлов, в случае
чего мы должны были бы выполнять ввод-вывод при помощи read и write, мы
используем указатели на FILE стандартного С, вызывая для их инициализации fdopen.
После этого мы можем передавать данные редактору при помощи указателя sndf p
и читать данные редактора, используя указатель rcvfp. Вот код функции edinvoke,
который очень напоминает первую часть fsort:
static FILE «sndfp, *rcvfp;
static bool edinvoke(void)
{
int pfdout[2], pfdin[2];
pid_t pid;
388 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
ec_neg1( pipe(pfdcut) )
ec_neg1( pipe(pfdin) )
switch (pid = fcrk()) {
case -1:
EC_FAIL
case 0:
ec_neg1( dup2(pfdcut[0], ST0IN_FILEN0) )
ec_neg1( dup2(pfdin[1], ST00UT_FILENO) )
ec_neg1( clcse(pfdcut[0]) )
ec_neg1( clcse(pfdcut[1]) )
ec_neg1( clcse(pfdin[0]) )
ec_neg1( clcse(pfdln[1]) )
execlp("ed", "ed", "-", NULL);
EC.FAIL
}
ec_neg1( clcse(pfdcut[0]) )
ec_neg1( clcse(pfdin[l]) )
ec_null( sndfp = fdcpen(pfdcut[1], "w") )
ec_null( rcvfp = fdcpen(pfdin[0], "r") )
return true;
EC_CLEANUP_BGN
if (pid == 0) {
EC_FLUSH("edinvcke");
_exit(EXIT_FAILURE);
}
return false;
EC_CLEANUP_EN0
}
Заметьте, что при ошибке мы в дочернем процессе вызываем .exit, а не exit, но
при этом мы должны для вывода сообщениия об ошибке вызвать EC_FLUSH, как
сделали в разделе 5.6.
Для записи и чтения канала мы используем функции edsnd, edrcv и turnaround.
Функция edrcv возвращает false, если доступных данных, выводимых редактором, больше
нет; она узнает об этом по наличию форсированного сообщения об ошибке
(действительный признак конца, т. е. значение NULL, возвращаемое функцией fgets, она
рассматривает как ошибку). Чтобы убедиться в наличии сообщения об ошибке, нужно
вызвать turnaround для перехода от отправления к получению данных. Вот три этих
фуНКЦИИ:
static bed edsnd(censt char *s)
<
ec_ecf( fputs(s, sndfp) )
return true;
EC_CLEANUP_BGN
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 389
return false;
EC_CLEANUP_END
}
static bccl edrcv(char «s, size_t smax)
{
ec_null( fgets(s, smax, rcvfp) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
static bccl turnarcund(vcid)
{
ec_false( edsnd("r end-cf-file\n") )
ec_ecf( fflush(sndfp) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Мы сбрасываем sndfp, чтобы редактор получил весь наш вывод, потому что
обычно данные, выводимые с использованием каналов, буферизуются.
Так как обычно нам нужно отображать все, что хочет сказать редактор,
желательно написать соответствующую функцию:
static bccl rcvall(vcid)
{
char s[200];
ec_false( turnarcund() )
while (true) {
ec_false( edrcv(s, sizecf(s)) )
if (strcmp(s, "?end-cf-file\n") == 0)
break;
printfC'Xs", s);
}
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
390 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
Наконец, вот функция main, которая взаимодействует с пользователем и редактором:
int main(void)
{
ohar s[100], line[200];
bool eof;
eo_false( prompt("File", s, sizeof(s), ieof) )
if (eof)
exit(EXIT_SUCCESS);
eo_false( edinvoke() )
snprintf(line, sizeof(line), "e Xs\n", s);
eo_false( edsnd(line) )
eo_false( rovall() );
while (true) {
eo_false( prompt("Searoh pattern", s, sizeof(s), ieof) )
if (eof)
break;
snprintf(line, sizeof(line), "g/Xs/p\n", s);
eo_false( edsnd(line) )
eo_false( rovall() );
}
eo_false( edsnd("q\n") )
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
statio bool prompt(oonst ohar «iisg, ohar «result, size_t resultmax,
bool *eofp)
<
char *p;
printf("\nXs? ", msg);
if (fgets(result, resultmax, stdin) == NULL) {
if (ferror(stdin))
EC.FAIL
*eofp = true;
}
else {
if ((p = strrohr(result, '\n')) 1= NULL)
*P = '\0';
♦eofp = false;
}
return true;
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 391
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Защищена ли эта программа от взаимоблокировок? Команды нашего редактора
довольно коротки, а емкость канала не может быть меньше 512 байтов, поэтому
родительский процесс никогда не заблокируется при записи данных во входной
канал редактора. Редактор никогда не заблокируется (разве что временно),
записывая данные в канал для родительского процесса, потому что родительский
процесс будет читать все, что редактор хочет сообщить. Сообщение о некорректном
файле не будет пропущено, если только кто-то не создаст файл с таким именем,
что лучше не делать. Не слишком убедительное доказательство, но, по крайней мере,
очевидных ловушек мы избежали.
Приведенный пример вывода был получен на Solaris. Под управлением FreeBSD,
Darwin и Linux эта программа вообще не заработала, потому что они включают ed
другой версии. Если вас интересуют подробности, см. упражнение 6.12.
6.6 Двунаправленное взаимодействие
с использованием двунаправленных каналов
Начиная обсуждение каналов (в разделе 6.2.1), я сказал, что первый дескриптор файла
(pfd[D]) открывается для чтения, второй (pfd[1]) для записи, а все, что
записывается в pfd[l ], можно прочитать из pfd[D]. Затем я привел несколько полезных
примеров, в которых один процесс писал данные, а второй читал. В некоторых
примерах было реализовано двунаправленное взаимодействие при помощи двух
однонаправленных каналов, что очень похоже на автомагистраль, состоящую, по сути,
из двух дорог с односторонним движением.
Все, что я сказал о каналах, верно, но это не означает, что дескрипторы файлов
открываются только для чтения или только для записи. Поясню сказанное на
примере: вот небольшая программа, выводящая информацию о режиме доступа к
файловым дескрипторам канала:
void pipe_access_mode(void)
{
int pfd[2], flags, i;
ec_neg1( pipe(pfd) )
for (i = D; i < 2; i++) {
ec_neg1( flags = fcntl(pfd[i], F.GETFL) )
if ((flags & D.ACCMDDE) == DJTODNLY)
printf("pfd[Xd] D.RDDNLAn", i);
if ((flags & D_ACCMDDE) == D.WRONLY)
printf("pfd[Xd] O.WRONLAn", i);
392 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
if ((flags & 0_ACCM0DE) == 0_RDWR)
printf("pfd[Xd] 0_RDWR\n", i);
}
return;
EC_CLEANUP_BGN
EC_FLUSH("pipe_access_mcde")
EC_CLEANUP_END
}
Выполнив этот код на системе Linux, я получил то, что вы, вероятно, и ожидали:
pfd[0] 0_RDONLY
pfd[1] O.WRONLY
Но посмотрите, что сообщили мне FreeBSD и Solaris:
pfd[0] 0_RDWR
pfd[1] 0_RDWR
Обе стороны канала открыты для чтения и записи! Оказывается, что на этих двух
(и на других) системах все, что записывается в pfd[0], можно прочитать из pfd[1],
все, что записывается в pfd[l], может быть прочитано из pfd[0], при этом данные
никогда не смешиваются. По сути, каналы, реализованные в этих системах,
являются разделенными магистралями, защищенными от коллизий. Это означает, что
мы можем организовать двунаправленное взаимодействие, используя только один
канал, потому что он является двунаправленным.
Сейчас я покажу, как это сделать. Вот измененная версия edinvoke из последнего
примера предыдущего раздела:
static FILE «sndfp, «rcvfp;
static bccl edinvcke(vcid)
{
int pfd[2];
pid_t pid;
ec_neg1( pipe(pfd) )
switch (fcrk()) {
case -1:
EC_FAIL
case 0:
ec_neg1( dup2(pfd[0], STDIN_FILEN0) )
ec_neg1( dup2(pfd[0], STDOUT.FILEN0) )
ec_neg1( close(pfd[0]) )
execlpC'ed", "ed", "-", NULL);
EC_FAIL
}
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 393
ec_null( sndfp = fdopen(pfd[1], "w") )
ec_null( rcvfp = fdopen(pfd[1], "r") )
return true;
EC_CLEANUP_BGN
if (pid == 0) {
EC_FLUSH("edinvoke");
_exit(EXIT_FAILURE);
}
return false;
EC_CLEANUP_END
}
Все остальные фрагменты программы остались прежними. Она работает точно так
же, как и раньше, но с одним каналом вместо двух.
Использование двунаправленных каналов вместо двух однонаправленных имеет два
недостатка.
• Данный подход нестандартен, поэтому такой код непортируем.
• Писатель, которому нужно еще и читать данные, не может закрыть сторону
записи канала для имитации конца файла, потому что ему принадлежит только
одна сторона. Это, например, означает, что пример fsort из предыдущего
раздела нельзя переписать с использованием одного канала (попробуйте сами, если
не верите).
Таким образом, я советую вам забыть, что каналы бывают двунаправленными, и
использовать для реализации двунаправленного взаимодействия два канала.
Упражнения
6.1. Узнайте, какие сообщения об ошибках генерируют командные оболочки в ответ
на ввод некорректных команд, и посмотрите, что происходит дальше.
Исследуйте как можно больше оболочек (например sh, ksh, bash, csh). Попробуйте
для начала следующие примеры:
echo abc > f1 > f2
echo def | cat < f1
echo ghi > f1 | cat
cat < f1 < f2
echo jkl | | cat
echo jki | > f1
6.2. В функции gettoken из раздела 6.4 в состоянии INQUOTE при вызове getchar я мало
внимания уделяю концу файла. Проблема ли это? Почему да или почему нет?
6.3- Реализуйте в оболочке из раздела 6.4 возможность подстановки параметра
(например, echo $PATH).
6.4. Реализуйте в оболочке из раздела 6.4 поддержку знаков подстановки
(генерирование имен файлов). Если ваша система поддерживает стандартную функ-
394 ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия
цию glob (большинство систем ее поддерживают), используйте ее. Если нет,
вы можете сопоставлять со знаком подстановки только последний компонент
путей (т. е. команда is */*.c недопустима).
6.5. Реализуйте в оболочке из раздела 6.4 встроенный оператор goto. Можете ли
вы реализовать его как внешнюю команду, чтобы она выполнялась как
дочерний процесс? (Совет: вернитесь к обсуждению описаний открытых файлов в
разделе 2.2.3).
6.6. Реализуйте в оболочке из раздела 6.4 оператор if.
6.7. Разработайте и реализуйте оболочку-меню. Вместо вывода приглашения к вводу
команд она должна отображать меню, в котором пользователь будет просто
выбирать нужный пункт. Вы также должны будете создать меню для выбора
аргументов команд. Информация о различных командах не должна быть
встроенной: оболочка должна извлекать данные о командах и их аргументах из БД.
6.8. Разработайте и реализуйте оконную оболочку. Используйте библиотеку Curses,
если у вас есть к ней доступ. Разделите экран терминала на две половины (что
легче: разделить его по горизонтали или по вертикали?). Запустите в каждой
половине дочерний процесс оболочки. Выбор активного окна должен
осуществляться путем нажатия функциональной или управляющей клавиши (скажем,
Ctrl-w). Если окно становится неактивным, выполняемая в нем команда
должна продолжать выводить данные на экран, а при запросе ввода
блокироваться, пока пользователь не активирует окно и не введет ответ. Если окно
заполняется, оно прокручивается, при этом вытесненные строки исчезают
навсегда. Реализуйте только строчный ввод и вывод. Вопросы для размышления: куда
должен отправляться стандартный вывод команды: непосредственно монитору,
специально разработанному фильтру или обратно оконной оболочке? А ввод?
Необязательные дополнения (от трудных до очень трудных): реализуйте
возможность прокрутки окна в обратном направлении (в определенных
пределах), чтобы пользователь мог вернуться к информации, вытесненной за
пределы окна; реализуйте обработку вывода, ориентированного на экран, а не
только строчного вывода.
6.9. Напишите программу, которая записывает свой идентификатор процесса в
стандартный вывод, после чего читает список идентификаторов процессов из
своего стандартного источника ввода. Если в списке есть ее собственный
идентификатор, она выводит список (используя дескриптор файла 2), в
противном случае добавляет в список свой идентификатор процесса и записывает
весь список в свой стандартный вывод. Затем все повторяется. Создайте пять
процессов, выполняющих эту программу, организуйте их в кольцо и позвольте
им немного поиграть (это компьютерная игра для самого компьютера).
6.10. Опираясь на подход, похожий на тот, что был реализован в интерактивной
программе поиска из раздела 6.5, напишите для калькулятора dc клиентскую
программу, позволяющую пользователю вводить выражения с
использованием инфиксной нотации (2 + 3) вместо постфиксной (2 3+). (Именно это
делает команда be).
ГЛАВА 6 Базовые механизмы межпроцессного взаимодействия 395
6.11. Измените последний пример из раздела 6.3 так, чтобы процесс wc был
родителем who.
6.12. Если вы используете FreeBSD, Darwin или Linux, попробуйте узнать, почему
программа поиска из раздела 6.6 не работает. Подсказка: она генерирует ошибки
с определенной целью, что всегда сопряжено с проблемами, и версии ed,
входящей в состав этих систем, это не нравится. Ошибка ли это,
задокументировано ли это на странице man или и то и другое? Есть ли какой-нибудь способ
исправить ситуацию?
6.13- Усовершенствуйте программу из упражнения 5.14, чтобы она поддерживала
атрибуты процессов из приложения А, которые были рассмотрены в этой главе.
Улучшенные механизмы
взаимодействия процессов
7.1 Введение
До сих пор мы рассматривали возможность взаимодействия двух процессов с
помощью неименованных каналов. К сожалению, такого рода взаимодействия
возможны только между родительскими и дочерними процессами, при условии, что
перед запуском дочернего процесса канал уже был создан, так как передать
файловый дескриптор от процесса к процессу можно только через механизм
наследования. Это ограничение сильно сужает область применения неименованных каналов.
Реальная жизнь диктует свои правила — нам нужна еще большая гибкость, еще
большая управляемость и эффективность.
К счастью, современные UNIX-системы помимо неименованных каналов,
предоставляют еще целый ряд механизмов взаимодействия процессов (IPC, Interprocess
Communication). Среди них именованные каналы, очереди сообщений, семафоры,
блокировка файлов, общая память и сокеты. В этой главе будут рассмотрены
механизмы, ограничивающиеся рамками одной системы. Сокеты, с помощью которых
можно устанавливать соединения между системами, станут темой обсуждения
следующей главы.
Большинство механизмов IPC обладают достаточной гибкостью для
удовлетворения самых разнообразных требований, предъявляемых приложениями. Но
начинающим программистам будет проще понять их, если мы ограничимся наиболее
простым случаем обмена сообщениями одного сервера с несколькими клиентами.
Чтобы облегчить задачу сравнения характеристик различных механизмов, я дам
понятие «упрощенного интерфейса обмена сообщениями» (SMI, Simple Messaging
Interface). Затем покажу, как можно реализовать его шестью различными
способами: с использованием именованных каналов (FIFO), очередей сообщений System
V, очередей сообщений POSIX, разделяемой памяти и семафоров System V, общей
памяти и семафоров POSIX, а также сокетов (в следующей главе). На основе
полученных знаний вы сможете уверенно выбрать наиболее подходящие механизмы для
своих собственных приложений.
398 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Когда существует столько способов реализации одного и того же, в пору задаться
вопросом: «какой из способов лучше?». Есть, по крайней мере, три критерия,
выбора «лучшего»:
• удобство в использовании и соответствие требованиям приложения;
• уереносимость, на тот случай, если вам понадобится перенести свое
приложение на другую платформу, например, из HP-UX, AIX или Solaris в Linux;
• эффективность.
По мере представления каждого механизма IPC, основной упор будет делаться на
первые два критерия. Эффективность же слишком сильно зависит от конкретной
реализации механизма и от самого приложения. Возможно, вы придете к идее
создания своей «прослойки» (аналогичной SMI). Это позволит вам проверить
эффективность того или иного механизма в вашем приложении. Результаты своих
испытаний я представлю в конце этой главы. Разумеется, я не претендую на истину в
последней инстанции — результаты, которые получите вы, могут отличаться от моих.
7.2 Именованные каналы (FIFO)
Именованные каналы сочетают в себе свойства неименованных каналов и обычных
файлов. Подобно файлам, они имеют имя. Любой процесс, наделенный
соответствующими правами, сможет открыть его на запись или на чтение. В отличие от
неименованных каналов, доступ к FIFO может получить любой процесс, поскольку здесь
не требуется наследование дескрипторов. Однако открытый именованный канал своим
поведением скорее напоминает неименованный канал, чем обычный файл.
Как правило, при открытии FIFO на чтение, системный вызов open блокируется
до тех пор, пока с другой стороны канал не будет открыт для записи, обычно
другим процессом. То же самое происходит и при открытии именованного канала на
запись — вызов open блокируется до тех пор, пока с другой стороны канал не
будет открыт на чтение. Таким образом, процесс, открывший именованный канал
первым, будет вынужден ожидать другой процесс. Это позволяет синхронизировать
процессы перед началом передачи данных по каналу.
Если открытие FIFO производится с флагом 0_N0NBL0CK, вызов open, открывающий
канал на чтение, сразу же вернет управление в вызывающую программу (с
открытым файловым дескриптором). В случае открытия именованного канала на запись
с флагом o_nonblock при отсутствии процесса открывшего канал на чтение, вызов
open вернет значение -1 и код ошибки ENXI0 в переменной errno. Это означает, что
флаг 0_N0NBL0CK больше подходит для открытия FIFO на чтение, чем на запись, иначе
придется обыгрывать ошибочную ситуацию и, возможно, повторно запускать
системный вызов open. Цель такого асимметричного поведения состоит в том, чтобы
предотвратить запись данных в канал, если нет процесса, который был бы готов
принять их, потому что в UNIX не предусмотрена возможность постоянного
хранения данных в FIFO. Здесь можно провести аналогию с водопроводной трубой —
нет смысла крутить кран, когда труба не подключена с обеих сторон. Если к
моменту закрытия всех дескрипторов в канале остаются какие-либо данные, они бу-
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 399
дут безвозвратно утеряны. Точно как в водопроводной трубе — если открутить ее с
обеих сторон, вода просто вытечет.
Флаг 0_N0NBL0CK оказывает такое же влияние на вызовы read и write, как и в случае с
неименованными каналами (более подробное описание вы найдете в разделе 6.2.2).
7.2.1 Создание именованного канала
В отличие от обычных файлов, создание именованного канала невозможно
выполнить с помощью системного вызова open, для этого должен использоваться
отдельный ВЫЗОВ:
mkfifo — создает именованный канал
«include <sys/stat.h>
int mkfifo(
const char *path, /*
mode_t perms /*
);
/* Возвращает О в случае
в переменной errno) */
полное
права
успеха
имя канала
доступа
-1 - в
*/
•/
случае
ошибки
(код
ошибки -
Аргумент perms используется аналогично третьему аргументу вызова open — он
определяет права доступа к каналу.
Очевидно, что FIFO может использоваться в качестве замены неименованных
каналов. Вместо обращения к pips создается FIFO и открываются два файловых
дескриптора — на чтение и на запись.' После этого можно работать с ним как с
простым неименованным каналом. Однако именованные каналы, используемые таким
образом, не имеют никаких преимуществ перед неименованными каналами, а в чем-
то даже неудобны: они более «тяжеловесны», для получения дескрипторов нужно
дважды обращаться к системным вызовам и, кроме того, существует риск конфликтов
имен, как в случае с временными файлами.
Именованные каналы не предназначались для замены неименованных каналов. Они
были добавлены для обеспечения возможности взаимодействия серверных и
клиентских процессов. Не забывайте: основное ограничение неименованных каналов
состоит в том, что дескрипторы, используемые для чтения и записи, могут быть
переданы другому процессу только через механизм наследования. Учитывая, что
любой процесс, обладающий соответствующими правами, может открыть FIFO,
серверный процесс может создать FIFO с фиксированным именем, к которому будут
подключаться клиенты.
В этой главе мы будем использовать механизмы взаимодействия для обмена
сообщениями, небольшими блоками структурированных данных между процессами, а
' Некоторые системы допускают возможность одновременного открытия на чтение и на
запись (0_RDWR), но это противоречит стандартам.
400 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
не между потоками данных, как это делалось в главе 6. Однако, имейте ввиду, что
FIFO могут с таким же успехом использоваться и для передачи потоков данных.
7.2.2 Простой пример FIFO
Знакомство с FIFO мы начнем с написания двух программ — сервера и клиента.
Сервер непрерывно ожидает сообщений от клиентов. В данном примере, под
«сообщением» подразумевается простая строка. Сервер должен преобразовать все
символы строки в верхний регистр, отправить ее обратно клиенту и перейти к
ожиданию нового сообщения, возможно от другого клиента. Клиент передает
некоторую строку серверу, получает от него результат, выводит полученную строку и
на этом завершает работу.
Сначала запускается сервер:
$ srasg_server&
сервер запущен
[1] 8725
$
Затем клиент:
$ smsg_client
клиент 8747 запущен
клиент 8747: яблочный соус -> ЯБЛОЧНЫЙ СОУС
клиент 8747: тигр -> ТИГР
клиент 8747: гора -> ГОРА
Клиент 8747 закончил работу
На рис. 7.1 показано взаимодействие сервера и клиентов с помощью FIFO. Сервер
создает единственный канал с именем «fifo_server», работающий на ввод, который
будет использоваться всеми клиентами. Каждый из клиентов (на рисунке их два)
создает свой собственный входной канал, в имя которого внедряется
идентификатор процесса для придания уникальности. По этому каналу он будет получать от
сервера преобразованную строк)' сообщения. На рисунке клиент с
идентификатором 8748 передает сообщение (представленное в виде прямоугольника),
содержащее строку для преобразования («тигр») и свой идентификатор (8748). Сервер читает
сообщение из входного канала «fifoserver», преобразует полученную строку
(в результате получается «ТИГР») и затем, используя идентификатор клиента,
конструирует имя FIFO, по которому следует передать ответ. Таким образом, передача
данных к серверу осуществляется по единственному каналу, а передача ответов
производится по каналам, отдельным для каждого клиента.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 401
fifo8747
■
сервер
t
—
"Ч
\
" ""' 1
ТИГР
8748
1
клиент 8747
тигр
8748
\
fifo8748
клиент 8748
Рис. 7.1. Сервер и два клиента
И клиенты и сервер используют один и тот же алгоритм создания имени FIFO по
идентификатору клиента. В моем примере все они используют одну и ту же функцию:
dooI make_fifo_nanie(pid_t pid, ohar «name, size_t namejnax)
<
snprintf(name, namejnax, "fifoXld", (long)pid);
return true;
}
3 начале файлов с исходными текстами клиента и сервера находится определение
имени FIFO сервера и структура сообщения:
fdefine SERVER_FIFO_NAME "fifo.server"
struot simple jnessage {
pid_t sm_olientpid;
ohar sm_data[200];
};
Исходный текст программы-сервера:
#inolude <looale.h>
int ntain(void)
<
int fd_server, fd_olient, i;
ssize_t nread;
struot simplejnessage msg;
ohar fifojiams[100];
setlooale( LC_ALL, "" );
printf("oepeep запущен\п");
if (mkfifo(SERVER_FIFO_NAME, PERH_FILE)
EC.FAIL
eojiegK fd_server = open(SERVER_FIFOj^AHE, 0_RD0NLY) )
1 44 errno != EEXIST)
402 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
while (true) {
ec_neg1( nread = read(fd_server, &rasg, sizecf(rasg)) )
if (nread == 0) {
errnc = ENETDOWN;
EC.FAIL
>
fcr (i = 0; msg.sra_data[i] != '\0'; i++)
rasg.sra_data[i] = tcupper(rasg.sra_data[i]);
ec_false( raake.fifc.naniediisg.sni.clientpid, fifc.narae,
sizecf(fifc_name)) )
ec_neg1( fd_client = cpen(fifc_narae, 0_WR0NLY) )
ec_neg1( write(fd_client, &msg, sizecf(rasg)) )
ec_neg1( clcse(fd_client) )
}
/* в эту течку программа фактически никогда не попадет */
ec_neg1( clcse(fd_server) )
exit(EXIT.SUCCESS);
EC.CLEANUP.BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
>
Сервер выполняет следующие действия.
• Создает FIFO и в случае неудачи прекращает работу только когда rakfifc
возвращает код ошибки отличный от EEX1ST, который означает что именованный
канал уже существует. Он мог остаться от предыдущего запуска сервера.
• Открывает FIFO на чтение. В этом месте серверный процесс блокируется до тех
пор, пока не появится клиент это происходит, когда сервер запускается раньше
клиента.
• В цикле производится чтение сообщения, преобразование символов в верхний
регистр, открытие канала к клиенту, запись результата в канал, после чего
канал закрывается.
• В примере не предусмотрена явная остановка сервера, он остается в работе
неопределенно долгое время, пока не будет принудительно завершен командой kill.
Вы уже наверняка представляете себе, что должна делать программа-клиент. Ниже
приводится ее исходный код:
int main(int argc, char *argv[])
{
int fd.server, fd_client = -1, i;
ssizejt nread;
struct siraplejuessage msg;
char fifc_narae[100];
char *wcrk[] = {
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 403
"яблсчный ссус",
"тигр",
"гсра",
NULL
};
printf("ioiMeHT Xld запущен\п", (lcng)getpidO);
msg.sm_cllentpld = getpid();
ec_false( make_fifc_name(msg.sm_cllentpld, fifc_name,
sizecf(fifc_nanie)) )
if (mkfifc(fifc_name, PERM.FILE) = -1 && errnc l= EEXIST)
EC_FAIL
ec_neg1( fd_server = cpen(SERVER_FIFO_NAME, 0_WR0NLY) )
fcr (1 = 0; wcrk[l] 1= NULL; i++) {
strcpy(msg.sm_data, wcrk[i]);
ec_neg1( write(fd_server, &tnsg, sizecf(msg)) )
If (fd_cllent = -1)
ec_neg1( fd_client = cpen(fifc_nanie, 0_RD0NLY) )
ec_neg1( nread = read(fd_cllent, &msg, slzecf(msg)) )
if (nread = 0) {
errnc = ENETDOWN;
EC_FAIL
}
printf("клиент Xld: Xs -> Xs\n", (lcng)getpid(),
wcrk[i], msg.sni_data);
>
ec_neg1( clcse(fd_server) )
ec_neg1( clcse(fd_client) )
ec_neg1( unlink(fifc_name) )
printf("Клиент Xld закончил рабсту\п", (lcng)getpidO);
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Клиент выполняет следующие действия.
• Создает собственный именованный канал.
• Открывает канал сервера на запись. В этой точке клиент будет заблокирован до
тех пор, пока сервер не откроет свой FIFO на чтение — это происходит, когда
клиент запускается раньше сервера.
• Каждая преобразуемая строка оформляется в виде сообщения и передается
серверу. После этого открывается клиентский канал и выполняется чтение
преобразованной строки.
404 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
• После преобразования всех строк , оба дескриптора закрываются, а клиентский
канал удаляется, так как весьма маловероятно, что этим каналом захочет
воспользоваться какой-нибудь другой процесс.
У меня для вас две новости, и обе плохие. Первая: и клиент и сервер содержат ошибки,
близкие по своей природе. Вторая: я несколько погрешил против истины, когда
привел вывод, полученный от клиентской программы. На самом деле клиент
вывел следующее:
$ smsg_client
клиент 8747 запущен
клиент 8747: яблочный соус -> ЯБЛОЧНЫЙ СОУС
ERROR: 0: main [/aup/c7/smsg_client.c:46] 0
*»* ? (100: "Network is down") ***
$
Что же произошло? В чем причина? Место, где появилась ошибка, и содержимое
переменной errno явно указывают, что клиент прочитал из канала признак конца
файла. Но почему? Ведь клиент буквально только что передал серверу запрос?
Проблема заключается в рассинхронизации процессов — после передачи первого
запроса клиентский канал остался открытым на чтение со стороны клиента,
поэтому сервер уже не блокируется на открытии клиентского канала и сразу же
после записи ответа закрывает дескриптор. В результате, когда клиент пытается
прочитать ответ сервера, из-за отсутствия дескриптора, открытого на запись, он
получает признак конца файла, как это было описано в разделе 6.2.2.
Ошибка устраняется довольно просто. Достаточно лишь на стороне клиента открыть
для этого же канала второй дескриптор на запись, несмотря на то, что он никогда
не будет использоваться для записи данных в канал. Благодаря этой хитрости,
системный вызов read будет блокироваться до момента появления данных в FIFO
(записываемых сервером). Таким образом, для устранения проблемы нужно просто
добавить новую переменную:
int fd_client_w = -1;
и затем открыть второй дескриптор в теле цикла:
if (fd.client == -1)
ec_neg1( fd_client = open(fifo_narae, O.RDONLY) )
if (fd_client_w == -1)
ec_neg1( fd_client_w = open(fifo_name, 0_WR0NLY) )
(Разумеется, этот дескриптор следует закрыть вместе с fd_client.) В ряде
UNIX-систем эту ошибку можно исправить, открыв дескриптор FIFO на стороне клиента с
флагом 0.RDWR, но как я уже говорил это отступление от стандартов.
Итак, перезапустив исправленную версию клиентского приложения, я получил:
клиент 8937 запущен
клиент 8937: яблочный соус -> ЯБЛОЧНЫЙ СОУС
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 405
клиент 8937: тигр -> ТИГР
клиент 8937: гора -> ГОРА
Клиент 8937 закончил работу
ERROR: 0: main [/aup/o7/smsg_server.o:33] 0
*** ? (100: "Network is down") ***
Опять что-то не так? Теперь уже сервер сообщает, что получил признак конца файла
из своего FIFO. Проблема та же, что и в случае с клиентом. Закончив работу,
клиент закрыл со своей стороны дескриптор серверного канала, в результате сервер
получил признак конца файла. Но нам нужно, чтобы сервер продолжал работу, в
идеале заблокировавшись на очередном вызове read в теле цикла до тех пор, пока
в канале не появятся данные. Похожая проблема — похожий способ избавления от
нее. Нужно лишь добавить второй дескриптор на стороне сервера, который будет
открыт на запись. (Я не буду приводить новый исходный код, вы легко
разберетесь с этим сами.)
После исправления ошибок в сервере, обе программы не только работают без
ошибок, но и существует возможность одновременной работы нескольких клиентов.-
$ smsg_olient 4 smsg_olient 4 smsg_olient & smsg_olient
клиент 9001 запущен
клиент 9001: яблочный ооуо -> ЯБЛОЧНЫЙ СОУС
клиент 9001: тигр -> ТИГР
[2] 9001
клиент 9002 запущен
клиент 9002: яблочный соуо -> ЯБЛОЧНЫЙ СОУС
клиент 9002: тигр -> ТИГР
[3] 9002
[4] 9003
клиент 9004 запущен
клиент 9004: яблочный ооуо -> ЯБЛОЧНЫЙ СОУС
клиент 9004: тигр -> ТИГР
клиент 9003 запущен
клиент 9003: яблочный соус -> ЯБЛОЧНЫЙ СОУС
клиент 9003: тигр -> ТИГР
клиент 9002: гора -> ГОРА
клиент 9001: гора -> ГОРА
клиент 9004: гора -> ГОРА
клиент 9003: гора -> ГОРА
Клиент 9001 закончил работу
Клиент 9002 закончил работу
Клиент 9004 закончил работу
[2] Done smsg_olient
[3]- Done smsg_client
$ Client 9003 закончил работу
[4]+ Done siesg_ollent
406 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
7.2.3 Достоинства и недостатки именованных каналов
Достоинства.
• Просты в понимании и использовании, потому что по своей природе являются
обычными каналми и для работы с ними используются системные вызовы
базовых операций ввода-вывода (open, read, write и т. п.).
• Доступны в любой версии UNIX.
• Достаточно эффективны (см. табл. 7.2 в конце главы).
• Хорошо работают как с потоками данных, так и с отдельными сообщениями.
Для повышения скорости обмена, читающий процесс может считывать из
канала сразу порцию сообщений за раз, а пишущий процесс может записывать
данные большими порциями.
Недостатки.
• Из одного канала может читать только кто-то один, одновременное
сосуществование нескольких «читателей» не допускается. (Более подробно эта проблема
обсуждалась в разделе 6.2.2.)
• Копирование данных из пространства пользователя одного процесса в
пространство ядра и обратно в пространство пользователя другого процесса — очень
дорогостоящая операция. (Очереди сообщений и сокеты также обладают этим
недостатком.) Таким образом, для особо критических применений
именованные каналы не подходят.
• Если размер сообщения слишком велик (см. раздел 6.2.2), вызов write может
оказаться заблокированным. Если к этой проблеме подойти без должного
внимания, приложение может зависнуть.
7.3 Упрощенный интерфейс обмена сообщениями
Иногда бывает полезно представить операции передачи и приема сообщений в виде
абстрактной прослойки. На то есть, по крайней мере, две причины:
• некоторые моменты, как например создание второго дескриптора на запись,
могут быть реализованы внутри прослойки, что облегчит написание программ
и сделает их более надежными;
• внутри прослойки можно реализовать работу с различными механизмами
взаимодействия процессов. Это позволит вам экспериментировать с ними без
внесения существенных изменений в исходный код программы.
Свою прослойку я назвал «Упрощенный интерфейс обмена сообщениями» (SMI).'
7.3.1 Типы и функции SMI
В этом разделе мы рассмотрим лишь общие принципы построения SMI, а варианты
реализации я дам позднее. Для начала рассмотрим обобщенную структуру сообщения:
' Она была разработана специально для этой книги и не является каким-либо стандартом.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 407
struct smi_msg — структура
struct smijnsg {
lcng smijutype; /*
struct client_id {
lcng c_id1;
lcng c_id2;
} srai_client;
char sral_data[13;
};
ЭТС
сообщения
поле всегда
ДЛЯ
SMI
дслжнс идти
первым
•/
Первые два поля, smi_type и smi_ciient, необходимы для некоторых из реализаций,
в чем мы с вами скоро убедимся. В разделе 7.2.2 мы уже видели, что клиент может
поставлять свой идентификатор серверу. В нашей структуре для хранения этой
информации предназначено поле smi_ciient.c_idl. Назначение smi_type и
smi_ciient.c_id2 будет описано ниже.
Собственно сообщение (поле smi_data) может быть вообще чем угодно, если
выполняются следующие условия.
• Сервер и клиент могут исполняться на разных компьютерах, поэтому это поле
не должно содержать указателей или каких-либо данных, которые
бессмысленно передавать по сети.
• Различные аппаратные платформы могут иметь разный порядок следования байт,
поэтому числа в двоичном представлении должны быть переведены в формат
сетевого стандарта. Более подробно эта тема будет обсуждаться в разделе 8.1.4.
В нашем простом примере, сервер и клиент должны договориться о
фиксированном размере сообщения, от которого зависит размер массива smi_data. После того,
как вы разберетесь с основными положениями этой и следующей главы, вы без труда
сможете преодолеть накладываемые ограничения и реализовать свой собственный
SMI для использования в своих приложениях.
Перед выполнением операций по приему или передаче сообщений, сервер и
клиент должны открыть очередь сообщений, а по завершении работы — закрыть ее.
По сути, операция передачи сообщения означает помещение его в очередь.
Аналогично, операция чтения — это изъятие сообщения из очереди. Конкретная
реализация очереди скрыта в недрах SMI, подобно тому, как скрыта реализация типа
FILE в недрах стандартной библиотеки ввода-вывода.
Функции открытия и закрытия очереди сообщений:
SMI types — основные типы
typedef veld *SMIQ;
typedef enum {
SMI_SERVER,
SMI_CLIENT
} SMIENTITY;
«define SERVER_NAME.
.MAX
50
SMI
/* очередь сообщений */
/* сервер */
/* клиент */
/* максимальный размер имени сервера */
14 Зак. 4030
408 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
smi_open — открывает очередь сообщений SMI
SMIQ *smi_open(
oonst ohar «name, /* имя оервера »/
SMIENTITY entity, /* оущнооть открываемого объекта */
size_t msgsize /* фиксированный размер оообщения */
);
/* Возвращает указатель на очередь оообщений или NULL - в олучае ошибки
(код ошибки - в переменной еггпо) */
smiclose — закрывает очередь сообщений SMI
bool snii_oIose(
SMIQ *sqp /* очередь */
);
/* В олучае уопеха возвращает true, в олучае ошибки -
в переменной еггпо) */
- false (код ошибки -
Имя очереди сообщений, которое передается в smi.open, должно быть заранее
известно как серверу, так и клиентам. Если перед открытием очередь необходимо
создать, то это нужно делать внутри smi.open. Данной реализацией не
предусматривается обслуживание прав доступа, отсюда и слово «упрощенный» в названии
интерфейса. Остается ли очередь после закрытия, спецификацией интерфейса не
оговаривается — принятие решения остается за конкретной реализацией.
Аргумент entity может принимать значения SMI.SERVER или SMI.CLIENT, в
зависимости от типа программы, открывающей очередь. Нашей реализацией
предусмотрено одновременное существование только одного сервера и множества клиентов.
Аргумент msgsize хранит размер поля smi.data в структуре smi.msg.
Прием и передача сообщений могут осуществляться простой передачей указателя
на буфер в соответствующие функции, до некоторой степени напоминающие
вызовы read и write. (Функции не принимают аргумент с размером буфера,
поскольку клиент и сервер договариваются о его величине при открытии очереди.):
ec_false( smi_send(sqp, buffer) )
ec_false( smi_receive(sqp, buffer) )
Но если бы мы реализовали такие функции, нам пришлось бы копировать каждое
сообщение от передающего процесса в область ядра и затем из области ядра — опять
в область принимающего процесса. Чтобы избежать дорогостоящих операций
копирования, мы немного усложним интерфейс и будем использовать две функции
для передачи и две функции для приема сообщения. Первая функция из каждой пары
будет просто получать адрес сообщения, вторая — освобождать его. Благодаря первой
функции, все вызывающие программы получают указатель на сообщение. С
помощью второй функции они освобождают полученный адрес, который не может су-
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
409
ществовать бесконечно долго — память, занимаемая сообщением, может быть
освобождена или в сегмент общей памяти может быть записана другая информация.
Проще будет начать с рассмотрения принимающих функций:
smi_receive_getaddr — возвращает адрес принятого сообщения SMI
bool smi_reoeive_getaddr(
SMIQ *sqp, I* очередь »/
void »*addr /* оообщение »/
);
/* В олучае уопеха возвращает true, в олучае ошибки - false (код ошибки -
а переменной errno) */
smijreceivejrelease
— отключается
bool smi_reoeive_release(
SMIQ *sqp /* очередь */
/* В олучае уопеха
а переменной еггпо)
аозвращает
*/
true,
от
а
принятого сообщения
олучае
ошибки -
■ false (код
ошибки -
Аргумент addr имеет тип void, а не struct smijnsg, по той простой причине, что каждое
приложение может определить свою структуру сообщения, в которой первые два
поля идентичны первым двум полям структуры smijnsg (smi_type и smi.client).
Порядок использования этих функций в приложении может быть примерно
следующим:
struct myjnsg *msg;
ec_false( smi_receive_getaddr(sqp, (void **)&msg) )
/* обработка данных в msg->smijjata »/
ec_false( smi_receive_release(sqp) )
Фактически, прием сообщения происходит в момент вызова smi_ receive jgetadd г.
Обратите внимание: приложение не занимается выделением памяти под
сообщение, это делается самим интерфейсом.
Функции передачи очень похожи на функции приема, с небольшим отличием —
в них предусмотрена идентификация клиента:
smi_send_getaddr — возвращает адрес передаваемого сообщения SMI
bool smi_sen_getaddr(
SMIQ *sqp,
struct client_id *client,
void ««addr
)•
/* В случае успеха возвращает
в переменной еггпо) «/
Л
/*
/*
очередь */
идентификатор клиенте
сообщение »/
true, в случае ошибки -
(только для сервера)
false (код ошибки -
410 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
smi_send_release — передает сообщение SMI
bool smi_send_release (
SMIQ *sqp /* очередь */
);
/» В олучае уопеха возвращает true,
в переменной еггпо) */
в олучае
и освобождает
ошибки -
■ false
адрес
(код оши
бки -
Фактически, передача сообщения происходит в момент вызова smi_send_ release.
Второй аргумент функции smi_send_getaddr используется сервером для
идентификации клиента, которому следует передать ответ. Он является указателем на struot
olient_id в принятом от клиента сообщении. Клиенту такая идентификация не нужна
(она выполняется на этапе обращения к smi.open), поэтому он передает значение
NULL в этом аргументе.
Цикл приема запроса и передачи ответа на стороне сервера мог бы выглядеть
примерно так:
struot myjnsg *msg_ln, *msg_out;
eo_false( sml_reoelve_getaddr(sqp, (void **)&msg_in) )
/* обработка данных в msg_ln->sml_data •/
ec_false( sml_send_getaddr(sqp, &msg_in->sml_ollent, (void **)imsg_out) )
eo_false( sml_reoeive_release(sqp) )
/* запиоь ответа в msg_out->smi_data */
ec_false( smi_send_release(sqp) )
Обратите внимание:
• сервер использует два отдельных адреса (msg_in и msg_out) приемного и
передающего буферов;
• сервер не производит отсоединения от msg_in до тех пор, пока не передаст
ответ клиенту, потому что ему необходима структура msg_in->smi_client. Фактически,
функция smi_receive_release может быть вызвана даже после smi_send_release —
в общем случае процедуры приема и отправки сообщения являются
независимыми друг от друга. Структура client_id может быть также скопирована во
временную переменную и затем использоваться при обращении к smi_send_getaddr.
На стороне клиента функции smi_send_getaddr и smi_send_release используются
аналогичным образом:
struct myjnsg *msg_out;
ec_false( smi_send_getaddr(sqp, NULL, (void **)&msg_out) )
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 411
/• запись данных в msg_cut->smi_data */
ec_faise( smi_send_reiease(sqp) )
В следующем разделе будет представлен более полный пример.
7.3.2 Пример реализации сервера и клиента
с использованием SMI
Для демонстрации основных принципов использования функций SMI мы
перепишем программы сервера и клиента из раздела 7.2.2 так, чтобы для обмена
сообщения они использовали интерфейс SMI. Начнем с сервера (все необходимые
определения находятся в отдельном заголовочном файле, который подключается как к
программе сервера, так и к программе клиента):
«include <iccale.h>
«define SERVER_NAME "smsg_server"
«define DATA.SIZE 200
int main(vcid)
<
SHIQ *sqp;
struct smljnsg *msg_in, *msg_cut;
int i;
seticcaie(LC_ALL, "");
printf("сервер запущен\п");
ec_nuii( sqp = smi_cpen(SERVER_NAME, SMI_SERVER, DATA_SIZE) )
whiie (true) {
ec_faise( smi_receive_getaddr(sqp, (vcid **)&msg_in) )
ec_faise( smi_send_getaddr(sqp, &msg_in->smi_client,
(vcid **)&msg_cut) )
fcr (i = 0; msg_in->smi_data[i] != '\0'; 1++)
msg_cut->smi_data[i] = tcupper(msg_in->smi_data[i]);
msg_cut->smi_data[i] = '\0';
ec_faise( smi_receive_reiease(sqp) )
ec_faise( smi_send_reiease(sqp) )
}
/* в эту течку программа фактически никегда не попадет */
ec_faise( smi_cicse(sqp) )
exit(EXIT_SUCCESS);
EC_CLEANUP_B6N
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
412 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
И далее — исходный код клиента:
int main(int argc, char *argv[])
{
SMIQ *sqp;
struct smijnsg *msg;
int i;
char »wcrk[] = {
"яблочный ссус",
"тигр",
"гсра",
NULL
};
printf("клиент Xld запущен\п", (lcng)getpid());
ec_null( sqp = srni_cpen(SERVER_NAME, SMI_CLIENT, DATA_SIZE) )
fcr (i = 0; wcrk[i] l= NULL; i++) {
ec_false( srni_send_getaddr(sqp, NULL, (vcid »*)&msg) )
strcpy(rnsg->srni_data, wcrk[i]);
ec_false( smi_send_release(sqp) )
ec_faise( srni_receive_getaddr(sqp, (vcid **)&msg) )
printf("клиент Xld: Xs -> Xs\n", (lcng)getpid(),
wcrk[i], msg->srni_data);
ec_false( srni_receive_release(sqp) )
}
ec_false( smi_clcse(sqp) )
printf("Клиент lid завершил рабсту\п", (lcng)getpid());
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Обратите внимание: из программы исчезли малопонятные действия по открытию
второго дескриптора канала на запись и все манипуляции с именами FIFO. Возможно
в данном случае слово «скрыты» подошло бы больше, чем «исчезли».
7.3.3 Реализация SMI с именованными каналами (FIFO)
Вся сложность обмена сообщениями через FIFO была вынесена из программ
сервера и клиента и перекочевала внутрь реализации SMI. С целью повышения
производительности и снижения накладных расходов, реализация серверной части
будет сохранять дескриптор канала клиента открытым, а не открывать и закрывать
его всякий раз, как мы делали это в примере из раздела 7.2.2.
В именовании функций мы будем придерживаться шаблона: smifcn_fifc, где smifcn
— это обобщенное имя операции SMI (например smi_send_getaddr_fifc является
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
413
реализацией smi_send_getaddr). Трансляцию обобщенных имен можно выполнять с
помощью небольшого файла-обертки, который включает в себя набор небольших
функций, например:
bocl smi_send_getaddr(SMIQ *sqp, struct client_id «client, vcid **addr)
{
return smi_send_getaddr_fifc(sqp, client, addr);
}
Использование такого файла дает следующие преимущества:
• приложение становится независимым от конкретной реализации, пример вы
видели в предыдущем разделе. Оно может выполнять необходимые операции в
терминах обобщенных функций SMI (например smi_send_getaddr), а конкретную
реализацию подключать в виде файла-обертки на этапе компиляции;
• если программа должна использовать различные механизмы взаимодействия,
она может обращаться к фактическим именам функций (например smi_send_
getaddr.fifc или smi_send_getaddr_skt). Основное назначение файлов-оберток —
предоставить возможность протестировать программу с различными
механизмами взаимодействия процессов и выбрать наилучший для каждого конкретного
случая. Что собственно и было сделано мною, при составлении табл. 7.2 в
конце этой главы.
Я не буду приводить исходный код файлов-оберток, вы найдете их на web-сайте
[AUP2003].
Реализацию взаимодействия через именованные каналы мы начнем с очереди
сообщений. И сервер и клиент будут использовать одну и ту же структуру данных.
«define MAX_CLIENTS 20
typedef struct {
SMIENTITY sq_entity; /* сущность (сервер или клиент) */
int sq_fd_server; /* дескриптор для чтения и ... */
int sq_fd_server_w; I* ■ ■ • для записи на стсрсне сервера */
char sq_name[SERVER_NAME_MAX]; /* имя сервера */
struct <
int cl_fd; /* дескриптор на стсрсне клиента */
pid_t cl_pid; /* идентификатор процесса клиента */
} sq_cllents[MAX_CLIENTS];
struct client_id sq_client; /* идентификатор клиента */
size_t sqjnsgsize; /* размер сообщения */
struct smijnsg *sq_msg; /* буфер сообщения */
} SMIQ_FIF0;
Мы ограничили количество одновременно работающих с сервером клиентов, числом
20. Чтобы обойти это ограничение, вместо .массива можно использовать односвяз-
ный список клиентов, но для простоты реализации ограничимся массивом. И кли-
414 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
енты и сервер используют sq.fd.server для обмена данными. Кроме того, сервер
использует sq.f d.server.w для хранения дескриптора, открытого на запись, как это
было описано в разделе 7.2.2. Массив sq.clients используется сервером для
хранения дескрипторов клиентских каналов, чтобы не выполнять повторные операции
открытия и закрытия дескрипторов для каждого сообщения. На стороне клиента
дескриптор его собственного канала будет представлен первым элементом
массива — sq_ciients[0]. Элемент sq.ciients[1] будет использоваться клиентом для
хранения дескриптора своего канала, открытого на запись. С точки зрения клиента,
использование массива и дополнительного поля sq_fd_server_w — напрасная трата
памяти, но, на мой взгляд, работать с универсальной структурой намного удобнее,
к тому же объем неиспользуемой памяти не так велик, чтобы беспокоиться по
этому поводу.
Идентифицирующая клиента информация передается сервером в функцию
smi_send_getaddr_fifc и сохраняется в sq.ciient для последующей передачи в
функцию smi_send_reiease_fifc. Размер буфера и указатель на буфер с сообщением,
которые передаются в smi_cpen_fifc, сохраняются в sq.msgsize и sq_msg соответственно.
(Обратите внимание: в случае с FIFO, очередями сообщений и сокетами, операция
копирования данных в пространство ядра и обратно — вещь неизбежная.)
Инициализация массива sq_ciients вынесена в отдельную функцию и должна
производиться как сервером, так и клиентом:
static vcid ciients_bgn(SMIQ_FIFO *p)
{
int i;
fcr (i = 0; i < MAX.CLIENTS; i++)
p->sq_ciients[i].ci_fd = -1;
}
По завершении работы, программа должна вызывать функцию ciients.end:
static vcid ciients_end(SMIQ_FIFO *p)
{
ciients_cicse_aii(p);
}
static vcid ciients_clcse_aii(SMIQ_FIFO *p)
{
int i;
fcr (i = 0; i < MAX_CLIENTS; i++)
if (p->sq_ciients[i].ci_fd != -1) {
(void)cicse(p->sq_ciients[i].cl_fd);
p->sq_ciients[i].ci_fd = -1;
}
}
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 415
Мы встретим обращения к этим функциям в smi_cpen_fifc и smi_clcse_fifc.
Когда сервер получает сообщение, он обращается к clients_f ind, чтобы определить,
был ли открыт дескриптор канала клиента. Если клиент не был найден,
возвращается индекс первого свободного элемента массива или -1 — когда достигнуто
ограничение на количество клиентов:
static lnt cllents_flnd(SMI(LFIFO *p, pld_t pld)
{
lnt 1, avail = -1;
fcr (1 = 0; 1 < MAX.CLIENTS; 1++) {
If (p->sq_cllents[l].cl_pld == pld)
return 1;
If (p->sq_cllents[l].cl_fd == -1 && avail == -1)
avail = 1;
}
If (avail != -1)
p->sq_cllents[avall].cl_pid = pld;
return avail;
}
Далее следуют функции конструирования имен каналов. Как и прежде, канал
сервера носит фиксированное имя, а имена каналов клиентов конструируются с
использованием идентификаторов процессов.
static veld make_flfc_name_server(ccnst SMI0_FIF0 *p, char «flfcname,
size_t flfcnamejnax)
{
snprintf(flfcname, flfcnamejnax, "/tmp/smififc-Xs", p->sq_name);
}
static veld make_flfc_name_client(pld_t pid, char *flfcname,
slze_t fifenamejnax)
{
snprlntf(flfcname, fifenamejnax, "/tmp/smiflfcXld", (lcng)pid);
}
Теперь можно перейти к функции smij;pen_fifc:
SMIO *smlj:pen_fifc(ccnst char *name, SMIENTITY entity, slze.t msgsize)
{
SMI0_FIF0 *p = NULL;
char fifcname[SERVER_NAMEJ4AX + 50];
ecjtulK p = callccd, sizecf(SMI0_FIF0)) )
p->sqjnsgsize = msgsize + cffsetcf(struct smijnsg, smi_data);
ecjtulK p->sqjnsg = callccd, p->sq_msgsize) )
p->sq_entity = entity;
416 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
if (strlen(narae) >= SERVER_NAME_MAX) {
errnc = ENAMETOOLONG;
EC_FAIL
}
strcpy(p->sq_narae, name);
raake_fifc_narae_server(p, fifcnarae, sizecf(fifcnarae));
if (p->sq_entity == SMI.SERVER) {
clients_bgn(p);
if (rakflfc(fifcnarae, PERM.FILE) == -1 && errnc != EEXIST)
EC.FAIL
ec.negK p->sq_fd_server = cpen(fifcnarae, 0_RD0NLY) )
ec_neg1( p->sq_fd_server_w = cpen(fifcnarae, 0_WR0NLY) )
}
else {
ec_neg1( p->sq_fd_server = cpen(fifcnarae, O.WRONLY) )
make_fifc_name_client(getpid(), fifcnarae, sizecf(fifcnarae));
(vcid)unlink(fifcnarae);
ec_neg1( rakfifc(fifcnarae, PERM.FILE) )
ec.negK p->sq_clients[0].cl_fd =
cpen(fifonarae, 0_RD0NLY | 0.N0NBL0CK) )
ec_false( setblcck(p->sq_clients[0].cl_fd, true) )
ec.negK p->sq_clients[1].cl_fd = cpen(fifcnarae, O.WRONLY) )
>
return (SMIQ «)p;
EC_CLEANUP_BGN
if (p != NULL) {
free(p->sq_msg);
free(p);
}
return NULL;
EC_CLEANUP_END
}
В целом, текст функции должен быть вам понятен, если вы разобрались с
примером из раздела 7.2.2. Но на некоторых моментах мне хотелось бы остановиться
отдельно. На стороне сервера FIFO открывается последовательностью:
ec_neg1( p->sq_fd_server = cpen(fifcnarae, O.RDONLY) )
ec.negK p->sq_fd_server_w = cpen(fifcnarae, O.WRONLY) )
Первый вызов заблокирует сервер до тех пор, пока какой-нибудь из клиентов не
откроет канал сервера на запись — это нормально, поскольку наш сервер не
предусматривает каких-либо действий в отсутствии клиентов. Однако порядок открытия
FIFO на стороне клиента более сложный:
ec.negK p->sq_clients[0].cl_fd =
open(fifcnarae, O.RDONLY | O.NONBLOCK) )
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 417
ec_false( setblcck(p->sq_clients[0].cl_fd, true) )
ec_neg1( p->sq_clients[1].cl_fd = cpen(fifcname, 0_WR0NLY) )
Без флага 0_N0NBL0CK клиент был бы заблокирован в первом обращении к вызову среп
до тех пор, пока сервер не откроет канал на запись. Но сервер не может сделать
этого, не получив запрос от клиента. Получается тупиковая ситуация! Чтобы выйти из
нее, мы открываем дескриптор с флагом 0_N0NBL0CK, в результате будет получен
дескриптор канала без ожидания открытия дескриптора на запись с другой стороны.
Но в следующей строке флаг O.NONBLOCK сбрасывается, поскольку нам необходимо, чтобы
обращение к вызову read блокировалось до появления данных в канале. В примере
из раздела 7.2.2 мы не использовали такой подход, потому что клиент открывал
дескриптор своего канала только после передачи запроса серверу. Здесь это не
годится, так как весь процесс взаимодействия разбит на отдельные функции (иногда
модульность требует выполнения дополнительных действий).
Ниже приводится текст сопутствующей функции smi_clcse_fifc:
bccl smi_clcse_fifc(SM10 *sqp)
{
SM10.F1F0 *p = (SMI0_F1F0 *)sqp;
clients_end(p);
(vcid)clcse(p->sq_fd_server);
if (p->sq_entity == SMI.CLIENT) {
char fifcname[SERVER_NAME_MAX + 50];
make_fifc_name_client(getpid(), fifcname, sizecf(fifcname));
(vcid)unlink(fifcname);
}
else
(vcid)clcse(p->sq_fd_server_w);
free(p->sq_msg);
free(p);
return true;
>
Теперь перейдем к функции smi_send_getaddr_fifo, которая лишь сохраняет
идентификатор клиента и возвращает указатель на буфер:
bccl smi_send_getaddr_fifc(SMlQ *sqp, struct client_id «client,
vcid **addr)
{
SMI0_F1F0 *p = (SM10_F1F0 *)sqp;
if (p->sq_entity == SMI_SERVER)
p->sq_client = *client;
*addr = p->sq_msg;
return true;
}
418 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Фактически, все действия по передаче сообщения выполняются в функции
smi_send_reiease_fifc:
bcci smi_send_reiease_fifc(SMiQ *sqp)
{
SMiQ_FiF0 *p = (SMiQ.FiFO *)sqp;
ssize_t nwrite;
if (p->sq_entity == SMi.SERVER) {
int nciient = ciients_find(p, p->sq_ciient.c_id1);
if (nciient == -1 || p->sq_ciients[nciient].ci_fd == -1) {
errnc = EADDRNOTAVAiL;
EC.FAiL
}
ec_neg1( nwrite = write(p->sq_ciients[nciient].ci_fd, p->sq_msg,
p->sq_msgsize) )
}
eise {
p->sq_msg->smi_ciient.c_id1 = (icng)getpid();
ec_neg1( nwrite = write(p->sq_fd_server, p->sq_msg,
p->sq_msgsize) )
}
return true;
EC_CLEANUP_BGN
return faise;
EC.CLEANUP.END
}
Здесь нет ничего нового по сравнению с примером из раздела 7.2.2.
Все необходимые действия по приему сообщения выполняются в функции
srai_receive_getaddr_fifc:
bcci smi_receive_getaddr_fifc(SHIQ «sqp, vcid **addr)
{
SMIQ_FIF0 *p = (SMIQ.FIFO *)sqp;
ssize.t nread;
if (p->sq_entity == SMI.SERVER) {
int nciient;
char fifcname[SERVER_NAME_MAX + 50];
whiie (true) {
ec_neg1( nread = read(p->sq_fd_server, p->sq_msg,
p->sq_msgsize) )
if (nread == 0) {
errnc = ENETDOWN;
EC.FAIL
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 419
}
if (nread < cffsetcf(struct smijnsg, smi_data)) {
errnc = E2BIG;
EC_FAIL
>
if ((nciient = ciients_find(p,
(pid_t)p->sq_msg->smi_ciient.c_id1)) == -1) {
continue; /* клиент не зарегистрирован */
>
if (p->sq_ciients[nciient].ci_fd == -1) {
make_fifc_name_ciient((pid_t)p->sq_nisg->srai_ciient.c_id1,
fifcname, sizecf(fifcname));
ec_neg1( p->sq_ciients[nciient].ci_fd =
cpen(fifcname, C.WRCNLY) )
}
break;
>
>
else
ec_neg1( nread = read(p->sq_ciients[C].ci_fd, p->sq_msg,
p->sq_msgsize) )
*addr = p->sq_msg;
return true;
EC_CLEANUP_BGN
return faise;
EC_CLEANUP_END
}
Обратите внимание на строки:
if ((nciient = ciients_find(p, (pid_t)mp->smi_ciient)) = -1) {
continue; /* клиент не зарегистрирован */
}
Если cUents.find возвращает -1, это означает, что сервер получил сообщение, но
не может сохранить данные, идентифицирующие клиента, в массиве sq_clients, a
соответственно не может открыть канал для отправки ответа. Сервер не может даже
передать сообщение об ошибке. Поэтому в нашем примере мы просто
игнорируем ошибку, оставляя клиента заблокированным на операции чтения. Выходы из этой
неприятной ситуации могут быть следующие:
• послать клиенту сигнал, a smi_cpen_f if с изменить так, чтобы клиент мог поймать
этот сигнал;
• создать дополнительную переменную для хранения «аварийного» дескриптора,
с помощью которого можно было бы передать сообщение об ошибке;
• изменить код клиента таким образом, чтобы он прерывал операцию чтения по
истечении некоторого времени, чтобы не оказаться заблокированным навсегда.
420 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
У нас осталась нереализованной еще одна функция, но она вообще ничего не делает:
bool smi_receive_release_fifo(SMIQ *sqp)
{
return true;
}
На этом мы закончили реализацию полного набора функций SMI для FIFO. Даже с
описанными выше ограничениями, они вполне пригодны для употребления в
ваших приложениях, по крайней мере для построения прототипа.
7.4 System V IPC
Как уже говорилось в разделе 1.1.7, существует два набора системных вызовов для
работы с очередями сообщений, семафорами и общей памятью. Самый старый из
них называется System V IPC, более современный — POSIX IPC. В этой главе мы
рассмотрим оба набора, сначала очереди сообщений System V и POSIX, затем
семафоры обоих типов и в заключение — общую память. Этот раздел посвящен базовым
концепциям, применимым к механизмам System V IPC. Механизмы POSIX IPC
будут обсуждаться в разделе 7.6.
7АЛ Объекты System V IPC
Существует три вида объектов System V IPC: очереди сообщений, наборы
семафоров и сегменты общей памяти. Они не являются файлами, они даже не являются
специальными файлами, но у них есть своя система имен и права доступа. Ниже
приводятся основные характеристики объектов System V IPC.
• Они могут существовать только в пределах одной системы. Они не могут
использоваться для организации взаимодействия через сеть.
• Срок жизни объектов не может превышать срока жизни ядра — при
перезагрузке они уничтожаются.
• Доступ к объектам осуществляется через целочисленный идентификатор,
который не изменяется на протяжении жизни объекта. Любой процесс, которому
известен идентификатор объекта, может обращаться к нему напрямую —
необходимость в предварительном его открытии отпадает. Это отличает их от
дескрипторов, которые являются неотъемлемой частью процесса и исчезают по его
завершении. (Объекты System V IPC еще имеют ключи, о которых я расскажу в
следующем разделе.)
• Объекты не связаны с индексными узлами или полными именами, поэтому при
работе с ними не могут быть использованы традиционные системные вызовы,
такие как unlink, stat, read или write.
Обращения ко всем трем типам объектов производятся с помощью системных
вызовов, именование которых следует схеме: Xget и Xctl, где X — это msg, sem или
shm. Таким образом, полные имена всех шести системных вызовов: msgget, semget, shmget,
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 421
msgotl, semotl и shinctl. Когда речь будет идти обо всех трех типах объектов, я буду
использовать обозначения Xget и Xctl.
Дополнительно, для управления объектами, существует всего пять системных
вызовов: msgsnd и msgrcv для передачи и приема сообщений, semcp для манипуляций
наборами семафоров, shmat и shmdt для присоединения и отсоединения сегментов
разделяемой памяти.
7.4.2 Идентификаторы, ключи и системный вызов ftok
Идентификатор, присваиваемый объекту во время его создания, в общем случае
может изменяться при перезагрузке. Для упрощения получения идентификатора
объекта, который должен совместно использоваться различными процессами,
существуют постоянные ключи, которые никогда не изменяют свое значение.
Процесс может указать ключ при создании объекта вызовом Xget или при получении
идентификатора тем же вызовом, если объект уже существует. Однако ключ на
самом деле не идентифицирует объект в том смысле, как полное имя
идентифицирует конкретный файл, поскольку объекты существуют только до момента остановки
системы. После очередной загрузки с помощью ключа создается совершенно
новый объект, возможно с другим идентификатором.
Ключи имеют тип key_t, который не обязан быть даже числом, хотя некоторыми
системными вызовами он используется именно таким образом. При желании вы
можете определить ключи в своих программах так:
«define MSGQ_KEY 1234
«define SEM_KEY 1235
«define SHM_KEY1 1236
«define SHM_KEY2 1237
Это даст возможность любому процессу, который обратится к msgget с ключом hsgq_key,
получить доступ к одной и той же очереди сообщений.
Основная проблема с ключами состоит в необходимости придерживаться
некоторой схемы именования, чтобы избежать конфликтов между приложениями. Это
довольно сложно, поэтому был создан системный вызов ftok, который генерирует
уникальные ключи на основе полного имени файла.
ftok — генерирует ключ
«include <sys/ipc.h>
key_t ftck(
const char *path,
int id
);
/* В случае успеха вез
в переменной еггпс) */
System V IPC
/* полнее имя существующего
/* желаемый номер
вращает ключ или -1
ключа */
в случае
файла
сшибки
*/
(кед сшибки -
422 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Фактически, системный вызов ftok может сгенерировать целый набор ключей для
одного и того же аргумента path. Аргумент id — это желаемый номер ключа, он не
может иметь значение 0. Вызов ftok использует только 8 младших бит этого
аргумента. Например, пусть необходимо создать четыре ключа (один для очереди
сообщений, один для набора семафоров и два для сегментов разделяемой памяти).
Тогда можно четырежды вызвать ftok, передав ему в первом аргументе полное имя
одного и того же файла (например /trnp/myappkeys) и четыре разных значения во
втором аргументе: «q», «s», «m» и «п» (или любые другие, по вашему выбору). Файл
должен существовать, поскольку ftok не создает его.
Я уже говорил о том, что объекты System V IPC не являются файлами и никак не
связаны с индексными узлами. Имя существующего файла используется только для
придания уникальности генерируемым ключам. Содержимое файла не имеет
никакого значения. Кроме того, совершенно не гарантируется получение тех же
самых ключей, после того как файл будет удален и создан заново, даже если полное
имя файла останется неизменным, поэтому желательно создавать файл во время
установки приложения (или на первом его запуске) и удалять его только во время
деинсталляции приложения.
Различные имена файлов гарантируют генерацию различных ключей только если
они находятся в пределах одной файловой системы (см. раздел 324). Если вас
волнует проблема случайного получения одинакового ключа различными
приложениями, можно использовать специальный ключ IPC.PRIVATE при обращении к
вызову Xget (будет описано в разделе 7.5.1), который гарантирует уникальность
объекта IPC и исключает необходимость обращения к ftok, как и использования
ключей вообще. Существует еще одна небольшая проблема — сгенерированный ключ
нужно как-то сделать доступным для других процессов. Можно записать его в файл,
имя которого заранее предопределено. Однако с целью упрощения примеров, мы
будем предполагать отсутствие конфликтов имен при обращении к вызову ftok.
Подводем некоторые итоги.
• Доступ к объектам System V IPC производится с помощью идентификаторов. Как
процесс получит идентификатор — неважно. Он может быть передан в виде
аргумента вызову ехео или в виде сообщения. Он может быть получен из файла
или любым другим способом.
• При желании доступ к объекту можно получить с помощью ключа, который в
отличие от идентификатора не изменяется.
• Поскольку управление ключами довольно сложная проблема, можно прибегнуть
к услугам системного вызова ftok, который генерирует их, исходя из полного
имени существующего файла. Вероятность генерации неуникальных ключей
достаточно низка, к тому же можно создавать объекты IPC.PRIVATE.
Вы можете задать вполне резонный вопрос: «К чему все эти идентификаторы, ключи,
ftok, когда UNIX уже обладает такими замечательными механизмами, как полное
имя файла и дескриптор?» Ответ на него вы найдете чуть позже, когда мы рассмот-
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 423
рим некоторые примеры использования объектов System V IPC. Вы увидите, что
сервер, получив от клиента идентификатор очереди, может сразу же ответить ему,
без выполнения лишних операций по поиску и открытию индексного узла,
которые выполняет системный вызов open.
Один из недостатков объектов System V IPC заключается в том, что их
идентификаторы не являются файловыми дескрипторами, а поэтому к ним не применимы
системные вызовы select и poll (см. раздел 4.2). Хотя я уже демонстрировал одно
из решений этой проблемы в разделе 5.18.
Но так или иначе, системные вызовы System V IPC полностью стандартизованы и
вряд ли когда-нибудь подвергнутся изменениям.
7.4.3 Принадлежность и права доступа
к объектам System V IPC
Если бы объекты System V IPC были файлами, к ним были бы применимы те же самые
правила доступа, что и для файлов, и я не стал бы затрагивать этот вопрос. Но они
не являются файлами и имеют свою систему проверки прав доступа. К счастью, она
не слишком отличается от той, что используется для работы с файлами.
Права доступа задаются девятью битами: права на чтение, запись и исполнение для
владельца, группы и остальных. Однако, права на «исполнение» не имеют
никакого смысла применительно к объектам System V IPC, поэтому они обычно не
используются. Биты прав доступа устанавливаются при создании объекта вызовом Xget и
позднее могут быть изменены вызовом Xctl.
Как и файлы, объекты System V IPC обладают идентификаторами пользователя и
группы, последний из которых обозначает идентификатор создателя. Для изменения
владельца может быть использован вызов Xctl. Изменить идентификатор
создателя нельзя.
Для проверки прав доступа к объекту используются действующие
идентификаторы пользователя и группы, точно также как и для файлов. Права доступа для
«остальных» проверяются только в случае несовпадения идентификаторов
пользователя или группы. Однако совпадение считается полным, если идентификатор
пользователя или группы процесса совпадает с идентификатором владельца или
создателя объекта.
Возможность иметь различные идентификаторы для владельца и создателя
позволяет администратору (скажем, «dbmsadm»), с правами которого работает сервер базы
данных, быть создателем очереди сообщений и иметь доступ как к очереди
сообщений, так и к файлам базы данных. А рядовому пользователю (скажем, «dbmsuser»)
иметь доступ только к очереди сообщений.
Для получения или изменения прав доступа вызовом Xctl, используется следующая
структура:
424 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
struct ipcjperm — структура прав доступа к объектам System V IPC
struct ipc_perm {
идентификатор пользователя владельца •/
идентификатор группы владельца */
идентификатор пользователя создателя «/
идентификатор группы создателя */
биты прав доступа */
};
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
/
/
/
/
/•
7.4.4 Утилиты System V IPC
Поскольку объекты System V IPC не являются файлами и не имеют индексных
узлов, вы не можете использовать такие системные вызовы, как unlink или stat, и
таким образом, вы не можете удалять их с помощью rm или вывести список объектов
с помощью is. Однако специально для этих случаев в системе присутствуют две
команды: ipcrm, которая удаляет объекты, и ipcs, которая выводит отчет об их
состоянии. За дополнительной информацией об используемых опциях обращайтесь
к [SUS2002] или к своей системной документации. Здесь я перечислю лишь самые
основные.
Вызов утилиты ipcrm производится с одной, или более, парой аргументов:
-X Y
где X — это символ «q» или «Q» для обозначения очереди сообщений, «s» или «S» —
для набора семафоров и «т» или «М» — для сегмента разделяемой памяти. Если
аргумент X представлен в виде символа нижнего регистра, то Y обозначает
идентификатор объекта, верхнего регистра — ключ.* Например, команда:
ipcrm -q 50
удалит очередь сообщений с идентификатором 50. При удалении файла, удаляется
лишь запись из файла каталога, а индексный узел и данные на диске продолжают
оставаться на месте до тех пор, пока не будет закрыт последний файловый
дескриптор, ссылающийся на удаляемый файл, что позволяет процессам работать с
файлом, как ни в чем не бывало. Подобное поведение не определено для объектов
System V IPC — они могут быть удалены сразу, вызывая хаос среди использовавших
их процессов.. Поэтому утилита ipcrm используется, как правило, только в часы
обслуживания системы администратором или когда вы точно знаете, что удаляемый
объект нигде не используется.
Эквивалент команды is — ipcs. Используется без аргументов и выводит нечто
подобное:
' Это стандарт. В Linux (или в GNU) это делается несколько иначе. Подробное описание ищите
в системной документации.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 425
IPC status from <running systera> as of Wed Mar 12 15:04:06 MST 2003
T 10
Message Queues:
q 50
Shared Memory:
m 0
m 102
m 103
m 104
m 105
Semaphores:
s 131072
s 131073
KEY
0x1007b8c
0x500004b7
0
0
0
0
0x1007b8d
0
М00Е OWNER
-rw-rw-rw- marc
-rw-r-r- root
-rw-rw-rw- maro
-rw-rw-rw- maro
-rw-rw-rw- maro
-rw-rw-rw- maro
-ra-ra-ra- marc
-ra-ra-ra- marc
GROUP
sysadmin
root
sysadmin
sysadmin
sysadmin
sysadmin
sysadmin
sysadmin
Обратите внимание, некоторые из ключей имеют значение 0. Это закрытые объекты,
созданные вызовом Xget, с ключом IPC.PRIVATE. Делается это в том случае, когда другому
процессу передается идентификатор объекта, и ему нет необходимости
запрашивать его с помощью Xget по ключу. Мы увидим это при реализации функций SMI,
работающих через очереди сообщений System V IPC (раздел 7.5.3)-
7.5 Очереди сообщений System V
Теперь мы готовы перейти к рассмотрению системных вызовов, предназначенных
для работы с очередями сообщений System V.
7.5.1 Системные вызовы для работы с очередями
сообщений System V
Начнем с системного вызова msgget:
msgget — возвращает идентификатор очереди сообщений
«include <sys/msg.h>
int msgget(
key_t key, /* ключ */
int flags /* флаги »/
);
/* Возвращает идентификатор или -1 в случае ошибки (код ошибки
в переменной еггпо) */
Как я уже говорил в разделе 7.4, системный вызов msgget возвращает
идентификатор существующей очереди по заданному ключу. Если во втором аргументе
присутствует флаг IPC_CREAT, при необходимости создается новая очередь. В этом
случае последние 9 бит второго аргумента трактуются как биты прав доступа. В
качестве идентификаторов создателя и владельца принимаются действующие
идентификаторы пользователя и группы процесса, обратившегося к msgget.
426 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
В качестве битов прав доступа можно использовать идентификаторы «S_», о
которых я рассказывал в разделе 2.3. Не забывайте: в качестве признака создания
должен передаваться флаг IPC.CREAT, а не 0.CREAT!' Аналогичным образом, чтобы
обращение к msgget завершалось неудачей, если очередь уже существует, следует
использовать флаг IPC.EXCL.
Если в аргументе key передается идентификатор IPC_PRIVATE, создается закрытая
очередь, которая не связана с каким-либо ключом. При каждом обращении к msgget
с ключом IPC_PRIVATE (в этом случае флаг IPC.CREAT можно опустить) вы будете
получать совершенно новую очередь. Это идеально подходит для клиентов, которые
создают очередь для получения ответов от сервера.
Управлять существующими очередями можно с помощью системного вызова msgctl:
msgctl — управляет очередью сообщений
«include <sys/msg.h>
int msgctK
int msqid, /»
int cmd, /»
struct msqid.ds «data /»
);
/» Возвращает О, в случае усг
в переменней errnc) »/
идентификатор »/
команда */
параметры команды */
еха или -1 в случае сшибки
(кед
сшибки -
struct msqidds — структура
struct msqid.ds {
struct ipc_perm msg.
msgqnum.t msg_qnum;
msglen.t msg_qbytes;
pid.t msg.Ispid;
pid_t msg.Irpid;
time.t msg.stime;
time.t msg.rtime;
time.t msg.ctime;
};
для msgctl
.perm; /» права доступа »/
/• количестве сообщений в очереди */
/• максимальный размер очереди в байтах */
/* идентификатор последнего процесса, */
/• вызвавшего msgsnd */
/» идентификатор последнего процесса,
/» вызвавшего msgrev */
/» время последнего обращения к msgsnd */
/» время последнего обращения к msgrev »/
/• время последнего обращения к msgctl •/
/» в' результате которого изменилось »/
/» одно из лелей структуры »/
Почему разработчики System V IPC не использовали те же самые идентификаторы, что и
системный вызов open? Потому что много лет тому назад (в середине 70-х годов прошлого
столетия) системный вызов open еще не использовал флаги. Поэтому вопрос должен
звучать несколько иначе: «Почему разработчики подсистемы ввода-вывода не использовали
те же флаги, что и System V IPC?». Ответ довольно прост — потому что все символы System
V IPC начинаются с приставки «IPC» и, кроме того, основным разработчикам из Bell Labs
не нравились вызовы System V IPC.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 427
Аргумент cmd может принимать одно из трех значений:
IPC_RMID Удаляет очередь с идентификатором msqid. Для выполнения
этой операции процесс должен обладать либо правами
суперпользователя, либо правами создателя, либо правами владельца
очереди. Аргумент data игнорируется и может принимать
значение NULL.
IPC.STAT Заполняет структуру, на которую указывает аргумент data,
сведениями о заданной очереди.
ipc_set Устанавливает значения четырех параметров очереди:
msg.perm.uid
i»sg_perm.gid
msg.perm.mode
msg.qbytes
из структуры, на которую указывает аргумент data. Процесс
должен обладать теми же правами, что и в случае команды
IPC_RMID. Изменить значение msg.qbytes может только
процесс, обладающий правами суперпользователя.
Сообщения помещаются в очередь и извлекаются из нее посредством системных
вызовов msgsnd и msgrcv:
msgsnd — помещает сообщение в очередь
«include <sys/msg.h>
int msgsnd(
int msqid, /* идентификатор »/
ccnst vcid *msgp, /* сообщение */
size_t msgsize, /* размер сообщения */
int flags /* флаги */
);
/* Всзвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
msgrcv — извлекает сообщения из очереди
«include <sys/msg.h>
ssize_t msgrcv(
int msqid, /* идентификатор */
vcid *msgp, /* ссебщение */
size_t mtextsize, /* размер буфера mtext (см. ниже) */
leng msgtype, /* тип запрещенного сообщения */
int flags /* флаги */
);
/* Всзвращает числе байт, размещенных в mtext или -1 в случае сшибки (кед
сшибки - в переменней еггпс) */
428 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
struct msg — типичная структура для работы с системными вызовами msgsnd
и msgrcv
struct msg {
long ratype; /» тип сообщения */
char mtext[MTEXTSIZE]; /* текст сообщения */
};
Структура для работы с системными вызовами msgsnd и msgrcv может быть вообще
любой, главное условие — первое поле этой структуры, которое используется для
хранения типа сообщения, должно иметь тип long. Таким образом, вы можете
группировать свои сообщения по типам, указывать тип при помещении их в очередь и
затем извлекать из очереди сообщения требуемого типа.
• Если в msgrcv в аргументе msgtype передано число 0, возвращается первое
сообщение из очереди, независимо от его типа.
• Если msgtype > 0 — возвращается первое сообщение заданного типа.
• Если msgtype < 0 — возвращается первое сообщение с наименьшим значением
типа, которое меньше или равно абсолютному значению аргумента msgtype.
Например, если в очереди имеются сообщения с типами 5, б и 17, и аргумент
msgtype == -6, то msgrcv вернет первое сообщение с типом 5.
Если вы не предусматриваете разбиение сообщений на типы, используйте
значение 1 (тип сообщения не может быть равен нулю) для указания типа сообщения
при помещении его в очередь, и 0 — для аргумента msgtype вызова msgrcv. Чаще
всего типы используются для обозначения уровней приоритетов сообщений. Также
можно использовать единую очередь для работы с несколькими серверами и
клиентами, где каждому из них будет соответствовать свой тип сообщений, но это
довольно неуклюжий способ организации взаимодействия.
Аргумент msgslze определяет размер поля mtext в структуре msg, а не всей
структуры. Аналогичным образом, msgrcv возвращает количество байт в поле mtext, а не
размер всей структуры. Если длина принимаемого сообщения превышает значение
аргумента mtextsize и задан флаг MSG.NOERROR, вызов msgrcv завершается без
признака ошибки, при этом в буфер помещаются только первые mtextsize байт
сообщения. Но в целом такой прием считается дурным тоном.
По достижении ограничения на количество сообщений в очереди или общего числа
байт в очереди, вызов msgsnd обычно блокируется. Во избежание блокировки
следует передавать ему флаг ipc.nowait (System V версия флага 0_N0NBL0CK). В этом
случае системный вызов немедленно вернет управление в вызывающую программу с
признаком ошибки и кодом ошибки EAGAIN. (Об ограничениях мы поговорим в
следующем разделе.)
Если в очереди нет сообщений запрашиваемого типа, то системный вызов msgrcv
обычно блокируется. Во избежание блокировки, как и в случае с msgsnd, вы можете
указать флаг IPC.NOWAIT в аргументе flags. Поскольку очереди сообщений не
связаны с файловыми дескрипторами, вы не можете воспользоваться системными вы-
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 429
зовами select или poll, поэтому старайтесь избегать алгоритмов, требующих
ожидания более чем в одной очереди. Для этих целей можно порекомендовать
использование различных типов сообщений. Если все-таки необходимо организовать
работу с несколькими очередями, вам подойдет методика, описанная в разделе 5.18.
7.5.2 Ограничения очередей сообщений System V
Для очередей сообщений System V существует ряд ограничений, с которыми вы
наверняка столкнетесь в своей практике.
• Ограничение на общий размер всех сообщений в очереди. Вы можете изменить
его с помощью системного вызова msgctl, указав новое значение в поле msg.qbytes
структуры msqid_ds.
• Ограничение на количество сообщений в очереди.
• Ограничение на размер одного сообщения.
• Ограничение на количество очередей.
Превышение первых двух ограничений вообще не считается ошибкой.
Приложение может быть либо заблокировано на какое-то время в вызове rasgsnd, либо
может избежать блокировки, как это было описано в предыдущем разделе.
Превышение остальных ограничений — это уже серьезная проблема, решить
которую можно только переконфигурацией ядра. К сожалению, нет даже способа, с
помощью которого можно было бы достаточно просто узнать величину этих
пределов (sysconf здесь не помощник). Путем экспериментов мне удалось установить,
что в Solaris существует ограничение на 40 сообщений, но, как оказалось, это
ограничение является максимальным количеством сообщений во всех очередях. В
FreeBSD максимальное число сообщений в одной очереди оказалось равным 20. В
Linux я вообще не смог достичь предельного значения. Что касается
максимального размера одного сообщения, то в Solaris и FreeBSD у меня получилось 2048 байт,
в Linux — 8192 байта.'
7.5.3 Реализация SMI с очередями сообщений System V
Работа с очередями сообщений на самом деле не вызывает никаких сложностей и
я продемонстрирую правоту своих слов на примере реализации интерфейса SMI.
Организация внутренней очереди сообщений стала намного проще, чем в случае
использования именованных каналов (см. раздел 7.3.3):
typedef struct {
SMIENTITY sq_entity; /* сущность (сервер или клиент) */
int sq_qid_server; /* идентификатор сервера */
int sq_qid_client; /* идентификатор клиента */
char sq_name[SERVER_NAME_MAX]; /* имя сервера */
' В Darwin версии 6.6, которой я пользовался, вообще нет поддержки очередей сообщений
System V.
430 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
struct cllent_id sq_cllent; /* идентификация клиента •/
slze_t sq_msgslze; /» размер сссбщения »/
struct smljnsg »sq_msg; /* буфер сссбщения »/
} SMIQ_MSG;
Обратите внимание: теперь серверу не нужна информация по каждому из
клиентов, которая ранее использовалась для оптимизации доступа к дескрипторам,
потому что теперь сервер имеет возможность напрямую послать сообщение в
нужную очередь вызовом msgsnd. Фактически, серверу даже не нужно поле sq_qld_cllent.
Функция открытия очереди сообщений SMI также упростилась:
SMIQ «sml_cpen_msg(ccnst char ♦name, SMIENTITY entity, slze_t msgslze)
{
SMIQ_MSG «p = NULL;
char msgname[SERVER_NAME_MAX + 1GG];
key_t key;
ec_null( p = callcc(1, slzecf(SMIQ_MSG)) )
p->sq_msgslze = msgslze + cffsetcf(struct smljnsg, sml_data);
ec_null( p->sq_msg = callcc(1, p->sq_msgslze) )
p->sq_qld_server = p->sq_qld_client = -1;
p->sq_entlty = entity;
If (strlen(name) >= SERVER_NAME_MAX) {
errnc = ENAMETGGLONG;
EC.FAIL
}
strcpy(p->sq_name, name);
mkmsg_name_server(p, msgname, slzecf(msgname));
(vcld)clcse(cpen(msgname, G_WRGNLY | G_CREAT, 0));
ec_neg1( key = ftck(msgname, 1) )
If (p->sq_entity = SMI_SERVER) {
If ((p->sq_qid_server = msgget(key, PERM_FILE)) != -1)
(vcld)msgctl(p->sq_qld_server, IPC_RMID, NULL);
ec_neg1( p->sq_qld_server = msgget(key, IPC_CREAT | PERM_FILE) )
}
else {
ec_neg1( p->sq_qid_server = msgget(key, G) )
ec_neg1( p->sq_qid_client = msgget(IPC_PRIVATE, PERM_FILE) );
}
return (SMIQ »)p;
EC_CLEANUP_BGN
If (p != NULL)
(vcid)sml_clcse_msg((SMIQ »)p);
return NULL;
EC_CLEANUP_END
}
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 431
static void mkmsg_name_server(oonst SMIQ_MSG «p, ohar *msgname,
size_t msgname_max)
{
snprintf(msgname, msgname_max, "/tmp/smimsg-Xs", p->sq_name);
}
Функция mkmsg_name_server создает в каталоге полное имя файла, соответствующее
имени сервера, /tmp. Если данный файл не существует, то он создается
непосредственно перед вызовом ftok. Если очередь с заданным ключом существует — она
удаляется. После этого создается новая очередь. (Возможно, в вашем случае вы
предпочтете побеспокоиться об уже запущенных клиентах-и сохранить существующую
очередь, вместо того, чтобы бездумно уничтожать ее со всеми имеющимися в ней
сообщениями.) Клиенты получают доступ к очереди сервера, исходя из
предположения, что он уже запущен и создают свои собственные очереди.
Ниже приводится исходный код smi_olose_msg:
booi smi_oIose_msg(SMIQ »sqp)
{
SMIQ_MSG *p = (SMIQ_MSG *)sqp;
if (p->sq_entity == SMi_SERVER) {
ohar msgname[FILENAME_MAX];
(void)msgoti(p->sq_qid_server, IPC_RMID, NULL);
mkmsg_name_server(p, msgname, sizeof(msgname));
(void)uniink(msgname);
>
eise
(void)msgoti(p->sq_qid_oiient, IPC_RMID, NULL);
free(p->sq_msg);
free(p);
return true;
Далее следует исходный код функций smi_send_getaddr_msg и smi_send_reiease_msg:
booi smi_send_getaddr_msg(SMiQ *sqp, struct ciient_id *oiient,
void **addr)
{
SMIQ_MSG *p = (SMIQ_MSG *)sqp;
if (p->sq_entity == SMI_SERVER)
p->sq_oiient = «client;
*addr = p->sq_msg;
return true;
booi smi_send_reiease_msg(SMIQ *sqp)
{
432 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
SMIQ_MSG *р = (SMIQ_MSG *)sqp;
int qid_receiver;
p->sq_msg->smi_mtype = 1;
if (p->sq_entity == SMI_SERVER)
qid_receiver = p->sq_ciient.c_id1;
else {
qid_receiver = p->sq_qid_server;
p->sq_msg->smi_ciient.c_id1 = p->sq_qid_ciient;
}
ec_neg1( msgsnd(qid_receiver, p->sq_msg,
p->sq_msgsize - sizecf(p->sq_msg->smi_mtype), 0) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
К моменту вызова smi_send_reiease_msg клиент уже знает идентификатор очереди
сервера, а идентификатор своей очереди он помещает в поле структуры
сообщения. Сервер получает идентификатор очереди клиента из поля структуры SMIQ.MSG,
адрес которой был получен при обращении к smi_send_getaddr_msg. Теперь вам
должно быть понятно, почему тип сообщения записывается прямо в структуру smi_msg
— это позволяет использовать ее непосредственно в обращениях к msgsnd и msgrcv.
Обратите внимание: системному вызову msgsnd передается размер только буфера с
данными, а не всей структуры.
И, наконец, функции smi_receive_getaddr_msg и smi_receive.release_msg:
bcci smi_receive_getaddr_msg(SMIQ *sqp, vcid **addr)
{
SMIQ.MSG *p = (SMIQ_MSG *)sqp;
int qid_receiver;
ssize_t nrcv;
if (p->sq_entity == SMIJERVER)
qid_receiver = p->sq_qid_server;
else
qid_receiver = p->sq_qid_client;
ec_neg1( nrcv = msgrcv(qid_receiver, p->sq_msg,
p->sq_msgsize - sizeof(p->sq_msg->smi_mtype), 0, 0) )
*addr = p->sq_msg;
return true;
EC_CLEANUP_BGN
return false;
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 433
EC_CLEANUP_END
}
bool smi_receive_release_msg(SHIQ *sqp)
{
return true;
}
К моменту вызова этих функций и сервер, и клиент уже имеют идентификаторы
очередей друг друга, которые сохраняются в структуре SMI0_MSG.
7.5.4 Достоинства и недостатки очередей сообщений
System V
Этот вариант реализации SMI характеризует очереди сообщений System V с самой
лучшей стороны: процесс может послать сообщение в очередь, имея только ее
идентификатор — не нужно тратить дополнительное время для получения
доступа к ней. В случае очередей сообщений не возникает проблем и при наличии
нескольких процессов, изымающих сообщения из нее (в этом плане FIFO далеко не
безопасны).
Один из недостатков заключается в наличии различных ограничений, которые очень
сложно определить во время исполнения. Но самый большой недостаток состоит в том,
что практически все механизмы IPC, кроме сокетов, могут использоваться только в рамках
одной системы. Для современных приложений это слишком большое ограничение.
Первая редакция книги давала такой совет:
[Очереди сообщений System Vj слишком сложны, недостаточно документированы
и не переносимы. Старайтесь избегать их по мере возможности!
Теперь очереди сообщений уже не кажутся такими сложными, есть вещи гораздо
сложнее: потоки, сокеты и многое другое. Они по-прежнему плохо
документированы, но не настолько, чтобы полностью отказаться от них. Очереди стали
достаточно переносимы, они определены в спецификации SUS и присутствуют в
большинстве систем (надеемся, что скоро они появятся и в ОС Darwin). Таким образом,
пока вы знаете имеющиеся ограничения, нет причин избегать их.
7.6 POSIXIPC
В этом разделе мы рассмотрим некоторые общие моменты POSIX IPC, которые имеют
отношение к очередям сообщений, семафорам и общей памяти POSIX.
7.6.1 История развития POSIX IPC
Свою историю POSIX IPC начали со спецификации POSIX ЮОЗ.Ь-1993 (кратко —
POSIX1993), появившейся как часть расширений реального времени. Описываемые
здесь очереди сообщений POSIX, до сих пор остаются необязательной составляю-
434 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
щей (по крайней мере, так обстоит дело в POSIX2001). В противовес им System V
IPC являются обязательными к реализации в любой из систем,
сертифицированных Open Group UNIX.
Исторически сложилось так, что System V IPC, по сути не являясь стандартом,
реализована практически во всех основных ОС, в то время как POSIX IPC, будучи
стандартом, на протяжении более 10 лет по-прежнему остаются необязательными к
реализации и отсутствуют в целом ряде UNIX-систем, Особенно это касается FreeBSD,
Linux и Darwin. В теории, POSIX IPC являются более переносимыми, чем System V
IPC, но на практике все наоборот.
Кроме того, реализация механизмов взаимодействия процессов POSIX не
обязательно должна обладать эффективностью, достаточной для использования в системах
реального времени. Стандарт не накладывает никаких ограничений на уровень
производительности, а некоторые производители обеспокоены лишь тем, чтобы
их системы просто соответствовали стандартам.
Таким образом, как мы вскоре увидим, системные вызовы POSIX IPC обладают
большей функциональностью, чем System V IPC, но вы можете быть лишены
возможности использовать их в своих приложениях.
7.6.2 Имена POSIX IPC
В разделе 7.4.2 я уже отмечал, что для идентификации объектов System V IPC
использует ключи и имеет дополнительную возможность генерации уникальных ключей по
полному имени файла системным вызовом ftok. Стандарт POSIX IPC использует
упрощенную схему именования объектов, где в качестве имен служат строки символов.
Именование объектов POSIX должно подчиняться тем же правилами, что и полные
имена файлов. Стандарт требует: если имя объекта начинается с символа«/», то все
ссылки на это имя подразумевают один и тот же объект. Если имя объекта
начинается с любого другого символа, требование снимается, но в этом случае возникает
ряд проблем.
• Имя не обязательно должно соответствовать файлу в файловой системе. Объекты
POSIX IPC, подобно объектам System V IPC, не являются файлами. Операции с
файловой системой довольно «тяжеловесны», поэтому, с целью повышения
эффективности, реализации POSIX IPC могут свободно отказаться от них,
используя для поиска объектов, например, хэш-таблицы в памяти.
• В большинстве UNIX-систем обычный пользователь лишен права на запись в
корневой каталог, что порождает определенные проблемы, если конкретная
реализация POSIX IPC создает файлы, соответствующие именам объектов. Одно из
решений проблемы — выделение администратором по крайне мере одного каталога
для общего использования, например /ipc, однако, прочтите следующий пункт.
• Интерпретация символов «/■>, кроме первого, в имени объекта может
отличаться в разных реализациях. Некоторые реализации могут трактовать их как
обычные символы, другие — как разделители элементов пути к файлу, а отдельные
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 435
реализации, такие как в Solaris — вообще запрещают их использовать. Таким
образом, в целях повышения переносимости программ, вы должны избегать
использования символов «/» внутри имен объектов (разумеется, кроме самого
первого символа имени).
Так что же делать? Можно написать маленькую программу инициализации,
которая будет просто создавать все объекты вашего приложения (без символов«/»внутри
имен), и дать ей право на запись в корневой каталог. В процессе разработки либо
используйте программу инициализации, либо разрабатывайте приложение в
системе, которая не создает файлов, соответствующих объектам POSIX IPC. Примеры,
приводимые в этой книге, разрабатывались в ОС Solaris, реализация POSIX IPC в
которой не создает файлов и поэтому все работает прекрасно.
Альтернативный вариант — создать в исходном коде блоки «ifdef для различных
систем, используя в каждом из них свои правила именования. Но такой подход имеет
свои отрицательные стороны: какой бы набор вы не предусмотрели, рано или
поздно понадобится добавить еще какую-нибудь систему, в результате кому-нибудь
придется заняться исправлением вашей программы со всеми вытекающими
отсюда последствиями.
Еще один способ — помещать шаблоны имен объектов в конфигурационные
файлы и брать их оттуда во время исполнения. Тогда, выбор схемы именования можно
производить во время установки программы, а не на этапе компиляции. Но при таком
подходе возникает проблема с приложениями, которые были установлены не
совсем корректно. Возможно, схема именования объектов System V IPC с
использованием ключей и системного вызова ftok не так уж и плоха.
7.6.3 Макросы проверки функциональных
возможностей POSIX IPC
Как уже отмечалось в разделе 1.5.4, спецификация SUS и ранние стандарты POSIX
предусматривают наличие макроопределений для проверки тех или иных
функциональных возможностей во время исполнения. К сожалению, системы, не
предусматривающие дополнительных функциональных возможностей, могут не
поддерживать макросы для их проверки. Поэтому на практике использование
макросов фактически не повышает степень переносимости программ.
Однако если хотите, можете попробовать использовать их в своих приложениях,
но перед этим внимательно прочитайте первую главу, где я в общих чертах
объяснил основные принципы работы с ними, а затем с помощью [SUS2002] или
соответствующего стандарта POSIX посмотрите, какие макросы что проверяют.
Я не буду использовать макросы проверки в своих примерах. Примеры либо
компилируются и работают (в системах, которые поддерживают необходимую
функциональность), либо не компилируются вообще.
436 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
7.6.4 Утилиты POSIX IPC
Для просмотра и удаления объектов System V IPC у нас были достаточно удобные
утилиты ipcs и ipcrm (см. раздел 7.4.4). Для объектов POSIX IPC никаких утилит не
предусматривается, поэтому данный раздел такой короткий. Таким образом, вы не
сможете даже проверить, не осталось ли каких-нибудь объектов в результате
неправильной работы приложения.
7.7 Очереди сообщений POSIX
В этом разделе я расскажу об очередях сообщений POSIX и покажу реализацию SMI
на их основе.
7.7.1 Системные вызовы для работы с очередями
сообщений POSIX
Оказывается, что проблема именования — это единственное скользкое место при
работе с очередями сообщений POSIX. Все остальное делается достаточно просто.
Сначала мы рассмотрим системный вызов mq_cpen, который открывает очередь
сообщений.
Вызов mq.cpen очень напоминает вызов среп, несмотря на то, что возвращает
дескриптор очереди сообщений, который не является файловым дескриптором, а
создаваемые им объекты не являются файлами, Прискорбно, но факт — вы не
можете использовать полученные дескрипторы в системных вызовах select и pell, как
и в случае с очередями System V. Однако вы можете получить сигнал о
поступлении нового сообщения с помощью mq.nctify (см. ниже).
mq_open — открывает очередь сообщений
«include <mqueue.h>
mqd_t mq_cpen(
censt char «name, /* имя POSIX IPC */
int flags /* флаги (за исключением 0_CREAT) */
);
/* Возвращает дескриптор очереди или -1 в случае сшибки (кед сшибки -
в переменней errnc) */
mqd_t mq_cpen(
censt char «name, /* имя POSIX IPC */
int flags, /* флаги (включая 0_CREAT) •/
mcde_t perms, /* права дсступа */
struct mq_attr «attr /* атрибуты (или NULL) */
);
/* Возвращает дескриптор счереди или -1 в случае сшибки (кед ошибки -
в переменной errno) */
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 437
struct mq_attr
и mq_setattr
struct mq
long
long
long
long
>;
— структура для использования в вызовах mq.
_attr {
mq.
mq.
mq.
mq.
flags;
.raaxmsg;
.rasgsize;
.currasgs;
/•
A
/•
/*
флаги */
максимальное число сообщений */
максимальный размер одного сообщени
количество сообщений в очереди «/
open,
1 */
mq
.getattr
Аргумент flags может быть собран с помощью тех же макросов, что и для вызова
open (например 0_CREAT), никаких специальных макросов, как в случае с msgget, не
предусмотрено.
• В зависимости от того, как будет использоваться очередь, вы можете
использовать ОДИН ИЗ макросов: O.RDONLY, 0.WR0NLY ИЛИ 0.RDWR.
• Флаг 0.CREAT используется для создания новой очереди, если таковой не
существует. В этом случае используется вторая форма обращения к системному
вызову.
• С флагом 0.EXCL новая очередь создается только тогда, когда она не существует.
В противном случае будет возвращен признак ошибки.
• Флаг 0_N0NBL0CK создает очередь без возможности блокировки. Это означает, что
mq.send и mq.receive будут возвращать признак ошибки с кодом EAGAIN, если
очередь переполнена или пуста. По умолчанию очередь создается с возможностью
блокировки.
Когда создается новая очередь с флагом O.CREAT, аргумент perms, представляющий
права доступа, интерпретируется аналогично очередям сообщений System V и
файлам. Для назначения прав могут использоваться обычные флаги «S_» (см. раздел 2.3).
Право на чтение и запись означает возможность извлекать сообщения из очереди
и помещать их в очередь соответственно. Выдача прав на исполнение не имеет
смысла. Как и файлы, очереди имеют идентификаторы владельца и группы, они
устанавливаются в соответствии с действующими идентификаторами процесса,
создавшего очередь. Позднее проверка прав доступа того или иного процесса к
очереди будет осуществляться точно так же, как и в случае с файлами.
Кроме того, при создании новой очереди с флагом 0_CREAT, если аргумент attr не
является пустым указателем, очереди присваиваются два первых атрибута из
структуры mq.attr. Эти атрибуты устанавливают максимальное число сообщений в
очереди и максимальный размер одного сообщения, которое может быть помещено в
очередь. Стандарты не оговаривают каких-либо ограничений на эти атрибуты, но
если система обнаружит нехватку памяти, mq_open вернет признак ошибки с кодом
EN0SPC. В большинстве реализаций очередь сообщений представляет собой
связанный список, поэтому на момент вызова mq_open исчерпание свободного
пространства не возникает, так как, невзирая на величину атрибутов, очередь фактически пуста.
Если в аргументе attr передается пустой указатель, атрибуты очереди
устанавливаются в значения по умолчанию, характерные для той или иной системы.
438 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Если при чтении предыдущих параграфов вы так и не уловили отличий, и
по-прежнему считаете, что вызов mq.cpen очень похож на среп — все равно можете смело
двигаться дальше, только помните, что mq_cpen возвращает дескриптор очереди, а
не дескриптор файла.
Закрывается очередь обращением к системному вызову mq.clcse:
mq_close — закрывает очередь сообщений
«include <mqueue.h>
int mq_clcse(
mqd_t mqd /* дескриптор счереди */
);
/» Возвращает 0 в случае успеха, -1 в случае
в переменней еггпо) »/
сшибки
(кед сшибки -
Как и очереди System V, очереди сообщений POSIX остаются существовать в
системе до тех пор, пока не произойдет перезагрузка системы или пока не будет
произведено обращение к системному вызову mq.unlink:
mq_unlink — удаляет очередь сообщений
«include <mqueue.h>
int mq_unlink(
const char «name /*
);
/* Возвращает О в случае
в переменней errnc) */
имя POSIX IPC
успеха, -1 в
*/
случае
сшибки
(кед
сшибки -
Как и unlink, системный вызов mq_unlink делает имя очереди недоступным сразу же,
но фактическое ее разрушение откладывает до того момента, когда будет закрыт
последний ссылающийся на нее дескриптор. (Надеюсь, вы не забыли, что вызовом
msgctl очереди System V удаляются немедленно, что может привести к
некорректной работе приложений.) Таким образом, если вам нужно обеспечить доступ к
очереди единственного процесса, вы можете вызвать mq.unlink сразу же после mq_cpen,
как мы делали это с временными файлами.
Итак, вы готовы послать сообщение? Для этой цели предназначен системный
вызов mq_send, как вы уже наверняка догадались и сами:
mq_send — помещает
«include <mqueue.h>
int raq_send(
mqd_t mqd,
censt char *msg,
size_t rasgsize,
сообщение в очередь
/* дескриптор счереди */
/* сообщение */
/* размер сообщения */
unsigned priority /* приоритет */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней errnc)
•/
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 439
Системный вызов mq_send помещает сообщение размером msgsize в очередь
впереди сообщений с более низким приоритетом (аргумент priority), но позади
сообщений с тем же самым приоритетом. Приоритет можно задать, как минимум, в
диапазоне от 0 до 31; более точные границы диапазона вы можете узнать с
помощью sysccnf (см. раздел 1.5.5).
Как уже отмечалось вызов mq_send может быть заблокирован, если очередь
заполнена до отказа, разумеется, при условии, что флаг 0_NONBLOCK сброшен.
Прием сообщения производится системным вызовом mq_receive:
mq_receive — извлекает сообщение из очереди
«include <mqueue.h>
ssize.t mq_receive(
mqd_t mqd, /* дескриптор спереди сссбщений •/
char *msg, /* буфер для сссбщения */
size_t msgsize, /* размер буфера */
unsigned «pricrityp /* приоритет принятсгс сссбщения или NULL */
);
I* Возвращает размер сссбщения или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
Системный вызов mq_receive извлекает самое старое сообщение с наивысшим
приоритетом. Фактически, это самое первое сообщение с наивысшим приоритетом.
Аргумент msgsize не так прост, как кажется. Как и следовало ожидать, он
определяет размер буфера, на который указывает аргумент msg. Но это число должно быть
по крайней мере не меньше атрибута mq_msgsize (см. описание вызова mq_open), в
противном случае mq_receive будет терпеть неудачу, даже если сообщение
является достаточно маленьким, чтобы уместиться в буфер. В принципе это неплохо:
вы можете диагностировать проблему нехватки буфера на самых ранних этапах
разработки приложения. Фактический размер сообщения попадает в программу в
виде возвращаемого значения.
По адресу pricrityp записывается приоритет принятого сообщения. Полученное
сообщение удаляется из очереди, возможность просмотреть сообщение без его
извлечения не предусмотрена. Если очередь пуста, то mq_receive блокируется при
условии, что флаг 0_N0NBL0CK сброшен.
Системные вызовы mq_send и mq_receive имеют родственные им вызовы, которые
позволяют вернуть управление приложению через установленное время, если они
окажутся заблокированными:
15 Зак. 4030
440 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
mq_timedsend — помещает
ожидания
«include <mqueue.h>
«include <tlme.h>
int mq_timedsend(
mqd_t mqd,
ccnst char *msg,
size_t msgsize,
unsigned priority
ccnst struct timespec
);
/* Возвращает О в случае у
в переменней еггпс) •/
сообщение в очередь с предельным временем
/• дескриптор счереди */
/• сообщение */
/* размер сообщения */
/* приоритет •/
«tmcut /* предельнее время сжидания •/
спеха,
-1 в случае сшибки (кед сшибки -
mq_timedreceive — извлекает сообщение из очереди с предельным
временем ожидания
«include <mqueue.h>
«Include <tlme.h>
ssize_t mq_tlmedreceive(
mqd_t mqd,
char *msg,
size_t msgsize,
unsigned «pricrityp
*/
ccnst struct timespec
/* дескриптор счереди */
/* буфер для ссебщения •/
/• размер буфера »/
/• приоритет принятого ссебщения или NULL
•tmcut /* предельнее время сжидания •/
/• Возвращает размер ссебщения или -1 в случае сшибки (кед сшибки -
в переменной еггпс) */
Аргумент tmcut имеет смысл только в том случае, если вызов блокируется и не
установлен флаг 0_N0NBLOCK. По истечении установленного интервала времени в
вызывающую программу возвращается признак ошибки с кодом ETIMEDOUT.
Как я уже упоминал, работать с несколькими очередями или с очередью и еще чем-
нибудь, что может привести к блокировке в системном вызове, очень неудобно,
поскольку вы лишены возможности выполнить проверку дескриптора очереди с
помощью вызова select или pell. Однако вы можете заставить систему извещать
приложение о прибытии нового сообщения посредством сигналов. (Другое
решение вы найдете в разделе 5.18.) Сделать это можно с помощью системного вызова
mq_notify:
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 441
mq_notify — включает или
«include <mqueue.h>
int mq_nctify(
mqd_t mqd,
ccnst struct sigevent
);
/* Всзвращает О в случае
в переменней errnc) */
выключает режим асинхронных уведомлений
/*
«ер /*
/спеха,
дескриптср спереди
извещение */
-1 в случае сшибки
•/
(кед
сшибки -
Когда процесс или поток включают режим уведомлений, они будут получать
сигнал в момент поступления сообщения в пустую очередь mqd, при условии, что к этому
моменту процесс или поток не будут заблокированы в mq_receive — в этом случае
произойдет выход из заблокированного mq.receive. Только один процесс или
поток могут подписаться на получение уведомлений. Попытка включить режим
уведомления из второго процесса или потока потерпит неудачу. Режим уведомлений
отключается, когда процесс или поток получают сигнал или когда они
обращаются к системному вызову mq_nctify с аргументом sigevent равным NULL.
При поступлении сообщения в непустую очередь уведомление процесса не
производится, таким образом, mq_nctify не гарантирует отправку сигнала по прибытии
каждого сообщения. Например, первое сообщение, помещенное в пустую очередь,
вызовет передачу сигнала процессу, но если до извлечения первого сообщения в
очередь поступит второе, то уведомление передано не будет, так как фактически в
этот момент очередь не пуста. Таким образом, действия приложения должны быть
примерно следующими:
• после вызова mq_nctify следует извлечь (вызовом mq_receive) все сообщения из
очереди, с установленным флагом 0_N0NBL0CK;
• после прибытия сигнала необходимо сразу же повторно подписаться на
уведомления вызовом mq_nctify и выполнить действия, предусмотренные на
предыдущем шаге.
Тип сигнала и другие характеристики уведомления определяются исходя из
содержимого структуры sigevent (подробное описание вы найдете в разделе 9-5.6).
В заключение рассмотрим системные вызовы, с помощью которых можно получить
и изменить атрибуты очереди сообщений:
mq getattf — возвращает
«include <mqueue.h>
int mq_getattr(
mqd_t mqd,
struct mq_attr *attr
);
/» Всзвращает О в случае
в переменней errnc) */
атрибуты очереди сообщений
/» дескриптср счереди */
/* атрибуты •/
успеха, -1 в случае сшибки
(кед сшибки -
442 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
mq_setattr — изменяет атрибуты
«include <mqueue.h>
int mq_setattr(
mqd_t mqd,
ccnst struct mq_attr *attr,
struct mq_attr «cldattr
);
/* Возвращает О в случае успеха
в переменней еггпс) */
очереди сообщений
/* дескриптор счереди */
/* нсвые атрибуты */
/* старые атрибуты или NULL */
-1 в случае сшибки (кед сшибки -
Системный вызов mq_getatt г заполняет структуру (была описана вместе с вызовом mq_cpen)
текущими значениями атрибутов очереди mqd. Наибольший интерес для нас может
представлять атрибут mq_cuniisgs, который содержит число сообщений в очереди.
Системный вызов mq_setatt r устанавливает или сбрасывает флаг 0_N0NBL0CK. В
соответствии со стандартом, это единственное, что он может делать (однако
некоторые реализации могут предусматривать возможность изменения других атрибутов).
Таким образом, системный вызов mq_setattr в некоторой степени напоминает fcntl.
Если аргумент cldattr не является пустым указателем, по заданному адресу
записываются прежние значения атрибутов.
7.7.2 Реализация SMI с очередями сообщений POSIX
Теперь перейдем к рассмотрению реализации SMI через очереди POSIX. Для
начала приведу внутреннюю структуру очереди сообщений. В своей основе она
практически повторяет ту, что использовалась с именованными каналами, потому что
в этой реализации мы опять сталкиваемся со старыми проблемами:
#define MAX_CLIENTS 20
typedef struct {
SMIENTITY sq_entity; /* сущность (сервер или клиент) */
mqd_t sq_mqd_server; /* дескриптор счереди ссебщений сервера */
char sq_name[SERVER_NAME_MAX]; /* имя сервера */
struct client_infc { /* клиенты испельзуют тслькс sq_clients[0] */
mqd_t cljnqd; /* дескриптор счереди ссебщений клиента */
pid_t cl_pid; /* идентификатср прсцесса клиента */
} sq_clients[MAX_CLIENTS];
struct client_id sq_client; /* идентификатср клиента */
size_t sqjnsgsize; /« размер ссебщения */
struct smijnsg *sq_msg; /* буфер ссебщения */
} SMIQ_MQ;
Как и в случае с FIFO, чтобы избежать выполнения ненужных и дорогостоящих
операций по открытию и закрытию дескриптора, сервер будет сохранять дескриптор
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 443
открытой очереди для каждого клиента. Для большинства серверных приложений
такой подход не слишком обременителен, так как всегда желательно поддерживать
прямую связь со своими клиентами. Первый элемент массива будет
использоваться клиентами для хранения дескриптора собственной очереди.
Для преобразования имен SMI в имена POSIX IPC предназначены две следующие
функции:
static void niake_niq_nanie_server(const SHIQ_HQ *p, char «mqname,
size_t mqnamejnax)
{
snprintf(mqname, mqname.max, "/smimq-sXs", p->sq_name);
static void make_mq.name.ciient(pid_t pid, char •mqname,
size.t mqname.max)
{
snprintf(mqname, mqname.max, "/smimq-cXd", pid);
}
Имена похожи на имена файлов в корневом каталоге, однако в Solaris это далеко
не так (см. раздел 7.6.2).
Клиент передает серверу вместе с сообщением свой идентификатор. По этому
идентификатору сервер будет определять имя очереди клиента и открывать ее с
помощью следующей функции:
static mqd_t get_client_mqd(SMIQ_MQ *p, pid_t pid)
{
int i, avail = -1;
char mqname[SERVER.NAHE.MAX + 100];
for (i = 0; i < HAX.CLIENTS; i++) {
if (p->sq.ciients[i].ci.pid == pid)
return p->sq.ciients[i].ci_mqd;
if (avaii == -1 && p->sq_ciients[i].ci.pid == 0)
avaii = i;
>
errno = ECONNREFUSED;
ec_neg1( avaii )
p->sq_ciients[avaii].ci_pid = pid;
make_mq_name_ciient(pid, mqname, sizeof(mqname));
ec_neg1( p->sq_ciients[avaii].ci_mqd = mq_open(mqname, 0_WR0NLY) )
return p->sq_ciients[avail].ci_mqd;
EC_CLEANUP_BGN
return (mqd_t)-1;
EC_CLEANUP_END
>
444 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
В первую очередь функция проверяет, был ли уже открыт дескриптор очереди
клиента. Если дескриптор открыт не был и не превышено предельное число
клиентов, очередь открывается, а дескриптор сохраняется в массиве для последующего
использования.
Теперь мы готовы перейти к рассмотрению функции smi_cpen_mq:
static pid_t my_pid;
SMIQ *smi_cpen_mq(ccnst char «name, SMIENTITY entity, size_t msgsize)
{
SMIQ_MQ *p = NULL;
char mqname[SERVER_NAME_MAX + 1QQ];
struct mq_attr attr = {Q};
my_pid = getpid();
ec_nuii( p = caiiccd, sizecf(SMIQ_MQ)) )
p->sq_msgsize = msgsize + cffsetcf(struct smijnsg, smi_data);
ec_nuii( p->sq_msg = caiicc(1, p->sq_msgsize) )
p->sq_*ntity = entity;
p->sq_*qd_server = p->sq_ciients[Q].ci_mqd = (mqd_t)-1;
if (strlen(name) >= SERVER_NAME_MAX) {
errnc = ENAMETQQLQNG;
EC.FAIL
}
strcpy(p->sq_name, name);
make_mq_name_server(p, mqname, sizecf(mqname));
attr.mq_maxmsg = 1QQ;
attr.mq.msgsize = p->sq_msgsize;
if (p->sq_entity == SMi.SERVER) {
(void)mq_unlink(mqname);
ec_cmp( errno, ENQSYS )
ec_neg1( p->sq_mqd_server = mq_cpen(mqname, Q_RDQNLY | Q_CREAT,
PERM_FILE, &attr) )
}
else {
ec_neg1( p->sq_mqd_server = mq_open(mqname, Q_WRQNLY) )
make_mq_name_client(my_pid, mqname, sizeof(mqname));
ec_neg1( p->sq_clients[Q].ci_mqd = mq_open(mqname,
Q.RDONLY | Q_CREAT, PERM.FILE, &attr) )
}
return (SMIQ *)p;
EC.CLEANUP.BGN
if (p != NULL)
(void)smi_ciose_mq((SMIQ *)p);
return NULL;
EC.CLEANUP.END
}
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 445
После инициализации внутренней очереди сообщений SMI производится
инициализация структуры mq.attr — устанавливается ограничение на 100 сообщений в
очереди и размер одного сообщения (из третьего аргумента). Затем на сервере
уничтожается старая очередь и создается новая. Мы не делаем аналогичных
действий на клиенте, потому что имя очереди конструируется из идентификатора
процесса клиента и едва ли может встретиться многократно (хотя, с другой
стороны, это бы нам не повредило).
Функция закрытия очереди не делает ничего неожиданного:
bool smi_oiose_mq(SMlQ *sqp)
{
SH1Q_MQ *p = (SH1Q_MQ *)sqp;
ohar msgname[SERVER_NAME_MAX + 100];
if (p->sq_entity == SMI_SERVER) {
make_mq_name_server(p, msgname, sizeof(msgname));
(void)mq_olose(p->sq_mqd_server);
(void)mq_unlink(msgname);
}
else {
make_mq_name_olient(my_pid, msgname, sizeof(msgname));
(void)mq_olose(p->sq_mqd_server);
(void)mq_unlink(msgname);
}
free(p->sq_msg);
free(p);
return true;
Далее следует пара функций smi_send_getaddr_mq и smi_send_release_mq:
bool smi_send_getaddr_mq(SMIQ *sqp, struct client_id «client,
void **addr)
{
SMIQ.MQ *p = (SM1Q_MQ «)sqp;
if (p->sq_entity == SM1.SERVER)
p->sq_client = «client;
♦addr = p->sq_msg;
return true;
}
bool smi_send_release_mq(SMIQ «sqp)
{
SMIQ.MQ *p = (SMIQ.MQ Osqp;
mqd_t mqd_receiver;
446 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
if (p->sq_entity == SMI.SERVER)
ec_neg1( mqd_receiver = get_ciient_mqd(p, p->sq_ciient.c_id1) )
eise {
mqd.receiver = p->sq_mqd_server;
p->sq_msg->smi_client.c_id1 = my_pid;
}
ec_neg1( mq_send(mqd_receiver, (ccnst char *)p->sq_msg,
p->sq_msgsize, 0) )
return true;
EC_CLEANUP_BGN
return faise;
EC_CLEANUP_END
}
Как и в предыдущих примерах реализации SMI, функция smi_send_getaddr_mq на
стороне сервера просто запоминает идентификатор клиента и возвращает адрес
буфера. Фактически, вся работа выполняется в smi_send_reiease_mq. В ней сервер
отыскивает дескриптор очереди клиента обращением к функции get.ciientjnqd.
Клиент уже имеет открытый дескриптор очереди сервера, поэтому он просто
помещает идентификатор своего процесса в сообщение. (Дескриптор очереди
сообщений не может передаваться между процессами, в отличие от идентификаторов
очередей сообщений System V.)
И, наконец, мы подошли к функциям приема сообщений:
booi smi_receive_getaddr_mq(SMIQ *sqp, void «»addr)
{
SMIQ_MQ *p = (SMIQ.MQ *)sqp;
mqd_t mqd.receiver;
ssize_t nrcv;
if (p->sq_entity == SMI.SERVER)
mqd_receiver = p->sq_mqd_server;
eise
mqd_receiver = p->sq_ciients[0].ci_mqd;
ec_neg1( nrcv = mq_receive(mqd_receiver, (char *)p->sq_msg,
p->sq_msgsize, NULL) )
*addr = p->sq_msg;
return true;
EC_CLEANUP_BGN
return faise;
EC_CLEANUP_END
}
booi smi_receive_reiease_mq(SMIQ *sqp)
{
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 447
return true;
7.7.3 Достоинства и недостатки очередей сообщений POSIX
Очереди сообщений POSIX обладают более четким и понятным интерфейсом, чем
очереди сообщений System V. Они ближе к UNIX, но используют свои собственные
дескрипторы, а не дескрипторы файлов, из-за чего невозможно использовать
системные вызовы poll и select. Это неудобство в некоторой степени компенсируется
наличием уведомлений. Но они довольно сложны в использовании (мы увидим это
в главе 9), потому что основаны на сигналах, а сигналы всегда были одной из
самых сложных тем.
Главный недостаток очередей сообщений POSIX (впрочем, как и всех остальных
механизмов взаимодействия процессов POSIX) в том, что они доступны не во всех
системах.
В табл. 7.1 приведены сравнительные характеристики очередей POSIX и System V:
Таблица 7.1. Очереди сообщений POSIX и System V
Критерий сравнения
POSIX
System V
Стандартизация
Обязательны к реализации
в сертифицированных системах?
Реализованы в основных UNIX-системах?
Ограничения
Безопасны в контексте потоков
(соответствуют SUS3)?
Система приоритетов сообщений
Прием первыми сообщений
с меньшим приоритетом
Уведомления
Используют файловые дескрипторы?
Эффективность
Копирование сообщений в пространство
ядра и обратно
Проблемы с переносимостью имен очередей
Да
Нет
Нет
Незначительные,
легко изменяются
Да
Да
Нет
Да
Нет
Зависит
от реализации
Да
Да
Да
Да
Значительные, трудны
в управлении
Да
Да
Да
Нет
Нет
Зависит
от реализации
Да
Да
Нет
Увидев все эти «за» и «против» возникает резонный вопрос «Почему группа POSIX
взялась за выработку новых стандартов обмена сообщениями, в то время как System
V существует уже более 10 лет? А раз уж взялись за дело, почему результаты
получились не лучше?».
448 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
На мой взгляд.
• Несмотря на то, что POSIX имеет схожие черты с System V, главная причина
появления новых стандартов состоит в том, что System V, реализованная на
основных UNIX-системах, плохо подходит для приложений реального времени, а
сопровождать две реализации с одним и тем же интерфейсом было бы
довольно сложно. Поэтому нужно было придать новый импульс развитию.
• В рамках процесса развития POSIX, практически не было возможности
требовать обеспечения того или иного уровня производительности двух похожих друг
на друга механизмов во многих, если не в большинстве, систем.
• Непереносимость имен POSIX IPC была вызвана недостаточной проработкой
деталей, которые были необходимы для исключения возможности привлечения
«тяжеловесных» операций с файловой системой.
• Группа POSIX не может нести ответственность за отсутствие реализаций.
Большая доля ответственности скорее лежит на плечах разработчиков, которые в
середине 1990 годов основные усилия сконцентрировали на сетевых
приложениях, в которых для обмена информацией использовались сокеты (см. главу 8)
и не проявляли должного интереса к альтернативным, несетевым способам
обмена сообщениями.
7.8 О семафорах
В этом разделе я расскажу об основных характеристиках семафоров и поясню, как
они могут быть реализованы с помощью файлов и очередей сообщений.
Системные вызовы для работы с семафорами System V и POSIX будут описаны в разделах
7.9 и 7.10.
7.8.1 Основные принципы использования семафоров
Мы уже встречались с мьютексами в разделе 5.17.3. В общем представлении,
семафор — это счетчик, который способствует предотвращению одновременного
доступа нескольких процессов или потоков к совместно используемому ресурсу.
Однако семафоры носят лишь рекомендательный характер. Если процесс или поток
не проверяют состояние семафора перед выполнением операций с совместно
используемым ресурсом, последствия могут оказаться самыми печальными.
В UNIX термином «семафор» обычно определяют объект, предназначенный для
синхронизации процессов. Термином «мьютекс» определяется «легковесный» объект,
предназначенный для синхронизации потоков. Мьютексы мы уже рассматривали
в разделе 5.17.3, здесь же я буду говорить только о семафорах.
Двоичный семафор имеет всего два состояния: «закрыто» и «открыто». В общем
случае семафоры могут иметь если не бесконечное, то, по крайней мере, достаточно
большое число состояний. По сути это обычный счетчик. Он уменьшается при
захвате семафора процессом («запирается») и увеличивается при освобождении
(«отпирается»). Если счетчик равен нулю, процесс, пытающийся захватить семафор,
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 449
должен подождать, пока другой процесс не освободит его, увеличив значение
счетчика — значение семафора не может быть отрицательным числом. Над
семафорами обычно выполняются два действия, которые абстрактно можно представить как
semwait и sempost. (Иногда они называются как «Р» и «V».*) Попробуем выразить эти
операции на языке С:
void serawait(int *sem)
{
while (*sera <= 0)
; /* просто ждать, ничего не делая */
(•sera)-;
>
void sempost(int *sera)
{
(*sem)++;
}
Семафор должен быть инициализирован обращением к sempost, иначе никто не
сможет захватить его. Например, пусть семафор представляет собой счетчик
свободных буферов. Первоначально имеется пять буферов. Поэтому на запуске
необходимо пять раз обратиться к sempost, либо нужно записать число 5 в переменную-
счетчик.
Тут я должен заметить, что подобная реализация семафоров не будет работать в
многозадачной среде по трем причинам.
• Разные процессы не могут получить доступ к переменной, на которую
указывает sem, потому что они имеют различные сегменты данных. (Хотя семафор можно
разместить в общей памяти.)
• Вышеприведенные функции не являются атомарными. Ядро может в любой
момент прервать их исполнение и передать управление другому процессу.
В результате развитие событий может пойти по следующему сценарию: процесс
1 завершает исполнение цикла while, но перед тем как перейти к операции
уменьшения счетчика, прерывается и управление передается процессу 2. Процесс 2
вызывает функцию semwait, обнаруживает, что семафор равен 1, и уменьшает
значение семафора до 0. После этого управление возвращается в процесс 1,
который уменьшает значение семафора еще на 1, в результате — семафор
приобретает недопустимое значение -1.
• semwait делает то, что называется занят ожиданием, если счетчик sem равен нулю.
Это далеко не лучший способ использования процессорного времени.
• «р» и «V» — начальные символы фразы «Proberen te verlagen» (нем.) и слова «Verhogen» (нем.),
которые означают — «попытка уменьшить» и «увеличить» соответственно. Были
предложены Эдсгером Дейкстрой. Некоторые источники утверждают, что символ «Р» происходит от
«prolagen», но такого слова в немецком языке нет, на самом деле это сокращение полной
стразы.
450 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Таким образом, операции semwait и sempost не могут быть реализованы в виде
функций в пользовательском пространстве. Реализация семафоров должна
находиться в ядре, которое обладает возможностью организации совместного доступа
к данным из нескольких процессов, в состоянии обеспечить атомарность
операций и может выделить лишние кванты времени одним процессам, пока другие
находятся в состоянии блокировки.
7.8.2 Реализация семафоров через файлы
и очереди сообщений
В разделе 2.4.3 я демонстрировал способ реализации грубого приближения
двоичных семафоров с помощью системного вызова open. Этот метод годится только в
том случае, когда доступ к разделяемому ресурсу осуществляется всего несколько
раз в процессе исполнения программы. Но в программах, интенсивно работающих
с ресурсом, это будет слишком накладно. Нам нужны такие семафоры, которые будут
потреблять незначительное количество вычислительных ресурсов на проверку и
установку.
Очередь сообщений также может использоваться в качестве семафора. Передача
сообщения в очередь эквивалентна операции sempost. Извлечение сообщения из
очереди эквивалентно операции semwait. Операция извлечения сообщения
блокируется, если очередь пуста.
Однако, если UNIX-система поддерживает очереди сообщений (System V или POSIX),
то она также поддерживает и семафоры.
7.9 Семафоры System V
Операции над семафорами System V и POSIX реализованы в виде более сложных
системных вызовов, чем простые операции semwait и sempost, хотя семафоры POSIX
не намного сложнее. Достоинства и недостатки обоих типов семафоров я перечислю
в разделе 7.10.3.
Начнем рассмотрение темы с семафоров System V. Системные вызовы,
реализующие операции над семафорами System V, слишком сложны даже для меня. Но я
постараюсь дать вам объем знаний, достаточный для реализации операций sempost и
semwait. Все остальное вы найдете в своей системной документации или в [SUS2002].
Сведения, которые приводились в разделе 7.4, вполне применимы и к семафорам,
поэтому я не буду повторно говорить о ключах, идентификаторах, правах доступа и пр.
Единственный интересный момент — системные вызовы System V оперируют не
одиночными семафорами, а целыми массивами. В рамках одной атомарной
операции вы можете выполнить несколько операций semwait и sempost. Сомнительно, чтобы
у вас когда-нибудь возникла потребность в этом, но, тем не менее, такая возможность
существует. В большинстве примеров мы будем работать только с одним семафором,
хотя позднее, ради удобства, воспользуемся набором из двух семафоров.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 451
Одно примечание, прежде чем мы двинемся дальше: семафоры System V
чрезвычайно сложны! Но любая программа, где используются семафоры, способна наглядно
продемонстрировать исключительность доступа к ресурсам, отсутствие тупиковых
ситуаций и равные возможности процессов (отсутствует эффект «голодания»,
когда какой-либо из процессов вообще не может получить доступ к ресурсу). Для
выявления временных характеристик простого тестирования будет недостаточно —
слишком велика зависимость от синхронизации процессов, придется проводить
очень сложный разбор результатов даже для простейших операций semwait и sempost.
Если же системные вызовы System V будут использованы во всем своем
великолепии, то подобный анализ станет вообще невозможен.
7.9.1 Системные вызовы для работы
с семафорами System V
Как и в случае с очередями сообщений, начнем с вызова Xget:
semget — возвращает идентификатор набора семафоров
«include <sys/sem.h>
int seraget(
key_t key, /* ключ »/
int nsems, /* количество семафоров */
int flags /* флаги */
);
/* Возвращает идентификатор или -1 в случае ошибки (код ошибки -
в переменной еггпо) */
Как и следовало ожидать, semget преобразует ключ в идентификатор набора
семафоров. Если набора семафоров еще не существует и установлен флаг IPC_CREAT,
будет создан новый набор. Аргумент nsems определяет количество семафоров,
нумерация которых начинается с нуля.
Сразу после создания семафоры еще не готовы к использованию — они должны
быть инициализированы системным вызовом semctl. Почему semget не
инициализирует семафоры нулевыми значениями — загадка. Однако SUS не требует
проведения инициализации во время создания, поэтому большинство реализаций этого
не делают.'
SUS утверждает следующее: «Структуры данных, связанные с каждым из семафоров в
наборе, не должны инициализироваться». Фраза «не должны» означает, что инициализация
недопустима, даже как опциональная возможность. Интересно, это преднамеренное
утверждение или просто ошибка по невнимательности?
452 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
semctl — управляет набором семафоров
«include <sys/sem.h>
int semctl(
int semid, /* идентификатор */
int semnum, /* нсиер семафсра */
int cmd, /* команда */
union semun arg /* аргумент команды */
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
union semun — объединение для semctl
union semun {
int val; /* целее числе */
struct semid_ds *buf; /* указатель на структуру */
unsigned shcrt *array; /* массив */
>;
struct semidds -
struct semid_ds
struct ipc.
- структура для semctl
{
.perm sem
unsigned shcrt sem_
time_t sem_
time_t sem.
>;
ctime;
ctime;
_perm;
nsems;
/•
/•
/•
/•
права
размер
время
время
доступа
набора
*/
V
псследнегс
обращения
последнего обращения
к
к
semcp
semctl
•/
•/
Странно, но определение union semun отсутствует в заголовочном файле sem.h, вам
придется добавлять его вручную.
В дополнение к общим для всех механизмов System V IPC командам (IPC_RHID, IPC_STAT
и IPC_SET) для работы с семафорами существует семь дополнительных команд:
GETNCNT Получить количество процессов, ожидающих увеличения
значения семафора semnuii.
GETZCNT Получить количество процессов, ожидающих, пока значение
семафора sennun не достигнет нуля.
GETPID Получить идентификатор процесса, обратившегося к semop
последним.
GETVAL Получить значение семафора эеяпип.
SETVAL Установить значение семафора sennun. Передается через arg.val.
GETALL Получить значения всех семафоров в наборе. Возвращаются
через arg.array.
SETALL Установить значения всех семафоров в наборе. Передаются
через arg.array.
Это просто поразительно! Чтобы установить сотню семафоров в нулевое значение
одним вызовом semctl, вам придется создать массив на 100 элементов и передать
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 453
его в виде аргумента! Но к нам это не относится, потому что мы будем
инициализировать семафоры по одному.
Команда IPC.RMID выполняет те же действия, что и в случае с очередями
сообщений (см. раздел 7.5.1).
Команда IPC.STAT заполняет структуру semid.ds, передаваемую в качестве
аргумента (через указатель buf в объединении). Команда IPC.SET используется для
изменения полей sem_perm. uid, sem_perm.gid и sem_perm.mode.
Как я уже говорил, semget не выполняет инициализацию семафоров, для этой цели
необходимо использовать semctl примерно таким образом:
ec_neg1(semid = semget(key, 1, PERM.FILE | IPC.CREAT) )
arg.val = 0;
ec_neg1( semctl(semid, 0, SETVAL, arg) )
К сожалению, поскольку для создания и инициализации семафоров необходимо
выполнить два системных вызова, возможна ситуация, когда второй процесс или
поток, получив идентификатор набора через semget, начнет работу с
неинициализированным набором семафоров. Чтобы избежать подобной проблемы, перед
использованием семафоров можно проверить значение поле sem_otime структуры
semid.ds, которое устанавливается в ноль вызовом semget. Для этого предлагается*
следующая схема действий.
• Процесс или поток, который создает семафор, должен вызвать semctl для его
инициализации и затем выполнить обращение к semop, чтобы в поле sem_otime
появилось ненулевое значение.
• Любой процесс, получивший идентификатор, должен ожидать, пока время
последнего обращения к semop станет отличным от нуля.
Этот подход годится только для вновь создаваемых наборов семафоров. Если к
моменту запуска приложения набор уже существует (например, остался после
предыдущего запуска приложения), поле sem.otime будет содержать ненулевое
значение, что вероятно может оказаться для вас неприемлемым. Поэтому обязательно
удаляйте семафоры по окончании работы программы — либо обращением к semctl,
либо с помощью утилиты ipcrm (см. раздел 7.4.4).
К сожалению, к моменту написания этих строк в FreeBSD и Darwin время
последнего обращения к semop никогда не изменяется." Таким образом, семафоры System V
не предусматривают надежного способа проверки готовности к использованию.
(К еще большему сожалению, в этих системах полностью отсутствует поддержка
семафоров POSIX.)
Системный вызов semop, используемый для управления набором семафоров, будет
описан в следующем разделе.
* Об этой методике я узнал из [Stel999], стр. 284. В первом издании книги я выполнял эти
действия неправильно.
" По заверениям разработчиков, эта ошибка будет исправлена в FreeBSD 5.1.
454 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
7.9.2 Упрощенный интерфейс для работы с семафорами
Все связанные с открытием семафоров сложности можно скрыть под маской
упрощенного интерфейса, реализацией которого мы сейчас и займемся. Первой будет
рассмотрена реализация для System V, реализацией интерфейса для POSIX мы
займемся позднее. Ниже приводится краткая характеристика функций SimpleSemOpen и
SimpleSemClose:
SimpleSemOpen — открывает семафор
«include "SimpleSem.h"
struct SimpleSem »SimpleSemOpen(
const char *name /* имя (в
);
/* Возвращает указатель на структ
в переменной еггпо) */
соответствии
уру или
NULL
с
в
правилами
случае
именования)
ошибки
(код
ч
ошибки -
SimpleSemClose — закрывает семафор
«include "SimpleSem.h"
bool SimpleSemClose(
struct SimpleSem *sem /* семафор */
);
/• В случае успеха возвращает true, в случае
в переменной еггпо) */
ошибки -
false (код
ошибки -
struct SimpleSem — структура для функций упрощенного интерфейса
struct SimpleSem {
union {
int sm_semid; /* идентификатор набора
void *sm_sem; /* указатель на тип sem
} sm;
};
семафоров System V
_t POSIX
4
Ч
Функция SimpleSemOpen вообще не имеет дополнительных аргументов. Она просто
открывает одиночный семафор по заданному имени (или ключу — в случае System V)
или в случае необходимости создает его с правами PERM_FILE (см. раздел 2.3).
Функция возвращает указатель на структуру SimpleSem, которая содержит все
необходимое для идентификации семафора другими функциями (в случае System V —
идентификатор набора семафоров). Нам нет нужды беспокоиться по поводу
содержимого структуры, поскольку остальные функции описываемого интерфейса
принимают только указатель на нее, например SimpleSemClose закрывает семафор и
освобождает занимаемую структурой память.
Ниже приводится исходный код функции SimpleSemOpen. В нем вы без труда
найдете строки, выполняющие последовательность действий по проверке готовности
семафора к использованию, которая была описана выше:
ГЛАВА 7 улучшенные механизмы взаимодействия процессов 455
struct SimpleSem *SimpleSemOpen(ccnst char *name)
<
struct SimpleSem *sem = NULL;
key_t key;
unicn semun {
int val;
struct semid.ds *buf;
unsigned shcrt *array;
> arg;
struct sembuf scp;
(vcid)clcse(cpen(name, O.WRONLY | O.CREAT, 0));
ec_neg1( key = ftck(name, 1) )
ec_null( sem = mallcc(sizecf(struct SimpleSem)) )
if ((sem->sm.sm_semid = semget(key, 1,
PERM_F1LE | IPC.CREAT | IPC.EXCL)) != -1) {
arg.val = 0;
ec_neg1( semctl(sem->sm.sm_semid, 0, SETVAL, arg) )
scp.sem_num = 0;
scp.sem_cp = 0;
scp.sem_flg = 0;
ec_neg1( semop(sem->sm.sm_semid, &scp, 1) )
}
else {
if (errnc == EEXIST) {
while (true)
if ((sem->sm.sm_semid = semget(key, 1, PERM.FILE)) == -1) {
if (errnc == EN0ENT) {
sleepO);
ccntinue;
}
else
EC.FAIL
>
else
break;
while (true) {
struct semid.ds buf;
arg.buf = &buf;
ec_neg1( semctl(sem->sm.sm_semid, 0, IPC.STAT, arg) )
if (buf.sem.otime == 0) {
sleepO);
ccntinue;
>
else
break;
}
>
456 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
else
EC.FAIL
}
return sera;
EC_CLEANUP_BGN
free(sem);
return NULL;
EC_CLEANUP_END
}
Последовательность действий:
(void)close(open(name, O.WRONLY | O.CREAT, 0));
ec_negl( key = ftok(name, 1) )
создает файл с заданным именем, если его не существует. Любая проблема,
связанная с open или close будет интерпретироваться как проблема с ftok. (Если вас это
не устраивает, можете выполнить проверку исполнения вызова open отдельно.)
Затем вызывается semget с флагами IPC.CREAT и IPC.EXCL, благодаря чему системный
вызов будет терпеть неудачу, если семафор уже существует. Все процессы и
потоки, получившие в этом месте признак ошибки, должны подождать инициализации
семафора. Процесс или поток, для которого semget завершился без признака ошибки,
должен инициализировать семафор обращением к semctl и затем вызвать semop (мы
вскоре рассмотрим его), который по сути ничего не делает, но в результате
побочного эффекта изменяет поле sera_otime.
Процессы и потоки, перешедшие в ожидание завершения инициализации,
сначала попадают в цикл, где они ждут получения идентификатора семафора, а затем в
цикл, где ждут изменения содержимого поля sem_otime.
Для семафоров System V функция SimpleSemClose не делает ничего сверхвыдающегося:
bool SimpleSemClose(struct SimpleSem *sem)
{
free(sem);
return true;
}
Чтобы удалить семафор, нужно обратиться к функции SimpleSemRemove:
SimpleSemRemove — удаляет семафор
«include "SimpleSem.h"
bool SimpleSemRemove(
struct SimpleSem «sem /* семафор */
);
/* В случае успеха возвращает true, в случае ошибки -
в переменной errno) */
- false (код ошибки -
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
457
Она допускает удаление несуществующего семафора, не считая это ошибкой.
Приложение должно начинаться обращением к этой функции, если вы действительно
хотите, чтобы всегда создавались новые семафоры.
bool SimpleSemRemove(const char «name)
{
key_t key;
int semid;
if ((key = ftok(name, 1)) == -1) {
if (errno != ENOENT)
EC.FAIL
}
else {
if ((semid = semget(key, 1, PERM.FILE)) == -1) {
if (errno != ENOENT)
EC_FAIL
}
else
ec_neg1( semctl(semid, 0, IPC.RMID) )
}
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Теперь можно вернуться к System V и рассмотреть системный вызов semop, который
как раз и отвечает за выполнение операций sempost и semwait:
semop — управляет набором
«include <sys/sem.h>
int semop(
int semid,
struct sembuf *sops,
size_t nsops
);
/* Возвращает О в случае
в переменной errno) */
/•
/•
/•
семафоров
идентификатор */
операции */
количество операций */
успеха, -1 в случае ошибки
(код ошибки -
struct sembuf — структура для semop
struct sembuf {
unsigned short sem_num;
short sem_op;
short sem.flg;
};
/* номер семафора */
/* операция над семафором */
/« флаги операции */
458 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Как я уже говорил, semop может воздействовать на целую группу семафоров и даже
на весь набор целиком. Перед выполнением вызова необходимо создать и
заполнить массив операций (где каждый элемент массива — это struct sembuf). Если нужно
выполнить операцию над одним семафором, достаточно создать всего одну
структуру sembuf и передать в вызов указатель на нее; аргумент nsops при этом должен
содержать число 1.
Каждая из операций может быть положительным числом, отрицательным числом
или нулем:
> 0 Значение sem_op добавляется к значению семафора (semposi).
< 0 Если абсолютное значение семафора меньше величины sem_op процесс
блокируется в ожидании, пока они, по крайней мере, не сравняются
(semwaif). В противном случае из значения семафора вычитается
абсолютное значение sem_op.
О Системный вызов блокируется до тех пор, пока значение семафора
не станет равным нулю.
Все операции, переданные semop, выполняются атомарно, а управление в
вызывающую программу возвращается только после выполнения всех операций. (Вызов
может быть прерван в результате принудительного завершения потока, по сигналу
или в результате удаления набора семафоров.)
Блокировку процесса можно предотвратить установкой флага IPC_N0WAIT в поле
sem_flg. Если выполнение какой-либо операции потребует блокировки, управление
в вызывающую программу будет возвращено немедленно. В этом случае ни одна
из операций выполняться не будет, потому что semop гарантирует атомарность всей
последовательности операций, то есть либо выполняются все операции, либо не
выполняется ни одна.
Существует еще одна особенность: для каждого семафора, значение которого
увеличивается или уменьшается процессом, вместе с фактическим значением
сохраняется величина корректировки. Так, если операции увеличения семафора
передается флаг ipc_undo, величина корректировки уменьшается, если семафор
уменьшается — величина корректировки увеличивается. По завершении процесса величина
корректировки прибавляется к текущему значению семафора, отменяя все
операции, выполненные процессом. Например, пусть процесс уменьшает значение
семафора {semwait) чтобы получить доступ-к буферу и увеличивает его (sempost), по
завершении работы с буфером. Тогда установленный флаг IPC.UND0 для всех
операций гарантирует разблокирование буфера для других процессов в случае
аварийного завершения приложения.
При реализации упрощенного интерфейса мы будем предусматривать выполнение
всего одной операции за одно обращение к системному вызову, увеличение и
уменьшение будет производиться только на 1, и мы не будем использовать флаги IPC_N0WAIT
и IPC.UNDO, что позволит значительно упростить функции SimpleSemWait и SimpleSemPost:
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 459
SimpleSemWait — уменьшает значение семафора
«include "SimpleSem.h"
bccl SlmpleSemWait(
struct SimpleSem «sem /* семафср */
);
/* В случае успеха всзвращает true, в случае сшибки ■
в переменней еггпс) */
,
- false (кед сшибки -
SimpleSemPost — увеличивает значение семафора
«Include "SimpleSem.h"
bccl SimpleSemPcst(
struct SimpleSem «sem /* семафср */
);
/* В случае успеха всзвращает true, в случае сшибки -
в переменней еггпс) •/
false (кед сшибки -
ИСХОДНЫЙ КОД функций:
bccl SlmpleSemWalt(struct SimpleSem *sem)
{
struct sembuf scp;
scp.sem_num = 0;
scp.sem_cp = -1;
scp.sem_flg = 0;
ec_neg1( semcp(sem->sm.sm_semid, 4scp, 1) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
bccl SlmpleSemPcst(struct SimpleSem «sem)
{
struct sembuf scp;
scp.semjium = 0;
scp.sem_cp = 1;
scp.sem.flg = 0;
ec_neg1( semcp(sem->sm.sm_semid, &scp, 1) )
return true;
EC_CLEANUP_BGN
return false;
460 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
EC_CLEANUP_END
>
Прежде чем мы двинемся дальше, я хотел бы упомянуть еще об одной области
применения семафоров: с их помощью можно обмениваться целыми числами между
процессами. Предположим, что клиент хочет передать серверу свой
идентификатор процесса. Он получает доступ к семафору и устанавливает его значение в
соответствии со своим идентификатором. После этого сервер может получить
идентификатор клиента, узнав значение семафора. Поскольку наборы семафоров
можно представить как массив, вы получаете возможность обмениваться массивами
целых чисел между процессами.
7.10 Семафоры POSIX
Эти семафоры являются частью стандарта POSIX, но, поскольку они не являются
обязательными к реализации, вы должны проверить их наличие с помощью
макроса _P0SIX_SEMAPH0RES (см. разделы 1.5.4 и 7.6.3). На сегодняшний день они не
полностью реализованы в FreeBSD, Darwin и в Linux.
7.10.1 Именованные семафоры POSIX
Семафоры POSIX намного проще семафоров System V. Пять системных вызовов,
предназначенных для работы с семафорами POSIX, практически в точности
соответствуют функциям упрощенного интерфейса для работы с семафорами из
предыдущего раздела:
sem_open — открывает именованный семафор
«include <semaphore.h>
sem_t *sem_open(
const char «name, /* имя POSIX IPC */
int flags /* флаги (за исключением 0_CREAT) */
);
/* Возвращает указатель на семафор или SEM_FAILED - в случае ошибки (код
ошибки - в переменной errno) */
sem_t *sem_open(
const char «name, /* имя POSIX IPC */
int flags, /* флаги (включая 0_CREAT) «/
mode_t perms, /* права доступа */
unsigned value /* начальное значение */
);
/* Возвращает указатель на семафор или SEM_FAILED - в случае ошибки (код
ошибки - в переменной errno) */
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 461
sem_close — закрывает именованный семафор
«include <semaphcre.h>
int sera_clcse(
sera_t »sera /» семафср »/
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) »/
sem_unlink — удаляет именованный семафор
«include <semaphcre.h>
int sera_unlink(
censt char »narae /»
);
/* Возвращает О в случае
в переменней еггпс) •/
имя P0SIX
успеха,
IPC
-1 в
•/
случае сшибки
(кед
ошибки -
sem_wait — уменьшает значение семафора
«include <semaphcre.h>
int sem_wait(
sera_t *sera /* семафср »/
);
/* Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки
в переменней еггпс) */
sem_post — увеличивает значение семафора
«include <semaphcre.h>
int sera_pcst(
sem_t *sera/* семафср »/
);
/» Возвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней errno) »/
Семафоры POSIX, как и семафоры System V, являются обычными счетчиками, но
увеличиваться или уменьшаться могут только на 1. Каждый объект sem_t
соответствует только одному семафору, а не целому набору, как в System V.
Системные вызовы открывающие, закрывающие и удаляющие семафоры следуют
обычному для POSIX IPC шаблону оформления системных вызовов. В частности,
имя семафора должно соответствовать правилам, которые обсуждались в разделе
7.6.2. Как вы наверняка догадались, вызов sem_post увеличивает значение семафора
на единицу, a sem_wait — уменьшает, блокируя процесс, если к моменту вызова
значение семафора было равно нулю.
462 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Нет необходимости указывать флаги 0_RD0NLY, 0_WR0NLY или o_RDWR при открытии
семафора вызовом sem_open, так как они не имеют смысла — какой толк от
семафора, если нельзя использовать sem_wait или sem_post?
Здесь отсутствуют проблемы, связанные с инициализаций семафоров. Мы просто
передаем начальное значение (чаще всего 0) вызову sem_open.
Проявляйте особую осторожность при анализе возвращаемого значения sem_open:
значение SEM_FAILED — это не NULL, которое обычно возвращается подобными
функциями в случае ошибки.
Реализация функций серии SimpleSem для работы с семафорами POSIX стала
значительно проще:
struot SimpleSem *SimpleSemOpen(oonst ohar «name)
{
struot SimpleSem *sem = NULL;
eo_null( sem = malloo(sizeof(struot SimpleSem)) )
if ((sem->sm.sm_sem = sem_open(name, 0_CREAT, PERM_FILE, 0)) ==
SEM_FAILED)
EC.FAIL
return sem;
EC_CLEANUP_BGN
free(sem);
return NULL;
EC_CLEANUP_END
>
bool SimpleSemClose(struot SimpleSem «sem)
{
eo_neg1( sem_olose(sem->sm.sm_sem) )
free(sem);
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool SimpleSemRemove(oonst ohar «name)
{
if (unlink(name) == -1 && errno != ENOENT)
EC.FAIL
return true;
EC_CLEANUP_BGN
return false;
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 463
EC_CLEANUP_END
}
bool SlmpleSemPost(struct SimpleSem »sem)
{
eo_neg1( sem_post(sem->sm.sm_sem) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
bool SlmpleSemWait(struct SimpleSem »sem)
{
ec_neg1( sem_wait(sem->sm.sm_sem) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Семафоры POSIX имеют ряд дополнительных особенностей, которые остались не
востребованными в интерфейсе SimpleSem. Прежде всего, вы можете узнать
текущее значение семафора:
sem_getvalue — возвращает значение
«include Semaphore
int sem_getvalue(
sem_t «restrict
int *valuep
);
/* Возвращает О в ci
в переменной errno)
h>
sem,
1учае
V
/•
/*
семафор
:семафора
•/
возвращаемое значение */
успеха, -1
в случае ошибки
(код
ошибки -
Если к моменту вызова sem_getvalue значение семафора было больше нуля,
возвращается значение семафора. Однако к моменту выхода из sem.getvalue оно может
измениться, таким образом, в этом значении мало пользы. Если значение
семафора равно нулю, возвращается отрицательное число, по абсолютному значению
равное количеству процессов, ожидающих на семафоре. Если таковых процессов не
имеется, возвращается ноль.
Существуют дополнительные системные вызовы, являющиеся вариациями на тему
sem_wait. Первый — sem_trywait:
464
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
sem_trywait — уменьшает
«include <semaphcre.h>
lnt sem_trywalt(
sem_t «sera /• семафор
);
/» Возвращает 0 в случае
в переменней errnc) */
значение семафора, если
*/
успеха,
-1 в случае сшибки
это
(кед
возможно
сшибки -
Если семафор уже равен нулю, вызов sem.trywalt возвращает признак ошибки с кодом
EAGAIN. (Другие системные вызовы, для работы в неблокирующем режиме, должны
использовать флаг 0_N0NBL00K или IP0.N0WAIT, здесь эта возможность вынесена в
отдельный системный вызов.)
Второй системный вызов, sem.tlmedwait, возвращающий управление через заданный
интервал времени, если значение семафора так и не стало больше нуля:
sem_timedwait — уменьшает значение семафора
«Include <semaphcre. h>
«Include <tlme.h>
lnt sem_tlmedwait(
sera_t «restrict sera, /*
censt struct tiraespec «time /•
);
/« Возвращает О в случае успеха, -
в переменней errnc) «/
семафор •/
абсолютное время
в случае сшибки
*/
(кед сшибки -
Время, передаваемое в sem_timedwait, должно быть абсолютным (например 13:23:17),
а не интервалом времени (например 27 секунд). Какие часы используются и с
какой точностью, зависит от того, поддерживаются ли таймеры POSIX. Если да, то
используются часы реального времени. Если нет — обычные системные часы.
(Обсуждение типа struct tiraespec см. в разделе 1.7.)
7.10.2 Неименованные семафоры POSIX
Краткий обзор: системные вызовы sera_open, sera_close и sera_unlink используются для
работы с именованными семафорами, существующими за пределами приложений
и доступными по их именам POSIX IPC. Вызов sera_open возвращает указатель на объект
типа sera_t, с которым вам едва ли придется работать напрямую. Память,
занимаемая этим объектом, освобождается вызовом sem_close. Семафоры POSIX
используются для разграничения доступа между потоками и приложениями.
Для ускорения работы с семафорами можно напрямую объявить объект sem_t в
программе:
sera.t sera;
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 465
или даже разместить в динамической памяти:
sem_t *semp = inalloo(sizeof(sem_t));
Но если объект размещается таким образом, необходимо инициализировать его
вызовом sem_init:
seminit — инициализирует неименованный семафор
«inolude <semaphore.h>
int sem_init(
sem_t «sent, /* оемафор */
int pshared, /* доотупен другим процеооам? */
unsigned value /* начальное значение */
);
/* Возвращает 0 в олучае уопеха, -1 в олучае ошибки (код ошибки -
в переменной еггпо) */
Для разрушения неименованного семафора следует использовать системный
вызов sem.destroy:
sem_destroy — разрушает
«include Semaphore.h>
int sem_destroy(
sem_t «sent /* оемафог
);
/* Возвращает 0 в олучае
в переменной еггпо) */
неименованный семафор
V
уопеха,
-1 в олучае
ошибки
(код
ошибки -
Вызов sem.destroy не освобождает память, занимаемую объектом sem_t, так как не
знает, каким способом была выделена эта память (malloc, статическое объявление
или что-то другое). Вы должны освободить память (если это необходимо)
самостоятельно. Разумеется, если семафор был размещен в глобальной памяти или в стеке,
от вас не требуется выполнять никаких дополнительных действий — память будет
освобождена автоматически, когда подойдет время.
Вызовы sem_init и sem.destroy являются заменителями sem.open и sem_olose. Вы используете
ту или иную пару, в зависимости от того, именованный у вас семафор или
неименованный. Для остальных системных вызовов (например sem_post или sem_timedwait) нет
никакой разницы, каким образом был получен объект sem_t.
Хорошо, неименованные семафоры работают быстрее, но ведь они находятся в
памяти процесса, и что это нам дает? А вот что:
• они прекрасно работают с потоками, особенно когда вам нужен именно
семафор со счетчиком, а не простой мьютекс, имеющий всего два состояния;
• они могут работать и с процессами, если находятся в разделяемой памяти, о
которой мы вскоре будем говорить. В этом случае аргумент pshared, вызова sem_init
должен иметь ненулевое значение.
466 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Основная область применений неименованных семафоров — работа с потоками,
особенно в Linux и FreeBSD (но не в Darwin), даже не смотря на то, что в этих ОС
семафоры POSIX реализованы не полностью. Эти системы поддерживают sem.init
(аргумент pshared может быть только нулем) и sem.destroy, но не поддерживают sem.open
и sem_close. Также этими системами поддерживаются sem.post, sem.wait и sem_trywait,
но не поддерживается sem.timedwait.
Главное правило при работе с неименованными семафорами: нельзя пользоваться
копиями объектов типа sem.t, так, следующий отрывок программы написан
неправильно:
void fcn(sem_t s)
{
sem_post(&s);
}
void fon2(void)
{
sem_t sem;
sem_init(&sem);
fen(sem);
}
При вызове fen будет создана копия объекта sem_t, что нарушает указанное
правило. Все части программы должны работать с фактическим объектом, указатель на
который передавался в sem_init. Мы рассмотрим пример работы с
неименованными семафорами, размещенными в общей памяти, в разделе 7.14.2.
7.10.3 Достоинства и недостатки семафоров POSIX
и System V
С точки зрения организации взаимодействий между процессами, семафоры System V
чрезвычайно сложны в использовании.и перегружены различными мелкими
особенностями, но они доступны практически во всех системах и выглядят не так плохо,
если некоторые моменты (в основном инициализация) спрятаны за удобным
интерфейсом, таким как SimpleSem. Семафоры POSIX намного проще в
использовании, но они доступны не везде. Если для вас существенное значение имеет
вопрос переносимости приложений, используйте семафоры System V. Если проблема
переносимости вас не волнует и ваша UNIX-система поддерживает семафоры POSIX,
безусловно, следует использовать их."
' Будьте крайне осторожны, заявляя, что вас не волнует проблема переносимости. В наши
дни практически каждое приложение является потенциальным кандидатом на то, чтобы быть
перенесенным под Linux. Даже если этого никогда не случится, аргумент переносимости
может оказаться немаловажным на переговорах с торговым агентом.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 467
С точки зрения организации взаимодействий внутри процессов, то есть между
потоками, семафоры System V слишком тяжеловесны и медлительны, а
неименованные семафоры POSIX, используемые для организации взаимодействий внутри
процессов, доступны в большинстве систем, где имеется реализация потоков POSIX.
Таким образом, если вам нужно нечто большее, чем простые мьютексы, используйте
семафоры POSIX.
7.10.4 Мьютексы, разделяемые между процессами
и блокировки чтения-записи
Я уже рассказывал о мьютексах в разделе 5.17.3, мы использовали их для
синхронизации потоков, но они находились в памяти процесса — по сути, это
неименованные семафоры POSIX, которые могут принимать только два значения.
Аналогично семафорам вы можете создать мьютекс в памяти и инициализировать его
вызовом pthread_mutex_init с атрибутом PTHREAD_PROCESS_SHARED. В этом случае он
может использоваться разными потоками из разных процессов. Однако такая
возможность поддерживается не всеми системами.
Существуют также блокировки чтения-записи POSIX, о которых я вообще ничего не
говорил в главе 5. Они очень похожи на мьютексы, но при этом существует разница
между получением блокировки на чтение и на запись (совместный и
исключительный доступ соответственно), что позволяет увеличить производительность. Как и в
случае с мьютексами, вы можете установить для них атрибут PTHREAD_PROCESS_SHARED.
7.11 Блокировка файлов
В этом разделе мы поговорим о том, как выполняется блокировка файлов в UNIX и
почему порой она не работает так, как нам надо.
7.11.1 Неудачный пример
Все мы иногда ошибаемся, и наши ошибки нередко подталкивают нас к поискам
правильного пути.
Чтобы как-то начать обсуждение блокировок файлов, я предлагаю рассмотреть
небольшой пример, в котором один процесс собирает данные в файл, а другой —
читает его. Файл представляет собой связанный список записей. Каждая запись
содержит два поля: первое — целое число, второе — смещение до начала
следующей записи. Связанный список отсортирован в порядке возрастания, но
физически записи располагаются в файле в порядке создания. Каждая запись
представлена в виде структуры:
struct rec {
int r_data;
off_t r_next;
>;
468 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Если мы будем вносить записи в файл в таком порядке: 1000, 999, 998, файл будет
выглядеть, как показано на рис. 7.2. Первая запись в файле — это просто
заголовок, который указывает, где находится начало списка.
0
1000
999
998
0
-—
,
Рис. 7.2. Связанный список записей
Функция main запускает дочерний процесс (prccessl), который будет строить
список, в то время как родительский процесс (prccess2) будет периодически
выполнять проверку корректности заполнения файла:
int main(vcid)
{
pi<J_t pid;
ec.negK pid = fcrk() )
if (pid == 0)
prccess1();
else {
prccess2();
ec_neg1( waitpid(pid, NULL, 0) )
}
exit(EXIT_SUCCESS);
EC.CLEANUP.BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
В первую запись заносится число 1000, во вторую — 999, в третью — 998, и т. д.:
«define OBNAME "terradb"
static vcid precess1(vcid)
{
int dbfd, data;
struct rec r;
ec_neg1( dbfd = cpen(OBNAME, 0_CREAT | O.TRUNC | O.RDWR, PERH_FILE) )
memset(&r, 0, sizecf(r));
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 469
ec_false( writerec(dbfd, &r, 0) )
fcr (data = 1000; data >= 0; data-)
ec_false( stcre(dbfd, data) )
ec_neg1( clcse(dbfd) )
exit(EXIT.SUCCESS);
EC.CLEANUP.BGN
exit(EXIT.FAILURE);
EC.CLEANUP.END
}
Чтобы упростить операции ввода-вывода, предусмотрены две функции: writerec,
которая заносит запись в заданную позицию в файле, и readrec, которая читает запись
из указанной позиции. Они обе рассматривают не полностью выполненные
операции ввода-вывода и получение признака конца файла как ошибку:
bccl readrecdnt dbfd, struct rec »r, cff_t cff)
{
ssize_t nread;
if ((nread = pread(dbfd, r, sizecf(struct rec), cff)) ==
sizecf(struct rec))
return true;
if (nread != -1)
errnc = ЕЮ;
EC_FAIL
return true;
EC.CLEANUP.BGN
return false;
EC.CLEANUP.END
}
bccl writerec(int dbfd, struct rec *r, cff.t cff)
{
ssize.t nwrcte;
if ((nwrcte = pwrite(dbfd, r, sizecf(struct rec), cff)) ==
sizecf(struct rec))
return true;
if (nwrote != -1)
errno = ЕЮ;
EC.FAIL
return true;
EC.CLEANUP.BGN
return false;
470 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
EC_CLEANUP_END
}
Естественно, можно было бы вызывать pwrite и pread напрямую, но так как для нас
важна точность исполнения операций, определенно имеет смысл добавить проверку
наличия ошибок.
Функция stcre (вызывается из prccessl) отвечает за добавление новой записи в файл,
не нарушая порядок сортировки в списке. Обратите внимание: она даже не
пытается хоть как-то минимизировать количество операций ввода-вывода.
bcol stcre(int dbfd, int data)
{
struct rec r, rnew;
off_t end, prev;
ec_neg1( end = lseek(dbfd, 0, SEEK.END) )
prev = 0;
ec_false( readrec(dbfd, 4r, prev) )
while (r.r_next l= 0) {
ec_false( readrec(dbfd, &r, r.r_next) )
If (r.r.data > data)
break;
prev = r.r_next;
}
ec_false( readrec(dbfd, &r, prev) )
rnew.r_next = r.r_next;
r.r_next = end;
ec_false( writerec(dbfd, 4r, prev) )
rnew.r_data = data;
usleep(1); /* дать другим псрабстать */
ec_false( writerec(dbfd, &rnew, end) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Прочитайте листинг внимательно, и вам все станет понятно. Обратите внимание:
при добавлении новой записи корректно обрабатываются случаи вставки как в
начало, так и в конец списка. Обращение к usleep (задержка на 1 микросекунду)
ближе к концу функции освобождает CPU, чтобы дать возможность другим процессам
поработать. Такой прием характерен для больших и сложных программ, в данном
же случае я просто хотел дать возможность поработать родительскому процессу,
который ведет проверку файла, параллельно дочернему процессу.
Исходный код функции prccess2, которая проверяет целостность файла:
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 471
static vcid prccess2(vcid)
int try, dbfd;
struct rec r1, r2;
fcr (try = 0; try < 10; try++)
if ((dbfd = cpen(DBNAME, 0_RDWR)) == -1) {
if (errnc == ENOENT) {
continue;
}
else
EC.FAIL
}
ec_neg1( dbfd )
fcr (try = 0; try < 100; try++) {
ec_false( readrec(dbfd, &П, 0) )
while (r1.r_next != 0) {
ec_false( readrec(dbfd, &r2, r1.r_next) )
if (r1.r_data > r2.r_data) {
printf("06Hapy»<eHa сшибка ссртирсвки (попытка: Xd)\n", try);
break;
}
r1 = r2;
}
}
ec_neg1( clcse(dbfd) )
return;
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Функция производит несколько попыток открыть файл с записями просто потому,
что дочернему процессу может потребоваться некоторое время на завершение своего
вызова open. Затем выполняется 100 попыток пройти по списку, в поисках таких
ошибок, как отсутствующая запись или нарушение порядка сортировки.
Уверенный в правильности своей программы, я, тем не менее, получил сообщение
об ошибке:
ERROR: 0: prccess2 [/aup/c7/f1.c: 107] readrec(dbfd, &r2, r1.r_next)
1: readrec [/aup/c7/f1.c:17] 0
»»* ЕЮ (5: "1/0 error") ***
Эта ошибка появилась из-за того, что функция store успела поправить ссылку на
новую запись, но не успела дописать ее в конец файла. Родительский процесс
обнаружил отсутствие записи и завершился с ошибкой.
16 3ак. 4030
472 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
В результате наших усилий мы получили неработающее приложение. Но нам хотя
бы известна природа ошибки — одновременный доступ к файлу из двух
процессов. Остается только попробовать скоординировать действия процессов.
Итак, вы готовы разобраться с этой проблемой?
7.11.2 Блокировка доступа к файлу с помощью семафоров
Самый очевидный способ исправить ошибку в предыдущем разделе — воспользоваться
семафором, чтобы не дать родительскому процессу выполнять поиск по файлу до
полного завершения операции добавления новой записи. С помощью упрощенного
интерфейса SimpleSem (см. раздел 7.9 2) мы можем добавить глобальное объявление:
static struct SimpleSem *sem;
После этого независимо друг от друга каждый из процессов может открыть
семафор:
ec_null( sem = SimpleSemOpen("sem") )
Кроме того, дочерний процесс (processi) должен инициировать семафор, сообщив,
что файл находится в непротиворечивом состоянии:
ec_false( SimpleSeraPost(sem) )
Критическая секция кода в функции store, которая выполняет вставку новой
записи в файл:
ec_false( SimpleSemWait(sem) )
ec_false( readrec(dbfd, &r, prev) )
rnew.r_next = r. r_next;
r. r_next = end;
ec_false( writerec(dbfd, &r, prev) )
rnew. r_data = data;
usleep(1); /* дать другим поработать */
ec_false( writerec(dbfd, &rnew, end) )
ec_false( SimpleSemPost(sem) )
Критическая секция кода в родительском процессе, которая выполняет поиск ошибок
в файле:
for (try = 0; try < 100; try++) {
ec_false( SimpleSemWait(sem) )
ec_false( readrec(dbfd, &П, 0) )
while (r1.r_next != 0) {
ec_false( readrec(dbfd, 4r2, r1.r_next) )
if (r1.r_data > r2.r_data) {
printf("Обнаружена ошибка сортировки (попытка: Xd)\n", try);
break;
}
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 473
П = г2;
}
ec_false( SimpleSemPost(sem) )
}
После внесения этих изменений приложение работает без нареканий (за
исключением FreeBSD и Darwin, в которых семафоры System V реализованы
недостаточно корректно, см. раздел 7.9.1).
Основная проблема с семафорами состоит в том, что не совсем понятно, как
выбрать имя семафора для произвольного файла. Это не является проблемой для
приложений, работающих с небольшим числом файлов с фиксированными именами,
но, если имена файлов и их количество заранее не известно, работа с семафорами
станет очень затруднительной. В этом случае нам пришлось бы изобрести
инструмент отображения имен файлов в имена семафоров. Кроме того, иногда требуется
заблокировать только часть файла, а это уже означает создание различных
семафоров для различных участков файла. А процесс управления семафорами —
открытие, закрытие, удаление — это же просто лишняя головная боль.
7.11.3 Системный вызов lockf
Проблема блокировки файлов на самом деле не так сложна, как кажется,
благодаря наличию специального системного вызова lockf, который может заблокировать
часть файла:
lockf -
- блокирует
«include <unistd
int
);
h
lockf(
int fd, /*
int op, /*
off_t len /♦
Возвращает О
доступ к
h>
определен
дескриптор файла */
операция
•/
размер участка */
в случае
в переменной errno) */
успеха, -1
ному участку ф
в случае
ошибки
айла
(код ошибки -
Дескриптор должен быть открыт на запись (с флагами 0_WR0NLY или 0_RDWR).
Блокируемый участок файла начинается от текущей позиции (установленной
вызовами read, write или lseek) и имеет протяженность len байт в сторону конца файла
или, если len отрицательное число, — в сторону начала файла. Байт, находящийся
в текущей позиции, не входит в блокируемый участок. Когда аргумент len является
положительным числом, он может содержать длину участка, который выходит за
границы файла, в предположении что в ближайшее время размер файла может быть
увеличен. Если аргумент len равен нулю, это означает, что блокируемый участок
простирается до конца файла, даже если после установки блокировки размер файла
474 ГЛАВА 7 улучшенные механизмы взаимодействия процессов
увеличится. Таким образом, чтобы заблокировать файл целиком, нужно установить
текущую позицию в файле в нулевое значение и передать 0 в аргументе 1еп.
Если блокировка устанавливается на перекрывающиеся участки файла, они
сливаются в один. Если с некоторого участка блокировка снимается, заблокированный
участок уменьшается в размерах и может быть преобразован в два
непересекающихся заблокированных участка.
Все блокировки снимаются с файла, когда закрывается любой из дескрипторов
данного файла, даже если дескриптор, который был передан в вызов lockf,
остается открытым.' Из этого следует, что все блокировки снимаются по завершении
процесса, поскольку в этот момент закрываются все дескрипторы. Блокировки не
наследуются дочерними процессами, порожденными вызовом fork.
Список возможных операций, для аргумента ор:
F.L0CK Устанавливает блокировку на часть файла. Вызов блокируется,
если любая часть указанного участка уже заблокирована
другим процессом.
f_tlock Похожа на f_lock, но, если любая часть указанного участка
уже заблокирована другим процессом, возвращает признак
ошибки С КОДОМ EA6AIN (или EACCESS).
f_test He устанавливает блокировку, но, если любая часть указанного
участка уже заблокирована другим процессом, возвращает
признак ошибки с кодом EAGAIN (или EACCESS).
F_UL0CK Снимает блокировку с участка файла.
Теперь вам не нужно беспокоиться по поводу именования семафора или открывать
нечто специфическое — lockf принимает тот же дескриптор, который вы
используете для доступа к файлу. Таким образом, мы легко и просто можем изменить пример
с семафорами из предыдущего раздела. Я покажу только критическую секцию кода
из функции store:
ec_neg1( lseek(dbfd, 0, SEEK_SET) )
ec_neg1( lockf(dbfd, FJ.0CK, 0) )
ec_false( readrec(dbfd, lr, prev) )
mew. r_next = r. r_next;
r. r_next = end;
ec_false( writerec(dbfd, &r, prev) )
rnew. r_data = data;
Из страниц справочного руководства FreeBSD: «Этот интерфейс следует совершенно
бестолковой семантике System V и IEEE Std 1003-1 -1998 (POSIX.1), которая требует, чтобы все
блокировки файла, связанные с данным процессом, снимались при закрытии любого
дескриптора данного файла в этом процессе. Это означает, что приложение должно знать обо
всех операциях над файлом, которые могут выполняться библиотечными
подпрограммами. Например, если приложение для обновления файла паролей устанавливает на него
блокировку и затем считывает из него запись обращением к функции getpwnam(3), то
блокировка будет утеряна, потому что getpwnam(3) открывает, читает и закрывает файл
паролей». BSD-системы имеют в своем распоряжении более удобный вызов — flock, но это
отступление от стандарта.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 475
jsleep(1); /* give up CPU */
ec_faise( writerec(dbfd, &rnew, end) )
ec_neg1( iseek(dbfd, 0, SEEK_SET) )
ec_neg1( icckf(dbfd, FJJLOCK, 0) )
и критическую секцию из функции prccess2:
for (try = 0; try < 100; try++) {
ec_neg1( iseek(dbfd, 0, SEEK_SET) )
ec_neg1( icckf(dbfd, FJ.0CK, 0) )
ec_faise( readrec(dbfd, &rl, 0) )
whiie (r1.r_next l= 0) {
ec_faise( readrec(dbfd, &r2, r1.r_next) )
if (r1.r_data > r2.r_data) {
printf("Обнаружена сшибка ссртирсвки (попытка: Xd)\n", try);
break;
}
П = r2;
}
ec_neg1( iseek(dbfd, 0, SEEK_SET) )
ec_neg1( icckf(dbfd, FJJLOCK, 0) )
}
Если вы предполагаете работать с файлом через стандартные библиотечные
функции (например f read, fwrite), не используйте системный вызов lcckf, вместо
этого пользуйтесь библиотечными функциями, которые принимают объекты типа FILE:
flcckfile, ftrylcckflle и funlcckfile.
7.11.4 Блокирование файлов с помощью системного
вызова fcntl
Файл можно также заблокировать с помощью системного вызова fcntl, который
впервые встретился нам в разделе 3.8.3- Он включает в себя функциональность,
предоставляемую вызовом lcckf (некоторые современные UNIX-системы
реализуют системный вызов lcckf через fcntl).
fcntl — управляет открытым файлом
«inciude <unistd.h>
«inciude <fcnti.h>
int fcnti(
int fd, /* дескриптор файг
int cp, /* операция */
/* необязательные
);
/* В случае успеха возвращает
в случае сшибки (кед сшибки -
а */
аргументы,
зависят от операции */
результат, зависящий от
в переменней errnc) */
операции или -1
476 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Для вызова fontl предусмотрены три операции, связанные с блокированием
файлов. Для них в третьем аргументе передается указатель на структуру:
struct flock
— структура для выполнения
ванием файлов
struct flock {
short
short
off_t
off_t
pid_t
};
l_type;
l_whence;
l_start;
l_len;
l_pid;
/•
/•
/•
/•
/•
/•
тип блокировки: F_
интерпретация поля
смещение от начала
длина блокируемого
операций, связанных с
RDLCK, F_WRLCK,
l.start */
файла */
участка */
F_UNLCK
блокиро-
•/
идентификатор процесса, установившего блокировку */
используется командой F_GETLK */
Три элемента структуры, l_whence, l.start и l.len, однозначно определяют участок
файла, над которым производится операция. Первые два напоминают аргументы
вызова lseek (l.whence может быть SEEK.SET, SEEK.CUR или SEEK.ENO), a l.start может
обозначать абсолютную позицию в файле, смещение относительно текущей
позиции и смещение относительно конца файла. Длина блокируемого участка задается
элементом l.len.
Системный вызов lockf может устанавливать только один тип блокировки, a fcntl
может установить блокировку для чтения или для записи, или по другому —
блокировку для совместного и монопольного использования. Блокировка для совместного
использования препятствует установке блокировки для монопольного
использования. Блокировка для монопольного использования препятствует установке блокировок
любого другого типа. На практике блокировка для совместного использования
устанавливается тогда, когда необходимо только читать данные из файла. Блокировка
для монопольного использования, когда необходимо писать данные в файл.
Тип блокировки определяется элементом l.type, причем значение F.UNLCK
означает снятие блокировки.
Перечень операций вызова fcntl, которые имеют отношение к блокировке файлов:
F_SETLK Выполняет указанную операцию, если это возможно. В
противном случае, возвращает признак ошибки с кодом EAGAIN или
EACCESS, если вызов сделан в неблокирующем режиме.
F.SETLKW Идентична предыдущей. Однако при невозможности
заблокировать файл процесс приостанавливается до тех пор, пока
блокировка не будет получена.
F.GETLK Возвращает сведения о блокировке, на которую указывает
структура, если таковая имеется. Все элементы передаваемой
структуры будут перезаписаны возвращаемыми значениями,
включая идентификатор процесса, который установил
блокировку. Если в данный момент блокировка не установлена, поля
структуры остаются без изменения, за исключением первого,
в котором возвращается значение F.unlck.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 477
Таким образом, fcntl может все то же самое, что и icckf. Кроме того, fcntl может
устанавливать блокировки, допускающие совместное использование файла и
блокировки для монопольного доступа. Дополнительно имеется возможность получить
сведения о существующих блокировках. Некоторые современные UNIX-системы
реализуют системный вызов icckf через fcntl. Также [SUS2002] утверждает, что вы
не должны ставить знак равенства между блокировками, полученными с помощью
lockf и fcntl. To есть, если блокировка была получена с помощью icckf, это не
значит, что вы можете снять ее с помощью fcntl. Фактически fcntl может ничего не
знать о блокировках, установленных с помощью icckf.
Аналогично блокировкам, установленным вызовом icckf, блокировки fcntl будут
сняты при закрытии дескриптора файла или по завершении процесса. Они также
не могут быть унаследованы через системный вызов fcrk.
7.11.5 Блокировки рекомендательного
и обязательного характера
Блокировки, установленные с помощью icckf или fcntl, как правило, оказывают
влияние только на эти системные вызовы и никак не затрагивают сами операции
ввода-вывода. Так, даже если вы установили блокировку, например, с помощью icckf,
другой процесс все равно сможет выполнить запись в файл, если он не
обращается к этому вызову. Такого рода блокировки называются рекомендательными. Они
прекрасно подходят только для сотрудничающих процессов, которые на
добровольной основе обращаются к системным вызовам icckf или fcntl.
Обязательные блокировки предполагают наложение запрета на конфликтующие
операции ввода-вывода. Стандарты POSIX и SUS вообще не оговаривают
возможность существования обязательных блокировок, но и не запрещают их.
В системах, которые поддерживают обязательные блокировки, для их установки вам
не нужно обращаться к системным вызовам icckf или fcntl. Вместо этого вы
должны установить биты прав доступа к файлу следующим образом: установить бит
«изменять идентификатор группы при исполнении» (set-group-ID) и сбросить бит,
выдающий право на исполнение для группы (group-execute). Ниже приводится
пример программы, которая устанавливает рекомендательную или обязательную
блокировку, в зависимости от наличия входного аргумента:
int main(int argc, char »argv[])
{
int fd;
mode_t perms = PERM_FILE;
if (fcrk() == 0) {
sleep(l); /* подождать предка */
ec_neg1( fd = open("tmpfile", 0_WR0NLY | 0_N0NBL0CK) )
ec_neg1( write(fd, "x", 1) )
printf("ncTOMOK выполнил запись в файл\п");
478 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
}
else {
(vold)unllnk("tmpfile");
If (argo == 2)
perms |= S_ISGID; /* обязательная блокировка */
eo_neg1( fd = openf'tmpflle", 0_CREAT | 0_RDWR, perms) )
eo_neg1( lookf(fd, F_L0CK, 0) )
prlntf("npeflOK уотановил блокировку\п");
eo_neg1( walt(NULL) )
}
exlt(EXIT_SUCCESS);
EC_CLEANUP_BGN
exlt(EXIT_FAILURE);
EC_CLEANUP_END
}
Ha Solaris, который поддерживает обязательные блокировки, я получил следующий
результат:
$ lookftest
предок уотановил блокировку
потомок выполнил запиоь в файл
$ lookftest х
предок уотановил блокировку
ERROR: 0: main [/aup/o7/lookftest.o: 11] write(fd, "x", 1)
*** EAGAIN (11: "Resource'temporarily unavailable") ***
$
Если в дочернем процессе убрать флаг 0_N0NBL0CK, он будет заблокирован до тех пор,
пока блокировка не будет снята.
7.11.6 Быстродействующие блокировки для баз данных
Блокировки файлов, предоставляемые системными вызовами lockf и fcntl, и даже
обязательные блокировки не подходят для случаев, когда требуется высокое
быстродействие, например, в системах управления базами данных, потому что они
потребляют значительное количество вычислительных ресурсов и очень сложно
обнаружить тупиковые ситуации в запутанных алгоритмах. Для приложений общего
характера блокировки файлов могут оказаться довольно неплохим инструментом.
Но в системах управления базами данных доступ к информации (а значит и к
файлам) осуществляется через процессы специально выделенные для этих целей что
позволяет организовать их в адресном пространстве процесса или в памяти,
которая используется для совместного доступа несколькими процессамиСУБД. Таким
образом, необходимость пользоваться системными вызовами для установки
блокировок вообще отпадает.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 479
7.12 Об общей памяти
В разделе 5.17 мы уже говорили, что потоки внутри процесса совместно
используют одни и те же статические данные — глобальные переменные и данные со
статическим классом размещения внутри функций. Процессы работают в
изолированных друг от друга адресных пространствах. Даже потомок, запущенный вызовом
fork, без обращения к exec получает всего лишь копию данных своего предка.
Общая память представляет собой область, которая может совместно использоваться
несколькими процессами. Как и в случае с потоками, чтобы упорядочить обмен
данными через разделяемую память, процессы должны пользоваться мьютексами
или семафорами.
Существует две версии общей памяти — System V и POSIX, точно так же как
семафоры и очереди сообщений. В обеих версиях процесс должен «открыть» сегмент
общей памяти и получить указатель на нее, который может свободно использоваться
обычными операторами языков программирования С и C++, не прибегая к
системным вызовам. Как правило, процессы получают разные значения указателей,
имеющие смысл только в пределах конкретного процесса, но они ссылаются на одну
и ту же область физической памяти. Как и прежде, мы начнем обсуждение с
версии System V, а затем перейдем к версии POSIX.
7.13 Общая память System V
К настоящему времени вам должны быть знакомы общие принципы работы с
системными вызовами System V. Как уже говорилось в разделе 7.4, для начала вам
нужен ключ, который вы можете получить, например, с помощью ftok. Затем с
помощью Xget (shmget) получаете идентификатор. Управление объектом производится с
помощью Xctl (shmctl). В случае общей памяти она присоединяется к процессу с
помощью вызова shmat, который возвращает указатель, и отсоединяется вызовом
shmdt по завершении работы с ней.
7.13.1 Системные вызовы для работы с общей
памятью System V
Ниже приводится краткое описание системных вызовов, используемых для
работы с общей памятью System V:
shmget — возвращает идентификатор сегмента общей памяти
«include <sys/shm.h>
int shmget(
key_t key, /* ключ */
size_t size, /* размер сегмента */
int flags /* флаги создания */
);
/* Возвращает идентификатор сегмента или -1 в случае ошибки (код ошибки
в переменной errno) */
480 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
shmctl — управляет сегментом
«include <sys/shm.h>
int shmctl(
int shmid,
int cmd,
struct shmid_ds «data
);
/» Всзвращает О в случае
в переменней errnc) »/
/•
/•
/•
общей памяти
идентификатор »/
ксманда »/
аргументы команды
•/
успеха, -1 в случае сшибки
(кед сшибки -
struct shmid_ds — структура для системного вызова shmctl
struct msqid_ds {
struct ipc_perm shm.
size_t shm_segsz;
pid_t shm_lpid;
pid_t shm_cpid;
shmatt_t shm_nattch;
time_t shm_atime;
time_t shm_dtime;
time_t shra_ctime;
};
.perm
/* права доступа */
/» размер сегмента в байтах »/
/» идентификатор прсцесса, последним »/
/» выполнившего операцию с памятью */
/* идентификатор прсцесса-ссздателя »/
/* текущее количестве подключений */
/» время последнего подключения */
/* время последнего отключения */
/» время последнего изменения этой структуры */
/» вызовем shmctl */
shmat — присоединяет сегмент общей памяти
«include <sys/shm.h>
vcid *shmat(
int shmid,
const vcid «shmaddr
int flags
);
/» Всзвращает указатель
errnc) */
/•
/•
/•
или
идентификатор */
желаемый адрес сегмента
флаги подключения
-1 в случае сшибки
•/
или
NULL
(кед сшибки -
•/
в переменней
shmdt — отсоединяет сегмент общей
«include <sys/shm.h>
int shmdt(
censt vcid «shmaddr /» указатель
);
/* Всзвращает 0 в случае успеха, -1
в переменней errnc) »/
памяти
на сегмент
в случае
•/
сшибки
(кед
сшибки -
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 481
Вызов shraget получает доступ к сегменту общей памяти, при необходимости создавая
его, если заданы флаги IPC.CREAT или IPC.PRIVATE. Аргумент size имеет смысл
только в том случае, если создается новый сегмент. Вновь созданный сегмент памяти
заполняется нулями.
Затем, чтобы получить возможность пользоваться сегментом памяти, нужно
обратиться к вызову shinat, который вернет указатель. Довольно странно, но в случае
ошибки возвращается значение -1, а не пустой указатель. Хотя надо признать, что
shinat никогда не вернет указатель, равный признаку ошибки, то есть -1.
Фактически указатели — обычные числа во всех UNIX-системах.
По завершении работы с сегментом он должен быть отсоединен системным
вызовом shindt, которому передается указатель, полученный от вызова shinat. Сегмент
памяти продолжает существовать в системе до тех пор, пока явно не будет удален
вызовом shmctl или утилитой ipcrm, или пока не произойдет перезагрузка системы.
Таким образом, после операции отсоединения вы снова можете присоединить
сегмент. Хотя в этом случае наверняка получите другой указатель.
Как правило, нас не волнует, какой адрес вернет shinat, поэтому во втором аргументе
можно передать значение NULL. Но, если хотите, можете попробовать запросить
подключение сегмента с адреса, заданного в аргументе shmaddr. Если ваше
требование не может быть удовлетворено, например, потому что этот адрес уже
используется процессом или по какой-нибудь другой причине, возвращается признак ошибки
с кодом EINVAL. Можно также указать флаг SHM_RND, чтобы округлить значение
возвращаемого указателя до ближайшей границы, однако я не буду погружаться в
детали. Если установлен другой флаг, SHM_RDONLY, будет возвращен адрес сегмента,
доступный только для чтения.
Вызов shmctl принимает те же команды, что и остальные механизмы System V: IPC.STAT,
IPC.SET и IPC.RMID. Команда IPC_SET устанавливает идентификаторы пользователя и
группы, и младшие 9 бит набора прав доступа.
Ниже приводится пример использования небольшого сегмента общей памяти
двумя процессами:
static int «getaddr(void)
{
key_t key;
int shmid, *p;
(void)olose(open("shmseg", O.WRONLY | O.CREAT, 0));
ec_neg1( key = ftok("shmseg", 1) )
ec_neg1( shmid = shmget(key, sizeof(int), IPC_CREAT | PERM_FILE) )
ec_neg1( p = shmat(shmid, NULL, 0) )
return p;
EC_CLEANUP_BGN
return NULL;
482 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
EC_CLEANUP_ENO
}
int main(void)
<
pid_t pid;
if ((pid = forkO) == 0) {
int *p, prev = 0;
eo_nuii( p = getaddr() )
whiie (*p != 99)
if (prev != *p) {
printf("noTOMOK обнаружил чиоло Xd\n", *p);
prev = *p;
}
printf("потомок завершил работу\п");
}
eise {
int *p;
eo_nuii( p = getaddrO )
for (*p =1; *p < 4; (*p)++)
sieep(1);
*p = 99;
}
exit(EXIT.SUCCESS);
EC_Ci.EANUP.BGN
exit(EXIT_FAILURE);
EC_CLEANUP_EN0
}
Каждый из процессов получает указатель на сегмент общей памяти, обратившись
к функции getaddr, один из которых создает сегмент, а второй просто получает к
нему доступ. В результате запуска программы у нас получилось следующее:
$ shmex
потомок обнаружил чиоло 1
потомок обнаружил чиоло 2
потомок обнаружил чиоло 3
$ потомок обнаружил чиоло 99
потомок завершил работу
Вам не кажется странным, что потомок обнаружил число 99 после завершения цикла?
Попробуем прояснить ситуацию. В некоторый момент в строке,-
while (*p |= 99)
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 483
в *р еще содержалось число 3, но к тому моменту, когда потомок добрался до строки:
printf("ncTCMCK обнаружил числе Xd\n", *p);
там уже находилось число 99. (Приглашение к вводу, символ «$», появилось
потому, что предок уже завершил работу. Это не дефект, такое часто происходит в UNIX-
системах.)
Вы удивитесь еще больше, когда вторично запустите программу:
$ shmex
петемск завершил работу
$
Программа перестала работать должным образом! Наша ошибка состоит в том, что
мы не удалили сегмент общей памяти, а просто отсоединили его, оставив там
число 99. На втором запуске процесс-потомок обнаружил это число даже до того, как
предок успел войти в цикл for. Очевидное решение проблемы состоит в том,
чтобы удалить сегмент по завершении работы программы.
7.13.2 Общая память и семафоры
Пример из предыдущего раздела содержит еще одну трудноуловимую ошибку. Как
отмечалось в разделе 5.17.3, вы не должны полагать, что операции с указателем на
память выполняются атомарно. Вообще не следует разделять память между
процессами, не предусмотрев какой-либо механизм, управляющий доступом к ней
(например семафоры). Попробуем исправить эти ошибки (функция getaddr осталась без
изменений):
int main(vcid)
{
pid_t pid;
ec_false( SimpleSemRemcve("shmexsem") )
if ((pid = fcrk()) == 0) {
struct SimpleSem *sem;
int *p, prev = 0, n;
ec_null( sem = SimpleSemOpen("shmexsem") )
ec_null( p = getaddr() )
while (true) {
ec_false( SimpleSemWait(sem) )
n = *p;
ec_false( SimpleSemPcst(sem) )
if (n == 99)
break;
if (prev 1= n) {
printf("ncTCMCK обнаружил числе Jd\n", n);
prev = n;
484 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
>
>
printf("nCTCMCK завершил рабсту\п");
ec_false( SimpleSemClcse(sem) )
>
else {
struct SimpleSem *sem;
int *p, i;
ec_null( sent = SimpleSemOpen("shmexsem") )
ec_null( p = getaddr() )
*P = 0;
ec_false( SlmpleSemPcst(sem) )
for (i = 1; i < 4; 1++) {
ec_false( SimpleSemWait(sem) )
*P = i;
ec_false( SimpleSemPcst(sem) )
sleepd);
>
ec_false( SimpleSemWait(sem) )
*p = 99;
ec_false( SimpleSemPcst(sem) )
ec_false( SimpleSemClcse(sem) )
}
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
>
Теперь мы получили более предсказуемый результат:
$ shmex2
пстсмск обнаружил числе 1
петемск обнаружил числе 2
пстсмск обнаружил числе 3
$ пстсмск завершил работу
shmex2
пстсмск обнаружил числе 1
пстсмск обнаружил числе 2
пстсмск обнаружил числе 3
$ пстсмск завершил работу
Взгляните, какие изменения нам пришлось внести для организации совместной
работы:
• дочерний процесс должен захватить семафор, переписать число из общей
памяти в локальную переменную и освободить семафор. После этого
обрабатывается полученное значение;
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
485
• родительский процесс аналогичным образом использует локальную
переменную в теле цикла for, захватывая семафор только для того, чтобы записать
число в общую память;
• изначально семафор заблокирован (имеет значение 0), благодаря этому предок
благополучно записывает 0 в общую память, после чего освобождает семафор,
обращением к функции SimpleSemPost. Теперь потомок может захватить семафор
и прочитать записанное туда число. Эта версия корректно работает и при
повторном запуске благодаря тому, что общая память инициализируется в самом
начале;
• на каждом запуске, семафор удаляется, чтобы всякий раз в нашем
распоряжении имелся новый семафор с начальным значением 0.
Хотя мы и устранили проблему с атомарностью, тем не менее потомок все еще далек
от совершенства. Взглянем на цикл работы потомка еще раз:
while (true) {
ec_false( SimpleSemWait(sem) )
n = *p;
ec_false( SimpleSemPost(sem) )
if (n == 99)
break;
if (prev != n) {
printf("noTOMOK обнаружил число Xd\n", n);
prev = n;
}
Он «крутит» цикл за циклом, запирает и отпирает семафор и все лишь для того, чтобы
убедиться, что число не изменилось. Большая часть работы выполняется впустую,
и семафор захватывается в большинстве случаев без всякой на то причины.
Можно ли предусмотреть возможность известить потомка об изменении значения
родителем? (Это напоминает переменные состояния, обсуждавшиеся в разделе 5.17.4.)
Проблему можно устранить с помощью второго семафора: семафор Сбудет
управлять доступом к общей памяти на запись, семафор R — на чтение. Родительский
процесс ожидает на семафоре вперед изменением значения в общей памяти и затем
освобождает семафор R. Дочерний процесс ожидает на семафоре R перед чтением
значения из общей памяти и затем освобождает семафор W. Чтобы процесс
обмена начался без лишних осложнений, семафор Одолжен быть инициализирован
числом 1, а семафор R — числом 0. Ниже приводится исправленная версия
программы (функция getaddr осталась без изменений):
int main(void)
{
pid_t pid;
486 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
ec_false( SlmpleSemRemcve("shmexsem") )
if ((pld = fcrkO) == 0) {
struct SlmpleSem «semR, «semW;
int «p, n;
ec_null( semR = SlnpleSemOpenC'shnexsenR") )
ec_null( senW = SinpleSenOpen("shnexsenW") )
ec_null( p = getaddrQ )
while (true) {
ec_false( SinpleSenWait(senR) )
n = »p;
ec_false( SimpleSemPcst(semW) )
If (n = 99)
break;
printfC'ncrcMCK обнаружил числе Xd\n", n);
}
printf("ncTCMCK завершил рабсту\п");
ec_false( SinpleSenClcse(senR) )
ec_false( SinpleSenClcse(senW) )
}
else {
struct SinpleSen «semR, *semW;
int *p, i;
ec_null( semR = SinpleSenOpen("shnexsenR") )
ec_null( semW = SlmpleSemOpenCshmexsemW") )
ec_null( p = getaddr() )
*P = 0;
ec_false( SimpleSemPcst(semW) )
fcr (i = 1; i < 4; i++) {
ec_false( SimpleSemWait(semW) )
*P = i;
ec_false( SimpleSemPcst(semR) )
sleep(1);
}
ec_false( SimpleSemWait(semW) )
*p = 99;
ec_false( SlmpleSemPost(semR) )
ec_false( SimpleSemClcse(semR) )
ec_false( SimpleSemClcse(semW) )
}
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 487
Обратите внимание: дочерний процесс больше не использует переменную prev для того,
чтобы обнаружить изменения, потому что теперь он получит доступ к памяти только
тогда, когда освободится семафор semR. Этот вариант стал намного эффективнее!
7.13.3 Реализация SMI с общей памятью System V
Предлагаю продолжить развитие идей из предыдущего раздела и использовать
общую память и семафоры для реализации функций SMI, который уже «умеет»
работать через именованные каналы (раздел 7.33) и очереди сообщений (разделы
7.5.3 и 7.7.2).
Сервер и каждый из клиентов будут использовать отдельный сегмент общей
памяти для поступающих сообщений и два семафора тем же способом, который был
использован в предыдущем примере. Таким образом, если запущено два клиента,
то в общем у нас получится три сегмента общей памяти и шесть семафоров. При
такой организации у нас не будет очереди сообщений — каждый из сегментов будет
хранить единственное сообщение, новое сообщение не может быть записано, пока
получатель не разберется с прежним сообщением и не освободит семафор записи
(W). Это не лучшее решение, поскольку каждый из клиентов сможет работать со
скоростью, не превышающей скорости сервера, но этого будет вполне
достаточно, чтобы продемонстрировать сам принцип обмена сообщениями через общую
память.
На рис. 7.3 показаны сервер и два клиента. Сегмент общей памяти сервера (mem-server)
используется для совместного доступа всеми тремя процессами. Сегмент mem-1 —
для совместного доступа сервером и первым клиентом, сегмент mem-2 — сервером
и вторым клиентом. Каждый из процессов имеет возможность прочитать
сообщение без его удаления из памяти.
При объяснении реализации SMI через разделяемую память, я будут исходить из
предположения, что вы уже знакомы с реализациями интерфейса через
именованные каналы и очереди сообщений, рассмотренные выше. Поэтому я не буду
возвращаться к описанию деталей, которые я разъяснял ранее.
Рис. 7.3. Сервер, разделяющий память с двумя клиентами
488 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Мы воспользуемся одной из особенностей семафоров System V и создадим один
набор из двух семафоров для каждого сегмента общей памяти. Семафор 0 —
управляет доступом на чтение, семафор 1 — на запись. Ниже приводится ряд
макроопределений, которыми мы будем пользоваться для удобства:
«define SEMI.READ О
«define SEMI_WRITE 1
«define SEMI_P0ST 1
«define SEMI_WAIT -1
Операциями над семафорами будет заниматься функция op.semi, которой будут
передаваться семафор и код операции над ним. Например, чтобы освободить
семафор, управляющий записью:
ec.negK op_semi(semid_receiver, SEMI_WRITE, SEMI.POST) )
Исходный код функции op.semi:
static int op_semi(int semid, int semjium, int sem.op)
{
struct sembuf sbuf;
int r;
sbuf.sem_num = sem_num;
sbuf.sem_op = sem_op;
sbuf.sem_flg = 0;
ec_neg1( r = semop(semid, &sbuf, 1) )
return r;
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_END
>
Предусмотрим вспомогательную функцию для инициализации семафоров:
static int init_semi(int semid)
{
union semun arg;
int r;
arg.val = 0;
semctKsemid, SEMI.WRITE, SETVAL, arg);
semctKsemid, SEMI.READ, SETVAL, arg);
/* Следуощий вызов изменит поле otime, */
/* позволив клиентам продолжить работу. •/
ес_пед1( г = op_serai(semid, SEMI.WRITE, SEMI.POST) )
return r;
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 489
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_END
}
Внутренняя структура данных, которая используется SMI:
typedef struct {
SMIENTITY sq.entity; /* сущность (клиент или сервер) */
int sq_semid_server; /* семафоры сервера */
int sq_semid_client; /* семафоры клиента */
int sq_shmid_server; /* разделяемая память сервера */
int sq_shmid_ciient; /* разделяемая память клиента */
struct smijnsg *msg_server; /* указатель на разд. память сервера */
struct smijnsg »msg_client; /* указатель на разд. память клиента ♦/
char sq_nante[SERVER_NAME_MAX];/* имя сервера */
struct ciient.id sq.ciient; /* идентификационная информ. о клиенте */
} SMIQ.SHM;
Клиент использует почти все элементы структуры, сервер — лишь некоторые. Клиент
хранит в структуре:
• свою сущность (SHI.CLIENT) и имя сервера;
• идентификаторы наборов семафоров сервера и своего собственного (по 2
семафора в каждом наборе);
• идентификаторы сегментов общей памяти сервера и своей собственной.
• указатели (полученные от shmat) на сегменты общей памяти сервера и своей
собственной.
Сервер хранит:
• СВОЮ СУЩНОСТЬ (SHI_SERVER) И ИМЯ;
• идентификатор своего набора семафоров;
• идентификатор своего сегмента общей памяти;
• указатель на свой сегмент общей памяти;
• идентификационные данные клиента, передаваемые функции sml_send_getaddr,
в результате чего они будут доступны в последующем вызове smi_send_reiease,
которая собственно и выполняет передачу сообщения. В поле c_idl структуры
clientid будет храниться идентификатор сегмента общей памяти, а в поле c_id2
— идентификатор набора семафоров.
Сервер не хранит никаких сведений о клиентах, потому что, с одной стороны, их
может быть довольно много, с другой все необходимые сведения клиент
поставляет в сообщении к серверу, подобно тому, как это делалось с очередями сообщений
System V (см. раздел 7.5.3).
Учитывая вышесказанное, исходный код функции smi_open_shm должен быть вам
понятен:
490 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
SMIQ •smi_open_shm(oonst ohar «name, SMIENTITY entity, size_t rnsgsize)
{
SMIQ.SHM *p = NULL;
ohar shmname[FILENAME_MAX];
int i;
key_t key;
eo_nuii( p = oaiiood, sizeof(SMIQ.SHM)) )
p->sq_entity = entity;
if (strien(narne) >= SERVER_NAME_MAX) {
errno = ENAMETQQLQNG;
EC.FAIL
}
stropy(p->sq_name, name);
mkshm_name_server(p, shinnaine, sizeof(shinnaine));
(void)oiose(open(shmname, Q_WRQNLY | Q_CREAT, Q));
eo_neg1( key = ftok(shinnaine, 1) )
if (p->sq_entity == SMI.SERVER) {
if ((p->sq_semid_server = seinget(key, 2, PERM_FILE)) != -1)
(void)shmoti(p->sq_semid_server, IPC_RMID, NULL);
eo_neg1( p->sq_seinid_server = semget(key, 2,
PERM.FILE | IPC.CREAT) )
p->sq_semid_oiient = -1;
if ((p->sq_shinid_server = shmget(key, Q, PERM.FILE)) != -1)
(void)shmotl(p->sq_shmid_server, IPC_RMID, NULL);
eo_neg1( p->sq_shinid_server = shmget(key, insgsize,
PERM.FILE | IPC_CREAT) )
p->sq_shinid_oiient = -1;
eo_neg1( init_semi(p->sq_semid_server) )
}
else {
eo_neg1( p->sq_seinid_server = seinget(key, 2, PERM_FILE) )
eo_neg1( p->sq_seinid_oiient = semget(IPC_PRIVATE, 2,
PERM.FILE | IPC.CREAT) )
eo_neg1( p->sq_shinid_server = shraget(key, insgsize, PERM_FILE) )
eo_neg1( p->sq_shinid_olient = shmget(IPC_PRIVATE, insgsize,
PERM.FILE | IPC.CREAT) )
eo_neg1( p->insg_oiient = shmat(p->sq_shmid_oiient, NULL, Q) )
eo_neg1( init_semi(p->sq_semid_oiient) )
for (i = Q; !smi_oiient_nowait && i < 1Q; i++) {
union semun arg;
struot seinid_ds ds;
arg.buf = &ds;
eo_neg1( seinoti(p->sq_seinid_server, SEMI_WRITE, IPC_STAT,
arg) )
if (ds.sem_otime > Q)
break;
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 491
sleep(1);
}
}
ec_neg1( p->msg_server = shmat(p->sq_shmid_server, NULL, 0) )
return (SHIQ *)p;
EC_CLEANUP_BGN
free(p);
return NULL;
EC_CLEANUP_END
>
Первая часть функции вплоть до вызова ftok практически идентична версии,
написанной для очередей сообщений System V в разделе 7.5.3- Затем сервер удаляет
старый набор семафоров, если таковой существует, и создает новый. Те же
операции выполняются и для сегмента общей памяти. Клиент создает свой набор
семафоров и сегмент общей памяти. И сервер, и клиент присоединяют сегмент общей
памяти сервера.
Обратите внимание на обращение к функции init_semi (исходный код
приводился выше), которая освобождает семафор записи (чтобы разрешить начало
обмена), попутно изменяет время последней операции, которое остальные процессы
воспринимают как сигнал о том, что семафор инициализирован (см. раздел 7.9-1).
Ранее мы уже рассматривали алгоритм ожидания инициализации семафора, здесь
используется более продуманный алгоритм:
• выполняется не более 10 попыток, чтобы не «застрять» здесь, потому что ОС
FreeBSD и Darwin не изменяют значение поля sem_otime;
• внутри реализации сокрыта переменная smi_client_nowait, которая
используется для предотвращения ожидания. Она используется для нужд тестирования, когда
заранее известно, что сервер уже запущен. Мы использовали ее в программе,
которая поставляла данные для таблицы в конце этой главы.
Функция smi_cIose_shm достаточно проста:
bool smi_cIose_shm(SMIQ *sqp)
{
SHIQ.SHH *p = (SMIQ_SHM *)sqp;
if (p->sq_entity == SHI_SERVER) {
char shmname[FILENAME_MAX];
(void)getaddr(-D;
ec_neg1( semctl(p->sq_semid_server, 0, IPC_RHID) )
(void)shmdt(p->msg_server);
(void)shmctI(p->sq_shmid_server, IPC_RHID, NULL);
mkshm_name_server(p, shmname, sizeof(shmname));
(void)unlink(shmname);
}
492 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
else {
ec_neg1( seractl(p->sq_seraid_client, 0, IPC_RMID) )
(vcid)shmdt(p->msg_server);
(vcid)shmdt(p->msg_client);
(void)shractl(p->sq_shraid_client, IPC.RMID, NULL);
}
free(p);
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Теперь рассмотрим функцию smi_send_getaddr_shm:
bccl srai_send_getaddr_shra(SMIQ *sqp, struct client_id «client,
vcid **addr)
{
SMICLSHM -p = (SMIQ.SHM *)sqp;
int seraid_receiver;
if (p->sq_entity == SMI.SERVER) {
seraid_receiver = client->c_id2;
p->sq_client = «client;
}
else
seraid_receiver = p->sq_seraid_server;
ec_neg1( op_semi(semid_receiver, SEMI_WRITE, SEMI_WAIT) )
if (p->sq_entity == SMI_SERVER)
ec_null( *addr = getaddr(client->c_id1) )
else {
«addr = p->rasg_server;
((struct srai_rasg *)*addr)->smi_client.c_id1 = p->sq_shnid_client;
((struct srai_rasg *)*addr)->smi_client.c_id2 = p->sq_seraid_client;
}
return true;
EC_CLEANUP_BGN
return false;
EC.CLEANUP.END
}
При передаче сообщения от сервера клиенту аргумент client должен указывать на
структуру с идентификационными данными клиента. При передаче от клиента
серверу этот аргумент игнорируется. Сначала рассмотрим код, который
исполняет сервер, а затем код, который исполняет клиент.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
493
На стороне сервера переменная semid_receiver получает значение из поля c_id2, затем
запоминается адрес структуры client_id для последующего использования в
smi_send_release_shm. После этого сервер пытается захватить семафор записи
(SEMI_WRITE), а так как он был заранее освобожден в функции semi_init,
исполнение сразу же продолжается дальше. Сообщение находится в сегменте общей
памяти с идентификатором в client->c_id1. Обращением к функции getaddr сервер
получает адрес сегмента в возвращаемом параметре add г. Вскоре мы рассмотрим
исходный код функции getaddr, а пока представляйте ее себе как обращение к
системному вызову shmat.
На стороне клиента работы выполняется немного меньше, так как он уже обладает
всеми сведениями, необходимыми для доступа к общей памяти и семафорам
сервера. Клиент устанавливает значение переменной semid_receiver прямо из
структуры SMIQ_shh, становится на семафор SEMI_WRITE, записывает адрес в возвращаемый
аргумент и заносит в структуру с сообщением свои идентификационные данные
(идентификаторы семафоров и сегмента общей памяти). Благодаря этому сервер
получит информацию об отправителе и сможет ответить ему.
Адрес, возвращаемый функцией smi_send_getaddr_shm, может без ограничений
использоваться вызывающим процессом, кроме того, сегмент общей памяти будет
закрыт для записи любым другим клиентом. (Как я уже отмечал выше, алгоритм
обслуживания входящих сообщений на стороне сервера можно было бы улучшить,
но это усложнит реализацию.)
Отправка сообщения производится функцией smi_send_release_shm:
bool smi_send_reIease_shm(SMIQ *sqp)
{
SMIQ_SHM *p = (SMIQ_SHM *)sqp;
int semid_receiver;
if (p->sq_entity == SMI.SERVER)
semid_receiver = p->sq_client.c_id2;
else
semid_receiver = p->sq_semid_server;
ec_neg1( op_semi(semid_receiver, SEMI_READ, SEMI_P0ST) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
Освободить семафор чтения SEMI_READ — это все, что должна сделать данная
функция, но для этого ей должен быть известен идентификатор семафора. На сервере
он находится в структуре client_id, которая была сохранена в функции smi_send_
getaddr_shm, на клиенте — он становится известен уже на этапе открытия интерфейса
функцией smi_open_shm.
494 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
К этому моменту вы без труда сможете самостоятельно разобраться в исходном коде
функции smi_receive_getaddr_shm:
bocl sml_recelve_getaddr_shm(SMlQ *sqp, veld **addr)
{
SM1Q_SHM *p = (SM1Q.SHM *)sqp;
lnt semld_recelver;
If (p->sq_entlty == SM1_SERVER)
semld.recelver = p->sq_semld_server;
else
semld_recelver = p->sq_semld_client;
ec_neg1( op_seml(semld_recelver, SEM1.READ, SEM1_WA1T) )
If (p->sq_entlty == SM1_SERVER)
•addr = p->msg_server;
else
•addr = p->msg_client;
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
и парной к ней, которая освобождает семафор записи SEMI.WRITE:
bocl smi_receive_release_shm(SMlQ *sqp)
{
SM1Q_SHM *p = (SM1Q.SHM *)sqp;
lnt semld.receiver;
If (p->sq_entity == SM1.SERVER)
semid_recelver = p->sq_semid_server;
else
semid_receiver = p->sq_semid_client;
ec_neg1( op_semi(semid_receiver, SEM1_WR1TE, SEMl.POST) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Вот и все! Много возни вокруг семафоров и совсем чуть-чуть — вокруг
разделяемой памяти. Если вам предстоит обмениваться сообщениями гигантского размера
(например по 100 000 байт), то это огромное достижение. Очереди сообщений
(неважно, System V или POSIX) скорее всего не смогут принять такие сообщения,
а даже если бы и могли, накладные расходы на копирование данных в простран-
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 495
ство ядра и обратно свели бы на нет все имеющиеся преимущества. В
противоположность им быстродействие SMI с общей памятью никак не зависит от размера
сообщения.
7.13.4 Достоинства и недостатки общей
памяти System V
Недостатков собственно не так много. Системные вызовы достаточно просты в
понимании, эффективны и, как правило, неплохо реализованы. Самый сложный
момент — необходимость синхронизации (по той же самой причине, что и в
случае с потоками). Программы могут работать с разделяемыми ресурсами очень
быстро, но неправильно, и порой очень трудно проверить правильность
выполняемых действий. Чтобы избежать подобных ошибок, вы должны все тщательно
проверить и лишний раз удостовериться в правильности алгоритма.*
7.14 Общая память POSIX
В этом разделе мы поговорим о разделяемой памяти POSIX и рассмотрим
реализацию SMI на основе разделяемой памяти и семафорах POSIX.
7.14.1 Системные вызовы для работы с общей
памятью POSIX
Созданию и открытию сегментов общей памяти POSIX сопутствуют все те же
неприятности, связанные с именованием, которые были описаны в разделе 7.6.2.
Системный вызов shm.open возвращает дескриптор файла так, как будто был открыт
обычный файл. В действительности сегменты общей памяти POSIX ведут себя
подобно файлам, отображенным в память. После открытия необходимо установить
размер сегмента вызовом ftruncate, с которым мы встречались в разделе 2.17, как
если бы это был обычный файл. Затем сегмент должен быть отображен в память
процесса вызовом mmap, который используется для отображения файлов в память
вообще, а не только сегментов общей памяти. Другими словами, прямое
отношение к общей памяти POSIX имеют только вызовы shm.open и shm_unlink. Все
остальные операции выполняются системными вызовами, которые используются для
работы с обычными файлами.
Ниже приводится краткое описание системных вызовов имеющих непосредственное
отношение к общей памяти:
Лет 30 тому назад один мой коллега заявил, что, если к программе не предъявляются
требования правильности, он сможет написать ее очень быстро — программа просто будет
выводить число 0 и больше ничего. Таким образом, если вы не можете гарантировать
корректную работу с общей памятью или с потоками — не используйте их. Медленный, но
правильный алгоритм куда эффективнее.
496 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
shm_open — открывает объект разделяемой памяти
«include <sys/mraan,h>
int shm_cpen(
const char «name, /• имя POSIX IPC •/
int flags, /* флаги */
mcde_t perms /• права доступа •/
);
/* Возвращает файловый дескриптор или -1 в случае сшибки (кед сшибки -
в переменней errnc) */
shmunlink — удаляет объект разделяемой памяти
«include <sys/mman.h>
int shm_unlink(
const char «name /• имя POSIX IPC */
);
/• Возвращает О в случае успеха, -1 в случае сшибки (код сшибки -
в переменней еггпс) */
Системный вызов shm.cpen принимает те же самые флаги, что и другие системные
вызовы, ориентированные на работу с файлами, — O.CREAT, 0.EXCL, 0_ТШС, 0.RD0NLY
и 0.RDWR. Вы не должны использовать флаг O.wronly. В случае создания нового объекта
в третьем аргументе передаются права доступа к объекту.
Как и другие объекты взаимодействия процессов (POSIX или System V), сегмент
общей памяти POSIX остается в системе после его закрытия, по крайней мере, до
перезагрузки системы.
По окончании работы с дескриптором он может быть закрыт обычным системным
вызовом close, как и любой другой файловый дескриптор.
[SUS2002] ничего не говорит о том, могут ли выполняться операции ввода-вывода
над объектом общей памяти с использованием, например, системных вызовов read
и write, однако такая возможность не исключается. Это позволило бы использовать
общую память как обычный файл. Посредством указателя на файл, отображенный
в памяти, программист может обращаться к нему обычными операторами C/C++,
не прибегая к услугам «дорогостоящих» системных вызовов. Однако, если бы
существовала возможность работы с общей памятью, как с обычным файлом, на мой
взгляд, она не осталась бы невостребованной.
Как правило, после создания сегмента общей памяти, производится установка его
размера вызовом ftruncate, поскольку изначально он имеет нулевую длину.
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 497
ftruncate — изменяет размер файла, заданного дескриптором
«include <unistd.h>
int ftruncate(
int fd, /* дескриптор файла */
cff_t length /* нсвый размер файла */
);
I* Всзвращает 0 в случае успеха, -1 в случае сшибки (кед сшибки -
в переменней еггпс) »/
Для отображения сегмента в адресное пространство процесса используется
системный вызов mmap (аналог shmat в System V):
mmap — отображает страницы памяти в
«include <sys/minan
veld *mmap(
vcid *addr,
size_t len,
int prct,
int flags,
int fd,
cff_t eff
);
/* Возвращает у
/•
/•
/•
/•
/•
/•
П>
адресное пространство процесса
желаемый адрес стсбражения или
размер сегмента */
защита сегмента (см.
флаги */
дескриптор файла */
смещение в файле или
казатель на сегмент или
сшибки - в переменней еггпс) */
ниже) */
NULL */
в объекте разделяемой памяти */
HAP_FAILED
в случае сшибки
(кед
С помощью аргумента add r можно указать начальный адрес, в который следует
отобразить сегмент общей памяти, при этом аргумент flags должен содержать флаг
MAP_FIXED. В противном случае этот аргумент должен иметь значение NULL. В наших
примерах мы будем поступать именно таким образом.
Отображаемая часть объекта начинается со смещения off и простирается на len байт,
если не нужно отображать весь объект (размер которого устанавливается вызовом
ftruncate). В наших примерах объекты будут отображаться целиком, поэтому
через аргумент off мы будем передавать число 0, а через аргумент len — размер объекта,
установленный вызовом ftruncate.
Аргумент prct может содержать либо значение PR0T_N0NE, что означает полную
недоступность объекта, либо комбинацию из следующих флагов, объединенных по
ИЛИ:
PR0T_REA0 Объект доступен для чтения.
PROT_write Объект доступен для записи.
PR0T_EXEC Объект доступен для исполнения (не поддерживается
в некоторых реализациях).
3 наших примерах мы будем использовать комбинацию PROT_read|PROT_write. Так-
-;е. в наших примерах мы будем использовать единственный флаг для аргумента
498 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
flags — MAP.SHARED, который означает, что все изменения в сегменте станут
немедленно видны другим процессам. Противоположный по смыслу флаг — MAP.PRIVATE
означает, что все изменения будут производиться только с образом объекта в
адресном пространстве процесса, другим процессам они видны не будут.
Использование флага MAP.PRIVATE для общейй памяти практически не имеет смысла.
Системный вызов mmap, как и shmat, в случае ошибки возвращает не пустой
указатель, а значение MAP.FAILED. При проверке возвращаемого значения вы должны
проверять именно MAP.FAILED, а не NULL.
Все вышесказанное означает: чтобы подготовить сегмент общей памяти для
чтения-записи, мы должны выполнить следующую последовательность:
ec_neg1( ftruncate(fd, len) )
mem = mmap(NULL, len, PROT.READ | PROT.WRITE, MAP.SHARED, fd, 0);
ec_cmp(mem, MAP.FAILED)
По окончании работы с отображенным сегментом разделяемой памяти, его нужно
отключить вызовом munmap:
munmap — отключает отображения
«include <sys/mman.h>
int munmap(
void *addr, /* указатель на
страниц
сегмент
size.t len /* длина сегмента */
),
1* Возвращает 0 в случае успеха
а переменной errno) */
в память
•/
-1 в случае
ошибки
процесса
(код
ошибки -
Вы не обязаны отключать все отображения, которые были выполнены вызовом mmap,
но в наших примерах мы будем отключать их. Для этого в аргументе add r будет
передаваться указатель, полученный от mmap, а в аргументе len — тот же самый
размер, который передавался вызову mmap.
7.14.2 Реализация SMI с разделяемой памятью POSIX
Реализация SMI с общей памятью POSLX очень напоминает реализацию с общей
памятью System V (см. раздел 7.13.3), за исключением семафоров. В этой
реализации вместо обычных семафоров мы будем использовать быстрые неименованные
семафоры POSIX. А чтобы обеспечить к ним доступ из разных процессов,
поместим семафоры в сегмент общей памяти каждого из процессов.
Чтобы у вас сложилось достаточное четкое представление, приведем раскладку
сегмента общей памяти для каждого из процессов:
struct shared.mem {
sem.t sm_sem_w;
sem.t sm_sem_r;
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 499
struct smijnsg smjnsg; /* имеет переменный размер - этот элемент */
/* должен быть последним в структуре */
>;
Блок данных (smi_msg) простирается до конца сегмента. Его размер будет
рассчитываться с помощью макроса:
#define MEM_SIZE(s)\
(sizeof(struct sharedjnem) - sizeof(struct smijnsg) + (s))
где s — третий аргумент функции smi_open_pshm.
Операциями над семафорами будет заниматься функция op.semi, которой будут
передаваться указатель на struct shared_mem и код операции:
«define SEMI_READ О
«define SEMI_WRITE 1
«define SEMI.DESTROY 2
«define SEMI.POST 1
«define SEMI_WAIT -1
static int op_semi(struct shared_mem *m, int sem_num, int sem_op)
{
sem_t *sem_p = NULL;
if (sem.num == SEMI.WRITE)
sem_p = &m->sm_sem_w;
else
sem_p = &m->sm_sem_r;
switch (sem_op) {
case SEMI_WAIT:
ec_neg1( sem_wait(sem_p) )
break;
case SEMI_P0ST;
ec_neg1( sem_post(sem_p) )
break;
case SEMI_DESTROY:
ec_neg1( sem_destrcy(sem_p) )
}
return 0;
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_END
>
Так, если предположить, что переменная т является указателем на сегмент общей
памяти, то мы можем выполнить нечто подобное:
500 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
ec_neg1( op_semi(m, SEMI_READ, SEMI_P0ST) )
ec_neg1( op_senii(ni, SEMI_WRITE, SEMI_WAIT) )
Системные вызовы POSIX не допускают простой передачи идентификатора сегмента
разделяемой памяти внутри сообщения. Поэтому мы будем использовать
методику, использованную при работе с именованными каналами, из раздела 7.3.3 —
клиент передает идентификатор своего процесса, сервер конструирует из него имя
объекта POSIX, после чего открывает объект и отображает его в свое адресное
пространство. Это довольно накладно, а потому сервер будет выполнять данную
последовательность операций только один раз для каждого из клиентов,
запоминая все необходимые данные в своем массиве. Все необходимые сведения будут
храниться В структуре SHIQ_PSHH:
«define MAX_CLIENTS 50
typedef struct {
SMIENTITY sq_entity; /» сущность (сервер или клиент) */
char sq_name[SERVER_NAME_MAX]; /• имя сервера */
int sq_srv_fd; /* дескриптор файла разделяемой памяти сервера */
struct sharedjnem *sq_srv_mem; /* указатель на сегмент памяти сервера */
struct client {
pid_t cl_pid; /* идентификатор процесса клиента */
int cl_fd; /* дескриптор файла разделяемой памяти клиента */
struct sharedjnem «cljnem; /* указатель на сегмент памяти клиента */
} sq_clients[MAX_CLIENTS]; /* клиент использует только [0] */
struct client_id sq_client; /» идентификатор клиента (для сервера) */
size_t sqjnsgsize; /» размер сообщения */
} SMIQ_PSHM;
Сервер может обслуживать до 50 клиентов. Единственное, чего мы не
предусмотрели, — это возможность для клиента сообщить серверу о прекращении работы,
чтобы тот мог повторно использовать элемент массива для работы с другим
клиентом.
Теперь рассмотрим, как функция smi_open_pshm заполняет структуру SHIQ_PSHM:
SMIQ *smi_open_pshm(const char «name, SMIENTITY entity, size_t rasgsize)
{
SMIQ_PSHM .p = NULL;
char shmname[SERVER_NAME_MAX + 50];
ec_null( p = calloc(1, sizeof(SMIQ_PSHM)) )
p->sq_entity = entity;
p->sqjnsgsize = rasgsize;
if (strlen(narae) >= SERVER_NAME_MAX) {
errno = ENAMET00L0NG;
EC.FAIL
}
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 501
strcpy(p->sq_name, name);
mkshm_name_server(p, shmname, sizecf(shmname));
if (p->sq_entity == SMI_SERVER) {
if ((p->sq_srv_fd = shm_cpen(shmname, O.RDWR, PERM.FILE)) l= -1) {
(vcid)shm_unlink(shmname);
(vcid)clcse(p->sq_srv_fd);
}
ec_neg1( p->sq_srv_fd = shm_cpen(shi»name, 0_R0WR | 0_CREAT,
PERM.FILE) )
ec_neg1( ftruncate(p->sq_srv_fd, MEM_SIZE(msgsize)) )
p->sq_srv_mem = mmap(NULL, MEM_SIZE(msgsize),
PROT.REAO | PROT_WRITE, MAP.SHAREO, p->sq_srv_fd, 0);
ec_cmp(p->sq_srv_mem, MAP_FAILEO)
ec_neg1( sem_init(&p->sq_srv_mem->sm_sem_w, true, 1) )
ec_neg1( sem_init(&p->sq_srv_mem->sm_sem_r, true, 0) )
}
else {
ec_neg1( p->sq_srv_fd = shm_cpen(shmname, 0_RDWR, PERM_FILE) )
p->sq_srv_mem = mmap(NULL, MEM_SIZE(msgsize),
PR0T_REA0 | PROT.WRITE, MAP.SHAREO, p->sq_srv_fd, 0);
ec_cmp(p->sq_srv_mem, MAP_FAILEO)
mkshm_name_client(getpid(), shmname, sizeof(shmname));
if ((p->sq_clients[0].cl_fd = shm_cpen(shmname, 0_R0WR, PERM_FILE))
l= -1) {
(void)shm_unlink(shmname);
(vcid)close(p->sq_clients[0].cl_fd);
}
ec_neg1( p->sq_clients[0].cl_fd = shm_cpen(shmnarne,
0_R0WR | O.CREAT, PERM_FILE) )
ec_neg1( ftruncate(p->sq_clients[0].cl_fd, MEM_SIZE(rnsgsize)) )
p->sq_clients[0].cl_mem = mmap(NULL, MEM_SIZE(rnsgsize),
PROT.REAO | PROT.WRITE, MAP.SHAREO, p->sq_clients[0].cl_fd, 0);
ec_cmp(p->sq_clients[0].cl_mem, MAP.FAILEO)
ec_neg1( sem_init(ip->sq_clients[0].cl_mem->sm_sem_w, true,
1) )
ec_neg1( sem_init(&p->sq_clients[0].cl_mem->sm_sem_r, true,
0) )
}
return (SMIQ *)p;
EC_CLEANUP_BGN
if (p != NULL)
(void)smi_close_pshm((SMIQ *)p);
return NULL;
EC_CLEANUP_ENO
}
502 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
static void mkshm_name_server(const SHIQ_PSHM *p, char «shmname,
size_t shmnamejnax)
{
snprintf(shmname, shmname_max, "/smipshm-Xs", p->sq_name);
>
static void mkshm_name_client(pid_t pid, char «shmname,
size_t shmnamejnax)
{
snprintf(shmname, shmname_max, "/smipshm-Xd", pid);
}
Первые несколько строк, вплоть до mkshm_name_server, в точности повторяют
предыдущую реализацию. Затем сервер создает новый объект общей памяти
(предварительно удалив старый), устанавливает его размер и отображает в свое адресное
пространство. После этого инициализируются семафоры.
Клиент также отображает сегмент общей памяти сервера в свое адресное
пространство, но не устанавливает его размер и не трогает семафоры сервера, потому что
эти действия выполняет сам сервер. Затем клиент создает свой сегмент
разделяемой памяти, устанавливает его размер, отображает в свое адресное пространство,
используя для хранения указателя нулевой элемент массива, и инициализирует свои
семафоры.
Как я уже говорил, сервер не отображает сегмент клиента в свое адресное
пространство до тех пор, пока не получит сообщение, поскольку заранее ему ничего не
известно о своих клиентах. Ниже приводится исходный код вспомогательной
функции, которая используется сервером для получения указателя на сегмент общей
памяти клиента по заданному идентификатору процесса:
static struct client *get_client(SMIQ_PSHM *p, pid_t pid)
{
int i, avail = -1;
char shmname[SERVER_NAME_MAX + 50];
for (i = 0; i < HAX_CL1ENTS; i++) {
if (p->sq_clients[i].cl_pid == pid)
return &p->sq_clients[i];
if (p->sq_clients[i].cl_pid == 0 && avail == -1)
avail = i;
}
if (avail == -1) {
errno = EAD0RN0TAVAIL;
EC_FA1L
}
p->sq_clients[avail].cl_pid = pid;
mkshm_name_client(pid, shmname, sizeof(shmname));
ec_neg1( p->sq_clients[avail].cl_fd = shm_open(shmname, 0_RDWR,
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 503
PERM_FILE) )
p->sq_clients[avail].cl_mem = mmap(NULL, MEM_SIZE(p->sq_msgsize),
PR0T_READ | PR0T_WRITE, MAP_SHARED, p->sq_clientstavail].cl_fd,
0);
ec_cmp(p->sq_clients[avail].cl_meiii, MAP_FAILED)
return &p->sq_clients[avail];
EC_CLEANUP_BGN
return NULL;
EC_CLEANUP_END
}
Чтобы увидеть, где вызывается get_client, рассмотрим исходный код функции
smi_send_getaddr_pshm:
bed smi_send_getaddr_pshm(SMIQ *sqp, struct client_id «client,
vcid **addr)
<
SMIQ_PSHM *p = (SMICLPSHM .)sqp;
struct client »cp;
struct sharedjnem *sm;
if (p->sq_entity == SMI_SERVER) {
p->sq_client = «client;
ec_null( cp = get_client(p, client->c_id1) )
sin = cp->cl_mem;
}
else
sin = p->sq_srv_mem;
ec_neg1( cp_sem±(sm, SEMI_WRITE, SEMI_WAIT) )
if (p->sq_entity == SMI_CLIENT)
sm->sm_rasg.smi_client.c_id1 = getpid();
•addr = ism—>sra_msg;
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Аргумент client используется только на стороне сервера, он представляет
указатель на структуру, которая была получена в сообщении, принятом от клиента.
Элемент c_id1 хранит идентификатор процесса клиента, который передается в get_client.
На стороне клиента адрес сегмента разделяемой памяти сервера определяется в
функции smi_open_pshm и размещается в структуре SMIQ_PSHM. Имея адрес сегмента
памяти, процесс становится на семафоре записи, и после того как он будет получен,
можно будет выполнить запись сообщения в разделяемую память. На стороне
клиента в сообщение вкладывается идентификатор процесса и на этом работа
функции завершается.
17 3ак. 4030
504 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
После записи сообщения для его передачи вызывается функция smi_send_release_pshm:
bool smi_send_release_pshm(SMIQ *sqp)
{
SMIQ.PSHM *p = (SMIQ.PSHM »)sqp;
struot olient »op;
struot shared.mem *sm;
if (p->sq_entity == SMI.SERVER) {
ec_null( op = get_oIient(p, p->sq_olient.o_id1) )
sm = op->oI_mem;
>
else
sm = p->sq_srv_mem;
eo_neg1( op_semi(sm, SEMI.READ, SEMI.POST) )
return true;
EC_CLEANUP_BGN
return false;
EC.CLEANUP.END
>
На сервере, данная функция с помощью get.client получает адрес сегмента
памяти клиента, который предварительно был сохранен функцией smi.send.getaddr.pshm.
К этому моменту клиентский сегмент уже отображен в память серверного
процесса, поэтому нам нужно просто найти его. На стороне клиента адрес сегмента
общей памяти сервера уже известен и находится в структуре SMIQ.PSHM. После этого
остается только освободить семафор чтения в заданном сегменте памяти. Это
позволит отработать функции smi_receive_getaddr_pshm:
bool smi_receive_getaddr_pshm(SMIQ *sqp, void *»addr)
{
SMIQ.PSHM «p = (SMIQ.PSHM »)sqp;
struct shared.mem *sm;
if (p->sq_entity == SMI.SERVER)
sm = p->sq_srv_mem;
else
sm = p->sq_clients[Q].cl_mem;
ec.negK op_semi(sm, SEMI.READ, SEMI.WAIT) )
•addr = ism->sm_msg;
return true;
EC_CLEANUP_BGN
return false;
EC.CLEANUP.END
}
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 505
Поскольку и сервер, и клиент принимают сообщение в свой сегмент общей
памяти, ее адрес уже известен и может быть передан в op.semi без необходимости
обращения к get_client. После того как семафор чтения будет захвачен, функция
возвращает адрес сообщения.
И, наконец, функция smi_receive_release_pshm, которая освобождает семафор
записи, после того как принимающая сторона прочитает сообщение:
bccl smi_receive_reiease_pshm(SMIQ *sqp)
{
SMIQ_PSHM *p = (SMIQ_PSHM «)sqp;
struct sharedjnem *sm;
if (p->sq_entity == SMI.SERVEP.)
sm = p->sq_srv_mem;
eise
sm = p->sq_ciients[0].ci_mem;
ec_neg1( cp_semi(sm, SEMI.WRITE, SEMI.POST) )
return true;
EC_CLEANUP_BGN
return faise;
EC_CLEANUP_END
>
Если вы до сих пор не проводили сравнительный анализ данной реализации SMI с
реализацией из раздела 7.133, сейчас самое время. Вы увидите, что они очень
похожи друг на друга с небольшими отличиями:
• в реализации с System V клиент имеет возможность передать серверу
идентификаторы набора семафоров и сегмента общей памяти, благодаря чему сервер
может быстро получить доступ к необходимым объектам и у него нет
необходимости хранить сведения о клиентах в массиве.
• в реализации с POSIX мы использовали неименованные семафоры, размещенные
в сегменте общей памяти. Теоретически такая организация должна дать прирост
производительности, хотя это во многом зависит от конкретной реализации.
7.14.3 Достоинства и недостатки общей памяти P0SIX
Версии разделяемой памяти POSIX и System V одинаково удобны в использовании.
Превосходство в производительности одной версии над другой во многом
зависит от конкретной реализации, но наиболее вероятно, что любая из реализаций
используют одни и те же механизмы ядра.
Главное преимущество общей памяти POSIX заключается в наличии системного
вызова mmap, который может отобразить в адресное пространство процесса любой
обычный файл, а не только объект общей памяти. К тому же у вас имеется возможность
отобразить в память не весь объект, а только часть его.
506 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
Ключевой недостаток общей памяти POSIX — эта реализация доступна не на всех
системах. Например, ее нет в Linux, FreeBSD и Darwin.
7.15 Сравнение производительности
Имея на руках реализацию SMI, использующую шесть различных механизмов
взаимодействия процессов (включая сокеты, см. следующую главу), не составит труда
написать программу, с помощью которой можно сравнить время, затрачиваемое
на обмен сообщениями различного размера. Я провел серию тестов, посылая в
каждом из них 5000 сообщений между 4 клиентами и одним сервером. Испытания
проводились на 4-х системах: Linux, FreeBSD, Solaris и Darwin. Полученные
результаты были нормализованы по времени обмена 100-байтовыми сообщениями
через именованные каналы на каждой из систем, чтобы уравнять имеющиеся
различия в аппаратных архитектурах. Результаты сведены в табл. 7.2. Сообщения
больших размеров можно было послать только при условии использования общей
памяти и очереди сообщений POSIX. К моменту написания этих строк ОС FreeBSD,
Linux и Darwin еще не поддерживали очереди сообщений и общую память POSIX.
Таблица 7.2. Производительность различных способов обмена сообщениями
Метод
FIFO
Очереди
сообщений
System V
Очереди
сообщений
POSIX
Общая
память
System V
Общая
память
POSIX
Сокеты
Сообщения
размером
100 байт
S:1.00
В: 1.00
Ш.00
L:1.00
S:0.90
В:0.62
L0.31
S:2.02
S:1.47
ВЮ.94
D:1.04
L0.55
S:1.27
S:1.84
B:0.81
D.1.04
L:0.75
Сообщения
размером
2000 байт
S:1.22
В: 1.46
D:1.29
L:1.51
S:1.82
B:3.76
L:0.64
S:2.40
S:1.41
B:0.91
D:1.07
L0.53
S:1.25
S:2.15
Brl.OO
D:1.27
L:0.95
Сообщения
размером
20 000 байт
размер сообщения
слишком велик
размер сообщения
слишком велик
S:7.03
S:1.55
В:0.90
D:1.02
L0.47
S: 1.41
S:10.15
В: 7.99
D: 5.52
L: 6.06
Сообщения
размером
100 000 байт
размер сообщения
слишком велик
размер сообщения
слишком велик
S:33.39
S:1.24
В:0.90
Ш.06
L0.51
S:1.37
S:44.13
В:35.38
D:25.86
L:31.91
S: Solaris, В: FreeBSD, D: Darwin, L Linux. Все величины приведены ко времени обмена
100-байтовыми сообщения через именованные каналы, для каждой из систем. В испытаниях
использовались сокеты домена AFUN1X (см. главу 8).
ГЛАВА 7 Улучшенные механизмы взаимодействия процессов 507
Некоторые комментарии к полученным результатам.
• Они не являются окончательными, потому что было проведено одно из
возможных испытаний. SMI — лишь один из возможных вариантов интерфейсов и в
нашей реализации он весьма далек от оптимального. Он разрабатывался лишь
как учебный пример.
• Даже приведенные значения вовсе не означают, что лучшие показатели для того
или иного метода действительно говорят о том, что он лучший. Просто
реализация FIFO в системе могла оказаться не на высоте.
• Очереди сообщений System V для обмена маленькими (100 байт) сообщениями
дают наилучшие показатели на всех системах кроме ОС Darwin, которая не
поддерживает их.
• Производительность интерфейса с сокетами почти так же хороша, как и с
очередями сообщений в обоих вариантах (а в некоторых случаях даже лучше) и
может выполнять обмен сообщениями любого размера. Кроме того, это
единственный механизм, который позволяет наладить обмен по сети между
процессами, находящимися на разных компьютерах, хотя в данной таблице
приведены результаты измерений, производившиеся на одиночной машине.
• В ОС Solaris производительность очередей сообщений POSIX уступает System V,
но зато могут обрабатывать сообщения гигантского размера.
• Как того и следовало ожидать, производительность обмена через общую память
не зависит от размера сообщения.
Таким образом, обобщая все вышесказанное, можно утверждать, что в случаях, когда
требуется наивысшая производительность, следует использовать очереди
сообщений System V для обмена маленькими сообщениями и общую память — для обмена
большими сообщениями. Мы не попробовали скомбинировать воедино очереди
сообщений и общую память System V, чтобы послать серверу короткое сообщение о
том, что для него есть работа, а общую память — для основного сообщения.
Если вопрос эффективности для вас не самое важное и большее значение имеет
простота реализации, используйте сокеты. Они достаточно эффективны, с их
помощью можно обмениваться сообщениями любого размера, но самое главное —
есть возможность наладить обмен между разными машинами в сети.
Эти рекомендации относятся только к взаимодействиям между процессами,
ориентированным на обмен сообщениями. В других случаях, производительность
разных методов взаимодействия может сильно отличаться от приводимых здесь цифр.
Упражнения
7.1. Реализуйте msgget, msgctl, msgsnd и msgrcv с помощью функций POSIX IPC.
7.2. Реализуйте mq_open, mq.close, mq_unlink, mq_send и mq_receive с помощью фун-кций
System V.
508 ГЛАВА 7 Улучшенные механизмы взаимодействия процессов
7.3- Реализуйте semget, semctl и semcp с помощью функций POSIX, предусматривая
только один семафор в наборе. Сможете ли вы обработать операции
увеличения и уменьшения более чем для одного семафора?
7.4. То же самое, что и в предыдущем упражнении, только предусмотрите
возможность существования нескольких семафоров в наборе.
7.5. Реализуйте sem_cpen, sem.clcse, sem.unlink, sem.pcst и sem.wait с помощью
функций System V.
7.6. Реализуйте shmget, shmctl, shmat и shmdt с помощью функций POSIX..
7.7. Можно ли реализовать shm.cpen, shm.unlink, ftruncate, rnmap и muninap с помощью
функций System V? Если нет, то почему? Сможете ли вы выполнить такую
реализацию?
7.8. Проведите эксперимент по сравнению производительности операций ввода-
вывода над обычными файлами и файлами, отображенными в память.
Включите в эксперимент исследование операций с последовательным и
произвольным доступом.
7.9- Проведите эксперимент по сравнению производительности блокировок
записи (lcckf) и блокировок записи и чтения (fcntl). Попробуйте разработать
такой алгоритм, который показывал бы наибольшее различие в
производительности.
7.10. Усовершенствуйте программу из упражнения 5.14 так, чтобы она могла
выводить атрибуты процесса (из Приложения А), о которых вы узнали в этой главе.
illlll 8
Сетевое
взаимодействие
и сокеты
Механизмы взаимодействия процессов, представленные в главах б и 7, разумеется,
не останутся у программистов без внимания и в будущем, но с каждым днем
появляется все больше и больше задач, решение которых требует организации
взаимодействия процессов, исполняющихся на разных компьютерах. Под фразой «на
разных компьютерах» я имею в виду компьютеры, не просто расположенные в одном
помещении или в одном здании, но компьютеры, которые могут находиться в
разных частях Земного Шара и быть связаны между собой через Интернет.
Основной способ организации обмена информацией между компьютерами,
объединенными в сеть, — сокеты. Для работы с ними существует восемь основных
системных вызовов, пять из которых предназначены исключительно для сокетов: socket,
bind, listen, accept, connect, read, write и olose. Однако, учитывая всю сложность
сетевых взаимодействий и многообразие протоколов связи, существует еще около 60
системных вызовов, имеющих отношение к сокетам, все они будут описаны в
данной главе.
Добравшись до этой главы, вы уже обладаете достаточным количеством знаний,
чтобы встретиться лицом к лицу с большим числом примеров программ, среди
которых вы найдете Web-браузер и Web-сервер. Возможно, перед тем как углубиться
в мир сетей UNIX, вы захотите познакомиться с ними поближе. Наиболее полная
книга по этой теме [Ste2003]. Кроме того, желательно чтобы вы ознакомились с
системной документацией и с описанием протоколов связи, которые вы
используете, особенно если они сильно отличаются от протоколов группы TCP/IP.
Изложение материала главы будет построено следующим образом: для начала мы
рассмотрим основные системные вызовы, предназначенные для работы с сокета-
ми, и разберем ряд простых примеров программ серверов и клиентов. Затем я
расскажу о принципах адресации и дополнительных параметрах сокетов.
Разработаем упрощенный интерфейс, за которым «спрячем» самые сложные моменты, а
потом на его основе напишем версию «Упрощенного интерфейса обмена сообщени-
510 ГЛАВА 8 Сетевое взаимодействие и сокеты
ями» (SMI). После этого мы перейдем к рассмотрению более сложных тем: сокеты
не требующие установления соединений, внеочередные (срочные) сообщения,
функции получения сведений о сетевом окружении и ряд других тем. В
заключение я немного расскажу о проблемах разработки серверов, которые могут
обслуживать тысячи клиентов одновременно.
8.1 Введение в сокеты
В этом разделе описываются основные системные вызовы для работы с сокетами.
8.1.1 Принцип действия сокетов
Тема сокетов является довольно сложной, поэтому начнем ее изучение,
отталкиваясь от аналогий из предыдущих глав. В разделе 7.2 говорилось, что процесс может
открыть именованный канал примерно таким образом:
fd = open("MyFifo", 0_RD0NLY);
А теперь разберемся с тем, что делает этот системный вызов.
1. Создает объект для операций ввода-вывода и назначает ему файловый дескриптор.
2. Связывает файловый дескриптор с внешним именем «MyFifo».
3. Ожидает, пока кто-нибудь, не откроет канал на запись.
4. Возвращает файловый дескриптор, который затем может быть использован в
системном вызове read.
Аналогичным образом работают и сокеты, только каждый шаг выделен в
отдельный системный вызов:
socket Создает объект (сокет) и назначает ему файловый дескриптор.
bind Устанавливает адрес (имя) сокета, чтобы другой процесс мог
сослаться на него.
listen Помечает сокет как предназначенный для приема запросов
на соединение от других сокетов.
accept Блокируется в ожидании запроса на соединение.
connect Устанавливает соединение с сокетом, который был
заблокирован на accept.
Именованные каналы не предусматривают различий между сервером и клиентом,
в том смысле, что оба они обращаются к одному и тому же системному вызову open,
хотя и с различными флагами (то есть 0_RDONLY и 0_WRONLY). Последовательность
создания и инициализации сокетов на стороне сервера отличается от
последовательности действий на стороне клиента, как показано на рис. 8.1.
ГЛАВА 8 Сетевое взаимодействие и сокеты
511
сервер
сервер
socket
Рис. 8.1. Установка соединения через сокеты
На стороне сервера:
• Вызывается sccket для создания объекта и связанного с ним файлового
дескриптора (шаг 2).
• Вызывается bind, для назначения сокету адреса (имени) (шаг 3).
• Вызывается listen, чтобы пометить сокет, как предназначенный для приема
запросов на соединение (шаг 4).
• Вызывается accept, чтобы дождаться и принять входящее соединение. После этого
вызов accept создает второй сокет с новым файловым дескриптором (шаг 5).
• В операциях ввода-вывода (системные вызовы read и write) участвует вновь
созданный (второй) файловый дескриптор (шаг 6).
На стороне клиента:
• Вызывается socket для создания объекта и связанного с ним файлового
дескриптора (шаг 2).
• Для установления соединения с сервером вызывается connect, которому в
качестве аргумента передается имя сервера, (шаг 3, не обязательно должен быть
синхронизирован с третьим шагом на стороне сервера).
• Файловый дескриптор сокета используется для выполнения операций ввода-
вывода (шаг б).
Прежде чем перейти к описанию системных вызовов, я предлагаю рассмотреть
пример программы. Она порождает дочерний процесс, который будет выступать в
роли клиента, а родительский процесс — в роли сервера:
«define SOCKETNAME "MySocket"
int main(vcid)
{
struct scckaddr_un sa;
512 ГЛАВА 8 Сетевое взаимодействие и сокеты
(vcid)unlink(SOCKETNAME);
strcpy(sa.sun_path, SOCKETNAME);
sa.sun_family = AF_UNIX;
if (fcrk() == 0) { /* пстсмск - клиент */
int fd.skt;
char buf[100];
ec.negK fd_skt = sccket(AF_UNIX, S0CK_STREAM, 0) )
while (ccnnect(fd_skt, (struct scckaddr »)4sa, sizecf(sa)) == -1)
if (errnc == ENOENT) {
sleepd);
continue;
}
else
EC_FAIL
ec.negK write(fd_skt, "Привет!", 7 ) )
ec.negK read(fd_skt, buf, sizecf(buf)) )
printf("Клиент пслучил сссбщение \"Xs\"\n", buf);
ec.negK clcse(fd.skt) )
exit(EXIT_SUCCESS);
}
else { /* предск - сервер */
int fd.skt, fd.client;
char buf[l00];
ec.negK fd.skt = sccket(AF_UNIX, S0CK_STREAH, 0) )
ec.negK bind(fd_skt, (struct scckaddr *)4sa, sizecf(sa)) )
ec.negK listen(fd_skt, S0MAXC0NN) )
ec.negK fd.client = accept(fd_skt, NULL, 0) )
ec.negK read (fd.client, buf, sizecf(buf)) )
printf("Сервер пслучил сссбщение \"Xs\"\n", buf);
ec.negK write(fd_client, "Пека!", 9 ) )
ec.negK close(fd.skt) )
ec.negK close(fd.client) )
exit(EXIT.SUCCESS);
}
EC.CLEANUP.BGN
exit(EXIT_FAILURE);
EC.CLEANUP.END
}
Первое, что мы видим, — адрес (имя), которое передается системным вызовам bind
и connect. Он представляет собой не просто строку, а целую структуру, которая хранит
строку в поле sun.path, и домен (область) адресов — в поле sun.family. Тема
адресации сокетов довольно объемна, и я не собираюсь обходить ее стороной, но на
ГЛАВА 8 Сетевое взаимодействие и сокеты 513
текущий момент вам достаточно будет знать, что домен AF.UNIX означает
«локальное взаимодействие, ограниченное рамками одной системы».
Системный вызов bind не может повторно использовать существующие имена для
сокетов типа AF.UNIX, поэтому производится удаление имени, чтобы гарантировать
его отсутствие.
Сервер (родительский процесс) выполняет последовательность из шести шагов,
приведенную на рис. 8.1. (Мы пока не будем останавливаться на малопонятных
аргументах вызовов listen и accept.) Клиент (дочерний процесс) также следует
заданной последовательности действий, но с одним небольшим отступлением: если
обращение к connect на стороне клиента произойдет раньше, чем сервер
обратится к bind, вызов connect потерпит неудачу, поскольку ему не с кем будет
устанавливать соединение. Поэтому в случае получения признака ошибки клиент ненадолго
«засыпает» и повторяет попытку.
Когда вызов accept возвращает управление, сервер выполняет операции
ввода-вывода над вновь созданным (вторичным) файловым дескриптором (не над тем,
который был получен вызовом socket). Клиент продолжает использовать свой
дескриптор (кстати, он у него единственный). Запомните: системные вызовы accept
и connect могут быть заблокированы. Остальные возвращают управление в
вызывающую программу немедленно, как только выполнять свою работу.
Соединение может существовать сколь угодно долго, пока одна из сторон не
разорвет его, и по своей сути напоминает двунаправленный неименованный канал
(см. раздел б.б).
Файл сокета действительно является файлом, это хорошо видно в выводе команды Is:
$ Is -1 MySocket
srwxr-xr-x 1 marc sysadrain 0 Apr 4 10:03 MySocket
В результате работы программы у меня получилось следующее:
Сервер получил сообщение "Привет!"
Клиент получил сообщение "Пока!"
Работа с сокетами действительно очень проста, за исключением следующих моментов.
• Обслуживание нескольких клиентов одним сервером.
• Наличие нескольких доменов (областей) адресов, включая AF_INET, который
представляет наибольший интерес, при организации взаимодействий через сеть.
• Ориентированность на обмен дейтаграммами, а не на потоки данных.
• Возможность обмена дейтаграммами без создания логического соединения.
• Большое количество вариантов тонкой настройки поведения сокетов,
особенно для доменов AF.INET И AF_INET6.
• Передача двоичных данных между компьютерами, аппаратные платформы
которых используют различные способы представления чисел.
• Обращение к узлам сети по именам (например www.basepath.com/aup).
514 ГЛАВА 7 Улучшенные мезанизмы взаимодействия процессов
Хорошо, я несколько погорячился, назвав их «моментами»; это не «моменты», это
довольно сложные темы, которые вы должны знать, если собираетесь писать
сетевые приложения, и обсуждению которых будет посвящена оставшаяся часть
данной главы.
8.1.2 Основные системные вызовы для работы с сокетами,
образующими логические соединения
Я не зря указал в заголовке раздела, что речь пойдет о сокетах, образующих
логические соединения. Эти сокеты формируют логический канал системными
вызовами connect и accept. Сокеты, не образующие такой канал, обмениваются
простыми дейтаграммамини для работы с ними не используются системные вызовы listen
и accept, а вызов connect используется для совершенно других целей. В этом
разделе мы будем говорить только о первой разновидности сокетов, о вторых мы
поговорим в разделе 8.6.
Рассмотрим пять основных системных вызовов, о которых я уже упоминал ранее.
Начнем с системного вызова socket:
socket — создает объект для организации сетевых взаимодействий
«include <sys/sccket. h>
int sccket(
int domain, /» домен адреса (AFJJNIX, AF_INET и т. п.) •/
int type, /* SOOK.STREAM, SOOK.DGRAM и т. п. •/
int protocol /• детализирует аргумент type; обычно - ноль •/
);
/• Возвращает файловый дескриптор или -1 в случае сшибки (кед сшибки -
в переменней еггпс) •/
Файловый дескриптор, возвращаемый вызовом socket, может использоваться в
следующих целях.
• Сервером — для приема входящих соединений системным вызовом accept
(фактические операции ввода-ввода выполняются над другим файловым
дескриптором — возвращаемым системным вызовом accept).
• Клиентом — для выполнения операций ввода-вывода после того, как
соединение будет установлено вызовом connect.
Аргументы domain* и type относятся к теме адресации сокетов, которой будет
посвящен отдельный раздел. Аргумент р rctcccl служит для детализации аргумента type,
если последний предполагает выбор между вариантами, но в большинстве случаев
устанавливается значение по умолчанию — ноль.
После создания сокета сервер должен присвоить ему адрес системным вызовом bind:
' Макросы, начинающиеся с комбинации символов AF_, иногда реализуются с именами,
начинающимися с PF . Стандартной считается нотация AF_, поэтому мы будем использовать
именно ее.
ГЛАВА 7 Улучшенные мезанизмы взаимодействия процессов 515
bind
— присваивает адрес сокету
«include <sys/socket.h>
int bind(
);
h
в
int socket_fd,
const struct sockaddr *sa,
socklen_t sa_len
Возвращает О в случае успеха
переменной errno) */
/*
/*
/•
или
файловый дескриптор
адрес
длина
-1 в
сокета
адреса
случае
*/
*/
ошибки
сокета
V
(код ошибки -
Как я уже говорил, проблеме адресации будет посвящен отдельный раздел (8.2).
В примере выше мы уже встречались с макросом AF_UNIX, с помощью которого
создаются сокеты для организации взаимодействий в пределах одной системы
(компьютера), подобно другим механизмам IPC, рассматривавшимся в главе 7. Позднее
мы увидим, как назначаются адреса из других доменов, включая AF_INET.
Запомните: в большинстве систем для адресов из домена AF_UNIX системный вызов
bind создает новый файл сокета — он не может повторно использовать
существующий файл.
Чтобы подготовить сокет к приему входящих соединений, сервер должен обратиться
к вызову listen:
listen
ливает
— подготавливает сокет к приему запросов
ограничение на размер очереди запросов
«include <sys/socket.h>
int
);
/•
listen(
int socket_fd, /* файловый дескриптор сокета
int backlog /* максимальное число запросов
на
•/
на
соединение и
уста на в-
соединение в очереди
Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки
в переменной errno) */
-
*/
В нашем примере из предыдущего раздела не было показано, что сервер может
обслуживать большое число клиентов. Но какой бы мощный сервер ни был, он не
может принимать соединения быстрее, чем это позволит системный вызов accept,
поэтому запросы на соединение помещаются в очередь, где дожидаются
обработки. Второй аргумент, backlog, ограничивает размер этой очереди. Как правило, когда
очередь заполнена до отказа, системный вызов connect на стороне клиента
возвращает признак ошибки с кодом ECONNREFUSED. В своих примерах я буду использовать
константу S0MAXC0NN, которая представляет максимально возможное число,
определяемое системой.
Прием соединений на стороне сервера выполняется системным вызовом accept:
516 ГЛАВА 8 Сетевое взаимодействие и сокеты
accept — принимает новое соединение и
«include <sys/sccket.h>
int accept(
int sccket_fd,
struct scckaddr »sa,
sccklen_t *sa_len
);
/• Возвращает файловый д
в переменней errnc) */
h
/•
/•
создает новый сокет
файловый дескриптор сскета »/
адрес
длина
ескриптср
сскета
адреса
или -1
или NULL
•/
в случае
•/
сшибки (кед сшибки -
Обычно системный вызов accept блокируется до поступления запроса на
соединение (от другого процесса, обратившегося к вызову connect), после чего создает новый
сокет для принятого соединения и возвращает новый файловый дескриптор.
Дескриптор может использоваться как обычными системными вызовами read и write,
так и другими вызовами, специфичными для сокетов, которые дают больший
контроль над операциями ввода-вывода: send, sendtc, sendmsg, recv, recvf rem (см. разделы
8.6.2, 8.6.3 и 8.9.1).
Если для файлового дескриптора установлен флаг O.NONBLOCK (с помощью вызова
fcntl, см. раздел 3.8.3) и очередь запросов пуста, вместо ожидания поступления
запроса вызов accept будет возвращать признак ошибки с кодом EAGAIN или EW0UL0BL0CK.
Файловый дескриптор сокета может использоваться в системных вызовах select или
pell — я продемонстрирую это в разделе 8.1.3. Обычно сервер помещает в набор fd.set
системного вызова select дескриптор своего сокета, который ожидает приема
запросов на соединение, а также файловые дескрипторы всех сокетов, которые
возвращает системный вызов accept. Если select или pell обнаружат готовность
файлового дескриптора серверного сокета, это означает, что сервер должен вызвать accept
(который не будет заблокирован). Готовность других сокетов означает, что от
клиента (или от клиентов) поступили новые данные, которые следует прочитать.
Если аргумент sa не NULL, по указанному адресу записываются сведения о сокете,
соединение с которым было принято. При этом в аргументе sa_len вы должны
передать указатель на переменную, которая хранит размер области памяти,
отведенной под сведения о сокете. По возвращении из системного вызова эта переменная
будет содержать фактический объем данных, записанных в sa.
Клиент после создания сокета просто вызывает connect, которому передает адрес
сервера:
ГЛАВА 8 Сетевое взаимодействие и сокеты 517
connect — устанавливает соединение
«include <sys/sccket.h>
int connect(
int sccket_fd,
ccnst struct scckadd
sccklen.t sa_len
);
/* Возвращает О в случае
в переменней errnc) */
/•
■ «sa, /*
/*
файловый дескриптор
адрес
длина
успеха или -1 е
еекета
адреса
1 случае
*/
*/
сшибки
еекета
•/
(кед сшибки -
Подобно системному вызову accept, connect блокируется до того момента, как
запрос на соединение будет принят сервером, но он не возвращает новый
файловый дескриптор — клиент выполняет операции ввода-вывода над прежним
дескриптором.
Если установлен флаг O.NONBLOCK, вызов connect не будет ожидать установления
соединения, а сразу же вернет признак ошибки с кодом EINPROQRESS. Запрос на
соединение при этом не теряется, а помещается в очередь. Все последующие обращения
к connect в этот период будут заканчиваться ошибкой с кодом EALREADY. После того
как соединение будет установлено, вы сможете использовать файловый
дескриптор по своему усмотрению. При желании можно использовать вызовы select или
pell, чтобы дождаться готовности дескриптора на запись (не на чтение). Типичная
последовательность действий в этом случае: вызвать connect в неблокирующем
режиме, выполнить дополнительные действия по инициализации и затем вызвать select
или pell, чтобы дождаться установления соединения.
8.1.3 Обслуживание нескольких клиентов
Теперь доработаем пример из раздела 8.1.1 так, чтобы наш сервер мог обслуживать
несколько клиентов одновременно. Ниже приводится серверная часть программы:
static bocl run_server(struct scckaddr_un »sap)
{
int fd_skt, fd_client, fd.hwm = 0, fd;
char buf[100];
fd_set set, read_set;
ssize.t nread;
ec.negK fd.skt = sccket(AF_UNIX, S0CK_STREAM, 0) )
ec_neg1( bind(fd_skt, (struct scckaddr *)sap, sizecf(*sap)) )
ec.negK listen (fd.skt, S0MAXC0NN) )
if (fd.skt > fd.hwm)
fd.hwm = fd.skt;
FD_ZER0(&set);
FD_SET(fd.skt, &set);
while (true) {
518 ГЛАВА 8 Сетевое взаимодействие и сокеты
read.set = set;
ec_neg1( select(fd_hwm + 1, 4read_set, NULL, NULL, NULL) )
fcr (fd = 0; fd <= fd.hwm; fd++)
if (FD_ISSET(fd, &read_set)) {
if (fd == fd.skt) {
ec_neg1( fd.client = accept(fd_skt, NULL, 0) )
FD_SET(fd_client, &set);
if (fd_client > fd_hwra)
fd_hwra = fd_client;
}
else {
ec_neg1( nread = read(fd, buf, sizecf(buf)) )
if (nread == 0) {
FD_CLR(fd, &set);
if (fd == fdjiwm)
fd_hwm-;
ec_neg1( clcse(fd) )
}
else {
printf("Сервер получил сообщение \"Xs\"\n", buf);
ec_neg1( write(fd, "Пока!", 9 ) )
}
}
}
}
ec_neg1( close(fd_skt) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Фактически весь новый код предназначен для обслуживания системного вызова
select, который уже обсуждался в разделе 4.2.3, тем не менее, повторно приведу его
краткое описание:
ГЛАВА 8 Сетевое взаимодействие и сокеты 519
select — ожидает готовности операций ввода-вывода
((include <sys/select.h>
int select(
int nfds, /* наибольший нсмер дескриптора + 1 */
fd_set *readset, /* набор дескрипторов, проверяемых */
/* на дсступнссть для чтения или NULL */
fd_set *writeset, /* набор дескрипторов, проверяемых */
/* на дсступнссть для записи или NULL */
fd_set «errcrset, /* набор дескрипторов, проверяемых */
/* на наличие сшибок или NULL */
struct timeval «timeout /* предельнее время ожидания (в микросекундах) */
/* или NULL */
);
/* Возвращает количестве установленных битов Returns или -1 в случае сшибки
(кед сшибки - в переменней еггпс) •/
В функции run_server определено два набора дескрипторов. Первый, с именем set,
хранит все файловые дескрипторы сокетов, сокет сервера, который принимает
запросы на соединение, и сокеты для связи с клиентами, созданные вызовом accept.
Для корректной работы select необходимо отслеживать наибольший номер
файлового дескриптора, делается это с помощью переменной fd_hwm, которая
обновляется при создании нового дескриптора вызовом accept, и при закрытии
файлового дескриптора с наибольшим порядковым номером. После закрытия
дескриптор удаляется из набора, это совершенно необходимо, в противном случае select
будет сообщать, что дескриптор готов для выполнения операции чтения, не в том
смысле, что есть данные для чтения, а в том смысле, что вызов read не будет
блокироваться.
Второй набор дескрипторов, с именем read_set, копируется из набора set перед
каждым обращением к select. Дело в том, что select модифицирует переданный ему
набор, возвращая таким способом список дескрипторов, готовых для выполнения
операции.*
После возврата из select выполняется поиск дескриптора, готового для выполнения
операции ввода-вывода (в наборе read_set). Если готовым оказался сокет fd_skt,
выполняется обращение к вызову accept. Если какой-либо другой — производится
прием данных, поступивших от клиента, и в ответ отправляется короткое сообщение.
Это одна из самых распространенных ошибок, когда программист предусматривает
единственный набор дескрипторов, который передается в select. Дело в том, что select
сбрасывает биты, соответствующие дескрипторам, не готовым к выполнению операций, в
результате при следующем обращении к этому системному вызову данные дескрипторы
обслуживаться не будут.
520 ГЛАВА 8 Сетевое взаимодействие и сокеты
Теперь рассмотрим исходный код функции, реализующей клиентскую часть
программы. Она осталась практически без изменений, только теперь в тело
сообщения вставляется идентификатор процесса клиента:
static bool run_client(struct sockaddr_un *sap)
{
if (forkO == 0) {
int fd_skt;
char buf[100];
ec_neg1( fd_skt = socket(AF_UNIX, S0CK_STREAM, 0) )
while (connect(fd_skt, (struct sockaddr *)sap, sizeof(*sap)) == -1)
if (errno == ENOENT) {
sleep(1);
continue;
}
else
EC_FAIL
snprintf(buf, sizeof(buf), "Привет от клиента Xldl",
(long)getpidO);
ec_neg1( write(fd_skt, buf, strlen(buf) + 1 ) )
ec_neg1( read(fd_skt, buf, sizeof(buf)) )
printf("Клиент получил сообщение \"Xs\"\n", buf);
ec_neg1( close(fd_skt) )
exit(EXIT_SUCCESS);
}
return true;
EC_CLEANUP_BGN
/* в эту точку попадает только потомок */
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
И в заключение — функция main, которая назначает адрес сокета, создает четыре
дочерних процесса и запускает сервер в родительском процессе:
«define SOCKETNAME "MySocket"
int main(void)
{
struct sockaddr_un sa;
int nclient;
(void)unlink(SOCKETNAME);
strcpy(sa.sun_path, SOCKETNAME);
sa.sun_family = AF_UNIX;
for (nclient = 1; nclient <= 4; nclient++)
ГЛАВА 8 Сетевое взаимодействие и сокеты 521
ec_false( run_client(&sa) )
ec_false( run_server(&sa) )
exit(EXlT_SUCCESS);
EC_CLEANUP_BGN
exit(EXlT_FAILURE);
EC_CLEANUP_END
}
В результате работы программы было получено следующее:
Сервер пслучил сообщение "Привет ст клиента 31786!"
Клиент пслучил сообщение "Пека!"
Сервер пслучил сообщение "Привет ст клиента 31785!"
Клиент пслучил сообщение "Пека!"
Сервер пслучил сообщение "Привет ст клиента 31784!"
Клиент пслучил сообщение "Пека!"
Сервер пслучил сообщение "Привет ст клиента 31787!"
Клиент пслучил сообщение "Пека!"
Мы знаем почти все, что нужно для написания достаточно сложных
клиент-серверных приложений, Нам осталось лишь разобраться с адресацией сокетов
(сокеты с адресами из домена AF_UNIX представляют, пожалуй, наименьший интерес) и
их настройкой. Эти вопросы будут обсуждаться ниже, а пока обсудим проблему
порядка следования байт в двоичном представлении чисел.
8.1.4 Порядок следования байт
Вам едва ли придется когда-либо столкнуться с проблемами при передаче строк
символов (например «Привет» или «Пока») между машинами по сети, потому что
порядок следования символов не зависит от аппаратной архитектуры. Однако при
передаче чисел в двоичном представлении могут появиться проблемы из-за того,
что разные архитектуры предполагают разный порядок следования байт в
машинном представлении чисел.
Взгляните на рис. 8.2. Здесь показаны различные способы представления
двухбайтового целого числа 0xD04C, размещенного по адресу 106204.
Обратный порядок
следования байт
106203 106202
106205 DO 4C 106204
106207 106206
106209 106208
Прямой порядок
следования байт
106202 106203
106204 DO 4C 106205
106206 106207
106208 106209
Рис. 8.2. Прямой и обратный порядок следования байт
522 ГЛАВА 8 Сетевое взаимодействие и сокеты
В архитектурах с обратным порядком следования байт адрес числа определяется
адресом его младшего байта, в архитектурах с прямым порядком следования байт
— старшего. Пока вы работаете с какой-либо одной архитектурой, вы не
обращаете внимания на принятый в ней порядок следования байт. Но как только вам
потребуется наладить обмен двоичными данными между разнотипными
аппаратными платформами, проблемы не заставят себя ждать. Представьте себе: компьютер,
аппаратура которого поддерживает обратный порядок следования байт,
отправляет число 0xD04C в виде следующей последовательности байт: первым
отправляется байт 4С (в памяти он располагается первым), следом за ним — DO.
Принимающая сторона, основанная на аппаратуре с прямым порядком следования байт, примет
эту последовательность и получит число 0x4CD0, так как она «считает», что
первым идет старший байт.
Решение проблемы в том, чтобы обеспечить такой порядок передачи, который
правильно интерпретируется всеми системами, независимо от аппаратной
платформы. Таким образом, все отправители должны преобразовывать отправляемые
данные из своего представления в аппаратно-независимое (часто называемое,
«сетевыми порядком следования байт»), а все получатели должны выполнять
обратное преобразование. Но делаться это должно только в том случае, если данные еще
не преобразованы в аппаратно-независимое представление. Например, если у вас
есть некоторое графическое изображение в формате JPEG, вы должны передавать
его в том виде, в каком оно существует — не следует выполнять преобразование
какой-либо части изображения в аппаратно-независимое представление.
В большинстве случаев от вас не требуется знать порядок следования байт в той
или иной архитектуре, равно как и порядок следования байт в аппаратно-незави-
симом представлении, потому что существует набор стандартных функций для
преобразования 16- и 32-битных целых чисел:
htons — преобразует 16-битное число в аппаратно-независимое
представление
«include <arpa/inet.h>
uint16_t htcns(
uint16_t hcstnum /* исходное 16-битнсе числе */
);
/* Возвращает числе с аппаратне-независимым перядкем следсвания байт */
htonl — 32-битное число в аппаратно-независимое представление
«include <a rpa/inet. h>
uint32_t htcnl(
uint32_t hcstnum /* исходное 32-битнсе числе */
);
/» Возвращает числе с аппаратне-независимым перядкем следсвания байт •/
ГЛАВА 8 Сетевое взаимодействие и сокеты 523
ntohs
ления
— преобразует 16-битное число из аппаратно-независимого
«include <arpa/inet
uir
);
/•
t16_t ntchs(
uint16_t netnum
Всзвращает числе
h>
/•
h
в
16-битнсе числе */
с аппаратне-независимым перядкем
аппаратне-зависимем представлении
следсвани
•/
представ-
я байт
•/
ntohl
ления
— преобразует 32-битное число из аппаратно-независимого
«include <arpa/inet
uin
);
/•
h>
t32_t ntchl(
uint32_t netnum /*
/•
Всзвращает числе
в
32-битнсе числе */
с аппаратне-независимым перядкем
аппаратне-зависимем представлении
следсвани
•/
представ-
я байт
•/
Суффиксы «s» и «1» в именах функций означают «shcrt» и «leng», хотя в некоторых
системах тип shcrt может занимать более 16 бит, равно как и тип long может
занимать больше 32 бит. Но, если вы внимательно взглянете на определения функций
еще раз, вы увидите, что они вообще не имеют никакого отношения к типам shcrt
и leng — они оперируют типами uint16_t и uint32_t.
Для демонстрации этих функций я приведу листинг короткой программы,
которая выводит байты числа 0xD04C в том порядке, в каком они расположены в
памяти, а потом преобразует его в сетевое (аппаратно-независимое) представление:
int main(vcid)
{
uint16_t nhest = 0xD04C, nnetwerk;
unsigned char *p;
p = (unsigned char *)&nhcst;
printf("Xx Xx\n", *p, *(p + 1));
nnetwerk = htcns(nhcst);
p = (unsigned char *)&nnetwcrk;
printf("Xx Xx\n", *p, *(p + 1));
exit(EXIT_SUCCESS);
>
Программа выполнялась на процессоре Intel Pentium, который имеет порядок
следования байт, отличный от сетевого, и дала следующий результат:
4с dO
do 4c
524 ГЛАВА 8 Сетевое взаимодействие и сокеты
Отсюда можно заключить, что процессор Pentium поддерживает обратный порядок
следования байт, а сетевой порядок соответствует прямому порядку следования байт.
Как правило, функции преобразования порядка следования байт применяются в
случаях, когда протокол взаимодействия требует передачи двоичных данных. Для
своих собственных нужд вы можете попробовать спроектировать приложение
таким образом, чтобы данные были представлены в виде строк символов (разумеется,
исключая данные, представленные в одном из стандартных форматов, например JPEG
или МРЗ). Именно таким образом работает протокол HTTP (см. раздел 8.4.2).
8.2 Адресация сокетов
Этот раздел целиком и полностью посвящен вопросу адресации сокетов (struct
sockaddr). Мы уже видели, как назначаются адреса из домена af_unix, которые
представляют собой обычное имя файла:
struct sockaddr_un sa;
strcpy(sa.sun_path, SOCKETNAME);
sa.sun_family = AFJJNIX;
Но это самый простой случай, другие типы адресов намного сложнее.
8.2.1 Структуры адресов сокетов
Для каждого из доменов адресов предназначена своя структура и свой
собственный заголовочный файл. Например, для AF_UNIX — sockaddr_un, для AF_INET — sockaddr_in,
для AF_X25 — sockaddr_x25 и так далее, хотя в [SUS2002] стандартизованы только первые
два типа. Один из подходов к созданию структуры мы уже видели при
использовании адресации типа af.unix. Он заключается в том, чтобы разместить в памяти
специфическую структуру, заполнить необходимые поля и передать указатель на
нее системному вызову bind или connect, попутно приведя указатель к типу struct
sockaddr*.
С точки зрения переносимости программ, оптимальный выбор — использовать для
хранения адресов сокетов переменные типа sockaddr.storage, который
гарантирует достаточный объем пространства в памяти под адрес любого типа.
Звучит не очень привлекательно, но на самом деле все не так плохо:
• если вы точно знаете, с каким доменом адресов вам предстоит работать, то
объявляете переменную соответствующего типа (например sockaddr_un) или
размещаете ее в динамической памяти (например, с помощью malloc);
• если вам необходимо предусмотреть возможность создания сокетов с
адресацией любого типа, то используете тип sockaddr_storage, но при обращении к таким
переменным вам необходимо будет выполнять приведение к конкретному типу;
• при обращении к системным вызовам bind и connect, вы в любом случае
должны выполнять приведение типов в соответствии с их прототипами. Другого
выхода у вас просто нет.
ГЛАВА 8 Сетевое взаимодействие и сокеты 525
Ниже приводится пример использования последних двух правил:
struct scckaddr.stcrage sas;
struct scckaddrjjn *sa = (struct scckaddr_un *)lsas;
sa->sun_family = AFJJNIX;
ec_neg1( bind(fd, (struct scckaddr *)sa, sizecf(*sa)) )
He стоит слишком сильно беспокоиться о последнем из этих правил — если вы что-
то забудете, компилятор напомнит вам. Не волнуйтесь и по поводу возможности
прямого обращения к структуре scckaddr.stcrage по ошибке — эта структура не имеет
ни одного поля, к которому вы могли бы обратиться. Поэтому подобные ошибки
также будут обнаружены компилятором на этапе компиляции, и вы сможете их
исправить, выполнив необходимое приведение типов.
8.2.2 Домен адресов afjjnix
struct sockaddrun —
((include <sys/un.h>
struct scckaddr.un {
sa_family_t sun_
char sun_path[X]
};
структура адреса
family;
/•
/*
AF.UNIX
имя ♦/
AF
•/
UNIX
Фактический размер имени я обозначил как х, потому что он зависит от типа
операционной системы. В большинстве случаев длина имени не превышает 90 байт.
Как использовать эту структуру, я объяснял выше.
8.2.3 Домен адресов af.inet
Как уже упоминалось ранее, домен адресов af_inet предназначен для организации
взаимодействий через Интернет (или через локальную сеть). Ниже приводится
описание структуры адресов из этого домена:
struct sockaddr_in — структура адресов из домена af_inet
«include <netinet/in.h>
struct scckaddr_in {
sa_farnily_t sin.farnily; /* AF.INET */
in_pcrt_t sin.pcrt; /* нсмер псрта (uint16_t) */
struct in.addr sin.addr; /* адрес IPv4 */
};
struct in.addr {
in_addr_t s_addr; /* адрес IPv4 (uint32_t) ♦/
};
526 ГЛАВА 8 Сетевое взаимодействие и сокеты
Адрес IPv4' (чаще его называют просто IP-адрес) представляет собой 32-битное
число, обозначающее адрес сетевого интерфейса. Для записи этого числа чаще всего
используется точечная нотация, в которой байты числа записываются отдельно друг
от друга и отделяются символом точки, например 216.10912570. Но и такой
способ записи адресов зачастую оказывается неудобным, поэтому, как правило,
используются описательные имена, например www.yahoo.com (см. раздел 8.2.5). Мы пока
оставим в стороне имена и разберемся с точечной нотацией записи адресов.
Каждый IP-адрес может быть связан с набором служб (сервисов), каждая из
которых имеет свой собственный номер порта. Значительная часть портов привязана
к определенным службам. Например, HTTP-серверы работают с портом 80, FTP-
серверы — с портом 21, серверы telnet — с портом 23, и т. д. Вы можете увидеть
соответствие служб и номеров портов в файле /etc/services. Для примера я
приведу часть содержимого этого файла (который насчитывает более 2000 строк) в моей
системе FreeBSD:
ftp
ftp
ssh
ssh
telnet
telnet
finger
finger
http
http
21/tcp
21/udp
22/tcp
22/udp
23/tcp
23/udp
79/tcp
79/udp
80/tcp
80/udp
«File Transfer [Control]
«File Transfer [Control]
«Secure Shell Login
«Secure Shell Login
www www-http «World Wide Web HTTP
www www-http «World Wide Web HTTP
Для номера порта и IP-адреса определены типы in_pcrt_t и in_addr_t, которые
фактически являются типами uint16_t и uint32_t соответственно, а порядок
следования байт в них должен соответствовать сетевому порядку. Это означает
необходимость использования функций htcns и htcni примерно таким образом:
struct scckaddr_in sa;
sa.sin.family = AF_I.NET;
sa.sin.pcrt = htcns(80);
sa.sin.addr.s.addr = htcnl((2l6UL « 24) + (109UL « 16) +
(125UL « 8) + 70UL); /* 216.109.125.70 */
Но для адресов, записываемых в точечной нотации, существует более удобная
функция, которая преобразует строку с адресом в целое число с сетевым порядком
следования байт:
' Internet Protocol версии 4.
ГЛАВА 8 Сетевое взаимодействие и сокеты 527
inetaddr — преобразует строку
тации, в целое число
«include <arpa/inet.h>
in_addr_t inet_addr(
ccnst char *cp /»
);
/* Возвращает IP-адрес
переменней еггпс не сп
IP-адрес
в виде (
ределенс)
с IP-адресом,
в течечней
in_addr_t)
*/
записанным в точечной
нстации
или
-1 в
•/
случае
сшибки
но-
(значение
Возвращаемое значение -1, приведенное к типу in_addr_t, в точности
соответствует IP-адресу 255.255255-255. Но это вполне приемлемо, так как такой адрес
считается неправильным при любых условиях.
Функция, выполняющая обратное преобразование:
inet_ntoa — преобразует целое число,
«include <arpa/inet.h>
char *inet_ntca(
struct in_addr in /*
);
/* Возвращает стрску •/
целее
числе
представляющее адрес IPv4,
представляющее
адрес */
в строку
Вместо функций inet_addr и inet_ntca вы можете использовать более
универсальные функции inet_ntcp и inet_ptcn (см. раздел 8.9.5), которые предоставляют
возможность обработки адресов IPv6.
Вернемся к нашему примеру и заменим функцию htcnl на inet_addr и добавим код,
который будет выполнять соединение с HTTP-сервером на порте 80, запрашивать
Web-страницу и отображать то, что будет принято от сервера:
«define REQUEST "GET / HTTP/1.0\r\n\r\n"
int main(vcid)
{
struct scckaddr_in sa;
int fd_skt;
char buf[1000];
ssize_t nread;
sa.sin_family = AF_INET;
sa.sin_pcrt = htcns(80);
sa.sin_addr.s_addr = inet_addr("216.109.125.70");
ec_neg1( fd_skt = sccket(AF_INET, S0CK_STREAM, 0) )
ec_neg1( ccnnect(fd_skt, (struct scckaddr *)&sa, sizecf(sa)) )
ec_neg1( write(fd_skt, REQUEST, strlen(REQUEST) ) )
ec_neg1( nread = read(fd_skt, buf, sizecf(buf)) )
(vcid)write(STD0UT_FILEN0, buf, nread);
528 ГЛАВА 8 Сетевое взаимодействие и сокеты
ec_neg1( clcse(fd_skt) )
exlt(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Запрос представляет собой команду GET, которая определена протоколом HTTP (см.
раздел 8.4.2). Ниже приводится часть того, что вывела программа, здесь вы без
труда узнаете начальную страничку Yahoo:
НТТР/1.1 200 0К
Oate: Sat, 19 Jul 2003 18:51:56 GMT
P3P: policyref="http://p3p.yahoc.ccm/w3c/p3p.xml", CP="CA0 DSP COR CUR A0M_
Cache-Centrol: private
Connection: close
Content-Type: text/html
<html><head>
<tltle>Yahccl</title>
8.2.4 Домен адресов AF_INET6
Если бы можно было использовать все 32 бита, IPv4 мог бы адресовать 4,3
миллиарда сетевых узлов. На сегодняшний день этого количества пока достаточно.
Однако из-за способа назначения адресов IPv4 это количество на самом деле много
меньше. Примерно в 1992 году было сделано предположение, что проблема
нехватки адресов начнет проявляться уже к середине 1995 года. Еще одна проблема
связана с «раздуванием» таблиц маршрутизации на магистральных
маршрутизаторах Интернета.
В результате была предпринята попытка разработать версию 6 протокола IP — IPv6,
который наряду с другими улучшениями увеличил размер адреса с 4 до 16 байт,
что позволяет адресовать до 2128 узлов сети."
Адреса IPv6 записываются как 8 групп по 2 байта в каждой, например: FEDC:BA98:
7654:3210:FEDC:BA98:7654:3210.
При создании сокетов из домена адресов AF_INET6 вы должны использовать
структуру sockaddr_in6, которая включает немного больше полей, чем ее
предшественница sookaddr_in:
' Это число превышает количество атомов во Вселенной! При таком количестве на каждый
квадратный метр поверхности Земного Шара приходится 665 секстиллионов (миллиардов
триллионов) адресов!
ГЛАВА 8 Сетевое взаимодействие и сокеты 529
struct sockaddr_in6 — структура адресов из домена AF_INET6
«include <netinet/in.h>
struct scckaddr_in6 {
sa_faraily_t sin6_faraily;
in_pcrt_t sin6_pcrt;
uint32_t sin6_flcwinfc;
struct in6_addr sin6_addr;
uint32_t sin6_sccpe_id;
>;
struct in6_addr {
uint8_t s6_addr[16];
};
/* AF_INET6 ♦/
/* нсмер псрта (uint16_t) */
/* класс трафика и информация с пстске */
/* адрес IPv4 •/
/* набср интерфейсов в сбласти видимости */
/* адрес IPv6 */
В этой структуре появились два новых поля, которые не имеют эквивалентов в
структуре scckaddr_in. Это поле sin6_flcwinfc, которое предназначено для хранения
«метки» потока и приоритета (в настоящее время пока не определено), a sin6_sccpe_id
идентифицирует набор интерфейсов для использования с определенными адресами
(зависит от реализации). Если вы предполагаете использовать эту структуру в
своих приложениях, инициализируйте поле sin6_sccpe_id значением 0. Вообще, перед
установкой фактических значений следует инициализировать нулями всю
структуру, потому что некоторые реализации предусматривают ряд дополнительных
полей, не описанных здесь.
На практике, прямая инициализация структуры scckaddr.ina производится
достаточно редко, потому что для этих целей служит вспомогательная функция getaddrinfc
(см. раздел 8.2.6).
Не допускается использовать функции inet_addr и inet.ntca с адресами IPv6.
Вместо них следует использовать функции inet.ntcp и inet_ptcn (см. раздел 8.9-5).
8.2.5 Доменная система именования (DNS)
Вам наверняка никогда не приходилось в адресной строке обозревателя
Интернета или FTP-клиента набирать непосредственно IP-адрес — практически всегда вы
набираете имя сервера, например: wwwyaboo.com или www.basepath.com. Имена узлов
сети транслируются в IP-адреса благодаря наличию всемирной распределенной базы
данных имен, которая называется DNS. Для доступа к DNS из приложений в ОС UNIX
существует ряд специализированных функций. Самой новой и самой
универсальной является getaddrinfc, мы будем использовать ее в наших примерах. Краткие
описания некоторых устаревших функций (таких как gethcstbyname) вы найдете в
разделе 8.8.1.
530 ГЛАВА 8 Сетевое взаимодействие и сокеты
В адресной строке, например http://www.basepatb.com/aup (адрес домашней
страницы сайта данной книги), имя сетевого узла представлено средней частью. Таким
образом, в обобщенном виде адрес записывается как':
scheme://hcstname/path
где scheme определяет способ доступа к ресурсу (например HTTP или FTP), hostname
— имя сетевого узла в том виде, в каком оно представлено в базе DNS, a path — путь
в пределах файловой системы сетевого узла.
В наших примерах мы использовали сокеты для того, чтобы установить
соединение с сетевым узлом, все дальнейшее взаимодействие осуществлялось в соответствии
со схемой (scheme). Например, в разделе 8.2.3 использовалась схема HTTP,
поэтому мы передавали HTTP-запрос
GET / НТТР/1.0
серверу, который интерпретировал его как просьбу выслать нам Web-страницу. Как
передается часть адреса path, зависит от используемой схемы. Для HTTP — это
аргумент команды GET (в нашем примере — первый символ «/»)• Сами сокеты никак
не беспокоятся ни по поводу пути к ресурсу, ни по поводу схемы.
В этой книге мы не будем рассматривать HTTP, HTML, FTP и вообще что-нибудь так
или иначе связанное со схемой взаимодействия — только самый минимум, который
необходим для понимания примеров. Лучший способ изучения разнообразных схем
взаимодействий — прочитать соответствующие RFC (Request for Comments) [RFC].
8.2.6 Вызов getaddrinfo
Вместо ручного заполнения структуры scckaddr_in и scckaddr_in6 гораздо проще
воспользоваться вспомогательной функцией getaddrinfc:
getaddrinfo — возвращает
«include <sys/sccket.h>
«include <netdb.h>
int getaddrinfc(
const char «ncdename,
const char *servname,
const struct addrinfc
информацию
•hint,
struct addrinfc **infcp
);
/* Возвращает О в случае
не определено) */
успеха
/•
/*
Л
Л
Л
или
имя
имя
об адресе сокета
узла */
службы (сервиса)
подсказка */
возвращаемые сведения
связанного описка */
код
сшибки (значение
*/
в
виде
•/
переменной еггпс
' Это упрощенное представление. Полный синтаксис URI намного сложнее.
ГЛАВА 8 Сетевое взаимодействие и сокеты 531
struct addrinfo — структура для getaddrinfc
struct addrinfc {
int ai_flags;
int ai_family;
int ai_sccktype;
int ai_prctcccl;
sccklen_t ai_addrlen;
struct scckaddr *ai_addr
char *ai_cancnname;
struct addrinfc *ai_next;
};
/* флаги •/
/* семействе (дсмен) адресов */
/* тип сскета */
/* прстсксл */
/* длина адреса */
/* адрес */
/* кансническсе название службы (сервиса) */
/* указатель на следующую структуру в списке */
Как правило, в аргументе ncdename передается имя сетевого узла, в servnarae —
номер порта (в виде строки), а в аргументе hint — предполагаемые домен адреса и
тип сокета. Обратно через параметр infep вы получаете связанный список
структур addrinfc, которые содержат информацию об адресе и прочие сведения,
которые соответствуют аргументу hint. Вам остается только выбрать нужную структуру
и передать поле ai_addr в вызов connect или bind, попутно приведя его к типу struct
scckaddr*.
В качестве «имени узла» может быть передано имя узла, которое присутствует в базе
DNS или в локальном файле /etc/hosts, адрес IPv4 в точечной нотации (в виде строки)
или адрес IPv6 в двухточечной нотации (также в виде строки).
Если поле ai_f lag имеет значение AI_PASSIVE, это означает, что возвращаемые
адреса предназначены для использования в вызове accept, т. е. функция getaddrinfc
вызывается на сервере. В случае со сброшенными флагами в вызове connect,
возвращаются адреса для использования на стороне клиента. Для протоколов, не
требующих установления логического соединения (например SOCK_DGRAH), эта
структура может использоваться в вызовах sendtc и sendmsg.
Функция getaddrinfc возвращает свои коды ошибок, которые нельзя
интерпретировать с помощью функций perrcr или strerrcr. Вместо них следует использовать
функцию gai_strerrcr, которая используется для интерпретации кодов ошибок,
возвращаемых getaddrinfc и getnameinfc (см. раздел 8.8.1):
gai
_strerror — возвращает строку с сообщен
«include <netdb.h>
censt char *gai_strerror(
int cede /* кед сшибки */
);
/* Возвращает строку с ссебщением
сб сшибке
ием
•/
об
ошибке
В наших примерах мы будем использовать макрос ec_ai, который «знает», как
интерпретировать коды ошибок, возвращаемые функциями getaddrinfc и getnameinfc.
Таким образом, мы не будем обращаться к gai.strerrcr напрямую.
532 ГЛАВА 8 Сетевое взаимодействие и сокеты
Ниже приводится небольшой пример использования функции getaddrinfc:
int main(vcid)
{
struct addrinfc *infcp = NULL, hint;
memset(&hint, 0, sizecf(hint));
hint.ai_famiiy = AF_INET;
hint.ai.sccktype = SOCK.STREAM;
ec_ai( getaddrinfc("www.yahcc.ccra", "80", ihint, &infcp) )
fcr ( ; infcp 1= NULL; infcp = infcp->ai_next) {
struct scckaddr_in *sa = (struct scckaddr_in *)infcp->ai_addr;
printf("Xs псрт; Xd прстсксл: Xd\n", inet_ntca(sa->sin_addr),
ntchs(sa->sin_pcrt), infcp->ai_prctccci);
}
exit(EXIT_SUCCESS);
EC_CLEANUP_B0N
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Результат работы программы:
66.218.70.48 псрт: 80 прстсксл: 0
66.218.70.49 псрт: 80 прстсксл: 0
66.218.71.88 псрт: 80 прстсксл: 0
66.218.71.86 псрт: 80 прстсксл: 0
66.218.70.50 псрт: 80 прстсксл: 0
66.218.71.91 псрт: 80 прстсксл: 0
66.218.71.80 псрт: 80 прстсксл: 0
66.218.71.84 псрт: 80 прстсксл: 0
66.218.71.90 псрт: 80 прстсксл: 0
66.218.71.93 псрт: 80 прстсксл: 0
66.218.71.94 псрт: 80 прстсксл: 0
66.218.71.92 порт: 80 протокол: 0
66.218.71.89 псрт: 80 прстскол: 0
Из этого примера видно, что имя www.ycihoo.com соответствует нескольким
IP-адресам. Благодаря «подсказке» (структура hint) мы знаем, что адреса относятся к
домену AF.INET типа S0CK_STREAM, таким образом, можно утверждать, что любой из этих
адресов может использоваться для доступа Web-серверу. Теперь попробуем изменить
пример из раздела 8.2.3 так, чтобы он использовал функцию getaddrinfc:
«define REQUEST "GET / HTTP/1.0\r\n\r\n"
int main(vcid)
{
struct addrinfc *infcp = NULL, hint;
ГЛАВА 8 Сетевое взаимодействие и сокеты 533
int fd_skt;
char buf[1000];
ssize_t nread;
memset(&hint, 0, sizeof(hint));
hint.ai_famiiy = AF_INET;
hint.ai_sccktype = S0CK_STREAM;
ec_ai( getaddrinfc("www.yahcc.ccm", "80", &hint, &infcp) )
ec_neg1( fd_skt = sccket(infcp->ai_famiiy, infcp->ai_socktype,
infcp->ai_prctccci) )
ec_neg1( ccnnect(fd_skt, (struct scckaddr *)infcp->ai_addr,
infcp->ai_addrien) )
ec_neg1( write(fd_skt, REQUEST, strien(REQUEST) ) )
ec_neg1( nread = read(fd_skt, buf, sizecf(buf)) )
(vcid)write(STDQUT_FILENQ, buf, nread);
ec_neg1( clcse(fd_skt) )
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
В результате запуска этой программы мы получили те же результаты, что и в
разделе 8.2.3-
Если ваша система, сервер DNS и тестируемый хост позволяют, можете попробовать
использовать getaddrinf о для получения доступа к сокетам другого типа (не S0CK_STREAM
и не из домена AF_INET) на порте 80. Для того чтобы обнаружить все доступные
сокеты, в качестве аргумента ncdename или servname можно передать пустой указатель
NULL (но только в одном из аргументов, ни в коем случае не в обоих), а в качестве
домена адресов указать AF.UNSPEC. Затем можно организовать поиск по полученным
структурам и выбрать из них наиболее подходящую. Например, можно попробовать
отыскать и использовать сокет из домена адресов AF_INET6 (IPv6). За
дополнительной информацией по использованию функции getaddrinfc обращайтесь к [SUS2002],
к системной документации или к главе 11 из [Ste2003].
Прежде чем завершить этот раздел, хочу сделать одно важное замечание: когда
список структур, полученный от getaddrinfc, станет ненужным, его следует удалить
с помощью функции freeaddrinfc:
freeaddrinfo — освобождает память,
«include <sys/sccket
«include <netdb.h>
vcid freeaddrinfc(
struct addrinfc
);
h>
»infcp /*
занимаемую списком
удаляемый
списск */
структур
534 ГЛАВА 8 Сетевое взаимодействие и сокеты
Мы не обращались к этой функции в наших примерах, потому что это были очень
маленькие программы, которые все равно завершаются через короткое время
после вызова getaddrinfc.
8.2.7 Вызов gethostname
Иногда программам необходимо узнать сетевое имя компьютера, на котором они
запущены (например Web-сервер из раздела 8.4.4). Обычно им достаточно
универсального имени «localhost». Но, если предусматривается взаимодействие с другими
компьютерами, необходимо знать сетевое имя, назначенное системным
администратором. Получить сетевое имя своего компьютера можно с помощью функции
gethcstname:
gethostname — возвращает сетевое
«include <unistd
п>
int gethcstname(
char «name, /• всзвращаемсе
size_t namelen /* размер буфера
у
1* Всзвраща§т 0 е
переменней еггпс
случае успеха или
не спределенс) */
имя
имя
•/
-1
•/
в случае
сшибки
(седержимсе
Пример обращения к этой функции вы найдете в разделе 8.4.4.
8.3 Параметры сокетов
Основная причина, по которой системный вызов sccket так прост, состоит в том,
что все параметры сокетов настраиваются в отдельном системном вызове:
setsockopt — устанавливает
«include <sys/sccket.h>
int setscckcpt(
int sccket_fd,
int level,
int cpticn,
censt vcid «value,
sccklen_t value_len
Л
Л
Л
Л
/•
параметры сокета
файловый дескриптор сскета
уровень прстсксла */
устанавливаемый параметр */
значение параметра */
длина значения •/
/« Возвращает О в случае успеха или -1 в случае сшибки
в переменней еггпс) */
*/
(кед
ошибки -
Четыре аргумента из пяти практически не требуют дополнительных пояснений:
sccket_fd — файловый дескриптор сокета, cpticn — имя (код) параметра, value —
указатель на присваиваемое значение и value_len — размер значения, которое
содержится в value. В качестве значений обычно выступают целые числа, но могут
использоваться и структуры.
ГЛАВА 8 Сетевое взаимодействие и сокеты 535
Второй аргумент, level, идентифицирует уровень протокола, которому
предназначается указанный параметр. Для манипуляции параметрами на уровне сокета
задается уровень S0L_S0CKET и, скорее всего, вы будете использовать этот уровень
значительно чаще других. В [SUS2002] определено еще б уровней протоколов
(определения находятся в заголовочном файле <netinet/in.h>):
ipproto.ip Протокол IP.
IPPR0T0_IPV6 Протокол IP версии 6.
IPPH0T0.ICMP Протокол управляющих сообщений (ICMP).
IPPR0T0_RAW Низкоуровневый сетевой протокол.
IPPR0T0_TCP Протокол управления передачей (TCP).
IPPR0T0_UDP Протокол обмена дейтаграммами (UDP).
В [SUS2002] определено несколько параметров для каждого из шести протоколов,
но, как правило, некоторые реализации дополняют их своим набором параметров.
К сожалению, перечислить все возможные параметры довольно сложно, поэтому
вам придется самостоятельно изучить документацию, соответствующую
используемому протоколу. Например, в ОС Solaris для получения сведений о параметре
IPPROTO.IP можно дать команду man ip.
Здесь же мы будем рассматривать только параметр S0L_S0CKET, за информацией по
остальным параметрам — обращайтесь к системной документации.
Получить параметры настройки сокета можно с помощью функции getsookopt,
которая имеет практически тот же набор входных аргументов:
getsockopt — возвращает параметры сокета
#inolude <sys/sooket
int getsookopt(
int sooket_fd,
int level.
int option,
void *value,
sooklen_t *value.
);
/* Возвращает 0 в ол}
в переменной errno) <
h>
_1еп
мае
Ч
/•
/•
/•
/•
/•
файловый деокриптор сокета
уровень протокола */
возвращаемый
возвращаемое
длина буфера
уопеха или -1 в
параметр
значение
*/
Ч
*/
параметра
олучае ошибки
(код
•/
ошибки -
Самое важное отличие getsockopt от setsockopt в том, что последний аргумент в
функции getsockopt, value_len, представляет собой указатель на переменную, в
которой должно находиться значение длины приемного буфера. По возвращении из
функции по заданному адресу будет записан фактический размер значения
параметра.
Ниже приводится очень короткий пример установки параметра SO_REUSEADDR (что
это означает, я объясню ниже):
int socket_option_value = 1;
18 3ак. 4030
536 ГЛАВА 8 Сетевое взаимодействие и сокеты
ec_neg1( setscckcpt(fd, S0L_S0CKET, SO.REUSEADDR,
&sccket_cpticn_value, sizecf(sccket_cpticn_value)) )
Я коротко опишу все переносимые параметры уровня S0L_S0CKET. Чаще всего в
качестве значений параметров выступают целые числа, в этом случае в аргументе value
должен передаваться указатель на тип int, а в value_len — значение sizecf (int). Булевы
значения передаются в виде целых чисел, где 1 означает true, 0 — false. He
используйте константу true, поскольку она может иметь значение -1. Для языка С — это
почти одно и то же (имеются в виду булевы выражения), но совершенно не
подходит для функции getscckcpt. В следующем списке меткой В обозначаются булевы
параметры, меткой I — целые, X — типы, которые будут описаны ниже. Маленький
символ, следующий за символом метки, обозначает: s используется при обращении
к setscckcpt, g — при обращении к getsockcpt.
Влияют ли какие-либо параметры на поведение сокета (например SO.KEEPALIVE) —
зависит от типа используемого протокола и его конкретной реализации.
SO.ACCEPTCONN Сокет предназначен для приема запросов на
соединение, то есть, было обращение к системному вызову
listen (Bg).
S0.BR0A0CAST Допускается передача широковещательных
сообщений, если они поддерживаются протоколом (Bsg).
SO.DEBUG Запись отладочной информации (Bsg).
S0_D0NTR0UTE Передавать сообщения в обход процедуры
маршрутизации прямо на сетевой интерфейс согласно адресу
назначения (Bsg).
S0_ERR0R Сокет в состоянии ошибки, которое сбрасывается
после получения этого параметра (Ig).
SO.KEEPALIVE Поддерживать соединение в активном состоянии,
периодически передавая через него служебные
сообщения. Если ответа получено не будет, связь
разрывается (запись в сокет с разорванным соединением
приводит к генерации сигнала SI6PIPE) (Bsg).
S0_LINGER Если включен и имеются не переданные сообщения,
системный вызов close блокируется до окончания
передачи или пока не истечет время задержки (Xsg).
Определение структуры linger:
struct linger {
int l_cncff; /* 1 - включенс, 0 - выключено */
int l_linger; /* время задержки в секундах */
};
S0_00BINLINE Принятые внеочередные (срочные) сообщения
помещаются прямо во входной поток. В противном случае
внеочередные сообщения можно получить только при
включенном флаге HSG_0OB (Bsg).
S0_RCVBUF Размер приемного буфера (Isg).
ГЛАВА 8 Сетевое взаимодействие и сокеты 537
SO.RCVLOWAT Нижняя граница объема принимаемых данных.
Операции приема данных (например read) блокируются
до тех пор, пока не будет получен минимальный
объем. По умолчанию — 1 байт (Isg).
S0.RCVTIME0 Максимальное время ожидания (структура timeval, см.
раздел 1.7.1) завершения заблокированной операции
приема данных. Значение времени 0 (по умолчанию)
равносильно отсутствию ограничений. По истечении
заданного интервала времени в вызывающую
программу возвращает число принятых байт или
признак ошибки С КОДОМ EAGAIN ИЛИ EWOULOBLOCK (Xsg).
S0.REUSEA00R Разрешает повторное использование локальных
адресов в системном вызове bind. В противном случае bind
будет возвращать признак ошибки с кодом EAODRINUSE,
если ранее, не позднее предопределенного системой
срока, сокет с таким адресом уже был создан. Очень
удобно при тестировании и отладке. (Bsg)
S0.SN0BUF Размер буфера передачи (Isg).
S0.SN0L0WAT Нижняя граница объема передаваемых данных.
Операции передачи данных (например write) не
блокируются, если объем передаваемых данных ниже
минимального объема. По умолчанию — 1 байт (Isg).
S0.SN0TIHE0 Максимальное время ожидания (структура timeval, см.
раздел 1.7.1) завершения заблокированной операции
передачи данных. Значение времени 0 (по умолчанию)
равносильно отсутствию ограничений. По
истечении заданного интервала времени в вызывающую
программу возвращает число переданных байт или
признак ошибки с кодом EAGAIN или EWOULOBLOCK (Xsg).
S0.TYPE Тип сокета, например SOCK.STREAM.
С целью демонстрации getsookopt в действии ниже приводится пример
программы, которая выводит значения параметров для сокетов некоторых типов:
typedef enum {0T.INT, 0T_LINGER, OT.TIHEVAL} OPTJTYPE;
statio void show(int skt, int level, int option,
OPTJYPE type)
{
sookIen_t Ien;
int n;
struot linger Ing;
struot timeval tv;
oonst ohar «name,
switoh (type) {
oase 0T_INT:
Ien = sizeof(n);
if (getsookopt(skt, level, option, &n, &Ien) == -1)
printfC'Xs ОШИБКА (Xs)\n", name, strerror(errno));
else
538 ГЛАВА 8 Сетевое взаимодействие и сокеты
printf("Xs = Xd\n", name, n);
break;
case 0T_LINGER:
Ien = sizecf(Ing);
if (getscckcpt(skt, level, cpticn, ilng, ilen) == -1)
printf("Xs ОШИБКА (Xs)\n", пале, strerrcr(errnc));
else
printf("Xs = I_cncff: Xd; I_Iinger: Xd сек.\п", name,
Ing.I_cncff, Ing.I.Iinger);
break;
case OT.TIMEVAL:
Ien = sizecf(tv);
if (getscckcpt(skt, level, cpticn, itv, &Ien) == -1)
printf("Xs ОШИБКА (Xs)\n", name, strerrcr(errnc));
else
printf("Xs = Xld сек.; Xld usees.\n", name,
(lcng)tv.tv_sec, (Icng)tv.tv_usec);
static vcid shcwall(int skt,
\
}
int
i
printf("\nXs\n", caption)
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
shcw(skt,
main(vcid)
int skt;
S0L_S00KET,
S0L.S00KET,
S0L.S00KET,
S0L.S00KET,
S0L.S00KET,
S0L.S00KET,
S0L_S00KET,
S0L.S00KET,
S0L_S00KET,
S0L_S00KET,
S0L.S00KET,
S0L.S00KET,
SOL.SOOKET,
SOL.SOOKET,
SOL.SOOKET,
SOL.SOOKET,
so.
SO.
so_
SO.
so_
so.
so.
so.
so.
so.
so.
so_
so_
so.
so.
so.
const char «caption)
.A00EPT00NN, "S0_A00EPT00NN", OT.INT);
.BR0A00AST, "S0.BR0A00AST", OT.INT);
.OEBUO, "SO.OEBUG", OT.INT);
.00NTR0UTE, "SO.OONTROUTE", OT.INT);
.ERROR, "SO.ERROR", OT.INT);
.KEEPALIVE, "SO.KEEPALIVE", OT.INT);
.LINOER, "SO.LINGER", OT.LINGER);
.OOBINLINE, "SO.OOBINLINE", OT.INT);
.ROVBUF, "SO.ROVBUF", OT.INT);
.ROVLOWAT, "S0.R0VL0WAT", OT.INT);
.ROVTIMEO, "S0.R0VTIME0", OT.TIMEVAL);
.REUSEAOOR, "SO.REUSEAOOR", OT.INT);
.SNOBUF, "SO.SNOBUF", OT.INT);
.SNOLOWAT, "SO.SNOLOWAT", OT.INT);
.SNOTIMEO, "SO.SNOTIMEO", OT.TIMEVAL);
TYPE, "SO.TYPE", OT.INT);
ec_neg1( skt = sccket(AF_INET, SOOK.STREAM,
shcwall(skt, "AF.INET SOOK.STREAM");
0) )
ГЛАВА 8 Сетевое взаимодействие и сокеты
539
ec_neg1( clcse(skt) )
ec_neg1( skt = sccket(AF_INET, S00K.00RAM, 0) )
shcwall(skt, "AF_I.NET SOOK.DGRAM");
ec_neg1( clcse(skt) )
exlt(EXIT_SUOOESS);
E0_0LEANUP_BGN
exlt(EXIT_FAILURE);
EO.OLEANUP.ENO
}
В ОС Solaris были получены следующие результаты:
AF_INET S00K.STREAM
S0.A00EPT00NN = 0
S0.BR0A00AST = 0
SO.DEBUO = 0
SO.DONTROUTE = 0
SO.ERROR = 0
SO.KEEPALIVE = 0
SO.LINOER = l_cncff: 0; l_linger: 0 сек.
SO.OOBINLINE = 0
SO.ROVBUF = 65536
SO.ROVLOWAT ОШИБКА (Option net supported by protocol)
SO.ROVTIMEO ОШИБКА (Option net supported by protocol)
SO.REUSEAOOR = 0
SO.SNDBUF = 65536
S0_SNDL0WAT ОШИБКА (Option net supported by protocol)
SO.SNOTIMEO ОШИБКА (Option net supported by protocol)
SO.TYPE = 2
AF.INET S00K.D0RAM
S0_A00EPT00NN = 0
S0_BR0A00AST = 0
SO.DEBUO = 0
S0_00NTR0UTE = 0
SO.ERROR = 0
SO.KEEPALIVE = 0
S0_LINGER = l_cncff: 0; l.linger: 0 сек.
SO.OOBINLINE = 0
S0_RCVBUF = 65536
SO.RCVLOWAT ОШИБКА (Option net supported by protocol)
S0_RCVTIME0 ОШИБКА (Option net supported by protocol)
SO.REUSEADDR = 0
SO.SNDBUF = 65536
SO.SNDLOWAT ОШИБКА (Option net supported by protocol)
SO.SNDTIMEO ОШИБКА (Option not supported by protocol)
SO.TYPE = 1
540 ГЛАВА 8 Сетевое взаимодействие и сокеты
8.4 Упрощенный интерфейс сокетов
Чтобы избежать прямого обращения к системным вызовам getaddrinfo, sooket, bind,
oonneot и прочим, так или иначе связанным с обслуживанием сокетов, мы
разработаем высокоуровневый «Упрощенный Интерфейс Сокетов» (Simple Socket Interface,
SSI), за которым «спрячем» все тонкости и сложности работы с сокетами.
8.4.1 Функции упрощенного интерфейса
Реализация интерфейса предполагает хранение состояния соединения в
структуре с типом SSI, которую мы вскоре обсудим. Программа будет получать указатель
на структуру при открытии соединения функцией ssi.open и передавать ее в ssi.olose
при закрытии соединения:
ssi_open — открывает соединение SSI
SSI *ssi_open(
oonst ohar *name, /* имя оервера */
bool server /* вызов производится на отороне оервера? */
);
I* Возвращает указатель на SSI или NULL в олучае ошибки (код ошибки -
в переменной еггпо) */
ssi_close — закрывает соединение SSI
bool ssi_oIose(
SSI *ssip /* указатель
);
/* Возвращает true в олучае
в переменной еггпо) */
на SSI */
уопеха или
false в
олучае
ошибки
(код
ошибки -
Функция ssi.open конструирует адресную информацию для сокета (например, с
помощью getaddrinfo) и обращается к системным вызовам sooket, bind, listen и oonneot.
Имя сервера, передаваемое в ssi.open, трактуется как адрес из домена AF.INET, если
оно начинается с комбинации символов «//» (которая не является частью имени).
За адресом должен идти номер порта, отделенный символом двоеточия. Если имя
не начинается с комбинации «//», оно будет воспринято как адрес из домена AF.UNIX.
Несколько примеров:
//www.basepath.ooni:80 AF.INET-ооединение о оетевым узлом www.basepath.oom,
о портом 80
//fireoraoker:31000 AF.INET-ооединение о оетевым узлом fireoraoker
о портом 31000
//216.109.125.43:21 AF.INET-ооединение о оетевым узлом 216.109.125.43
с портом 21
MyServer AF.UNIX-соединение
ГЛАВА 8 Сетевое взаимодействие и сокеты 541
Обращением к функции ssi_wait_server сервер ожидает готовности файлового де-скрип-
тора сокета клиента. Эта функция обращается к системным вызовам select и accept:
ssi_wait_server — ожидает запрос на соединение от клиента
int ssi_wait_server(
SSI «ssip /* указатель на SSI
);
/• Возвращает файловый дескриптср
в переменней errnc) */
*/
или
-1 в случае
сшибки
(кед сшибки -
За получением файлового дескриптора сокета, соединенного с сервером, клиент
обращается к функции ssi_get_server_fd:
ssijjet
ента
int
);
/•
_server_fd — возвращает файловый дескриптор сокета на
ssi_get_server_fd(
SSI «ssip /• указатель на SSI
Зсзвращает файловый дескриптср
в переменней errnc) */
•/
или
-1 в случае
сшибки
(кед
стороне
сшибки -
кли-
В заключение, при получении от клиента признака конца файла, сервер вызывает
функцию ssi_cicse_fd, поскольку в этот момент он точно знает, что дескриптор
больше не нужен:
ssi_close_fd — закрывает файловый
bcci ssi_cicse_fd(
SSI «ssip, /♦ указатель
int fd /* файловый
);
/• Всзвращает true в случае
в переменней errnc) */
дескриптор
на SSI */
дескриптср
успеха
или
*/
faise в
на
стороне
случае
ошибки
клиента
(кед сшибки -
Этих функций будет вполне достаточно для реализации несложных приложений
клиентов и серверов с использованием сокетов (SOCK.STREAM) из доменов AF.UNIX и
AF.INET. Мы увидим это на примерах простейших Web-сервера и Web-браузера. После
которых перейдем к рассмотрению реализации интерфейса SSI.
8.4.2 Краткое введение в протокол HTTP
HTTP — протокол передачи гипертекстовой информации (Hypertext Transfer
Protocol). Полное описание протокола вы найдете в документе под названием RFC 2616
на сайте [RFC]. Здесь будет приведено лишь очень краткое его описание, но
вполне достаточное, чтобы разобраться в примерах программ.
В терминах HTTP клиент обычно называется Web-браузер, а сервер — как Web-
сервером. После установления соединения, клиент инициирует обмен передачей
серверу строки запроса следующего вида:
542 ГЛАВА 8 Сетевое взаимодействие и сокеты
GET path HTTP/version\r\n\r\n
где path определяет запрашиваемый документ (например /index.html), a version —
предпочтительную версию протокола HTTP (например 1.0).
Сервер удостоверяется в наличии документа и проверяет обладает ли клиент
правом доступа к документу. Если документ не найден, сервер возвращает ответ в виде
строки:
НТТР/1.1 404 Not Found\r\n
за которой следует HTML-документ, поясняющий ошибку.
Если файл может быть передан клиенту, возвращается ответ в виде строки:
НТТР/1.1 200 0К\г\п
Каждому документу, будь то страничка HTML, текстовый файл, рисунок в формате
JPEG или что-то еще, предшествует заголовок, описывающий документ. В общем
виде (по крайней мере, в наших примерах) заголовок можно представить так:
Server: serveгпате\г\п
Content-Length: length\r\n
Content-Type: type\r\n\r\n
Строка servemame может быть чем угодно, в наших примерах она будет содержать
«AUP-ws». Параметр length определяет размер документа в байтах, что дает клиенту
возможность заранее узнать объем принимаемых данных. Параметр type — это, так
называемый MIME-тип (Multipurpose Internet Mail Extensions — многоцелевые
расширения почты Интернета). Из всего многообразия типов MIME мы будем
использовать только два: «text/html» и «image/jpeg». Вслед за заголовком следует
содержимое документа в том виде, в каком он находится в файловой системе сервера.
8.4.3 Web-браузер на основе SSI
Ниже приводится исходный код простого Web-браузера под названием minibr. Он
будет получать HTML-документы от сервера, но поскольку наш браузер не знает,
как обрабатывать теги HTML все полученные данные будут просто выводиться на
устройство стандартного вывода:
int main(void)
{
char url[100], s[500], «path = "", *p;
SSI *ssip;
int fd;
ssize_t nread;
while (true) {
printfCURL: ");
if (fgets(url, sizeof(url), stdin) == NULL)
ГЛАВА 8 Сетевое взаимодействие и сокеты 543
break;
if ((p = strrchr(url, '\n')) 1= NULL)
*p = '\0';
If ((p = strchr(url, '/')) != NULL) {
path = p + 1;
*p = '\0';
}
snprlntf(s, slzecf(s), "//4s:80", url);
ec_null( sslp s ssl_cpen(s, false) )
ec_neg1( fd = ssl_get_server_fd(sslp) )
snprlntf(s, slzecf(s), "GET /Xs HTTP/1.0\r\n\r\n", path);
ec_neg1( wrlteall(fd, s, strlen(s)) )
while (true) {
switch (nread = read(fd, s, slzecf(s))) {
case 0:
prlntf("KcHeu файла\п");
break;
case -1:
EC.FAIL
default:
ec_neg1( writeall(STD0UT_FILEN0, s, nread) )
ccntlnue;
}
break;
}
ec_false( ssi_clcse(ssip) )
}
ec_false( Iferrcr(stdin) )
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT.FAILURE);
EC_CLEANUP_END
}
Сразу после обращения к функции fgets полученный URL разбивается на имя узла
сети и имя документа (см. раздел 8.2.5). Затем в начало имени сервера добавляется
комбинация «//», а в конец — номер порта: 80. Полученная строка передается в
ssi_cpen. После установления соединения с сервером из имени документа
формируется строка запроса HTTP GET, которая передается серверу.
Ниже приводится пример типичного сеанса работы программы minibr (текст
получаемых страниц сокращен):
URL: www.basepath.ccni
HTTP/1.1 200 0К
Date: Sat, 19 Jul 2003 19:01:41 GMT
Server: Apache/1.3.27 (Unix) FrcntPage/5.0.2.2510 mcdjk/1.1.0
544 ГЛАВА 8 Сетевое взаимодействие и сокеты
Last-Modified: Thu, 15 May 2003 19:56:49 GMT
ETag: "61744-191-3ec3f101"
Acoept-Ranges: bytes
Oontent-Length: 401
Oonnection: olose
Oontent-Type: text/html
<ID00TYPE HTML PUBLIO "-//IETF//DTD HTML//EN">
<html>
<head>
URL: 216.109.125.70
HTTP/1.1 200 OK
Date: Sat, 19 Jul 2003 19:02:49 GMT
P3P: policyref="http://p3p.yahoo.com/w3o/p3p.xml", OP="OAO DSP OOR OUR ADM
Oaohe-Oontrol: private
Oonnection: close
Oontent-Type: text/html
<html><head>
<title>Yahoo!</title>
8.4.4 Web-сервер на основе SSI
Теперь настал черед простейшего Web-сервера. После получения запроса GET он
извлекает из него имя документа и отправляет клиенту строку ответа, заголовок
документа и сам документ в точном соответствии с процедурой, описанной в
разделе 8.4.2.
Начнем с ряда вспомогательных макроопределений:
«define HEADER\
"HTTP/1.0 *s\r\n"\
"Server: AUP-ws\r\n"\
"Oontent-Length: Xld\r\n"
«define 00NTENT_TEXT\
"Oontent-Type: text/html\r\n\r\n"
«define 00NTENT_JPEG\
"Oontent-Type: image/jpeg\r\n\r\n"
«define HTML_N0TF0UND\
"<ID00TYPE html PUBLIO \"-//IETF//DTD HTML 2.0//EN\">\n"\
"<html><headxtitle>Error 404</title>\n"\
ГЛАВА 8 Сетевое взаимодействие и сокеты 545
"</head><bcdy>\n"\
"<|12>3апрсшенный документ не найден на сервере AUP-ws</h2>"\
"</bcdyX/htral>\r\n"
Обратите внимание: макроопределение HEADER содержит спецификаторы формата,
которые будут замещаться кодом состояния и размером документа, в функции
send_header:
static vcid send_header(SSI *ssip, const char *msg, cff_t len,
ccnst char «path, int fd)
{
char buf[1000], «dot;
snprintf(buf, sizeof(buf), HEAOER, rasg, (long)len);
ec_neg1( writeall(fd, buf, strlen(buf)) )
dct = strrchr(path, '.');
if (dot != NULL && (strcasecrap(dct, ".jpg") == 0 ||
strcasecmp(dct, ".jpeg") == 0))
ec_neg1( writeall(fd, C0NTENT_JPEG, strlen(CONTENT_JPEG)) )
else
ec_neg1( writeall(fd, C0NTENT_TEXT, strlen(CONTENT_TEXT)) )
return;
EC_CLEANUP_BGN
EC_FLUSH("Ошибка передачи ответа (возмсжнс прсдслжение работы)")
EC_CLEANUP_END
}
В аргументе msg передается либо строка «404 Not Found», либо строка «200 ОК».
В аргументе len — размер документа. В аргументе path — имя документа, которое
используется только для того, чтобы определить его тип — изображение в
формате JPEG или файл в формате HTML. За передачу самого документа отвечает
функция, вызывающая send.header.
Обращение к функции send.header производится из handle.request, которая
обрабатывает запрос GET, поступивший от клиента:
«define 0EFAULT_00C "index.html"
«define WEB_R0OT "/aup/webrcot/"
static bccl handle_request(SSI *ssip, char *s, int fd)
{
char *tkn, buf[1000], path[500];
int ntkn;
FILE «in;
struct stat statbuf;
ssize_t nread;
546 ГЛАВА 8 Сетевое взаимодействие и сокеты
for (ntkn = 1; (tkn = strtok(s, " ")) l= NULL; ntkn++) {
s = NULL;
switoh (ntkn) {
oase 1:
if (stroaseomp(tkn, "get") l= 0) {
printf("HeM3BeoTHbiu запроо\п");
return faise;
}
oontinue;
oase 2:
break;
}
break;
}
snprintf(path, sizeof(path) - 1 - strien(0EFAULT_00C),
"XsXs", WEB_R00T, tkn);
if (stat(path, istatbuf) == 0 && S_ISOIR(statbuf.stjnode)) {
if (path[strien(path) - 1] l= '/')
stroat(path, "/");
stroat(path, OEFAULT.OOC);
}
if (stat(path, istatbuf) == -1 |I
(in = fopen(path, "rb")) == NULL) {
send_header(ssip, "404 Not Found", strien(HTML_N0TF0UN0), "",
fd);
eo_neg1( writeaii(fd, HTML.NOTFOUNO, strien(HTML.NOTFOUNO)) )
return faise;
}
send_header(ssip, "200 OK", statbuf.st.size, path, fd);
whiie ((nread = fread(buf, 1, sizeof(buf), in)) > 0)
eo_neg1( writeaii(fd, buf, nread) )
ec_eof( foiose(in) )
return true;
EC.CLEANUP.BGN
EC_FLUSH("0uni6i<a передачи ответа (возможно продолжение работы)")
return faise;
EC.CLEANUP.ENO
}
Цикл for проверяет действительно ли получен запрос GET, и извлекает из запроса
имя документа, который должен находиться в подкаталоге WEB_R00T. Это
гарантирует невозможность получения от сервера произвольного файла из любого места
в файловой системе. Если в качестве имени документа было передано имя
каталога, в конец его добавляется имя файла по умолчанию — index.html, в противном
случае предполагается, что запрошен конкретный файл в формате JPEG или HTML.
ГЛАВА 8 Сетевое взаимодействие и сокеты 547
Если запрошенный документ не найден возвращается заголовок с кодом
состояния 404, вслед за которым следует некоторый текст в формате HTML. Иначе
клиенту отправляется заголовок с кодом состояния 200 и запрошенный документ.
Обратите внимание: это может быть как файл в формате JPEG, так и текстовый
документ в формате HTML.
И, наконец, функция main, которая обращается к функциям SSI в процессе
обслуживания клиентов:
«define PORT ":8000"
int main(vcid)
{
SSI *ssip = NULL;
char msg[1600];
ssize_t nrcv;
int fd;
char hcst[100] = "//";
ec_neg1( gethcstname(&hcst[2], sizecf(hcst) - 2 - strlen(PORT)) )
strcat(hcst, PORT);
printf("Соединение с узлсм \"Xs\"\n", hcst);
ec_null( ssip = ssi_cpen(hcst, true) )
printf("\t...соединение устансвленс\п");
while (true) {
ec_neg1( fd = ssi_wait_server(ssip) )
switch (nrcv = read(fd, msg, sizecf(msg) - 1)) {
case -1:
printf("Ошибка чтения (всзмсжнс продолжение рабсты)\п");
case 0:
ec_false( ssi_clcse_fd(ssip, fd) )
continue;
default:
msg[nrcv] = '\0';
(vcid)handle_request(ssip, rasg, fd);
}
}
ec_false( ssi_clcse(ssip) )
printf("KcHeu.\n");
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
return EXIT.FAILURE;
EC_CLEANUP_END
Функция ssi.cpen формирует имя сервера:
//hcst:8000
548 ГЛАВА 8 Сетевое взаимодействие и сокеты
Мы использовали порт с номером 8000 потому, что на испытуемой системе порт
80 уже занят настоящим Web-сервером (Apache), а доступ к портам ниже 1024
ограничен суперпользователем.
После установления соединения управление передается в цикл, где выполняется
ожидание запроса от клиента, производится его чтение и обработка вызовом
функции handle.request. Если от клиента получен признак конца файла, соединение
просто закрывается, в этом случае вызывается ssi_close_fd.
Наш Web-сервер действительно работает! Вы можете проверить это с помощью
любого браузера, даже с помощью того, что был рассмотрен в предыдущем
разделе. Для примера на рис. 8.3 показан результат обращения по адресу http://suse2.8000:
' & Mm fit tat vw ицюу аи*т«ач w»w>» нчр «* »цгш
Рис. 8.3. Результат обращения к SSI Web-серверу из Web-браузера Safari
Содержимое файла /aup/webroot/index.html на сервере suse2 (имя suse2 было
выбрано потому, что сервер был скомпилирован и запущен под управлением SuSE
Linux):
<!D0CTYPE HTML PUBLIC "-//W3C//DTD HTML 3,2 Final//EN">
<html>
<head>
</head>
<body>
<h1>Marc's Bike</h1>
<pximg src="picture. jpg">
</body>
</html>
*
ГЛАВА 8 Сетевое взаимодействие и сокеты 549
8.4.5 Реализация SSI
Мы рассмотрели все вопросы, связанные с реализацией упрощенного интерфейса
сокетов (SSI), и даже изучили ряд примеров программ. С целью повторения
некоторых тем рекомендую просмотреть еще раз разделы 8.1.3, 8.2.2 и 8,2.6.
Определение типа SSI:
«define SSI_NAHE_SIZE 200
typedef struot {
bool ssi_server; /* оервер или клиент? */
int ssi.domain; /* AF_INET или AFJJNIX •/
int ssi_fd; /* файловый дескриптор оокета */
ohar ssi_name_server[SSI_NAME_SIZE]; /* имя оервера */
fd_set ssi_fd_set; /* набор деокрипторов »/
/* для оиотемного вызова seleot */
int ssi_fdjiwra; /* набольший номер деокриптора */
> SSI;
По мере изучения исходных кодов примеров мы увидим, как используются поля
этой структуры.
В первую очередь мы рассмотрим вспомогательную функцию создания структуры
с адресной информацией для сокета из доменов AF_UNIX или AF_INET. Через аргумент
паше в функцию передается строка вида «bost.port» для сокетов с адресом из домена
AF_INET и простое имя для сокетов с адресом из домена AFJJNIX. Аргумент will_bind
определяет, как будет использоваться адресная информация сокета для передачи в
системный вызов bind или oonneot, как это уже объяснялось в разделе 8.2.6.
bool make_sookaddr(struot sookaddr *sa, sookIen_t «Ien, oonst ohar *name,
int domain, bool will_bind)
{
struot addrinfo «infop = NULL;
if (domain == AFJJNIX) {
struot sockaddr_un *sunp = (struot sockaddr_un »)sa;
if (strlen(name) >= sizeof(sunp->sun_path)) {
errno = ENAMET00L0NG;
EC.FAIL
}
stropy(sunp->sun_path, name);
sunp->sunj=amily = AFJJNIX;
•Ien = sizeof(*sunp);
}
else {
struot addrinfo hint;
char nodename[SSI_NAMEJ5IZE], «servicename;
550 ГЛАВА 8 Сетевое взаимодействие и сокеты
memset(&hint, 0, sizeof(hint))
hint.ai_famiiy = domain;
hint.ai_sooktype = SOCK.STREAM
if (wiii_bind)
hint.ai.fiags = Ai.PASSIVE
stropy(nodename, name);
servicename = strohr(nodename, ':');
if (servioename == NULL) {
errno = EiNVAL;
EC.FAiL
>
♦servioename = '\0'I
servioename++;
eo_ai( getaddrinfo(nodename, servioename, ihint, iinfop) )
memopy(sa, infop->ai_addr, infop->ai_addrien);
*len = infop->ai_addrlen;
freeaddrinfo(infop);
}
return true;
EC.CLEANUP.BGN
if (infop != NULL)
freeaddrinfo(infop);
return faise;
EC.CLEANUP.END
}
Я намеренно предусмотрел независимость функции make_sockaddr от структуры SSI,
на тот случай, если вы захотите использовать ее в своих программах.
Далее следуют две функции, которые вызываются из ssi.open и ssl.close для
обслуживания поля, хранящего наибольший номер файлового дескриптора:
static void set_fd_hwm(SSI *ssip, int fd)
{
if (fd > ssip->ssi_fd_hwm)
ssip->ssi_fd_hwm = fd;
}
static void reset_fd_hwm(SSI *ssip, int fd)
{
if (fd == ssip->ssi_fd_hwm)
ssip->ssi_fd_hwm-;
}
Теперь мы с вами готовы перейти к реализации функции ssi.open, которая
впрочем, не содержит ничего такого, о чем бы мы еще не говорили:
ГЛАВА 8 Сетевое взаимодействие и сокеты
SSI *ssi_cpen(ccnst char *name_server, bed server)
{
SSI *ssip = NULL;
struct scckaddr_stcrage sa;
sccklen_t sa_len;
ec_null( ssip = callcc(1, sizecf(SSI)) )
sslp->ssi_server = server;
if (strncmp(nanie_server, "//", 2) = 0) {
sslp->ssl_dcmain = AF_INET;
nanie_server += 2;
}
else {
ssip->ssl_dcmain = AFJJNIX;
if (ssip->ssi_server)
(vcid)unllnk(name_server);
>
if (strlen(nanie_server) >= sizecf(ssip->ssi_name_server)) {
errnc = ENAMET00L0NG;
EC.FAIL
>
strcpy(ssip->ssi_name_server, nanie_server);
ec_false( make_scckaddr((struct scckaddr *)&sa, &sa_Ien,
ssip->ssi_nanie_server, ssip->ssi_dcmain, ssip->ssl_server) )
ec_neg1( ssip->ssl_fd = sccket(ssip->ssl_dcnialn, S0CK_STREAM, 0) )
set_fd_hwm(ssip, ssip->ssi_fd);
if (ssip->ssi_dcnialn == AF_INET) {
int sccket_cptlcn_value = 1;
ec_neg1( setscckcpt(ssip->ssi_fd, S0L_S0CKET, S0_REUSEA0DR,
&sccket_cpticn_value, sizecf(sccket_cpticn_value)) )
>
if (ssip->ssi_server) {
F0_ZER0(4ssip->ssi_fd_set);
ec_neg1( bind(sslp->ssi_fd, (struct scckaddr *)&sa, sa_Ien) )
ec_neg1( Iisten(sslp->ssl_fd, S0MAXC0NN) )
F0_SET(ssip->ssi_fd, &ssip->ssi_fd_set);
>
else
ec_neg1( ccnnect(sslp->ssi_fd, (struct scckaddr O&sa, sa_Ien) )
return ssip;
EC_CLEANUP_BGN
free(sslp);
return NULL;
EC_CLEANUP_END
>
552 ГЛАВА 8 Сетевое взаимодействие и сокеты
Параметр SO.REUSEADDR уже описывался в разделе 8.3.
Функция закрытия SSI очень проста для понимания:
bool ssi_olose(SSI *sslp)
{
If (sslp->ssl_server) {
lnt fd;
for (fd = 0; fd <= ssip->ssi_fd_hwm; fd++)
If (F0_ISSET(fd, &ssip->ssi_fd_set))
(vold)olose(fd);
If (ssip->ssi_domain == AFJJNIX)
(vold)unlink(sslp->ssl_nanie_server);
>
else
(vold)olose(ssip->ssl_fd);
free(ssip);
return true;
>
Следующая функция — ssi_wait_server. По существу она представляет собой слегка
модифицированный код примера из раздела 8.1.3. Функция возвращает готовый
дескриптор сокета, но сама его не использует:
lnt ssi_walt_server(SSI *ssip)
{
if (sslp->ssi_server) {
fd.set fd_set_read;
int fd, olientfd;
struot sookaddr.un from;
sooklen_t from_len = sizeof(from);
while (true) {
fd_set_read = sslp->ssi_fd_set;
eo_neg1( seleot(ssip->ssl_fd_hwm + 1, 4fd_set_read, NULL, NULL,
NULL) )
for (fd = 0; fd <= ssip->ssi_fd_hwn; fd++)
if (F0_ISSET(fd, &fd_set_read)) {
If (fd == ssip->ssi_fd) {
eo_neg1( olientfd = aooept(ssip->ssl_fd,
(struot sookaddr *)&frora, 4fron_len) );
FD_SET( olientfd, &ssip->ssl_fd_set);
set_fd_hwn(ssip, olientfd);
continue;
>
else
return fd;
ГЛАВА 8 Сетевое взаимодействие и сокеты 553
}
}
}
else {
errno = ENOTSUP;
EC_FAIL
}
EC_CLEANUP_BGN
return -1;
EC_CLEANUP_END
}
Следующие две функции — проще некуда:
int ssi_get_server_fd(SSI *ssip)
<
return ssip->ssi_fd;
}
bool ssi_olose_fd(SSI «ssip, int fd)
<
ec_neg1( close(fd) );
FD_CLR(fd, &ssip->ssi_fd_set);
reset_fd_hwra(ssip, fd);
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
На этом можно считать законченным рассмотрение реализации упрощенного
интерфейса сокетов.
8.5 Реализация SMI с сокетами
В этом разделе мы постараемся реализовать очередную версию упрощенного
интерфейса обмена сообщениями (SMI) из главы 7 с использованием сокетов. Это даст
нам возможность сравнить эту версию с пятью предыдущими. С целью упрощения
примеров вместо непосредственного обращения к системным вызовам мы будем
использовать функции SSI.
Тема данного раздела предполагает, что вы уже знакомы с материалом главы 7 и
приведенными в ней реализациями SMI. Если это не так, рекомендую оставить пока
этот раздел и вернуться к главе 7.
554 ГЛАВА 8 Сетевое взаимодействие и сокеты
Внутренняя структура очереди SHIQ.SKT в данной реализации очень проста, в
основном благодаря тому, что все действия по отслеживанию клиентов на свои
плечи взвалил SSI:
typedef struct {
SMIENTITY sq_entity; /* сущность (сервер или клиент) */
SSI *sq_ssip; /* структура SSI */
struct client_id sq.client; /* идентификация клиента */
size_t sqjnsgsize; /* размер сссбщения */
struct smi_msg *sq_msg; /* буфер с сообщением */
} SMIQ.SKT;
При открытии интерфейса обмена сообщениями практически ничего не
приходится делать, потому что основную работу выполняет ssi_cpen:
SMIQ *smi_cpen_skt(ccnst char «name, SMIENTITY entity, size.t msgsize)
{
SMIQ.SKT *p = NULL;
ec_null( p = callccd, sizecf(SMIQ.SKT)) )
p->sq_rasgsize = msgsize + cffsetcf(struct smi.msg, smi.data);
ec_null( p->sq_msg = callcc(1, p->sq_rasgsize) )
P->sq_entity = entity;
ec_null( p->sq_ssip = ssi_cpen(narae, entity == SMI.SERVER) )
return (SMIQ *)p;
EC_CLEANUP_BGN
(vcid)srai_cIcse_skt((SMIQ *)p);
return NULL;
EC_CLEANUP_END
}
Функция закрытия также очень проста:
bed smi_dcse_skt(SMIQ *sqp)
{
SMIQ.SKT *p = (SMIQ.SKT *)sqp;
SSI *ssip;
if (p != NULL) {
ssip = p->sq_ssip;
free(p->sq_rasg);
free(p);
if (ssip != NULL)
ec_false( ssi.clcse(ssip) )
}
return true;
ГЛАВА 8 Сетевое взаимодействие и сокеты 555
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
В предыдущих реализациях SMI (см. главу 7) мы уже видели — все, что требуется
от функции sini_send_getaddr_skt на стороне сервера, — это сохранить указатель
структуру client_id и вернуть адрес сообщения:
bccl sml_send_getaddr_skt(SMlQ »sqp, struct client_ld «client,
vcid »*addr)
{
SM1Q_SKT »p = (SM1Q_SKT <-)sqp;
If (p->sq_entlty == SMl.SERVER)
p->sq_cllent = «client;
•addr = p->sq_msg;
return true;
}
Функция smi_send_release_skt записывает сообщение (с помощью функции writeall
из раздела 2.9) в файловый дескриптор сокета, связанного с клиентом, если вызов
производится на стороне сервера, и в файловый дескриптор, связанный с
сервером, — на стороне клиента:
bccl smi_send_release_skt(SMlQ »sqp)
{
SM1Q_SKT *p = (SMIQ_SKT *)sqp;
int fd;
if (p->sq_entity == SM1_SERVER)
ec_neg1( fd = p->sq_client.c_id1 )
else
ec_neg1( fd = ssi_get_server_fd(p->sq_ssip) )
ec_neg1( writeall(fd, p->sq_nisg, p->sq_msgsize) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
Для сервера процедура приема сообщений заключается в ожидании готовности к
чтению файловых дескрипторов сокетов. Это действие выполняет функция SSI
ssi_wait_server. Кроме того, сервер должен запомнить файловый дескриптор
сокета, соединенного с клиентом, чтобы знать кому отвечать. На стороне клиента,
получение файлового дескриптора сокета, соединенного с сервером, выполняется
обращением к функции ssi_get_server_fd.
556 ГЛАВА 8 Сетевое взаимодействие и сокеты
bccl sml_recelve_getaddr_skt(SMIQ *sqp, veld **addr)
{
SMIQ_SKT «p = (SMIQ_SKT *)sqp;
sslze.t nread;
lnt fd;
•addr = p->sq_msg;
while (true) {
If (p->sq_entlty == SMI_SERVER)
ec_neg1( fd = ssl_walt_server(p->sq_sslp) )
else
ec_neg1( fd = ssi_get_server_fd(p->sq_ssip) )
ec_neg1( nread = readall(fd, p->sq_insg, p->sq_msgsize) )
if (nread == 0) {
if (p->sq_entity == SMI_SERVER) {
ec_false( ssi_clcse_fd(p->sq_ssip, fd) )
continue;
>
else {
errnc = ENQMSG;
EC_FAIL
>
>
else
break;
>
if (p->sq_entlty == SMI_SERVER)
p->sq_nisg->snii_cllent.c_ld1 = fd;
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
И последняя функция, которая вообще ничего не делает:
bccl smi_receive_release_skt(SMIQ *sqp)
{
return true;
}
С этой реализацией SMI все примеры программ из главы 7 будут производить
обмен сообщениями через сокеты. В разделе 7.15 уже приводилось сравнение
производительности сокетов относительно других методов взаимодействий процессов.
Обратите внимание: наши примеры программ из главы 7 используют простые имена
для сервера. Это означает, что ssi_cpen будет создавать сокеты из домена адресов
ГЛАВА 8 Сетевое взаимодействие и сокеты 557
AF.UNIX. Достаточно просто можно организовать обмен сообщениями по сети
через сокеты из домена AF_INET, для этого надо только сделать так, чтобы имя
сервера начиналось с комбинации«//», но в этом случае работоспособной останется
только версия SMI, реализованная через сокеты.
8.6 Сокеты, не требующие создания соединения
Все предыдущие примеры использовали сокеты типа SOCK.STREAM, которые создают
логические соединения. Сервер вызывал listen и accept, а к нему подключались один
или более клиентов. Соединение оставалось открытым до тех пор, пока одна из
сторон не закрывала его.
Сокеты, не требующие создания соединений, не обращаются к системным
вызовам listen и accept. В этом случае передающая сторона указывает только адрес
передачи, а принимающая сторона — адрес отправителя, либо она принимает
данные независимо от адреса отправителя. Принимающая сторона всегда должна
передавать в bind свое внешнее имя, так как только в этом случае передающая
сторона сможет что-нибудь отправить. Таким образом, работа с сокетами и с
принимающей стороны, и с передающей строится практически одинаковым образом. На
основе таких сокетов по-прежнему можно выстраивать взаимоотношения типа
клиент-сервер между узлами сети. Но более уместным будет считать их равными
по положению.
8.6.1 О дейтаграммах
В отсутствие постоянных соединений обмен производится независимыми друг от
друга небольшими блоками данных, которые называются дейтаграммами. При
обмене дейтаграммами вам не гарантируется, что они придут к принимающей
стороне в том же порядке, в каком покинули компьютер отправителя, равно как нет
гарантий, что где-нибудь по пути часть дейтаграмм не будет потеряна. В отличие
от них, SOCK.STREAM гарантирует как сохранность, так и порядок следования. Чтобы
нагляднее увидеть разницу, можете представить себе дейтаграмму как простое
письмо, отправляемое по почте, a SOCK_stream — как телефонный разговор.
Во многих приложениях порядок следования дейтаграмм (равно как и возможность
потери некоторых из них) не имеет большого значения. Например, предположим,
что у нас имеется приложение, с помощью которого производится заказ книги в
библиотеке. Программа выводит перед пользователем форму заказа, после
заполнения которой передает серверу дейтаграмму с информацией о заказе. Получив заказ,
сервер высылает подтверждение. Возможно, порядок следования дейтаграмм и
можно было бы рассматривать как своего рода несправедливость, если одну и ту
же книгу одновременно заказали два человека, и один из них получил отказ
только потому, что его дейтаграмма опоздала на 1 секунду. Но, поскольку клиенты
ничего не знают о действиях друг друга, они не будут проявлять недовольство. В
редких случаях, когда дейтаграмма с заказом будет потеряна, клиентская программа
558 ГЛАВА 8 Сетевое взаимодействие и сокеты
не получит подтверждения и повторит передачу заказа. В данном случае обмен
дейтаграммами много эффективнее, чем создание постоянного соединения,
которое должно быть разорвано сразу же после передачи заказа. С другой стороны, если
клиент собирается просматривать каталог книг в интерактивном режиме, то
организация работы через постоянное соединение предпочтительней.
На рис. 8.4 показаны два узла сети, обменивающиеся дейтаграммами. Сравните его
с рис. 8.1, на котором было показано, как устанавливается логическое соединение:
исчезли операции подготовки сокета к приему соединений и собственно прием
соединений, и с обеих сторон сокетам присваиваются адреса.
Рис. 8.4. Обмен дейтаграммами между узлами сети
8.6.2 Системные вызовы sendto и recvf rom
Во всех предыдущих примерах передача данных между сокетами производилась
системным вызовом write (или функцией writeall), которые работают с файловым
дескриптором. Но, если соединение между сокетами не установлено, единственное,
что у нас есть, это дескриптор своего собственного сокета, поэтому нам нужно как-
то отправить сообщение по адресу получателя. В этом нам поможет системный вызов
sendto:
sendto — передает сообщение по заданному адресу
«include <sys/sccket.h>
ssize_t sendtc(
int sccket_fd, /*
const vcid «message, /»
size_t length, /•
int flags, /»
const struct scckaddr *sa, /*
sccklen_t sa_len /*
файловый дескриптор сокета
сообщение »/
длина сообщения •/
флаги */
адрес получателя */
длина адреса получателя */
Возвращает количестве переданных байт или -1 в случае сшибки (кед сшибки
в переменней errnc) •/
Аргумент socket_fd — это сокет передающей стороны. Принимающая сторона
определяется аргументами sa и sa_len. Успешное завершение передачи вовсе не
означает, что сообщение было принято с другой стороны — возвращаемое значение
свидетельствует только об отсутствии ошибок во время передачи.
Вызову можно передать набор флагов, но особое значение имеет флаг MSG_00B,
который используется для передачи внеочередных (срочных) сообщений (см. раздел 8.7).
ГЛАВА 8 Сетевое взаимодействие и сокеты 559
Для сокетов, не требующих установления соединений, сообщение длиной length байт
рассматривается как единая дейтаграмма. Если канал передачи сокета заполнен до
предела, системный вызов блокируется до тех пор, пока все сообщение не сможет
быть помещено в буфер передачи. Если был установлен флаг 0.N0NBL0CK и
сообщение не помещается в буфер передачи, в вызывающую программу возвращается
признак ошибки с кодом EAGAIN или EW0ULDBL0CK.
Можно использовать вызов sendtc и для сокетов, образующих логическое
соединение. В этом случае адрес получателя игнорируется, а передача данных
осуществляется аналогично системному вызову write. Единственное преимущество перед write
— возможность задать флаги, но, если это все, что вам нужно, лучше
воспользоваться системным вызовом send.
Теперь рассмотрим пример программы, которая выполняет действия,
отображенные на рис. 8.4. Узел сети номером 2 принимает сообщения от узла с номером 1,
вносит некоторые изменения и отправляет их обратно. Узел 1 отправляет 4
сообщения узлу 2 и выводит полученные ответы. Обратите внимание: в отличие от
предыдущих примеров оба узла обращаются к системному вызову bind, но ни один
из них не вызывает ни listen, ни accept, ни connect:
«define S0CKETNAME1 "SktOne"
«define S0CKETNAME2 "SktTwc"
«define MSG.SIZE 100
int main(vcid)
{
struct scckaddr_un sal, sa2;
strcpy(sa1.sun_path, S0CKETNAME1);
sa1.sun_family = AFJJNIX;
strcpy(sa2.sun_path, S0CKETNAME2);
sa2.sun_family = AFJJNIX;
(vcid)unlink(SOCKETNAMED;
(vcid)unlink(S0CKETNAME2);
if (fcrk() == 0) { /* Узел № 1 */
int fd_skt;
ssize_t nread;
char msg[MSG_SIZE];
int i;
sleep(1); /* псдсждать пека запустится узел Nt 2 */
ec_neg1( fd_skt = sccket(AF_UN!X, S0CK_DGRAM, 0) )
ec_neg1( bind(fd_skt, (struct scckaddr *)&sa1, sizecf(sal)) )
fcr (i = 1; i <= 4; i++) {
snprintf(msg, sizecf(msg), "Дейтаграмма №Xd", i);
ec_neg1( sendtc(fd_skt, msg, sizecf(msg), 0,
(struct scckaddr *)&sa2, sizecf(sa2)) )
ec_neg1( nread = read(fd_skt, msg, sizecf(msg)) )
if (nread != sizecf(msg)) {
560 ГЛАВА 8 Сетевое взаимодействие и сокеты
ргШтО'Узел № 1 получил короткое оообщение\п");
break;
}
ргШт("Узел № 2 ответил: Y'Xs\"\n", msg);
}
eo_neg1( oiose(fd_skt) )
exit(EXIT_SUCCESS);
}
eise { /* Узел № 2 */
int fd_skt;
ssize_t nread;
ohar msg[MSG_SIZE];
eo_neg1( fd.skt = sooket(AF_UNIX, SOCK_DGRAM, 0) )
eo_neg1( bind(fd_skt, (struot sookaddr «)&sa2, sizeof(sa2)) )
eo_neg1( nread = read(fd_skt, rasg, sizeof(rasg)) )
whiie (true) {
if (nread l= sizeof(rasg)) {
printf("y3efl № 2 получил короткое оообщение\п");
break;
}
msg[0] = 'д';
eo_neg1( sendto(fd_skt, rasg, sizeof(rasg), 0,
(struot sookaddr *)4sal, sizeof(sal)) )
}
eo_neg1( oiose(fd_skt) )
exit(EXIT_SUCCESS);
}
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Мы запускаем два процесса из одной программы только лишь для того, чтобы
упростить исходный код примера, в реальной жизни такой прием используется
довольно редко. Перед началом обмена дочерний процесс приостанавливается (узел
№ 1), чтобы дать возможность родительскому процессу (узел № 2) обратиться к
вызову read до того, как потомок передаст хоть что-нибудь. Это далеко не лучшее
решение, но в данной ситуации вполне сносное. Более правильный способ
синхронизации вы найдете в разделе 92.
В результате работы программы было получено следующее.-
Узел № 2 ответил: "дейтаграмма №1"
Узел № 2 ответил: "дейтаграмма №2"
Узел № 2 ответил: "дейтаграмма №3"
Узел № 2 ответил: "дейтаграмма №4"
ГЛАВА 8 Сетевое взаимодействие и сокеты 561
Прием дейтаграмм системным вызовом read — не лучший способ, поскольку он не
возвращает информации об отправителе. Необходимости в этом ранее не
возникало, потому что вызов accept возвращал нам файловый дескриптор сокета,
соединенного с клиентом, благодаря чему у нас была возможность ответить ему
вызовом write. В только что рассмотренном примере узел № 2 исходит из
предположения, что все запросы приходят от узла № 1, но вообще такой способ не
характерен для реальных приложений.
Для приема дейтаграмм больше подходит системный вызов recvf rem:
reevfrom — принимает сообщение
«Include <sys/sccket.h>
ssize_t recvfrora(
int sccket_fd,
vcid «buffer,
size_t length,
int flags,
struct scckaddr «sa,
sccklen_t «sa_len
);
/* Возвращает количестве
сшибки - в переменней ег
/*
/*
/*
/*
/*
/*
из сокета
файловый дескриптор сскета •/
приемный буфер для сообщения •/
размер
флаги
адрес
длина
принятых
гпс)
*/
буфера */
*/
передающей стерсны •/
адреса •/
байт, 0 или -1 - в случае сшибки
(кед
Первые три аргумента в точности соответствуют входным аргументам системного
вызова read. Дополнительно имеются аргумент с флагами и два аргумента, через
которые возвращается адрес отправителя. Перед обращением к reevfrom вы
должны выделить достаточный объем памяти под аргумент sa и записать в sa.len
размер выделенного пространства. В вызывающую программу через sa.len будет
возвращен фактический размер адреса. После этого полученные от reevfrom
аргументы sa и sa.len могут быть переданы в sendtc без каких-либо изменений.
Подобно sendtc, вызов recvf rem рассматривает принимаемое сообщение как нечто
единое и неделимое. Если флаг O.NONBLOCK не установлен, вызов блокируется до
окончания получения всего сообщения, если установлен — в вызывающую
программу возвращается признак ошибки с кодом EAGA1N или EW0ULDBL0CK.
При желании recvf rem можно использовать для приема сообщений из сокетов,
образующих логическое соединение. В этом случае вы получаете информацию об
адресе отправителя, правда в этом мало смысла, поскольку sendtc все равно
игнорирует ее при передаче сообщений через установленное соединение. Для того чтобы
получить адрес другого конца соединения, лучше использовать системный вызов
getscckname (см. раздел 8.9.2).
Существует три переносимых флага, которые могут использоваться в вызове recvf rem:
HSG_00B Принимает внеочередные сообщения, если таковые имеются,
см. раздел 8.7.
562 ГЛАВА 8 Сетевое взаимодействие и сокеты
MSG.PEEK Возвращает сообщение, но оставляет его в приемном буфере,
для одной из последующих операций чтения через read или
recvf гон.
HSG.WAITALL Используется только рдя сокетов с установленным
соединением. Блокирует recvf гон до тех пор, пока не будет получено
запрошенное число байт, при условии, что флаг MSG.PEEK
сброшен, вызов не был прерван сигналом, соединение не было
разорвано и не возникло ошибки. Игнорируется для сокетов, не
требующих установления соединения, потому что в этом
случае сообщение всегда является единым и неделимым.
Ниже приводится пример приложения, которое запускает несколько узлов,
выполняющих передачу данных на один и тот же адрес, и использует recvf rem вместо read:
«define SOCKETNAME_SERVER "SktOne"
«define SOCKETNAME_CLIENT "SktTwc"
static struct scckaddr_un sa_server;
«define MSG.SIZE 100
static vcid run_client(int nclient)
{
struct scckaddr.un sa.client;
int fd_skt;
ssize_t nrecv;
char msg[MSG_SIZE];
int i;
if (fcrk() == 0) { /* клиент */
sleep(1); /* псдсждать пека запустится сервер */
ec.negK fd.skt = sccket(AF_UNIX, S0CK_DGRAM, 0) )
snprintf(sa.client.sun.path, sizecf(sa_client.sun.path),
"Xs-Xd", SOCKETNAME.CLIENT, nclient);
(vcid)unlink(sa_client.sun_path);
sa_client.sun_family = AF_UNIX;
ec_neg1( bind(fd_skt, (struct scckaddr *)&sa_client,
sizecf(sa_client)) )
fcr (i = 1; i <= 4; i++) {
snprintf(msg, sizecf(msg), "Дейтаграмма №Xd", i);
ec_neg1( sendtc(fd_skt, msg, sizecf(msg), 0,
(struct scckaddr *)&sa_server, sizecf(sa.server)) )
ec_neg1( nrecv = read(fd_skt, msg, sizecf(msg)) )
if (nrecv != sizecf(msg)) {
printf("клиент пелучил керстксе сссбщение\п");
break;
>
printf("0T сервера пслучен ствет: \"Xs\"\n", msg);
>
ГЛАВА 8 Сетевое взаимодействие и сокеты 563
ec_neg1( clcse(fd_skt) )
exit(EXlT_SUCCESS);
}
return;
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
static vcid run_server(vcid)
{
int fd_skt;
ssize_t nrecv;
char msg[MSG_SlZE];
struct scckaddr_stcrage sa;
sccklen_t sa_len;
ec_neg1( fd_skt = sccket(AF_UNIX, SOCK_DGRAM, 0) )
ec_neg1( bind(fd_skt, (struct scckaddr *)&sa_server, sizecf(sa_server)) )
while (true) {
sa_len = sizecf(sa);
ec_neg1( nrecv = recvfrcm(fd_skt, msg, sizecf(msg), 0,
(struct scckaddr *)&sa, &sa_len) )
if (nrecv != sizecf(msg)) {
printf("сервер пслучил ксрстксе сссбщение\п");
break;
}
msg[0] = 'д';
ec_neg1( sendtc(fd_skt, msg, sizecf(msg), 0,
(struct sockaddr *)&sa, sa_len) )
}
ec_neg1( clcse(fd_skt) )
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
int niain(vcid)
{
int nclient;
strcpy(sa_server.sun_path, SOCKETNAME_SERVER);
sa_server.sun_family = AF_UNIX;
(vcid)unlink(SOCKETNAME.SERVER);
564 ГЛАВА 8 Сетевое взаимодействие и сокеты
>
fcr (nclient = 1; nclient <= 3; nclient++)
run_client(nclient);
run_server();
exit(EXIT_SUCCESS);
В этом примере адрес сервера известен как серверу, так и клиентам. Адреса
клиентов известны только клиентам. Сервер получает их адреса через системный вызов
recvf rem. Я оставил обращение к системному вызову read на стороне клиентов, хотя
он тоже может быть изменен на обращение к вызову recvf rem. Ниже приводится
результат работы программы:
От сервера пелучен ствет: "дейтаграмма №1"
От сервера пелучен ствет: "дейтаграмма №2"
От сервера пелучен ствет: "дейтаграмма №3"
От сервера пелучен ствет: "дейтаграмма №4"
От сервера пелучен ствет: "дейтаграмма №1"
От сервера пелучен ствет: "дейтаграмма №2"
От сервера пелучен ствет: "дейтаграмма №3"
От сервера пелучен ствет: "дейтаграмма №4"
От сервера пелучен ствет: "дейтаграмма №1"
От сервера пелучен ствет: "дейтаграмма №2"
От сервера пелучен ствет: "дейтаграмма №3"
От сервера пелучен ствет: "дейтаграмма №4"
8.6.3 sendmsg и reevmsg
Системные вызовы sendmsg и reevmsg являются до некоторой степени аналогами
системных вызовов sendtc и recvf rem. Они используют те же самые флаги и
возвращают те же самые значения, но используют один аргумент, который указывает на
структуру msghdr, содержащую адрес и сообщение. Они могут выполнять чтение
вразброс и запись со слиянием, подобно системным вызовам readv и writev,
которые уже обсуждались в разделе 2.15.
sendmsg — передает сообщение сокету с помощью структуры msghdr
«include <sys/sccket.h>
ssize_t sendrasg(
int sccket_fd, /* файловый дескрилтер сскета */
censt struct msghdr «message, /* ссебшение */
int flags /* флаги */
);
/• Всзвращает количестве переданных байт или -1 в случае сшибки (кед сшибки
- в переменней еггпс) */
ГЛАВА 8 Сетевое взаимодействие и сокеты 565
recvmsg — принимает сообщение из сокета с помощью структуры msghdr
«include <sys/sccket.h>
ssize_t recvmsg(
int sccket_fd, /» файловый дескриптор сскета »/
struct msghdr «message, /* сообщение »/
int flags /» флаги »/
);
/* Возвращает количестве принятых байт, 0 или -1 - в случае шибки (кед
сшибки - в переменней еггпс) »/
struct msghdr — структура для системных вызовов sendmsg и recvmsg
struct msghdr {
vcid »msg_name;
sccklen_t msg.namelen;
struct icvec »msg_icv;
int msg_icvlen;
vcid »msg_ccntrcl;
sccklen_t msg_ccntrcllen;
int msg.flags;
};
/» необязательный адрес */
/» размер адреса »/
/» массив буферсв чтения/записи »/
/» количестве буферсв в массиве msg_icv »/
/» служебные данные »/
/» длина буфера ее служебными данными */
/» флаги - в случае приема сообщений »/
Системный вызов sendmsg получает адрес сокета принимающей стороны из не
совсем правильно названного поля msg.name, а длину адреса — из msg.namelen.
Передаваемые данные находятся в массиве msg_icv, который состоит из элементов типа
icvec и используется точно так же как и в системном вызове writev. Поле msg_icvlen
содержит количество элементов в массиве msg.icv. Мы уже рассматривали вопрос
заполнения этих полей в разделе 2.15.
Для системного вызова recvmsg в поле msg_name можно передать значение NULL или
адрес буфера с размером msg.namelen, в котором будет возвращен адрес сокета
отправителя подобно вызову recvf rem. По возвращении из recvmsg в аргумент msg_namelen
будет записан фактический размер адреса сокета отправителя. Аргументы msg.icv
и msg.icvlen используются аналогично системному вызову readv. В поле flags
может быть возвращен один переносимый флаг — MSG_TRUNC, который означает, что
полученное сообщение слишком велико и не умещается в предоставленный буфер.
Поля, в которых находятся адрес сокета и его длина, используются только для
обмена дейтаграммами, в противном случае эти поля игнорируются.
Два других поля, msg_ccntrcl и msg.ccntrcllen, я не буду затрагивать-, скажу лишь, что
они используются для хранения служебной информации, которая имеет
отношение к правам доступа. Более подробные сведения о них вы найдете в [SUS2002] и в
документации на систему.
Ниже приводится исходный код функции run_server и предыдущего раздела,
который был переработан под использование функций recvmsg и sendmsg:
566 ГЛАВА 8 Сетевое взаимодействие и сокеты
static vcid run_server(vcid)
{
int fd_skt;
ssize_t nrecv;
char rasg[MSG_SIZE];
struct scckaddr_stcrage sa;
struct rasghdr in;
struct icvec v;
ec_neg1( fd_skt = sccket(AF_UNIX, SOCK_DGRAM, D) )
ec_neg1( bind(fd_skt, (struct scckaddr *)&sa_server, sizecf(sa_server)) )
while (true) {
memsetUm, D, sizecf(ra));
i4.rasg_nanie = &sa;
m.msg_nanielen = sizecf(sa);
v.icv_base = msg;
v.icv_len = sizecf(rasg);
ra.rasg_icv = &v;
ra.rasg_icvlen = 1;
ec_neg1( nrecv = recvmsg(fd_skt, &ra, D) )
if (nrecv != sizecf(msg)) {
printf("сервер пслучил ксрстксе сссбщение\п");
break;
}
((char *)m.msg_icv->icv_base)[D] = 'д';
ec_neg1( sendrasg(fd_skt, &ra, D) )
}
ec_neg1( close(fd_skt) )
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Здесь немножко добавилось работы по инициализации структур, но сами
системные вызовы очень просты в обращении. Представьте себе приложение, которое
должно рассылать различные сообщения по разным адресам, и для вас сразу
станет очевидной выгода от хранения адреса и сообщения в одной структуре.
8.6.4 Системный вызов connect для сокетов,
не образующих логические соединения
Как правило, системный вызов connect (см. раздел 8.1.2) используется клиентом для
установления соединения с сервером. Но он может использоваться и для сокетов,
не образующих логических соединений. В этом случае обращение к вызову connect
устанавливает адрес по умолчанию для последующих вызовов send и recv, которые
ГЛАВА 8 Сетевое взаимодействие и сокеты 567
не имеют аргумента, отвечающего за адрес. Подробнее об этих системных
вызовах читайте в разделе 8.9-1-
Обращение к connect с пустым указателем в аргументе sa удаляет адрес по
умолчанию.
8.7 Внеочередные данные
Иногда возникает необходимость в срочной передаче сообщения через
соединение. Если осуществлять передачу в обычном порядке, то оно не дойдет до адресата
раньше предыдущих сообщений. Поэтому сокеты предусматривают возможность
передачи срочных (внеочередных или, если хотите, первоочередных) сообщений.
При этом получатель также должен особым образом принимать внеочередные
сообщения.
Для передачи внеочередных сообщений системным вызовам sendto, sendmsg или send
следует установить флаг MSG_00B.
Принимающая сторона, использующая системный вызов select, для приема
первоочередных сообщений должна передавать ему дескрипторы в наборе,
проверяемом на наличие «ошибок», — четвертый аргумент вызова select. А для получения
самого сообщения в вызовы recvmsg, recvf rom или recv следует передавать флаг MSG_00B.
Другой способ — перед чтением обычных данных можно просто проверять
наличие внеочередных сообщений функциями приема с установленными флагами
0_N0NBL0CK (с ПОМОЩЬЮ fcntl) И MSG_00B.
Если установлен параметр S0_00BINLINE, срочные сообщения будут помещаться в
общий поток данных. Протокол может помечать внеочередные сообщения
специальной меткой, которая игнорируется при чтении данных, но может быть
обнаружена специальным системным вызовом:
sockatmark — проверка наличия метки первоочередных данных
#include <sys/sccket.h>
int scckatmark(
int sccket_fd /* файловый дескриптор сскета »/
);
/* Всзвращает 1 при наличии метки, 0 - при ее отсутствии, -1 - в случае
сшибки (кед сшибки - в переменней еггпс) »/
Вы должны учитывать, что scckatmark может вернуть 0, потому что в сокете нет данных
для чтения, но срочное сообщение может прийти между проверкой вызовом
scckatmark и операцией чтения, а поскольку метка игнорируется при чтении
данных, она будет утеряна, и вы так и не узнаете, что сообщение было внеочередным.
Один из способов избежать этой неприятности — перед обращением к scckatmark
следует убедиться, что данные пришли (например, с помощью вызова select). В этом
случае возвращаемое значение 0 будет однозначно говорить о наличии данных для
чтения, но не отмеченных, как срочные.
19 3ак. 4030
568 ГЛАВА 8 Сетевое взаимодействие и сокеты
Можно также настроить передачу сигнала SIGURG при получении первоочередных
данных. Для этого нужно вызвать fcntl с операцией F.SET0WN, чтобы указать
процесс или группу процессов, которые должны получать этот сигнал. За более
подробной информацией по этому вопросу обращайтесь к [SUS2002] или к
системной документации.
8.8 Получение сведений о сетевом окружении
Функции, описываемые в этом разделе, используются для получения сведений об узлах
сети, сетях, протоколах и службах (сервисах). С некоторыми из них мы уже
познакомились, например getaddrinfc (раздел 8.2.6) или gethcstname (раздел 8.2.7). Здесь я
расскажу об остальных функциях, сгруппировав их по типу возвращаемой информации.
8.8.1 Получение сведений об узлах сети
Для обзора базы данных хостов предназначены три следующих функции:
sethostent — запустить обзор базы
«include <netdb.h>
vcid sethcstent(
данных хостов
int staycpen /* оставить соединение открытым?
);
•/
gethostent — получить следующую
«include <netdb.h>
struct hcstent «gethcstent(vcid);
/* Возвращает очередную запись или
(содержимое переменней еггпс не сп
запись из базы
данных
NULL nc достижении
ределенс) */
кенца
хостов
списка
endhostent — завершить обзор базы данных хостов
«include <netdb.h>
vcid endhcstent(vcid);
struct hostent — структура для функций, работающих с
struct hcstent {
char
char
int h
int h
char
};
*h_name;
♦*h_aliases;
_addrtype;
_length;
**h_addr_list;
/•
/•
/•
/•
/•
официальное имя хеста
массив альтернативных
семействе адрессв (не
длина каждеге адреса
массив указателей на
•/
имен
тип)
•/
базой
хеста
•/
данных хостов
•/
сетевые адреса
•/
Функция gethcstent может вызываться в цикле для получения всех известных имен
сетевых узлов. Обзор базы данных начинается с вызова sethestent, входной аргу-
ГЛАВА 8 Сетевое взаимодействие и сокеты 569
мент которой определяет, должно ли остаться соединение с базой данных
открытым или gethostent должна открывать и закрывать соединение с базой всякий раз
при обращении к ней. По окончании работы должна вызываться функция endhostent.
Звучит многообещающе, но на самом деле все эти функции работают с файлом /
etc/hosts на локальной машине.
В структуре hostent имеются два массива, каждый из которых заканчивается
пустым указателем. Первый, h.aliases, — массив альтернативных имен хоста. Другой,
h_addr_list, — массив указателей на сетевые адреса. Адреса из домена AF.1NET
сохраняются в виде 32-битных чисел с сетевым порядком следования байт. Таким
образом, для того чтобы получить из них 32-битное аппаратно-зависимое
представление, нужно воспользоваться функцией ntohl (см. раздел 8.1.4), а чтобы получить
адрес в виде строки в точечной нотации — функцией inet.ntoa (см. раздел 8.2.3)
или inet_ntop (см. раздел 8.9.5).
Например:
statio void hostdb(void)
struot hostent *h;
sethostent(true);
while ((h = gethostentO) 1= NULL)
displayjiostent(h);
endhostent();
statio void display_hostent(struot hostent »h)
<
int i;
printf("HMR: Xs; тип: Xd; длина: Xd\n", h->h_narae, h->h_addrtype,
h->h_length);
for (i = 0; h->h_aliases[i] != NULL; i++)
printf("\tXs\n", h->h_aliases[i]);
if (h->h_addrtype == AF_INET) {
for (i = 0; h->h_addr_list[i] != NULL; i++)
printf("\tXs\n",
inet_ntoa(*((struot in_addr *)h->h_addr_list[i])));
}
В результате работы этой программы (исходный код функции main, которая
вызывает hostdb, опущен) были получены следующие результаты:
имя: looalhost; тип: 2; длина: 4
127.0.0.1
570 ГЛАВА 8 Сетевое взаимодействие и сокеты
имя: scl; тип: 2; длина: 4
lcghcst
192.168.0.10
имя: bsd; тип: 2; длина: 4
192.168.0.15
имя: suse2; тип: 2; длина: 4
suse2.MSH0ME
192.168.0.19
Для сравнения ниже приводится содержимое файла /etc/hcsts:
127.0.0.1 lccalhcst
192.168.0.10 scl lcghcst
192.168.0.15 bsd
192.168.0.19 suse2 suse2.MSH0ME
Чтобы отыскать хост по его имени, можно воспользоваться функцией gethcstbyname.
Эта функция пытается отыскать запрошенное имя не только в локальном файле /
etc/hcsts, но и обращается к DNS, если это возможно. До появления функции
getaddrinfc это был основной способ получения сетевого адреса хоста. Функция
gethcstbyname до сих пор широко применяется в программах, потому что она очень
старая и практически все начинали именно с нее. Но, в отличие от getaddrinfc,
gethostbyname возвращает только IP-адрес и поэтому она не освобождает нас от
необходимости заполнять структуру с адресом сокета вручную, как мы это делали
в разделе 8.2.3. Кроме того, gethcstbyname не умеет обрабатывать адреса IPv6. Ниже
приводится ее краткое описание:
gethostbyname — возвращает информацию о хосте по его имени
«include <netdb.h>
struct hcstent *gethcstbyname(
ccnst char *ncdename, /* имя хсста */
);
/* Возвращает указатель на структуру hcstent или NULL в случае сшибки (кед
сшибки - в переменней h_errnc) */
Замечание: в конце описания указано, что код ошибки размещается в переменной
h_errnc. Это не опечатка, функция действительно возвращает код ошибки не в errnc,
а в h_errnc. Кроме того, коды ошибок не соответствуют тем, что помещаются в еггпс.
(Я не стал создавать макрос «ее» специально для обработки этих кодов ошибок,
потому что я не пользуюсь функцией gethcstbyname.)
Ниже приводится пример обращения к функции gethcstbyname (функция displayjicstent
была взята из предыдущего примера):
static void gethcstbyname_ex(vcid)
{
struct hostent *h;
ГЛАВА 8 Сетевое взаимодействие и сокеты 571
if ((h = gethostbynameC'www.yahoo.oom")) == NULL) {
if (h_errno == H0ST_N0T_F0UND)
printf("xocT не найден\п");
eise
printf("h_errno = Xd\n", h_errno);
}
eise
display_hostent(h);
}
В результате использования этой функции было получено следующее:
имя: www.yahoo.akadns.net; тип: 2; длина: 4
www.yahoo.oom
66.218.71.95
66.218.70.49
66.218.71.88
66.218.71.81
66.218.71.86
66.218.71.92
66.218.71.94
66.218.71.89
66.218.70.48
66.218.71.93
66.218.70.50
66.218.71.87
66.218.71.84
Вы можете сравнить с тем, что было получено в разделе 8.2.6, где мы делали
практически то же самое, но только с использованием getaddrinfo.
Противоположная gethostbyname функция gethostbyaddr возвращает имя хоста
(возможно в результате обращения к DNS) по IP-адресу:
gethostbyaddr — возвращает информацию о хосте по его IP-адресу
«inoiude <netdb.h>
struot hostent *gethostbyaddr(
const void *addr, /* IP-адрео */
sookien_t ien, /* длина адреса */
int famiiy /* оемейотво (в SUS называетоя как "тип") */
);
/* Возвращает указатель на отруктуру hostent или NULL в олучае ошибки (код
ошибки - в переменной h_errno) */
Ниже приводится пример использования функции gethostbyaddr (описание
функции inet.addr вы найдете в разделе 8.2.3):
572 ГЛАВА 8 Сетевое взаимодействие и сокеты
static vcid gethcstbyaddr_ex(vcid)
{
struct hcstent *h;
in_addr_t a;
ec_neg1( a = inet_addr("66.218.71.94") )
if ((h = gethcstbyaddr(&a, sizecf(a), AF_INET)) == NULL) {
if (h_errnc == H0ST_N0T_F0UND)
printf("адрес не найден\п");
else
printf("h_errnc = Xd\n", h_errnc);
}
else
display_hcstent(h);
return;
EC_CLEANUP_BGN
EC_FLUSH("gethcstbyaddr_ex")
EC_CLEANUP_END
}
В результате работы функции было получено следующее:
имя: w15.www.scd.yahcc.ccm; тип: 2; длина: 4
66.218.71.94
Интересно, что, когда мы запрашивали адреса для имени wwwyahoo.com, один из
адресов в списке был (66.218.71.94), но, когда мы запросили имя хоста с этим адресом,
получили wl5wwwscdyahoo.com. Очевидно, это результат работы сервера DNS.
Функция gethcstbyaddr также считается устаревшей и не рекомендуется к
использованию. Современная функция, возвращающая сведения о хосте по IP-адресу,
называется getnameinfc:
getnameinfo — возвращает
«include <sys/sccket.h>
«include <netdb.h>
int getnameinfc(
ccnst struct scckaddr
sccklen_t sa_len,
char «ncdename,
sccklen_t ncdelen,
char «servnarae,
sccklen_t servlen,
unsigned flags
);
/* Возвращает О в случае v
информацию
*sa, /*
/•
/•
/•
/•
/•
/•
адрес
длина
о хосте по его IP-адресу
сскета
адреса
имя хеста •/
длина
•/
•/
имени хеста */
имя службы (сервиса) */
длина
флаги
лепеха или кед
имени службы */
*/
ошибки
•/
ГЛАВА 8 Сетевое взаимодействие и сокеты 573
Функция getnameinfo принимает не просто IP-адрес, а полный адрес сокета, потому
что она в состоянии обработать адреса из различных доменов. Самое главное, она
умеет обращаться с адресами IPv6, что недоступно функции gethostbyaddr.
Результат работы возвращается в двух буферах: имя хоста — в буфере nodename размером
nodelen и имя службы — в буфере servname длиной servien. Вы не сможете получить
соответствующее имя, если вместо буфера будет передан пустой указатель или указан
нулевой размер буфера.
Коды ошибок, возвращаемые getnameinfo, как и в случае с getaddrinfo, не
соответствуют тем, что обычно помещаются в переменную errno. Для их декодирования
можно использовать функцию gai.strerror (см. раздел 8.2.6). Либо используйте макрос
eo_ai, который был специально написан для работы с этими двумя функциями.
В аргументе flags можно передать набор флагов, управляющих работой функции,
объединенных операцией ИЛИ:
NI_N0FQDN Вместо полного доменного имени будет возвращено
только имя хоста.
NI.NUHERICHOST Вместо имени возвращается IP-адрес (в точечной
нотации для IPv4, в двухточечной для IPv6).
NI.NAHEREQD Возвращает признак ошибки, если имя хоста не
найдено. При отсутствии этого флага возвращается
IP-адрес.
NI.NUHERICSERV Вместо имени службы возвращает номер порта.
NI_DGRAM Выполняет поиск служб типа SOCK.DGRAH (UDP).
При отсутствии этого флага выполняется поиск служб
типа SOCK.STREAM (TCP).
Ниже приводится пример обращения к функции getnameinfo:
statio void getnameinfo_ex(void)
{
struot sookaddr_in sa;
ohar nodename[200], servname[200];
sa.sin.famliy = AF_INET;
sa.sin_port = htons(80);
sa.sin_addr.s_addr = inet_addr("216.109.125.70");
eo_ai( getnameinfo((struot sookaddr *)&sa, sizeof(sa), nodename,
sizeof(nodename), servname, sizeof(servname), 0) )
printf("MMp: Xs; олужба: Xs\n", nodename, servname);
return;
EC_CLEANUP_BQN
EC_FLUSH("getnameinfo_ex")
EC_CLEANUP_END
}
Результат работы:
574 ГЛАВА 8 Сетевое взаимодействие и сокеты
имя: w17.www.dcn.yahcc.ccm; служба: http
Еще одна функция, о которой хотелось бы упомянуть:
gethostid -
«include
- возвращает идентификатор
<unistd.h>
lcng gethcstid(vcid);
/* Всзвращает идентификатор */
локального хоста
Функция gethostid, похоже, возвращает локальный IP-адрес машины, но это
совершенно не обязательно. Все, что от нее требуется, — вернуть уникальный
идентификатор, который может быть установлен во время загрузки системы. Я упомянул
эту функцию лишь для того, чтобы вы случайно не перепутали ее с другой, более
полезной функцией, которая напрямую не связана с сетевыми взаимодействиями,
но предоставляет сведения, идентифицирующие систему
uname — возвращает сведения
«include <sys/utsname.h>
int unarae(
struct utsname *infc /* в
);
/* Всзвращает нестрицательнсе
сшибки - в переменней еггпс)
о системе
сзвращаемые
сведения
в случае успеха
*/
или
«/
-1 в
случае
сшибки
(кед
struct utsname — структура для функции
struct utsname {
char
char
char
char
char
};
sysname[];
ncdenameH;
release[];
versicn[];
machine[];
/*
/*
/*
/*
/*
название ОС */
сетевсе имя хеста
нсмер выпуска (в
uname
*/
виде стрски)
нсмер версии (в виде стрски)
тип аппаратной платформы или
*/
«/
медель
компьютера */
К сожалению, ни один из элементов структуры не стандартизован", поэтому вы не
сможете повсеместно использовать их для управления поведением приложения. Было
бы неплохо, скажем, проверить поле machine и обнаружить, что программа
исполняется процессором корпорации Intel. Однако каждая система, работающая на этих
процессорах, форматирует строку по-своему, в ней вообще могут отсутствовать слова
«Intel» или «х8б». Практически все, что вы можете сделать с этой информацией, —
это вывести ее. Разумеется, функция uname лежит в основе команды uname, наша версия
которой приводится ниже:
Пустые квадратные скобки, показанные в описании, не допускаются синтаксисом языка С.
Но суть их, я думаю, вам понятна — каждая реализация выделяет полям столько места, сколько
сочтет нужным.
ГЛАВА 8 Сетевое взаимодействие и сокеты 575
int main(vcid)
{
struct utsname infc;
ec_neg1( uname(&infc) )
printf("система = Xs\n", infc.sysname);
printf("имя хсста = Xs\n", infc.ncdename);
printf("HCMep выпуска = Xs\n", infc.release);
printf("версия = Xs\n", infc.version);
printf("miaT<|>cpMa = Xs\n", infc.machine);
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Результат работы программы на всех четырех тестовых системах:
система = SunDS
имя хсста = sci
нсмер выпуска = 5.8
версия = Generic_1D8529-13
платформа = i86pc
система = Darwin
имя хсста = Marc-Rcchkinds-Ccmputer.lccal.
нсмер выпуска =6.6
версия = Darwin Kernei Version 6.6: Thu May 1 21:48:54 PDT 20D3;
roCt:xnu/xnu-344.34.cbj"1/RELEASE_PPC
платформа = Pcwer Macintosh
система = FreeBSD
имя хсста = bsd.MSHDME
нсмер выпуска = 4.6-RELEASE
версия = FreeBSD 4.6-RELEASE #D: Tue Jun
платформа = 1386
система = Linux
имя хсста = suse2
нсмер выпуска = 2.4.18-4GB
версия = #1 Wed Mar 27 13:57:D5 UTC 20D2
платформа = 1686
8.8.2 Получение сведений о сетях
Функции из этой категории возвращают информацию о сетях, к которым
подключена машина. В первую очередь рассмотрим три функции, которые работают с базой
данных сетей:
576 ГЛАВА 8 Сетевое взаимодействие и сокеты
setnetent — запустить обзор базы данных сетей
#inciude <netdb.h>
void setnetent(
int staycpen /• оставить соединение с базой данных открытый? */
);
getnetent — получить очередную запись из базы данных
#inciude <netdb.h>
struct netent *getnetent(vcid);
/* Возвращает указатель на структуру netent или NULL nc достижении конца
списка (состояние переменней еггпс не определено) */
endnetent — завершить обзор базы данных
#inciude <netdb.h>
void endnetent(vcid);
struct netent — структура для функций, работающих с базой данных сетей
struct netent {
char *n_name; /* официальное имя сети */
char **n_aiiases; /* массиа альтернативных имен */
int n_addrtype; /* семействе адреса (не тип) */
uint32_t n_net; /* номер сети */
};
Пример использования функций:
static void netdb(vcid)
{
struct netent *n;
setnetent(true);
whiie ((n = getnetentO) != NULL)
dispiay_netent(n);
endnetentO;
}
static void dispiay_netent(struct netent *n)
{
int i;
printf("H»w: Xs; тип: Xd; номер: Xiu\n", n->n_narae, n->n_addrtype,
(unsigned lcng)n->n_net);
for (i = 0; n->n_aliases[i] 1= NULL; i++)
ГЛАВА 8 Сетевое взаимодействие и сокеты 577
printf("\tXs\n", n->n_aliases[i]);
>
Результат работы:
имя: lccpback; тип: 2; нсмер: 127
имя: arpanet; тип: 2; нсмер: 10
агра
Большинство систем, если не все, имеют в своем распоряжении петлевой (loopback)
сетевой интерфейс. Данный компьютер (работающий под управлением ОС Solaris)
соединен также с сетью Интернет, которой присвоено исторически сложившееся
имя «arpanet». He забывайте, что «тип» на самом деле определяет семейство (домен)
адресов, оказывается, макрос AF_I.NET определен как число 2.
Далее идут функции, которые возвращают номер и имя сети:
getnetbyname — возвращает информацию о сети по ее имени
«include <netdb.h>
struct netent *getnetbyname(
ccnst char *name /* имя сети (ссстветствует пело n_name) */
);
/* Возвращает указатель на структуру netent или NULL, если сеть не найдена
(ссстсяние переменней еггпс не спределенс) */
getnetbyaddr — возвращает информацию о сети по ее номеру
«include <netdb.h>
struct netent »getnetbyaddr(
uint32_t net, /* нсмер сети (ссстветствует пело n_net) */
int type /* семейство (соответствует поло n_addrtype) */
);
/* Возвращает указатель на структуру netent или NULL, если сеть не найдена
(ссстсяние переменней еггпс не спределенс) */
8.8.3 Получение сведений о протоколах
При описании функций, возвращающих сведения о протоколах, будем
придерживаться порядка, установленного в предыдущих разделах. Функции для работы с базой
данных протоколов:
setprotoent — запустить обзор
«include <netdb.h>
vcid setprctcent(
int stayepen /* оставить
);
базы
данных протоколов
соединение
с базой
данных открытым?
•/
578 ГЛАВА 8 Сетевое взаимодействие и сокеты
getprotoent — получить очередную запись из базы данных
«include <netdb.h>
struct prctcent *getprctcent(vcid);
/* Возвращает указатель на структуру
списка (ссстсяние переменней еггпс не
prctcent или
спределенс)
NULL nc достижении кенца
*/
endprotoent — завершить обзор базы
«include <netdb.h>
veld endprctcent(vcid);
данных протоколов
struct protoent — структура для функций, работающих с
токолов
struct prctcent {
char *p_name;
char **p_aliases;
int p_prctc;
};
/*
/*
/*
официальное название протокола
массив альтернативных имен */
нсмер прстсксла */
базой
*/
данных про-
В некоторых системах допускается передавать номер протокола в аргументе level
системных вызовов setscckept и getscckept, но это не соответствует стандартам.
Код следующего примера достаточно прозрачен, если вы читали предыдущие
разделы:
static vcid prctcdb(vcid)
{
struct prctcent *p;
setprctcent(true);
while ((p = getprctcentO) 1= NULL)
dis play_prctcent(p);
endprctcentO;
static vcid display_prctcent(struct prctcent *p)
{
int i;
printf("KMfl: Xs; нсмер: Xd\n", p->p_name, p->p_prctc);
for (i = 0; p->p_aliases[i] != NULL; i++)
printf("\tXs\n", p->p_aliases[i]);
}
Но выводимые сведения выглядят гораздо интереснее. В ОС SuSE Linux я получил
описание 135 протоколов. Ниже приводится только часть полученного списка:
ГЛАВА 8 Сетевое взаимодействие и сокеты 579
имя: mobile; номер: 55
MOBILE
имя: tlsp; номер: 56
TLSP
имя: skip; номер: 57
SKIP
имя: ipv6-iomp; номер: 58
IPV6-ICMP
ICMPV6
iompv6
iomp6
имя: ipv6-nonxt; номер: 59
IPv6-NoNxt
имя: ipv6-opts; номер: 60
IPv6-0pts
имя: oftp; номер: 62
CFTP
имя: sat-expak; номер: 64
SAT-EXPAK
имя: kryptolan; номер: 65
KRYPT0LAN
И завершим этот раздел функциями:
getprotobyname — возвращает сведения о протоколе по его
«inolude <netdb.h>
struot protoent *getprotobyname(
oonst ohar «name /* имя протокола
);
/* Возвращает указатель на отруктуру
найден (ооотояние переменной еггпо не
*/
protoent или
определено)
NULL,
•/
еоли
имени
протокол
не
getprotobynumber — возвращает сведения о протоколе
#include <netdb.h>
struot protoent *getprotobynumber(
int proto /* номер протокола »/
);
/* Возвращает указатель на отруктуру
найден (ооотояние переменной еггпо не
protoent или
определено)
NULL,
•/ .
по его номеру
еоли
протокол
не
8.8.4 Получение сведений о службах
Для получения сведений о службах существует свой набор функций, которые
используют файл /etc/services в качестве базы данных служб:
580 ГЛАВА 8 Сетевое взаимодействие и сокеты
setservent — запустить обзор базы данных служб
«include <netdb.h>
vcid setservent(
int staycpen /* оставить соединение с базой данных открытым? */
);
getservent — получить очередную запись из базы
«include <netdb.h>
struct servent «getservent(vcid);
/• Всзаращает указатель на структуру
списка (ссстсяние переменней еггпс не
servent или
спределенс)
данных
NULL
•/
пс достижении
кенца
endservent
«include
— завершить обзор
<netdb.h>
vcid endservent(vcid);
базы
данных
служб
struct servent — структура для функций, работающих с базой данных служб
struct servent {
char
char
int s
char
>;
*s_narae;
**s_aliases;
_pcrt;
*s_prctc;
/•
/•
/•
/•
официальное имя службы */
массив альтернативных имен «/
номер порта */
имя протокола для данной службы */
getservbyname — возвращает сведения о службе по ее имени
«include <netdb.h>
struct servent *getservbyname(
const char «name, /* имя службы */
const char «prctc /* имя протокола •/
);
/* Возвращает указатель на структуру prctoent или NULL, если служба не
найдена (ссстсяние переменней еггпс не спределенс) */
getservbyport —
«include <netdt
struct servent
int pert,
const char
);
/* Возвращает у
возвращает сведения о
>.п>
*getservbypcrt(
/* порт */
•prctc /* имя прстсксла
службе по
•/
казатель на структуру prctcent или
найдена (ссстсяние переменней еггпс не
спределенс)
номеру п
NULL,
•/
если
орта
служба не
ГЛАВА 8 Сетевое взаимодействие и сонеты 581
И наш последний пример подобного рода:
static vcid servdb(vcid)
{
struct servant *s;
setservent(true);
while ((s = getserventQ) != NULL)
dispiay_servent(s);
endserventO;
>
static vcid dispiay_servent(struct servant *s)
{
int i;
printf("MMH: Xs; псрт: Xd; прстсксл: Xs\n", s->s_name, s->s_pcrt,
s->s_prctc);
fcr (i = 0; s->s_aiiases[i] != NULL; i++)
printf("\tXs\n", s->s_aiiases[i]);
}
Программа выводит массу информации, потому что и сам файл /etc/services
достаточно велик. Ниже приводится только часть полученных результатов:
имя: ftp-data; псрт: 5120; прстсксл: tcp
имя: ftp-data; псрт: 5120; прстсксл: udp
имя: ftp; псрт: 5376; прстсксл: tcp
имя: fsp; псрт: 5376; прстсксл: udp
имя: ssh; псрт: 5632; прстсксл: tcp
имя: ssh; псрт: 5632; прстсксл: udp
имя: teinet; псрт: 5888; прстсксл: tcp
имя: teinet; псрт: 5888; прстсксл: udp
имя: smtp; псрт: 6400; прстсксл: tcp
maii
имя: smtp; псрт: 6400; прстсксл: udp
maii
имя: nsw-fe; псрт: 6912; прстсксл: tcp
имя: nsw-fe; псрт: 6912; прстсксл: udp
имя: msg-icp; псрт: 7424; прстсксл: tcp
имя: msg-icp; псрт: 7424; прстсксл: udp
имя: msg-auth; псрт: 7936; прстсксл: tcp
имя: msg-auth; псрт: 7936; протокол: udp
582 ГЛАВА 8 Сетевое взаимодействие и сокеты
8.8.5 Получение сведений о сетевых интерфейсах
Здесь приводится описание функций, которые возвращают имена сетевых
интерфейсов и их порядковые номера. Для начала рассмотрим функцию, возвращающую
массив структур if_nameindex с информацией обо всех сетевых интерфейсах, и
соответствующую ей функцию, которая освобождает память, занимаемую массивом:
ifnameindex — возвращает сведения обо всех сетевых интерфейсах и их
порядковых номерах
«include <net/if.h>
struct if_nafneindex *if_nameindex(vcid);
/• Возвращает массив или NULL в случае сшибки (кед сшибки - в переменней
еггпс) */
if_freenameindex — освобождает
«include <net/if.h>
vcid if_freenameindex(
struct if_nameindex
);
*ptr /*
память,
указатель
занимаемую
на массив
*/
массивом
struct ifnameindex —
структура для функций, работающих
ей о сетевых интерфейсах
struct if_nameindex {
unsigned if_index
char *if_name;
};
■ /*
/*
порядковый нсмер интерфейса
имя интерфейса */
•/
с информ
аци-
Ниже приводится пример обращения к этим функциям:
static vcid ifdb(vcid)
{
struct if_nameindex *ni;
int i;
ec_null( ni = if_nameindex() )
fcr (i = 0; ni[i].if_index 1=0 tI ni[i].if_name != NULL; i++)
printf("порядковый нсмер: Xd; имя: Xs\n", ni[i].if_index,
ni[i].if_name);
if_freenameindex(ni);
return;
EC_CLEANUP_BGN
EC_FLUSH("ifdb")
EC_CLEANUP_END
}
ГЛАВА 8 Сетевое взаимодействие и сокеты 583
В Solaris был получен следующий результат:
порядковый номер: 1; имя: 1о0
порядковый номер: 2; имя: iprbO
а в SuSE Linux:
порядковый номер: 1; имя: 1о
порядковый номер: 2; имя: ethO
В данном случае имена интерфейсов соответствуют именам сетей, которые были
получены в разделе 8.8.2: петлевой (loopback) интерфейс и интерфейс,
подключенный к сети Интернет.
Функция, которая преобразует имя сетевого интерфейса в его порядковый номер:
if_nametoindex — возвращает порядковый номер инт
#inolude <net/if.h>
unsigned if_nametoindex(
oonst ohar «ifnarae /<
);
/* Возвращает порядковый
errno не определено) */
имя
номер
оетевого
или 0 в
интерфейоа */
олучае ошибки
ерфейса по его имени
(ооотояние
переменной
Будьте внимательны! В случае, если интерфейс не найден, функция возвращает
значение 0, а не -1!
Для получения имени интерфейса по его порядковому номеру используется функция:
ifindextoname — возвращает имя интерфейса по
ffinolude <net/if.h>
ohar *if_indextoname(
его
unsigned ifindex, /* порядковый номер интерфейоа
ohar «ifname /* имя
);
/* Возвращает порядковый номер
в переменной errno) */
интерфейоа */
или NULL в олучае
порядковому номеру
*/
ошибки (код
ошибки -
Аргумент ifname должен указывать на буфер емкостью не менее IF_NAMESIZE байт с
учетом завершающего символа «\0». Функция также возвращает указатель на этот буфер.
8.9 Прочие системные вызовы
В этом разделе обсуждаются системные вызовы, не вошедшие ни в один из
предыдущих разделов.
584 ГЛАВА 8 Сетевое взаимодействие и сокеты
8.9.1 Вызовы send и recv
Эти два системных вызова очень напоминают вызовы write и read и
дополнительно позволяют определить флаги, управляющие приемом/передачей данных.
Поскольку они не имеют аргумента с адресом сокета то, как правило, используются с соке-
тами, которые создают логические соединения. Для сокетов, не создающих
соединений, гораздо удобнее пользоваться системными вызовами sendtc, sendmsg, recvf rem
и reevmsg.
send — передает данные в сокет
«include <sys/socket.h>
ssize_t send(
int sccket_fd, /* файловый дескриптор сскета »/
censt void *data, /* данные для передачи */
size_t length, /* объем передаваемых данных */
int flags /* флаги */
);
/* Возвращает числе переданных байт или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
recv — принимает данные из сокета
«include <sys/sccket.h>
ssize_t recv(
int sccket_fd, /* файловый дескриптор сскета */
vcid «buffer, /* буфер для принимаемых данных */
size_t length, /* сбъем буфера */
int flags /* флаги */
);
/* Всзвращает числе принятых байт, 0 или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
С обоими системными вызовами допускается использовать флаг MSG_00B, как это было
описано в разделе 8.7. Дополнительно для вызова recv можно указать флаги MSG.PEEK
и/или MSG.WAITALL, которые были описаны в разделе 8.6.2. В нашей реализации SMI
(см. раздел 8.5) мы могли бы использовать вызов recv с флагом MSG_WAITALL вместо
функции readall.
8.9.2 Вызовы getsockname и getpeername
Системный вызов getscckname возвращает адрес сокета, который был присвоен ему
вызовом bind:
ГЛАВА 8 Сетевое взаимодействие и сокеты 585
getsockname — возвращает адрес сокета
#inoiude <sys/sccket.h>
int getscckname(
int sooket_fd,
struct scckaddr *sa,
sookien_t *sa_ien
);
/* Возвращает О в случае
в переменней еггпс) */
/•
/•
/•
файловый дескриптор сокета */
адрес сокета */
размер буфера для
успеха, -1 в случае
адреса */
сшибки (кед сшибки -
Как обычно, для любой функции, возвращающей адресную информацию сокета,
аргумент sa должен представлять буфер достаточного размера, a sa.len — размер
этого буфера. По возвращении из системного буфера sa.len будет содержать
фактический объем информации, записанной в буфер.
Похожая функция, которая возвращает адрес сокета с противоположной стороны
соединения:
getpeername — возвращает адрес
единения
#inciude <sys/sccket.h>
int getpeername(
int sccket.fd,
struct scckaddr *sa,
scckien.t *sa_len
);
/* Возвращает О в случае
в переменней еггпс) */
/•
/•
/•
сокета с противоположной стороны со-
файловый дескриптор сскета */
адрес
оскета */
размер буфера для адреса */
успеха,
-1 в случае сшибки (кед сшибки -
8.9.3 Вызов socketpair
Сокеты из домена af.unix по своему поведению напоминают именованные
каналы: каждый из процессов, участвующих в обмене данными, должен создать свой
собственный сокет. В отличие от именованных каналов, системный вызов pipe
возвращает два файловых дескриптора, которые может унаследовать дочерний процесс.
Системный вызов socketpair совмещает в себе преимущества двух предыдущих
подходов — он возвращает два файловых дескриптора за одно обращение:
586 ГЛАВА 8 Сетевое взаимодействие и сокеты
socketpair — создает пару
«include <sys/sccket.h>
int sccketpair(
int dcniain,
int type,
int prctcccl
int sccket_vectcr[2]
);
/* Возвращает О в случае
в переменней errnc) */
сокетов
/♦ домен (AFJJNIX, AF.INET и т. п.) */
/* SOCK.STREAM, SOCK.DGRAM и т. п. */
/* детализирует аргумент type; сбычне - ноль */
/« возвращаемые файловые дескрипторы */
успеха или -1 в случае сшибки (кед сшибки -
Первые три аргумента в точности соответствуют аргументам системного вызова
socket. Перечень доступных вызову доменов адресов и типов сокетов — зависит от
конкретной реализации, но, как правило, все системы поддерживают AF_UNIX и
SOCK.STREAM.
8.9.4 Вызов shutdown
При закрытии соединения, если в нем есть еще не отправленные или
принимаемые данные, система в течение некоторого времени продолжает поддерживать его
в открытом состоянии. Чтобы ускорить процесс закрытия, вы можете указать, что
данные, имеющиеся в соединении, вас больше не интересуют, обратившись к
системному вызову shutewn:
shutdown — прекращает выполнение операций приема/передачи над сокетом
«include <sys/socket.h>
int shutdown (
int sccket_fd, /* файловый дескриптор сскета */
int hew /* SHUT.RD, SHUT_WR или SHUT_RDWR */
);
/* Всзвращает О в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней errnc) */
Значение в SHUT_RD аргументе hew останавливает операции приема, SHUT_WR —
передачи, SHUT_RDWR — операции обоих типов.
8.9.5 Вызовы inet_ntop и inet_pton
В разделе 8.2.3 уже обсуждались функции inet_ntca и inet_addr, предназначенные
для преобразования адреса IPv4 из двоичного представления в строку с точечной
нотацией и обратно. Следующие две функции, inet_ntcp и inet_ptcn, более
интеллектуальные. Они корректно обслуживают как адреса IPv4, так и адреса IPv6.
ГЛАВА 8 Сетевое взаимодействие и сокеты 587
inet_ntop — преобразует адрес IPv4 или IPv6 из двоичного представления в
строку
«include <arpa/inet.h>
ccnst char *inet_ntcp(
int dcmain, /* AF_INET или AF_INET6 */
ccnst vcid *src, /* указатель на адрес в двсичнсм фермате */
char *dst, /* возвращаемая стрска */
sccklen_t dst_len /* размер буфера dst »/
);
/* Всзвращает стрску или NULL в случае сшибки (кед сшибки - в переменней
errnc) */
inet_pton -
ление
«include
- преобразует адрес IPv4 или IPv6 из
<arpa/inet.h>
int inet_ptcn(
int dcmain,
ccnst char
vcid *dst
);
/* Всзвращает 1
в случае другсй
h
*src, /*
h
строки в двоичное г
AF_INET или AF_INET6 */
входная стрска */
буфер, куда будет помещен
в случае успеха, 0 -
сшибки (кед сшибки -
результат
сшибка в строке адреса
в переменней errnc) */
*/
или -1
редстав-
Функции inet_ntcp в аргументе sre передается указатель на адрес в двоичном
представлении: для IPv4 — 32-битное число, для IPv6 — массив из 16 байт. Результат
преобразования размещается в буфере, на который указывает аргумент dst.
Установка размеров буфера может производиться с помощью макросов: INET_ADDRSTRLEN
для IPv4 И INET6_ADDRSTRLEN для IPv6.
Для функции inet_ptcn исходная строка с адресом должна быть в точечной
нотации для IPv4 и в двухточечной нотации для IPv6. Результат преобразования
размещается в буфере, на который указывает аргумент dst. Разумеется, буфер должен иметь
размер достаточный для размещения адреса в двоичном представлении: 4 байта (32
бита) для IPv4 и 16 байт (128 бит) для IPv6.
Пример использования функций inet_ntcp и inet_ptcn:
static vcid cvt(vcid)
{
char ipv6[16], ipv6str[INET6_ADDRSTRLEN], ipv4str[INET_AD0RSTRLEN];
uint32_t ipv4;
int r;
ec_neg1( r = inet_ptcn(AF_INET, "66.218.71.94", &ipv4) )
if (r == 0)
printf("Ошибка в строке адреса\п");
588 ГЛАВА 8 Сетевое взаимодействие и сокеты
else {
ec_null( inet_ntcp(AF_INET, &ipv4, ipv4str, sizecf(ipv4str)) )
printf("Xs\n", ipv4str);
}
ec_neg1( r = inet_ptcn(AF_INET6,
"FEDC:BA98:7654:321D:FEDC:BA98:7654:321D", &ipv6) )
if (r == D)
printf("Ошибка в строке адреса\п");
else {
ec_null( inet_ntop(AF_INET6, &ipv6, ipv6str, sizeof(ipv6str)) )
printf("Xs\n", ipv6str);
}
return;
EC_CLEANUP_BGN
EC_FLUSH("cvt")
EC_CLEANUP_END
}
Результат работы функции:
66.218.71.94
fedc:Ьа98:7654:321D:fedc:ba98:7654:321D
8.10 Вопросы производительности
В разделе 8.4.4 я на примере Web-сервера демонстрировал возможность
одновременного обслуживания нескольких клиентов. А теперь представьте себе, что наш
Web-сервер попытается обслужить 1000 или даже 10 000 клиентов одновременно.
Возможно ли такое в принципе?
Составим список проблем, с которыми придется столкнуться.
• Системный вызов select должен будет отслеживать 10 001 файловый
дескриптор. Это число наверняка больше, чем fd_set сможет вместить. Но, даже если и
сможет, проверка всех дескрипторов в наборе будет отнимать очень много
времени.
• Существует системное ограничение на количество файловых дескрипторов,
которые процесс может держать открытыми, и это ограничение наверняка
меньше числа 10 001.
• Чтобы обеспечить приличное время отклика, нам придется запустить
множество процессов или потоков, но и эти ресурсы не бесконечны.
• Копирование данных из файлов в сокеты (через память процесса) — слишком
дорогостоящая операция. Сервер будет работать убийственно медленно при
одновременном обслуживании 10 000 клиентов.
ГЛАВА 8 Сетевое взаимодействие и сокеты 589
• Большое количество передаваемых и отправляемых данных приведет если не к
полному захвату памяти, то, по крайней мере, к существенному замедлению
работы системы.
Как видите, проблемы более чем серьезные. Это одна из причин, по которой
реальные серверы считаются одним из сложнейших видов программного обеспечения.
Если перед вами встанет задача обслужить 10 000 клиентов (или хотя бы 500) могу
посоветовать потрясающую статью: «The С10К Problem» («Проблема 10 000
клиентов»), которая описывает возникающие проблемы и рассматривает возможные пути
их решения [Keg2003].
Упражнения
8.1. Измените пример из раздела 8.1.3 так, чтобы он позволил осуществлять
обмен между компьютерами (AF_INET). Запустите программу-сервер и
программу-клиент на разных компьютерах.
8.2. На базе сведений из [RFC1288] реализуйте простейшую версию команды finger.
Она должна обрабатывать только аргумент вида userehost без дополнительных
параметров. Используйте сокеты SOCK.STREAH. Для решения следующего
упражнения вам потребуется предусмотреть возможность задания номера порта.
8.3. На базе сведений из [RFC1288] реализуйте простейшую версию сервера finger,
но вместо стандартного порта с номером 79, используйте один из не занятых
стандартными службами портов (например 3079). Используйте сокеты
SOCK.STREAM. Протестируйте ваш сервер с помощью команды finger,
разработанной в предыдущем упражнении. Если есть такая возможность, установите
ваш сервер на отдельной машине на стандартный порт 79 и протестируйте
его стандартной командой finger с другой машины.
8.4. То же самое, что и в упражнении 8.2, но на этот раз выполните реализацию
команды на сокетах, не образующих соединений.
8.5. То же самое, что и в упражнении 8.3, но на этот раз выполните реализацию
сервера на сокетах, не образующих соединений.
8.6. Разработайте и реализуйте простой пример взаимодействия, основываясь на
документе RFC 2549, который вы найдете на сайте [RFC].
8.7. Реализуйте Web-браузер с графическим интерфейсом с использованием
каких-либо библиотек, например Qt. Внимание: это довольно сложное задание!
Возможно, вам придется прибегнуть к помощи своих товарищей и выполнить
это упражнение как групповой проект. Подсказка: начните с обработки
вложенных таблиц. Если вы успешно справитесь с этой частью, все остальное вы
реализуете без особых трудностей.
8.8. Реализуйте SMI (Упрощенный интерфейс обмена сообщениями) на базе со-
кетов, не образующих соединений. Проведите эксперимент по измерению
быстродействия и сравните ваши результаты с приведенными в разделе 7.15.
590 ГЛАВА 8 Сетевое взаимодействие и сокеты
8.9. Разработайте сетевой поисковый робот, который начинает поиск с
заданного URL. Просматривает найденную страницу, извлекает из нее все обнаруженные
абсолютные ссылки (например, отыскивая строки, начинающиеся со слова
«href») и помещает их в свой список. Затем робот должен «обойти» все
найденные ссылки, получить по ссылкам Web-страницы и извлечь из них
дополнительные ссылки. Поиск повторять до тех пор, пока число ссылок в списке
не превысит некоторое значение или пока программа не будет прервана
пользователем. (Очевидно, ошибки, с которыми придется столкнуться
роботу, не должны расцениваться как фатальные — поиск должен продолжаться.)
Результаты должны выводиться на стандартное устройство вывода. Выводиться
должен как минимум список найденных URL. Желательно для каждого URL
выводить признак доступности (статус HTTP — 200), а в случае недоступности
— причину ошибки. Вы обязаны придерживаться протокола исключения
роботов (Robot Exclusion Protocol см. www.robotstxt.org) и не просматривать
страницы, от которых вас попросят «держаться подальше». Предусмотрите
наличие параметра «уникальный хост», чтобы предотвратить посещение одного и
того же сервера дважды (часть URL до первого символа «/»). Проведите поиск
с включенным параметром «уникальный хост». (Сначала я думал начать
«сканирование» с www.yahoo.com, но, как оказалось, все ссылки на начальной
страничке — относительные, поэтому я получил только один URL.) Будьте
внимательны к своим коллегам, запуская свой робот, — убедитесь, что он не
«съедает» всю пропускную способность канала.
8.10. Усовершенствуйте программу из упражнения 5.14 так, чтобы она могла
выводить атрибуты процесса (из Приложения А), о которых вы узнали в этой главе.
mil g
Сигналы и таймеры
9.1 Основы сигналов
Сигнал — это сообщение о наступлении некоторого события, например, нажатия
комбинации клавиш Ctrl+C, ошибки выполнения арифметической операции и т. п. Обычно
сигналы передаются процессам и потокам асинхронно, т. е. — прерывают работу
процесса или потока, вне зависимости от того, чем они в данный момент времени
были заняты. Сигнал может вызвать немедленное завершение процесса или быть
перехваченным и обработанным специальной функцией.
9.1.1 Введение в сигналы
Ниже приводится пример программы, которая демонстрирует перехват и обработку
сигналов. Она выводит число каждые три секунды, а при получении сигнала
прерывания (SIGINT), выводит сообщение и завершается:
static vcid fcn(int signura)
{
(vcid)write(STDOUT_FILENO, "Получен сигнал\п", 15);
_exit(EXIT_FAILURE);
}
int main(vcid)
<
int i;
struct sigacticn act;
memset(&act, 0, sizecf(act));
act.sa_handler = fen;
ec_neg1( sigaction(SIGINT, &act, NULL) )
for (i = 1; ; i++) <
sleep(3);
printf("Xd\n", i);
}
exit(EXIT_SUCCESS);
592 ГЛАВА 9 Сигналы и таймеры
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
Обращением к системному вызову sigacticn производится установка
функции-обработчика сигнала SIGINT. После запуска программы, когда на экране появилось число
2, я нажал клавиши Ctrl+C. Это привело к тому, что исполнение программы было
прервано (когда она находилась внутри sleep, или printf, или в момент
выполнения оператора цикла) и управление перешло функции fen, которая вывела текст
сообщения и завершила работу программы, обратившись к системному вызову .exit
(см. раздел 9-1.7, где объясняется, почему я не использовал системный вызов exit).
В результате, на экран было выведено следующее:
Получен сигнал
Если бы функция-обработчик не была установлена, нажатие комбинации Ctrl+C
привело бы к немедленному завершению работы процесса, потому что в этом
заключается реакция по умолчанию любого процесса на сигнал SIGINT.
С помощью системного вызова sigacticn можно заставить процесс игнорировать
сигнал SIGINT:
int nain(void)
{
int i;
struct sigacticn act;
memset(&act, 0, sizecf(act));
act.sa.handier = SIG_IGN;
ec_neg1( sigacticn(SIGINT, &act, NULL) )
fcr (i = 1; ; i++) {
sieep(3);
printf("Xd\n", i);
}
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT.FAILURE);
EC_CLEANUP_END
)
На этот раз программа будет продолжать выводить числа, невзирая на нажатия
клавиш Ctrl+C. Завершить ее можно нажатием комбинации Ctrl+\, которая
порождает сигнал sigquit.
Работа с сигналами достаточно сложна, потому что:
ГЛАВА 9 Сигналы и таймеры 593
• существует значительное количество различных сигналов и нередко
обстоятельства, их породившие, очень трудны для понимания;
• выбор той или иной реакции на сигнал может оказаться очень непростым;
• обработка сигнала может быть связана с серьезными трудностями.
Я постепенно расскажу вам обо всех проблемах, связанных с сигналами, и к концу
этой главы вам многое станет понятно.
9.1.2 Жизненный цикл сигналов
Сигнал рождается {генерируется) в тот момент, когда происходит событие,
ассоциированное с данным сигналом. Жизнь сигнала заканчивается в момент
доставки его приложению, то есть тогда, когда будет запущено действие, связанное с
сигналом. Существует три разновидности действий (реакций на сигнал):
1. Действие по умолчанию (SIG_DFL): может завершать, приостанавливать или
возобновлять работу процесса, а также игнорировать сигнал.
2. Игнорирование сигнала (SIG_IGN).
3. Перехват сигнала и вызов функции-обработчика.
В большинстве случаев сигналы генерируются естественным путем —
пользователем или системой. Например, операция деления на ноль порождает
естественный сигнал SIGFPE, завершение дочернего процесса порождает естественный
сигнал SIGCHLD. Но любой сигнал может быть сгенерирован искусственно, при
помощи одного из пяти системных вызовов: kill, killpg, pthread.kill, raise и sigqueue.
В следующем разделе, где будут перечислены все сигналы, я опишу естественные
причины, вызывающие их появление. Пять сигналов — SUGKILL, SIGSTOP, SIGTERM, SIGUSR1
и SIGUSR2 — всегда генерируются искусственно, обычно системным вызовом kill.
Между моментом рождения и моментом доставки сигнал находится на стадии
ожидания. Будет ли сигнал доставлен приложению несколько раз, если он продолжал
генерироваться в момент, когда еще находился в состоянии ожидания, зависит от
реализации. Но при разработке переносимых приложений вы не должны строить
свои алгоритмы на том или ином предположении. Более подробно эта тема
обсуждается в разделе 9.5.3-
Поток может заблокировать сигнал в состоянии ожидания.* Набор блокируемых
сигналов называется маской сигналов. Для создания масок и управления ими
существует целый ряд системных вызовов, которые будут описаны в разделе 9.1.5.
Сигнал может быть послан как конкретному потоку, так и всему процессу. В
последнем случае, если в процессе имеется более одного потока, не заблокировавшего
данный сигнал, предугадать, какому из них он будет доставлен, невозможно. В раз-
" Если в рамках процесса исполняется только один поток (т. е. приложение не является
многопоточным), то все, что говорится о потоке, в равной мере относится и ко всему процессу.
594 ГЛАВА 9 Сигналы и таймеры
деле 9-13 мы подробнее остановимся на том, в каких случаях и при каких
обстоятельствах сигналы передаются потокам, а в каких — процессам.'
Реакция на сигнал (или действие по сигналу) относится ко всему процессу, даже
если этот процесс исполняется в нескольких потоках, хотя, как я уже упоминал,
каждый поток может иметь свою маску сигналов.
Обычно в момент вызова функции-обработчика сигнал временно добавляется к
маске сигналов потока, что предотвращает возможность доставки этого же
сигнала до завершения обработчика. Таким образом, отпадает необходимость
беспокоиться о возможном рекурсивном обращении к функции, хотя, если функция
призвана обрабатывать несколько типов сигналов, рекурсивный вызов может произойти
в случае доставки сигнала с другим номером.
9.1.3 Типы сигналов
В [SUS2002] определено 28 различных сигналов, но большинство реализаций
дополняют этот список своими сигналами. Однако я не буду рассматривать
нестандартные сигналы, а вы, если захотите, всегда сможете отыскать их описание в
системной документации. Кроме того, существуют дополнительные сигналы, которые
являются частью расширений реального времени POSIX (см. раздел 95). Для
удобства разделим все сигналы SUS на группы. В следующем списке символы в скобках
описывают действие по умолчанию, принятое для сигнала (расшифровка значений
символов приводится ниже). Для сигналов, имеющих исключительно
искусственное происхождение, этот факт отмечен особо. Кстати, не забывайте, что любой
естественный сигнал может быть порожден искусственно.
• Обнаруженные ошибки
SIGBUS — попытка доступа к неопределенной части памяти (А)
SIGFPE — ошибка арифметической операции (А)
SIGILL — некорректная команда (А)
SIGPIPE — запись в канал, из которого никто не читает (Т)
SIGSEGV — недопустимое обращение к сегменту памяти (А)
SIGSYS — некорректное обращение к системному вызову (А)
SIGXCPU — исчерпан лимит процессорного времени (А)
SIGXFSZ — превышено ограничение на размер файла (А)
• Генерируемые пользователем или приложением
SIGABRT — обращение к системному вызову abort (A)
SIGHUP — обнаружен обрыв связи с терминалом или завершение
терминального процесса (Г)
SIGINT — прерывание работы (с клавиатуры) (Т)
SIGKILL — «ликвидировать», может иметь только искусственное происхождение (Т)
Все, что я буду говорить в этой главе о потоках, относится только к потокам POSIX.
Некоторые реализации, такие как «Linux Threads», широко используемые в Linux и FreeBSD, не
соответствуют стандарту POSIX и обслуживают сигналы нестандартным образом. Однако
новейшая версия — NPTL, прекрасно работает с сигналами.
ГЛАВА 9 Сигналы и таймеры 595
SIGQUIT — завершить (с клавиатуры) (А)
SIGTERM — завершить, может иметь только искусственное происхождение (Т)
SIGUSR1 — сигнал пользователя № 1, имеет только искусственное
происхождение (Т)
SIGUSR2 — сигнал пользователя №2, имеет только искусственное
происхождение (Т)
• Управление заданиями
SIGCHLD — дочерний процесс завершил или приостановил работу (I)
SIGC0NT — продолжить исполнение (с клавиатуры) (С)
SIGST0P — приостановить работу, может иметь только искусственное
происхождение (С)
SIGTSTP — сигнал с терминала, приостановить работу (с клавиатуры) (S)
SIGTTIN — попытка чтения из фонового процесса (S)
SIGTT0U — попытка записи из фонового процесса (S)
• События таймера
SIGALRM — истекло время таймера (Т)
SIGVTALRM — истекло время виртуального таймера (Т)
SIGPR0F — истекло время профилирующего таймера (Т)
• Прочие события
SIGP0LL — произошло ожидаемое событие (Т)
SIGTRAP — ловушка трассировщика — точка останова (А)
SIGURG — доступны внеочередные данные в сокете (I)
где символы в скобках имеют следующее значение:
I — сигнал игнорируется (от англ. «Ignore»)
Т — приводит к завершению процесса (от англ. «Terminate»)
А — то же самое, что и Т, но при этом выполняются дополнительные действия,
определяемые системой, например, создание файла с дампом памяти процесса (от
англ. «Abort»)
S — остановка (от англ. «Stop»)
С — продолжение после остановки (от англ. «Continue»)
Сигналы обнаружения ошибок, имеющие естественную природу, являются
результатом ошибок в программе. Для сигналов SIGBUS, SIGSEGV, SIGFPE и SIGILL точная причина
ошибки стандартами не оговаривается, но, как правило, все они обнаруживаются
на аппаратном уровне. Кроме того, эти четыре сигнала подчиняются
определенным правилам, в случае их естественного происхождения:
• если для этих сигналов, с помощью sigaction, установлено действие SIG_IGN, то
реакция приложения на них — не определена (зависит от системы);
• реакция приложения, в случае нормального возврата из функции-обработчика
сигнала — не определена (зависит от системы);
• результат блокирования сигнала не определен.
596 ГЛАВА 9 Сигналы и таймеры
Другими словами, если ошибка, обнаруженная аппаратурой ЭВМ, действительно
имеет место быть, ваша программа едва ли сможет продолжить работу. Крайне
небезопасно игнорировать сигналы такого рода, продолжать работу по выходе из
функции-обработчика или блокировать их. Наиболее правильный способ —
завершить работу программы в функции-обработчике.
Сигналы SIGINT и SIGQUIT, обычно являются результатом нажатия определенной
комбинации клавиш на клавиатуре (см. раздел 4.5-7). Сигнал SIGHUP, как правило,
возникает при разрыве связи с терминальным устройством. Сигнал SIGABRT
генерируется системным вызовом abort (см. раздел 9.1.9), a SIGTERM — командой kill —
это основной способ принудительного завершения процессов, например, когда
системный администратор дал команду на остановку системы. Сигналы SIGUSR1 и
SIGUSR2 не генерируются системой и могут свободно использоваться для
внутренних нужд приложения.
Сигналы, имеющие отношение к управлению заданиями, обсуждались в разделе 4.3.
Сигнал SIGALRM будет подробно обсуждаться в разделе 9.7.1. Другие два сигнала от
таймеров мы рассмотрим в разделе 9-7.4.
Среди прочих сигналов SIGP0LL может использоваться для организации работы с
потоками данных (см. раздел 4.9) — разрешается обращением к системному
вызову ioctl. В программах такой прием используется крайне редко, поэтому можете
пока не беспокоиться о нем. Назначение сигнала SIGURG описывалось в разделе 8.7.
Сигнал sigtrap используется программами-отладчиками.
Сигналы, вызванные естественными причинами, ничем не отличаются от
сигналов, сгенерированных искусственно. Сигналы обнаружения ошибок
естественного происхождения посылаются потоку, который породил эту ошибку. Все
остальные сигналы передаются процессу. Искусственно порожденные сигналы могут
посылаться как потоку, так и процессу, в зависимости от используемого
системного вызова (см. раздел 9-1.9).
В случае необходимости выполнения определенных действий перед завершением,
программа должна перехватывать и обрабатывать сигналы SIGHUP, SIGINT и SIGTERM.
На время разработки и отладки следует избегать изменения действия по
умолчанию для сигнала SIGTERM, чтобы оставить возможность принудительного
завершения программы с клавиатуры (при этом, автоматически создается файл с дампом
памяти). Возможность изменения реакции на другие сигналы используется довольно
редко. Но хорошо продуманная и отлаженная программа должна перехватывать все
возможные сигналы, выполнять сборку мусора, по возможности записывать
сообщение в файл журнала и выводить подробное описание возникшей ошибки.
Психологически, сообщение типа-. «Internal error 53: contact customer support»
(«Внутренняя ошибка 53: обратитесь в службу технической поддержки») выглядит
гораздо предпочтительнее, чем «Bus error — core dumped» («Ошибка шины — создан файл
с дампом памяти»). Более подробное обсуждение этой темы, с примером
программы, вы найдете в разделе 918.
ГЛАВА 9 Сигналы и таймеры 597
9.1.4 Прерывание системных вызовов
Сигнал может стать причиной прерывания работы системного вызова. Если в
результате действия по умолчанию или в функции-обработчике сигнала, процесс
завершается принудительно (как в примере, показанном выше) прерванный системный
вызов никогда не возобновляет свою работу. Если сигнал приостанавливает процесс, работа
системного вызова будет возобновлена, когда процесс продолжит исполнение.
Но даже если сигнал перехватывается обработчиком, который возвращает
управление в программу, работа системного вызова, как правило, не возобновляется.
В этом случае он возвращает признак ошибки с кодом EINTR. В отдельных случаях
преднамеренная выдача сигналов может даже оказаться полезной, например, можно
задать генерацию сигнала SIGALRM, который должен прервать заблокированный вызов
read, скажем, через 10 секунд. Но вообще, прерывание системных вызовов
сигналами может стать источником проблем, поскольку алгоритмы, как правило, не
предусматривают такую возможность.
Самое простое правило, которое следует соблюдать при работе с сигналами —
никогда не возвращать управление из обработчика сигнала, если вы не управляете
контекстом, в котором был сгенерирован сигнал. В случае, когда данное правило
неприменимо, можно передать в sigaction флаг SA.RESTART (см. раздел 9-1-6), тогда
системный вызов не будет прерван — он возобновит свою работу по возвращении
управления из функции-обработчика.
Прерван может быть только заблокированный системный вызов. В данном случае
понятие «заблокированный» означает, что системный вызов находится в ожидании
какого-либо события, наступление которого не может быть предсказано заранее,
например: приход данных от терминала или из сокета, завершение процесса, приход
сообщения, освобождение семафора и тому подобное. Системные вызовы, которые
просто затрачивают некоторое время на выполнение определенных действий,
например, чтение из файла или запуск нового процесса, не считаются заблокированными,
поскольку в этом случае они не ожидают непредсказуемого наступления события.
Не каждый заблокированный системный вызов может быть прерван. Например,
pthread_mutex_lock продолжает свою работу даже по прибытии сигнала (после
возврата из обработчика). Чтобы узнать, какие системные вызовы могут быть
прерваны, а какие нет, вам придется ознакомиться с соответствующей документацией,
желательно SUS.
9.1.5 Управление маской сигналов
Аналогично наборам файловых дескрипторов, используемым в системном вызове
select, маска сигналов представляет собой набор битов', для управления
которыми предназначены следующие функции:
В качестве альтернативы можно было бы использовать тип unsigned long, но во времена
появления этих функций, практически везде тип long имел длину 32 бита (тип long long еще
не появился), чего было явно недостаточно.
598 ГЛАВА 9 Сигналы и таймеры
sigemptyset — инициализирует пустой набор сигналов
«include <signal.h>
int sigemptyset(
sigset_t *set /* набср сигналсв
);
/* Всзвращает 0 в случае успеха или
в переменней еггпс) */
*/
-1 в случае
сшибки
(кед сшибки -
sigfillset — инициализирует полный
«include <signal.h>
int sigfillset(
sigset_t *set /* набср сигналсв
);
/* Всзвращает 0 в случае успеха или
в переменней еггпс) */
набор сигналов
•/
-1 в случае
сшибки
(кед
сшибки -
sigaddset — добавляет сигнал в набор
«include <signal.h>
int sigaddset(
sigset.t «set, /* набср сигналсв
int signurn /* нсмер сигнала
);
/* Всзвращает 0 в случае успеха или
в переменней еггпс) */
•/
•/
-1 в случае
сшибки
(кед
сшибки -
sigdelset — удаляет сигнал из набора
«include <signal.h>
int sigdelset(
sigset.t *set, /* набср сигналсв */
int signurn /* нсмер сигнала */
);
/* Всзвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
ГЛАВА 9 Сигналы и таймеры 599
sigismember — проверяет наличие сигнала в наборе
«include <signal.h>
int sigismember(
const sigset_t *set, /* набор сигналов */
int signum /* номер сигнала */
);
I* Возвращает 1 если сигнал включен в набор, 0 если не включен в набор или
-1 в случае сшибки (кед сшибки в переменней еггпс) */
Работа с маской сигналов типа sigset.t начинается с вызова функции sigemptyset
или sigfillset, а затем с помощью sigaddset или sigdelset добавляются нужные или
удаляются ненужные сигналы. Для проверки наличия сигнала в наборе
используется функция sigismember.
Для каждого из потоков можно определить только одну маску сигналов. Делается
это с помощью pthread.sigmask:
pthread sigmask — изменяет маску
int
);
/*
не
pthread_sigmask(
int hew,
const sigset_t «set,
sigset_t «cset
Возвращает О в случае
определено) */
1*
h
h
сигналов для потока
определяет
новая маска
старая
успеха ил»
порядок установки
сигналов */
маска сигналов */
кед
сшибки (состояние
новой
маски
переменней
*/
еггпс
Порядок установки новой маски сигналов set определяется аргументом hew:
SIG_BL0CK В качестве текущей маски будет принято объединение старой
и новой масок.
SIG.SETHASK В качестве текущей маски будет принята новая маска.
SIG_UNBLOCK В качестве текущей маски будет принято объединение старой
и логического дополнения новой маски.
Проще говоря, S1G_BL0CK добавляет к маске сигналы из набора set, S1G_UNBL0CK
удаляет из маски сигналы, находящиеся в наборе set, a SIG_SETMASK просто делает
набор set текущей маской.
Если аргумент cset не равен NULL, по заданному адресу возвращается старый набор
сигналов маски. Допускается передавать значение NULL в аргументе set. В этом
случае через параметр cset (если он не равен NULL) возвращается маска сигналов без
ее изменения (аргумент hew игнорируется).
Программа не может заблокировать сигналы S1GK1LL и S1GST0P. Эти сигналы нельзя
перехватить или игнорировать, они всегда передаются процессу и вызывают его
завершение или остановку соответственно.
20 Зак. 4030
600 ГЛАВА 9 Сигналы и таймеры
Если процесс представлен единственным потоком, вместо pthread.sigmask можно
использовать устаревшую функцию sigproomask, которая существовала еще до
появления стандартов POSIX. Эти две функции практически идентичны, за
исключением того, что sigproomask возвращает код ошибки в глобальной переменной еггпо:
sigprocmask — изменяет
маску сигналов
имеет единственный поток)
«inolude <signal.h>
int sigproomask(
int how,
oonst sigset_t «set,
sigset_t «oset
);
/* Возвращает О в случае
в переменной errno) */
/*
/•
/*
для потока (только если
определяет порядок
новая маока
отарая маока
успеха или -1 в
*/
•/
случае
процесс
уотановки новой маоки */
ошибки (код ошибки
-
Очень часто программы не предусматривают исполнение в нескольких потоках,
поэтому на этапе связывания библиотека «pthread» не подключается и функция
pthread.sigmask оказывается недоступной. Таким образом, единственное, что вам
остается — использовать функцию sigprocmask.*
Маска сигналов не должна применяться для игнорирования сигналов — для этого
предназначено действие SIG.IGN. Маска предназначена для временного блокирования
сигналов, чтобы защитить некоторую часть кода программы от прерывания по
сигналу. Например, код функции-обработчика, когда в момент вызова сигнал
временно добавляется к маске, что предотвращает возможность доставки этого же
сигнала до завершения обработчика."
Другой пример: при запуске приложение может установить маску на все сигналы,
после чего без лишних хлопот продолжить инициализацию, установку обработчиков
и тому подобное. Делается это с помощью обращений к функциям sigfillset и
pthread.sigmask (или sigproomask).
В этой главе вы найдете ряд других примеров с масками сигналов.
9.1.6 Системный вызов sigaotion
Реакция приложения на сигналы задается с помощью системного вызова sigaotion.
Он вызывается для каждого сигнала, действие которого необходимо изменить:
В моей версии Solaris функция pthread.sigmask доступна даже без библиотеки «pthread», но
она ровным счетом ничего не делает, поэтому я был вынужден заменить ее вызовом
sigprocmask.
Если выход из обработчика осуществляется с помощью longjmp, состояние маски может
оказаться неопределенным, поэтому лучше использовать для этих целей siglongjmp (см. раздел
9.6).
ГЛАВА 9 Сигналы и таймеры 601
sigaction — устанавливает реакцию приложения на сигнал
«include <signal.h>
int sigacticn(
int signum,
ccnst struct sigacticn «act
struct sigacticn *cact
);
/* Возвращает О в случае успеха
в переменней еггпс) */
/*
/*
/*
или
нсмер сигнала */
невсе действие */
старее действие */
-1 в случае сшибки
(кед сшибки -
struct
sigaction — структура для
struct sigacticn {
};
vcid (♦sa_handler)(int);
sigset_t sajnask;
int sa_flags;
vcid (*sa_sigacticn)(int
/*
Л
h
системного вызова
SIG.DFL,
сигналы,
флаги */
siginfc_t
SI6_I6N
кстсрые
*, vcid
или
sigacticn
указатель на
функцию
будут заблскирсваны */
*);
/* сбрабстчик сигнала
реальнсгс
времени
*/
*/
В аргументе act передается указатель на структуру, которая определяет реакцию на
сигнал для всего процесса — реакцию на сигнал для отдельного потока изменить
нельзя. Если аргумент cact не является пустым указателем, по заданному адресу
записывается описание прежней реакции процесса на сигнал. Если нужно лишь
получить описание действия сигнала, передайте в аргументе act значение NULL, в
этом случае никаких изменений вноситься не будет. В большинстве наших
примеров мы будем передавать значение NULL в аргументе cact, но в разделе 9-7.2 будет
рассмотрен пример, который сохраняет предыдущую реакцию на сигнал, а затем
восстанавливает ее.
Вы не можете изменить реакцию приложения на сигнал SI6KILL, который всегда
приводит к завершению приложения (не только отдельного потока) и SIGST0P,
который всегда приостанавливает исполнение процесса (не только потока).
В структуре sigacticn поле sa_handler определяет собственно реакцию на сигнал и
может иметь одно из следующих значений:
SI6_dfl Реакция по умолчанию, которая зависит от вида сигнала, как
это было описано в разделе 9.1.3, но приложение всегда либо
игнорирует сигнал, либо завершается, либо
приостанавливается, либо возобновляет работу.
SIG.IGN Приложение игнорирует сигнал, в результате доставка сигнала
не оказывает влияния на приложение. Имеется один побочный
эффект: когда для сигнала SIGCHLD определена реакция SIG_IGN, это
равносильно установке флага SA_N0CLDWAIT (см. ниже).
Но реакция SIG.dfl не имеет такого эффекта, хотя она
заключается в том, чтобы игнорировать сигнал.
функция Указатель на функцию-обработчик. В этом случае говорят:
«•сигнал будет перехвачен».
602 ГЛАВА 9 Сигналы и таймеры
Функции-обработчики сигналов выглядят примерно так, как это было показано в
разделе 9.1.1:
static void fcn(int signura)
{
(void)write(STDOUT_FILENO, "Получен сигнал\п", 11);
_exit(EXIT_FAILURE);
}
Функция может быть как статической, так и нестатической — главное, чтобы
определение функции совпадало с прототипом. В момент доставки сигнала вызывается
функция-обработчик, которой в качестве аргумента передается номер сигнала
(например SIGINT или SIGUSR1). Перечень сигналов и их имен приводится в разделе 9-1.3-
Вы можете предусмотреть отдельные функции для каждого из сигналов, можете все
сигналы обрабатывать одной функцией или выбрать нечто среднее между этими двумя
крайностями. Подробнее об обработчиках сигналов читайте в разделе 9-1-7.
Если системой поддерживаются сигналы реального времени, можно установить флаг
SA_SIGINFO (см. ниже) и передать указатель на обработчик в поле sa_sigaction. В этом
случае обработчик получает значительно больше сведений о сигнале. Более подробно
сигналы реального времени будут обсуждаться в разделе 9-5- Некоторые реализации
используют для хранения указателя sa_handler и sa_sigaction одну и туже область памяти,
поэтому записывайте адрес обработчика только в один из них — установив один и
обнулив другой, вы рискуете стереть только что записанный указатель.
Как я уже говорил, перехваченный сигнал блокируется на время исполнения
функции-обработчика, но имеется возможность дополнительно указать список
сигналов, которые должны быть блокированы. Для этого их нужно занести в набор sa_mask
с помощью функций, которые были описаны в разделе 9.1.5.
Если сигнал доставляется потоку, обработчик сигнала исполняется в контексте этого-
потока и, соответственно, временному изменению подвергается маска сигналов этого
потока. Помните, что существует два способа доставки сигнала в поток: либо
сигнал доставляется определенному потоку, либо он доставляется одному из потоков,
не заблокировавших данный сигнал, но предугадать — какому из них, заранее
невозможно.
Ниже приводится список переносимых флагов для поля sa_f lags. Обратите
внимание: первые два применимы только для сигнала SIGCHLD.
SA_N0CLDST0P He посылать сигнал при остановке и возобновлении дочернего
процесса.
SA_N0CLDWAIT He превращать дочерний процесс в «зомби». Действие флага
было описано в разделе 5.9. Явная установка реакции SIG_IGN
на сигнал SIGCHLD дает тот же эффект.
SA_NODEFER He добавлять сигнал к маске при вызове обработчика, если он
явно не указан в поле sa_nask. Этот флаг оставлен
исключительно для сохранения совместимости с устаревшей функцией
signal (см. раздел 94).
ГЛАВА 9 Сигналы и таймеры 603
SA_ONSTACK Доставлять сигнал на альтернативном стеке сигналов, если
таковой был объявлен обращением к функции sigaltstack
(см. раздел 9.3).
SA.RESETHANO Установить реакцию на сигнал в значение SIG_DFL и на входе
в функцию-обработчик сбрасывать флаг SA.SIGINFO.
Игнорируется для сигналов SIGILL и SIGTRAP. Дополнительно
выполняются действия, свойственные сигналу SA_NODEFER и точно
так же существует для совместимости с функцией signal.
SA_RESTART He прерывать исполнение системных вызовов (см. раздел
9.1.4). Пример использования флага вы найдете в разделе 9.7.4.
SA_SIGINFO « Указатель на функцию-обработчик следует брать из поля
sa.sigaction, а не из sa_handler (см. раздел 95).
А теперь коротко — что мы узнали о флагах.
• Флаги SA_N0CLDST0P и SA_NOCLDWAIT применимы только к сигналу SIGCHLD.
• Флаги SA_NODEFER и SA.RESETHAND существуют для сохранения совместимости с
устаревшей и ненадежной реализацией механизма сигналов, использования
которого вам следует избегать по мере возможности. (Хотя упражнение 94
потребует применения этого механизма.)
• Флаг SA_ONSTACK используется крайне редко и в весьма специфичных условиях.
• Флаг SA_SIGINFO используется при работе с сигналами реального времени.
• Флаг SA_RESTART иногда бывает очень удобен.
И так же кратко — о реакциях и их влиянии на потоки.
• Реакция на сигнал всегда относится ко всему процессу целиком. Она может быть
одной из следующих: процесс перехватывает сигнал, игнорирует его,
завершается, приостанавливается или возобновляет работу после остановки.
• Маски сигналов назначаются для отдельных потоков.
• С помощью обращения к sigaction вы можете заставить процесс игнорировать
сигнал или назначить функцию-обработчик. Если для сигнала выбрана реакция
по умолчанию (SIG_DFL), процесс может игнорировать сигнал, завершиться,
приостановиться или возобновить работу, но вы не можете выбрать конкретную
реакцию из этих четырех (см. раздел 9-1-3)-
• Реакции игнорирования, завершения, приостановки и возобновления всегда
относятся к процессу целиком.
• Обращение к обработчику сигнала производится всегда в рамках только
одного потока. Если сигнал доставляется процессу (когда не указан конкретный
поток) и в процессе имеется более одного потока, не заблокировавшего сигнал,
заранее предугадать, какому из них сигнал будет доставлен, невозможно.
В качестве примера ниже приводится исходный код функции, после обращения к
которой процесс будет игнорировать сигналы SIGINT и SIGQUIT. Она вызывается нашей
версией командного интерпретатора из раздела 6.4:
604 ГЛАВА 9 Сигналы и таймеры
static struct sigacticn entry_int, entry_quit;
static bccl igncre_sig(vcid)
{
static bocl first = true;
struct sigacticn act_igncre;
memset(&act_igncre, 0, sizecf(act_igncre));
act_igncre.sa_handler = S1G_IGN;
if (first) {
first = false;
ec_neg1( sigacticn(SlGlNT, &act_igncre, &entry_int) )
ec_neg1( sigacticn(SIGQUIT, &act_igncre, &entry_quit) )
}
else {
ec_neg1( sigacticn(SIGINT, &act_igncre, NULL) )
ec_neg1( sigacticn(SlGQUlT, &act_igncre, NULL) )
}
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
>
Обратите внимание на статическую переменную first типа bccl. Она служит для
того, чтобы сохранить прежнюю реакцию на сигнал только при первом
обращении к функции.
Ниже приводится функция восстановления прежней реакции на сигналы S1G1NT и
SIGQU1T, которая была сохранена обращением к функции igncre.sig:
static bccl entry_sig(vcid)
{
ec_neg1( sigaction(SIGINT, &entry_int, NULL) )
ec_neg1( sigaction(SIGQUIT, &entry_quit, NULL) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
ГЛАВА 9 Сигналы и таймеры 605
9.1.7 Обработчики сигналов
После того, как обращением к sigaction сигналу будет назначена
функция-обработчик, она будет вызываться в момент доставки сигнала. Если флаг SA_NODEFER не
установлен, перехваченный сигнал добавляется к маске сигналов на время
исполнения обработчика. Кроме того, к маске добавляются все сигналы из поля sa_mask
структуры sigaction. По завершении обработчика оригинальная маска сигналов
восстанавливается в первоначальное состояние, даже если она была явно
изменена внутри обработчика вызовами pthread.sigmask или sigprocmask.
Отлично, мы перехватили сигнал. Но что с ним делать дальше? Это во многом
зависит от типа сигнала и причин, его породивших.
• Сигнал может быть сгенерирован ядром при обнаружении ошибки. Например,
SIGFPE (ошибка выполнения арифметической операции) или SIGPIPE (запись в
канал, который не открыт на чтение с другой стороны). В подобных случаях
наиболее приемлемая реакция — завершить работу потока или процесса с
сообщением об ошибке. Возврат из обработчика в таком состоянии может
привести к ошибкам в вычислениях. А в случае ошибок, обнаруженных
аппаратными средствами, процесс все равно может быть завершен системой по
возвращении из обработчика.
• Сигнал может быть вызван действиями пользователя, например, нажатием
комбинации клавиш Ctrl+C (SIGINT). В этой ситуации можно завершить
приложение после сборки мусора или можно прервать длительные вычисления и
ожидать новых команд пользователя. Это лишь один из примеров реакции
приложения на сигнал — в любом случае, действия, выполняемые по сигналу, тесно
связаны с логикой работы приложения.
• Сигнал может быть порожден самим приложением. Например, чтобы сообщить
о готовности файла к обработке, можно послать сигнал SIGUSR1.
• Сигнал может быть сгенерирован таймером.
Но что бы вы ни делали, вы всегда должны подумать о том:
1. Что нужно сделать внутри обработчика и как изменить состояние приложения
в ответ на полученный сигнал.
2. Куда отправиться дальше. Из обработчика можно просто вернуться в
вызывающую программу, завершить работу приложения, выполнить длинный переход в
другую часть программы или сгенерировать новый сигнал.
Перечень системных вызовов и функций, которые могут быть использованы
внутри обработчика, весьма ограничен, потому что сигнал может возникнуть в точке,
из которой небезопасно выполнять повторный вызов некоторых функций.
Фактически SUS (версия 3) определяет всего 116 функций, безопасных в контексте
обработки асинхронных сигналов, список которых приводится в табл. 91:
606 ГЛАВА 9 Сигналы и таймеры
Таблица 9.1.
сигналов
accept
access
aio.error
aio_return
aio_suspend
alarm
bind
cfgetispeed
cfgetospeed
cfsetispeed
cfsetospeed
chdir
Chmod
chown
clock_gettime
close
connect
creat
dup
dup2
execle
execve
.exit/.Exit
fchmod
fchown
fcntl
fdatasync
fork
fpathconf
fstat
fsync
ftruncate
getegid
geteuld
getgid
getgroups
getpeername
getpgrp
getpid
Перечень функций, безопасных в контексте обработки асинхронных
getppid
getsockname
getsockopt
getuld
kill
link
listen
lseek
lstat
mkdir
mkfifo
open
pathconf
pause
pipe
poll
posix_trace_event
pselect
raise
read
readlink
recv
recvfrom
recvmsg
rename
rmdir
select
sem_post
send
sendmsg
sendto
setgid
setpgid
setsid
setsockopt
setuid
shutdown
sigaction
sigaddset
sigdelset
sigemptyset
sigfillset
sigismember
signal
sigpause
sigpending
sigprocnask
sigqueue
sigset
sigsuspend
sleep
socket
socketpair
stat
symlink
sysconf
tcdrain
tcflow
tcflush
tcgetattr
tcgetpgrp
tcsendbreak
tcsetattr
tcsetpgrp
time
timer_getoverrun
tii»er_gettii»e
timer_settime
tines
umask
uname
unlink
utime
wait
waitpid
write
Как правило, небезопасно обращаться из обработчика к высокоуровневым
библиотечным функциям и в том числе к своим собственным, поскольку вы не можете быть
полностью уверены в том, что они делают. Вы не можете даже вызвать printf,
кстати, именно по этой причине в нашем первом примере использовался системный
вызов write.
Хуже того, вы не можете обращаться к глобальным переменным, если их тип
отличается от volatile sig_atomic_t.
ГЛАВА 9 Сигналы и таймеры 607
С формальной точки зрения нет ничего предосудительного в обращении к
небезопасным функциям или глобальным переменным, но при условии, если вы точно
знаете, что небезопасная функция не была прервана по сигналу. Однако, учитывая,
что это условие соблюдается крайне редко и при весьма необычных
обстоятельствах, будет чрезвычайно неблагоразумно выполнять обращения к небезопасным
функциям из обработчика сигнала. Лучше ограничить себя набором из табл. 91, а
все необходимые переменные объявить с типом volatile sig_atomic_t.
В целом ситуация выглядит довольно рискованной, и это действительно так. Тем
не менее, вы сможете избежать опасностей и писать вполне надежные программы,
если будете придерживаться следующих рекомендаций.
• Сообщения об ошибках должны выводиться системным вызовом write (или другой
безопасной функцией), а завершение работы приложения — системным
вызовом _exit.
• Глобальные переменные, которые изменяются внутри обработчика, должны иметь
тип volatile sig_atomic_t, а возврат в приложение должен производиться при
условии, что для сигнала установлен флаг SA_RESTART.
• Лучше вообще отказаться от создания обработчиков сигналов. Вместо них
используйте потоки и системный вызов sigwait (см. раздел 9.2).
Если вы дошли до этих строк, то наверняка согласитесь с последней
рекомендацией! Но даже в случае полного отказа от обработчиков, сигналы по-прежнему могут
служить отличным инструментом решения ваших задач, потому что существует два
совершенно безопасных способа их обслуживания: sigsuspend (см. раздел 9.2.3) и
sigwait (см. раздел 9-2.2).
В заключение я хочу привести пример полноценного обработчика, который
сообщает номер полученного сигнала. Несмотря на то, что для сигнала установлен флаг
SA_RESTART, системный вызов sleep все равно будет прерван, потому что на него
действие этого флага не распространяется:
static volatile sig_atomic_t gotsig = -1;
static void handler(int signum)
{
gotsig = signum;
>
int main(void)
{
struct sigaction act;
time_t start, stop;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
act.sa_flags = SA_RESTART;
ec_neg1( sigaction(SIGINT, &act, NULL) )
608 ГЛАВА 9 Сигналы и таймеры
printf("Нажмите комбинацию клавиш Ctrl+C в течение 10 оек.\п");
eo_neg1( start = time(NULL) )
sleep(20);
eo_neg1( stop = time(NULL) )
prlntf("Ожидание длилооь Xld оек.\п", (long)(stop - start));
if (gotslg > 0)
prlntf("Получен оигнал о номером Ш\п", (long)gotslg);
else
рг1(Пт("Сигнал не был получен\п");
exlt(EXIT_SUCCESS);
EC_CLEANUP_BGN
exlt(EXIT_FAILURE);
EC_CLEANUP_END
}
Ниже приводится результат работы программы — я нажал Ctrl+C сразу после того,
как увидел первую строку:
Нажмите комбинацию клавиш Ctrl+C в течение 10 оек.
Ожидание длилооь 4 оек.
Получен оигнал о номером 2
9.1.8 Минимально необходимая обработка сигнала
Даже если вы решили полностью отказаться от использования обработчиков
сигналов, тем не менее, иногда необходимо предусмотреть некоторую минимальную
их обработку, чтобы избежать завершения приложения по неосторожности со
стороны пользователя. Кроме того, очень непрофессионально просто завершать
приложение в аварийном порядке, если ошибка вызвана, например, некорректным
обращением к памяти. В таких случаях гораздо лучше перехватить сигнал,
зафиксировать возникшую проблему в файле журнала и информировать о ней
пользователя, чем просто доверить командной оболочке (или чему-либо еще) вывести
малоинформативное сообщение «Program aborted — segmentation violation»
(«Программа завершена аварийно — ошибка обращения к памяти»).
Таким образом, большинство приложений должно, как минимум, выполнить
следующую последовательность действий.
• Заблокировать все сигналы при запуске программы (если процесс исполняется
в нескольких потоках, вместо sigprocmask используйте pthread_sigmask),
примерно таким образом:
sigset_t set;
ec_neg1( sigfillset(&set) )
eo_neg1( sigproomask(SIG_SETMASK, &set, NULL) )
ГЛАВА 9 Сигналы и таймеры 609
• Назначить реакцию SIG_IGN для всех сигналов, поступающих от клавиатуры
(например SIGINT), которые вы не собираетесь перехватывать.
• Создать обработчик для сигнала SIGTERM. При получении сигнала, обработчик
должен производить все необходимые действия по завершению приложения.
• Создать обработчик для каждого из сигналов обнаружения ошибок.
Обработчик должен выводить сообщение об ошибке на экран и/или в файл журнала,
после чего завершать приложение.
• Назначить реакцию SIG.IGN для сигнала SIGPIPE, чтобы системный вызов write
мог вернуться в вызывающую программу с признаком ошибки. Это гораздо
удобнее, чем обрабатывать сигнал.
• Разблокировать все сигналы системными вызовами sigemptyset и sigproomask (или
pthread_sigmask).
Ниже приводится исходный код функции, которая выполняет все эти действия:
statlo bool handIe_signaIs(void)
{
sigset_t set;
struot sigaotion aot;
eo_neg1( sigfillset(&set) )
eo_neg1( sigproomask(SIG_SETMASK, &set, NULL) )
memset(&aot, 0, sizeof(aot));
eo_neg1( sigfiIIset(&aot.sa_mask) )
aot.sajiandler = SIG_IGN;
eo_neg1( sigaotion(SIGHUP, &aot, NULL) )
eo_neg1( sigaotion(SIGINT, iaot, NULL) )
eo_neg1( sigaotion(SIGQUIT, iaot, NULL) )
eo_neg1( sigaotion(SIGPIPE, &aot, NULL) )
aot.sajiandler = handler;
eo_neg1( sigaotion(SIGTERM, iaot, NULL) )
eo_neg1( sigaotion(SIGBUS, &aot, NULL) )
eo_neg1( sigaotion(SIGFPE, iaot, NULL) )
eo_neg1( sigaotion(SIGILL, iaot, NULL) )
eo_neg1( sigaotion(SIGSEGV, &aot, NULL) )
eo_neg1( sigaotion(SIGSYS, &aot, NULL) )
eo_neg1( sigaotion(SIGXCPU,. &aot, NULL) )
eo_neg1( sigaotion(SIGXFSZ, &aot, NULL) )
eo_neg1( sigemptyset(&set) )
ec_neg1( sigproomask(SIG_SETMASK, iset, NULL) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
610 ГЛАВА 9 Сигналы и таймеры
и сам обработчик, с двумя сопутствующими функциями-,
static void handler(int signum)
{
}
int i;
struct {
int signum;
char «msg;
} sigmsg[] = {
{
SIGTERM,
{ SIGBUS,
{ SIGFPE,
{ SIGILL,
{ SIGSEGV,
{ SIGSYS,
{ SIGXCPU,
{ SIGXFSZ,
{ 0, NULL}
"Получен сигнал завершения" },
"Попытка доступа к неопределенному участку памяти" },
"Арифметическая сшибка" },
"Неверная инструкция" },
"Ошибка обращения к памяти" },
"Некорректнее обращение к системному вызову" },
"Исчерпан лимит прсцесссрнсгс времени" },
"Превышено ограничение на размер файла" },
};
clean_up();
for (i = 0; sigmsg[i].signum > 0; i++)
if (sigmsg[i].signum == signum) {
(vcid)write(STOERR_FILENG, sigmsg[i].msg,
strlen_safe(sigmsg[i].msg));
(vcid)write(STOERR_FILENG, "\n", 1);
break;
}
_exit(EXIT_FAILURE);
static void cleanjjp(vcid)
{
/•
Здесь должен размещаться кед псдгстсвки к завершению приложения
безопасный в контексте обработки асинхронных сигналов.
}
static size_t strlen_safe(ccnst char *s)
{
size_t n = 0;
while (*s++ != '\0')
n++;
return n;
ГЛАВА 9 Сигналы и таймеры 611
Основная идея состоит в том, что вы должны дописать код функции clean_up.
Я использовал свою собственную версию функции strlen, потому что, как бы
глупо это ни звучало, библиотечная функция strlen отнесена в разряд небезопасных
в контексте обработки асинхронных сигналов. По той же причине вместо fprintf
в теле обработчика использован системный вызов write. Обратите внимание:
вместо системного вызова exit используется _exit. Как уже упоминалось в разделе 57,
_exit не вызывает функцию atexit и не выталкивает стандартные буферы ввода-
вывода — это единственный способ безопасного завершения приложения при
получении асинхронного сигнала.
9.1.9 Искусственная генерация сигналов
Как уже упоминалось в разделе 9-1-3, каждый сигнал может быть порожден как
естественными причинами, так и в результате обращения к системным вызовам kill,
killpg, pthread_kill, abort, raise и sigqueue (см. раздел 9-5-4):
kill — посылает сигнал процессу
«include <signal.h>
int kill(
pid_t pid, /* идентификатор процесса ID или группы процессов •/
int signum /* сигнал */
);
/* Возвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
killpg — посылает сигнал группе процессов
«include <signal.h>
int killpg(
pid_t pgrp, /» идентификатор группы процессов »/
int signum /* сигнал */
);
/* Возвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки
в переменней еггпс) */
pthread_kill — посылает сигнал потоку
«include <signal.h>
int pthread_kill(
pthread_t thread_id, /• идентификатор потока */
int signum /» сигнал */
);
/* Возвращает 0 в случае успеха или кед сшибки (содержимое переменней еггпс
не определено) */
612 ГЛАВА 9 Сигналы и таймеры
abort
— посылает сигнал SIGABRT
«include <stdlib
vcid abcrt(vcid);
h
Управление не
h>
возвращается •/
raise — посылает сигнал тому же потоку, из которого был вызван
«include <signal.h>
int raise(
int signum /* сигнал */
);
/* Возвращает 0 в случае успеха или ненулевсе значение в случае сшибки
(кед сшибки - в переменней еггпс) */
С помощью системного вызова kill процесс может послать любой сигнал, не только
S1GKILL, если обладает необходимыми на то правами. Передача сигнала разрешена
без ограничений процессу, который обладает правами суперпользователя, в
противном случае действующий идентификатор пользователя посылающего
процесса должен соответствовать реальному идентификатору пользователя принимающего
процесса. Кто получит сигнал, определяется аргументом pid:
> 0 Сигнал будет передан процессу с идентификатором, равным
содержимому аргумента pid.
О Сигнал будет передан всем процессам, принадлежащим к той же группе,
что и процесс-отправитель.
< -1 Сигнал будет передан всем процессам, с идентификатором группы,
равным абсолютному значению аргумента pid.
-1 Сигнал будет передан всем процессам, которым передающий процесс
имеет право посылать сигналы, за исключением некоторых системных
процессов, перечень которых определяется каждой конкретной
реализацией.
Если в аргументе signum передается значение 0, системный вызов просто
проверяет правильность аргумента pid. Это один из способов убедиться в наличии процесса
или группы процессов. Если передающий процесс не обладает необходимыми
правами, вызов kill возвращает признак ошибки, но при этом код ошибки может
свидетельствовать о наличии искомого процесса или группы процессов: ESRCH —
процесс (группа процессов), соответствующий заданному аргументу pid, не
найден, EPERH — процесс (группа процессов) найден, но передающий процесс не
обладает необходимыми правами.
Системный вызов killpg, строго говоря, вообще не нужен. Он посылает сигнал
процессам, которые принадлежат к группе, с идентификатором, равным
содержимому аргумента рдгр. Таким образом, он совершенно идентичен обращению:
kilK-pgrp, signum);
ГЛАВА 9 Сигналы и таймеры 613
Системный вызов pthread_kill очень похож на вызов kill, но предназначен для
передачи сигнала только одному потоку — с идентификатором thread.id, который должен
находиться в том же процессе, что и поток, посылающий сигнал. Кроме того, он не
обладает возможностью посылать сигналы сразу нескольким потокам. Будьте
осторожны с вызовом pthread.kill не забывайте, что сигналы завершения, остановки и
возобновления воздействуют на весь процесс целиком. Таким образом, данный
системный вызов следует использовать только для посылки тех сигналов, которые вы
перехватываете. Если вы попытаетесь выполнить следующую строку:
pthread.kill(tid, SIGKILL);
не только поток tid завершит работу, но и весь процесс. (Чтобы принудительно
завершить работу потока, вы должны использовать системный вызов pthread_cancel.)
Системный вызов abort напоминает kill с аргументом SIGABRT, с той лишь
разницей, что системный вызов abort завершит работу процесса в любом случае (не важно,
перехватывается этот сигнал или нет); abort никогда не возвращает управление в
вызывающую программу. Например, предположим, что сигналу SIGABRT с помощью
sigaction назначена реакция SIG.IGN. В этом случае системный вызов abort
завершит работу приложения, а:
kill(getpid(), SIGABRT)
не окажет на процесс никакого влияния.
Системный вызов raise, на самом деле представляет собой обычную стандартную
функцию языка С. Он посылает сигнал вызвавшему потоку, что равносильно:
pthread_kill(pthread_self(), signum);
Однако системный вызов raise присутствует в любой системе, даже если она не
поддерживает потоки POSIX, в этом случае равноценная операция посылки
сигнала через обращение к системному вызову kill выглядит так:
kill(getpid(), signum);
9.1.10 Влияние системных вызовов fork, pthread.oreate
и ехео на сигналы
Существуют три характеристики, на которые оказывают влияние системные
вызовы fork, pthread.create и exec при запуске нового процесса, потока или программы:
1. Реакция на сигнал: в результате обращения к системному вызову fork,
потомок наследует реакцию на все сигналы от предка без изменений. В результате
обращения к системному вызову exec реакция SIG.DFL.таковой и остается.
Реакция SIG.IGN также не изменяется, за исключением сигнала SIGCHLD, для которого
устанавливается действие SIG_IGN или SIG_DFL, в зависимости от конкретной
реализации, а для перехватываемых сигналов устанавливается действие SIG_DFL.
614 ГЛАВА 9 Сигналы и таймеры
Поскольку реакция на любые сигналы относится к процессу в целом, вызов
pthread_create не оказывает на них никакого влияния.
2. Маска сигналов: полностью наследуется потомком, порожденным любым из
этих трех вызовов.
3. Ожидающие сигналы: потомками не наследуются.
Самый простой способ запомнить эти правила — объединить их в одно: «все
характеристики сигналов наследуются без изменений», для которого существует три
исключения.
• Если сигнал перехватывается, реакция на него будет изменена на SIG_DFL,
поскольку функции-обработчики будут затерты исполняемым кодом новой
программы, при обращении к exec.
• Сигналы, ожидающие обработки, имеют отношение к конкретному процессу или
потоку, поэтому они не будут наследоваться новым процессом или потоком.
• Разработчики стандартов больше беспокоились не столько о том, чтобы
обеспечить переносимость, сколько о том, чтобы не разрушить существующие
устои. Поэтому все, что относится к сигналу SIGCHLD, можно считать пустой
болтовней.
9.2 Ожидание сигнала
В этом разделе мы рассмотрим системные вызовы, которые позволяют процессу
дождаться доставки того или иного сигнала.
9.2.1 Системный вызов pause
Мы уже встречались с массой системных вызовов, которые могут быть
заблокированы в ожидании некоторого события. Например, при чтении строк с
терминального устройства, системный вызов read обычно блокируется в ожидании
окончания ввода строки. Системный вызов pause не делает ничего особенного и не ожи-
дает чего-то конкретного — он просто блокирует исполнение процесса.
pause — ожидает доставки сигнала
«include <unistd.h>
int pause(vcid);
/* В случае сшибки возвращает -1 (кед сшибки - в переменней еггпс) */
Поскольку в момент доставки сигнала практически любой системный вызов будет
прерван, можно смело утверждать, что системный вызов pause ожидает прибытия
сигнала. Если функция-обработчик сигнала возвращает управление в вызывающую
программу, то системный вызов pause возвращает признак ошибки с кодом EINTR,
но поскольку это единственный способ выхода из pause, нет нужды проверять
возвращаемое им значение.
ГЛАВА 9 Сигналы и таймеры 615
Но в реальных приложениях чаще используется более сложный системный вызов
sigwait.
9.2.2 Системный вызов sigwait
В отличие от pause, системный вызов sigwait позволяет указать, какой сигнал (или
сигналы) следует ожидать. При его использовании отпадает необходимость в
обработчиках сигналов, поскольку по возвращении из sigwait вы точно будете знать,
какой сигнал был доставлен.
sigwait — ожидает доставки сигнала
#include <signal.h>
int sigwait(
const sigset_t *set,
int «signum
);
/« Возвращает О в случае
/* набор
/* номер
ожидаемых сигналов
полученного
успеха или код ошибки
сигнала
•/
•/
•/
Аргумент set представляет собой набор сигналов (см. раздел 9-1-5), для ожидания
которых был вызван sigwait. Когда появляется один из заданных сигналов, его
номер возвращается через аргумент signum. Если к моменту вызова sigwait в
состоянии ожидания находился один или более сигналов, sigwait возвращает управление
немедленно, передавая через signum первый попавшийся сигнал. С технической точки
зрения, сигнал, возвращенный вызовом sigwait, считается «принятым», а не
«доставленным».
При использовании в программах системного вызова sigwait, вы должны
предусмотреть блокировку сигналов, которые будут проверяться этим системным
вызовом, чтобы сигнал не мог быть доставлен процессу. (Жизненный цикл сигналов
«посылка-ожидание-доставка» был описан в разделе 9.1-2.)
Если в ожидании одного и того же сигнала стоят два или более потоков, то
получит его только один из потоков, но предсказать, какой именно — невозможно. Если
сигнал посылается конкретному потоку, то его получит именно этот поток (если
он обратится к sigwait).
Как правило, системный вызов sigwait используется в следующих случаях:
• когда поток не может продолжать работу, не дождавшись определенного
события. Например, один поток может посылать сигнал SIGUSR1 другому потоку,
извещая о прибытии нового сообщения, хотя конкретно для этой цели лучше
использовать переменную состояния (см. раздел 5.17.4);
• когда обработкой сигналов занимается отдельный поток. То есть, когда вместо
функций-обработчиков используется отдельный поток. Но такой прием
срабатывает только если сигналы посылаются всему процессу — если сигнал
посылается конкретному потоку, принять его сможет только этот поток.
616
ГЛАВА 9 Сигналы и таймеры
Получение сигналов с помощью sigwait имеет свои преимущества перед
функциями-обработчиками. Самое главное, что отсутствуют ограничения, накладываемые
на обработчики сигналов. По возвращении управления из sigwait вы свободно
можете использовать любые функции и системные вызовы.
В разделе 918 мы рассматривали пример функции handle.signals, которая
устанавливала минимально необходимую реакцию приложения на сигналы (SIGFPE, SIPIPE
и другие). Теперь мы попробуем сделать то же самое, но только вынесем
обработку сигналов в отдельный поток, из которого будет вызываться sigwait, и
откажемся от обработчика сигналов:
static bccl handie_signals(vcid) /* не используйте зту функцию - см. ниже */
{
sigset_t *set;
struct sigacticn act;
pthread_t tid;
ec_nuii( set = mailcc(sizecf(»set)) )
ec_neg1( sigfillset(set) )
ec_rv( pthread_sigmask(SIG_SETMASK, set, NULL) )
memset(&act, 0, sizecf(act));
act.sa_handier = SIG_IGN;
ec_neg1( sigacticn(SIGHUP, iact, NULL) )
ec_neg1( sigacticn(SIGINT, &act, NULL) )
ec_neg1( sigacticn(SIGQUIT, iact, NULL) )
ec_neg1( sigacticn(SIGPIPE, iact, NULL) )
ec_neg1( sigemptyset(set) )
ec_neg1( sigaddset(set, SIGTERM) )
ec_neg1( sigaddset(set, SIGBUS) )
ec_neg1( sigaddset(set, SIGFPE) )
ec_neg1( sigaddset(set, SIGILL) )
ec_neg1( sigaddset(set, SIGSEGV) )
ec_neg1( sigaddset(set, SIGSYS) )
ec_neg1( sigaddset(set, SIGXCPU) )
ec_neg1( sigaddset(set, SIGXFSZ) )
ec_rv( pthread_sigmask(SIG_SETMASK, set, NULL) )
ec_rv( pthread_create(itid, NULL, sig_thread, set) )
return true;
EC_CLEANUP_BGN
return false;
EC_CLEANUP_END
}
static vcid *sig_thread(vcid *arg)
{
int signum;
ГЛАВА 9 Сигналы и таймеры 617
int i;
struct {
int signum;
char *msg;
} sigmsg[] = {
{ SIGTERM, "Пслучен сигнал завершения" },
{ SIGBUS, "Попытка доступа к неопределенному участку памяти" },
{ SIGFPE, "Арифметическая сшибка" },
{ SIGILL, "Неверная инструкция" },
{ SIGSEGV, "Ошибка обращения к памяти" },
{ SIGSYS, "Некорректнее обращение к системному вызову" },
{ SIGXCPU, "Исчерпан лимит прсцесссрнсгс времени" },
{ SIGXFSZ, "Превышено ограничение на размер файла" },
{ О, NULL}
};
while (true) {
ec_rv( sigwait((sigset_t *)arg, isignum) )
clean_up();
fcr (i = 0; sigmsg[i].signum > 0; i++)
if (sigmsg[i].signum == signum) {
fprintf(stderr, "Xs\n", sigmsg[i].msg);
break;
}
_exit(EXIT_FAILURE);
}
return (void *)true; /* эта строка никогда не будет исполнена */
EC_CLEANUP_BGN
EC.FLUSHC'slg.thread")
return (void *)false;
EC.CLEANUP.END
}
static void clean_up(vcid)
{
/*
Здесь должен размещаться кед подготовки к завершению приложения -
не обязательно безопасный в кентексте обработки асинхронных сигналов.
•/
}
Самое главное преимущество этой версии перед той, что приводилась в разделе
9.1.8, заключается в том, что на этот раз мы не ограничены списком безопасных
функций и можем свободно обращаться к любому системному вызову и к любой
библиотечной функции. Обратите внимание: текст комментария в функции clean_up
был изменен соответствующим образом.
618 ГЛАВА 9 Сигналы и таймеры
Самый большой недостаток этой версии в том, что она неработоспособна! По двум
очень серьезным причинам:
• если некий сигнал, например SIGSYS, передается конкретному потоку, а не
процессу, то sigwait в функции sig_thread никогда не получит его. Учитывая, что этот
сигнал заблокирован во всех потоках, он навсегда останется в состоянии
ожидания. Таким образом, для обработки сигналов обнаружения ошибок лучше все-
таки использовать специализированные функции-обработчики;
• если один из сигналов SIGBUS, SIGFPE, SIGILL или SIGSEGV будет заблокирован, а
причины, породившие его, носят естественный характер, то результат
предсказать практически невозможно (см. раздел 9.1.3). В большинстве случаев процесс
будет немедленно завершен и у системного вызова sigwait не будет
возможности вернуть управление вызывающей программе. Эта проблема не свойственна
версии, в которой использован обработчик сигналов, поскольку в ней все
четыре сигнала остаются разблокированными.
Нельзя сказать, что вызов sigwait совершенно бесполезен. Большинство сигналов
(за исключением группы обнаружения ошибок) посылаются процессу, при
условии их естественного происхождения (т. е. когда для генерации сигнала не
используется вызов pthread_kill). В этом случае реализация обработки сигналов на
основе отдельного потока выглядит гораздо предпочтительнее функций-обработчиков.
9.2.3 Системный вызов sigsuspend
Системный вызов sigsuspend — очень старый системный вызов, который также
предназначен для получения сигналов. Прежде чем мы перейдем к его
рассмотрению, я предлагаю исследовать проблему, для решения которой он предназначен. В
главе 8 мы рассматривали примеры, в которых порождались дочерние процессы,
соединяющиеся через сокеты с родительским процессом. В этих примерах было
очень важно, чтобы родительский процесс первым назначил адрес своему сокету,
поэтому мы использовали довольно грубый прием, приостанавливая на некоторое
время процессы-потомки. Мало того, что такой подход не надежен, поскольку
заданного времени может не хватить для запуска серверного процесса, но он еще и
не эффективен, потому что время приостановки потомков может оказаться
чересчур длинным. Чтобы вам было проще вспомнить, приведу ту же проблему в
упрощенном виде:
void tryl(void)
{
if (fork() == 0) {
printf("потомок\п");
exit(EXIT_SUCCESS);
}
printf("npeflOK\n");
return;
ГЛАВА 9 Сигналы и таймеры 619
Эта функция выводит следующее:
потомок
предок
Но нам нужно добиться, чтобы родительский процесс первым напечатал свое
сообщение и затем сообщил потомку, что тот может продолжить работу. Для начала
попробуем синхронизировать процессы с помощью сигнала SIGUSR1, который
будет перехвачен обработчиком:
statio volatile sig_atomio_t got_sig;
statio void handler(int signum)
{
if (signum =- SIGUSR1)
got_sig = 1;
}
void try2(void)
{
pid_t pid;
got_sig = 0;
ec_neg1( pid = fork() )
if (pid == 0) {
struct slgaction act;
memset(&act, 0, sizeof(act));
act.sajiandler = handler;
ec_neg1( sigaction(SIGUSR1, &act, NULL) )
while (got_sig == 0)
if (pause() == -1 && errno != EINTR)
EC_FAIL
printf("noTOMOK\n");
exit(EXIT_SUCCESS);
>
printf("npeflOK\n");
ec_neg1( kili(pid, SIGUSR1) )
return;
EC_CLEANUP_BGN
EC_FLUSH("try2")
EC_CLEANUP_EN0
}
Обработчик следует требованиям безопасности при обработке асинхронных
сигналов — он просто записывает единицу в переменную соответствующего типа.
620 ГЛАВА 9 Сигналы и таймеры
Дочерний процесс ожидает прибытия сигнала, обращаясь в цикле к системному
вызову pause. Теперь мы получили требуемый результат:
предок
потомок
Но в данной реализации есть две проблемы:
• если сигнал SIGUSR1 будет доставлен дочернему процессу до того, как тот
установит функцию-обработчик, процесс будет завершен. Одно из решений этой
проблемы — установить обработчик до обращения к вызову fork. Тогда
потомок унаследует его от родителя и все будет в порядке. Но это решение годится
только в случае, когда процессы связаны родственными отношениями
«родитель-потомок», а нам нужно такое решение, которое позволило бы
синхронизировать любые процессы;
• если сигнал SIGUSR1 будет доставлен в момент между выполнением оператора
while и обращением к pause, это приведет к тому, что системный вызов pause встанет
на «вечное» ожидание, поскольку сигнал будет получен и обработан еще до
обращения к нему.
Мы можем попробовать заблокировать сигнал до того момента, пока не будем
готовы принять его:
void try3(void)
{
sigset_t set;
pid_t pid;
got_sig = 0;
eo_neg1( sigemptyset(&set) )
eo_neg1( sigaddset(&set, SIGUSR1) )
eo_neg1( sigproomask(SIG_SETMASK, &set, NULL) )
eo_neg1( pid = fork() )
if (pid == 0) {
struot sigaotion aot;
sigsetjt suspendset;
memset(&aot, 0, sizeof(aot));
aot.sajiandler = handler;
eo_neg1( sigaotion(SIGUSR1, &aot, NULL) )
eo_neg1( sigfillset(&suspendset) )
eo_neg1( sigdelset(&suspendset, SIGUSR1) )
eo_neg1( sigproomask(SIG_SETMASK, isuspendset, NULL) )
while (got_sig == 0)
if (pause() == -1 && errno != EINTR)
EC.FAIL
printf("потомок\п");
exit(EXIT_SUCCESS);
ГЛАВА 9 Сигналы и таймеры 621
printf("npeflOK\n");
ec_neg1( kill(pid, SIGUSR1) )
return;
EC_CLEANUP_BGN
EC_FLUSH("try3")
EC_CLEANUP_END
>
Этот вариант полностью устранил первую проблему, поскольку теперь сигнал SIGUSR1
блокируется до момента установки обработчика. Но вторую проблему нам
устранить не удалось. Хотя промежуток времени между оператором while и
обращением к системному вызову pause очень мал, тем не менее его вполне достаточно,
чтобы получить сигнал.
Все, что нам нужно, это заблокировать сигнал, пока не будет запущен системный
вызов pause. Другими словами, нам нужно, чтобы операция разблокировки сигнала
и его ожидание была атомарной. Это как раз то, что делает системный вызов
sigsuspend:
sigsuspend — изменяет маску сигналов и ожидает доставки
«include <signal.h>
int sigsuspend(
const sigset_t «sigmask /*
);
/» Возвращает -1, как признак
в переменной errno) */
временная маска сигналов
ошибки, причем всегда (код
•/
сигнала
ошибки -
Системный вызов sigsuspend временно заменяет маску сигналов потока, заданную
в аргументе sigmask, и ожидает прибытия сигнала, для которого назначена реакция
— либо завершение процесса, либо перехват сигнала. Если в результате доставки
сигнала процесс должен завершиться, sigsuspend не возвращает управление в
вызывающую программу. Если сигнал перехватывается и обработчик возвращает
управление обычным образом системный вызов восстанавливает прежнюю маску
сигналов и возвращает управление с признаком ошибки. Код ошибки, как правило, E1NTR
(точно так же, как и в случае с системным вызовом pause).
На практике, маска сигналов, передаваемая вызову sigsuspend, снимает блокировку
с одного или более сигналов, которые были заблокированы до обращения к
sigsuspend. Хотя это и не является обязательным требованием, но едва ли тогда имеет
смысл обращаться к sigsuspend.
Отлично! Теперь мы можем избавиться от второй проблемы простой заменой
вызова pause на sigsuspend. Кроме того, теперь ненужен цикл while, потому что
последовательность операций по разблокировке сигнала и его ожиданию стала атомарной.
622 ГЛАВА 9 Сигналы и таймеры
void try4(void)
{
sigset_t set;
pid_t pid;
eo_neg1( sigemptyset(iset) )
eo_neg1( sigaddset(&set, SIGUSR1) )
eo_neg1( sigprooniask(SIG_SETMASK, &set, NULL) )
eo_neg1( pid = fork() )
if (pid == 0) {
struot sigaotion aot;
sigset_t suspendset;
raeraset(4aot, 0, sizeof(aot));
aot.sa_handler = handler;
eo_neg1( sigaotion(SIGUSR1, iaot, NULL) )
eo_neg1( sigfillset(isuspendset) )
eo_neg1( sigdelset(4suspendset, SIGUSR1) )
if (sigsuspend(isuspendset) == -1 && errno != EINTR)
EC.FAIL
printf("noTOMOK\n");
exit(EXIT_SUCCESS);
}
printf("npeflOK\n");
eo_neg1( kill(pid, SIGUSR1) )
return;
EC_CLEANUP_BGN
EC_FLUSH("try4")
EC_CLEANUP_END
>
Можно оставить функцию-обработчик в том виде, в каком она была и ранее, но
переменная got_sig нам теперь больше не нужна, поэтому уберем лишний код из
обработчика:
statio void handler(int signum)
{
}
Проверка значения, возвращаемого sigsuspend, на самом деле также не нужна,
потому что он всегда возвращает -1. Однако я добавил ее, чтобы не смущать
непонятным кодом тех читателей, которые не знают или не помнят всех подробностей
о вызове sigsuspend.
Обратите внимание.- сигнал SIGUSR1 блокируется до обращения к fork, в результате
дочерний процесс наследует маску сигналов, где SIGUSR1 оказывается
заблокированным. Если необходимо выполнить синхронизацию двух процессов, не связан-
ГЛАВА 9 Сигналы и таймеры 623
ных родственными отношениями, тот процесс, который обращается к sigsuspend,
должен сам устанавливать блокировку сигнала. Фактически, это частный случай
общего правила, которое рекомендует блокировать все сигналы на запуске
приложения, о чем я уже говорил в разделе 9-1-8.
Еще один важный момент: в отличие от sigwait, системный вызов sigsuspend
используется совместно с функцией-обработчиком сигнала. Но в этом случае обработчик,
как правило, не выполняет никаких действий. Он предназначен лишь для того, чтобы
пришедший сигнал мог прервать работу sigsuspend.
Другое значительное отличие sigsuspend от sigwait — при работе с sigwait сигнал
остается заблокированным; на самом деле в данной ситуации сигнал никогда не
доставляется процессу, sigwait сам изымает его из коллекции сигналов,
ожидающих обработки и возвращает вызывающей программе. Благодаря этому
отсутствует «условие гонки» между моментом снятия блокировки сигнала и ожиданием его
доставки, потому что блокировка никогда не снимается. Если выстроить
синхронизацию процессов на основе системного вызова sigwait, то код нашего примера
можно значительно упростить:
void try5(void)
{
sigset_t set;
pid_t pid;
ec_neg1( sigemptyset(&set) )
ec_neg1( sigaddset(&set, SIGUSR1) )
ec_neg1( sigprocmask(SIG_SETMASK, &set, NULL) )
ec_neg1( pid = fork() )
if (pid == 0) {
int signum;
ec_rv( sigwait(&set, &signum) )
printf("noTOMOK\n");
exit(EXIT_SUCCESS);
}
printf("npeflOK\n");
ec_neg1( kill(pid, SIGUSR1) )
return;
EC_CLEANUP_BGN
EC_FLUSH("try5")
EC_CLEANUP_EN0
}
Следует также упомянуть о том, что синхронизацию между родительским и
дочерним процессом можно выстроить на основе неименованных каналов, избавившись
от сложностей, связанных с сигналами:
624 ГЛАВА 9 Сигналы и таймеры
void try6(void)
{
int pfd[2];
pid_t pid;
eo_neg1( pipe(pfd) )
eo_neg1( pid = fork() )
if (pid == 0) {
ohar o;
eo_neg1( olose(pfd[1]) )
eo_neg1( read(pfd[0], &o, 1) )
eo_neg1( olose(pfd[0]) )
printf("noTOMOK\n");
exit(EXIT_SUCCESS);
}
printf("npeAOK\n");
eo_neg1( olose(pfd[0]) )
eo_neg1( olose(pfd[1]) )
return;
EC_CLEANUP_BGN
EC_FLUSH("try6")
EC_CLEANUP_END
}
В данном случае процесс-потомок будет заблокирован в системном вызове read до тех
пор, пока родительский процесс не закроет дескриптор канала со своей стороны.
9.3 Прочие системные вызовы для работы
с сигналами
С помощью системного вызова sigpending можно получить набор ожидающих
сигналов:
sigpending — возвращает набор
#inolude <signal.h>
int sigpending(
ожидающих обработки сигналов
sigset_t «set /* возвращаемый набор
у
/* Возвращает 0 в олучае успеха
в переменной еггпо) »/
или -1 в
оигналов */
олучае ошибки
(код
ошибки -
Проверить наличие того или иного сигнала в возвращаемом наборе можно с
помощью функции sigismember (см. раздел 9-1.5). Разумеется, к моменту проверки каждый
ГЛАВА 9 Сигналы и таймеры 625
конкретный сигнал может уже не быть в состоянии ожидания, если он не
заблокирован.
В разделе 9.1.6 упоминался флаг sa.onstack, в описании к которому говорилось, что
сигнал доставляется в альтернативном стеке сигналов. Установка альтернативного
стека производится системным вызовом sigaltstack:
sigaltstack — устанавливает и
#include <signal.h>
int sigaltstack(
ccnst stack_t «stack,
stack_t *cstack
);
/* Возвращает О в случае
в переменней errnc) */
/•
/•
возвращает
новый
старый
успеха или
зтек ^
стек
-1 в
контекст альтернативного
/
*/
случае
сшибки
(кед сшибки -
стека
Более подробные сведения об этом системном вызове вы найдете в системной
документации или в SUS.
Там же упоминался флаг Sa.RESTART, предотвращающий прерывание системных
вызовов. С помощью вызова siginterrupt вы можете установить или сбросить этот
флаг, без необходимости обращения к системному вызову sigacticn:
siginterrupt — устанавливает или сбрасывает флаг sa.restart
#include <signal.h>
int siginterrupt(
int signura, /* нсмер сигнала */
int en /* 0 - сбрссить, ненулевое значение - установить */
);
/* Возвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней errnc) */
9.4 Системные вызовы, не рекомендуемые
к использованию
Системные вызовы, описываемые в данном разделе, хотя и стандартизованы, но не
добавляют ничего нового к тем, что уже обсуждались выше. Я бы не стал тратить
ваше время на их изучение, но они понадобятся вам при выполнении упражнений
9.4 и 9.5.
Классический способ назначения действия для сигнала заключается в обращении
к системному вызову signal:
626 ГЛАВА 9 Сигналы и таймеры
signal — устанавливает реакцию п
#include <signal.h>
void (*signal(
int signum, /*
void («actHint) /*
))(int);
эиложения
номер сигнала
действие
/* Возвращает старое действие или
в переменной еггпо) */
•/
SIG.
*/
.ERR в
на
сигнал
случае
ошибки
(код
ошибки -
Это не совсем обычное объявление означает, что возвращаемое значение может
быть указателем на функцию-обработчик сигнала, которая принимает номер
сигнала (целое число) в виде аргумента.
Если вы собираетесь перехватывать сигнал с помощью этого системного вызова,
тогда вам придется столкнуться с двумя проблемами:
• в момент доставки сигнала реакция на него сбрасывается в SIG_DFL. Если вы по-
прежнему заинтересованы в том, чтобы продолжать перехватывать сигнал, вам
придется вызывать signal всякий раз при его получении;
• в момент вызова обработчика сигнал не блокируется, и доставка следующего
сигнала может завершить работу приложения.
Самый простой способ избежать этих проблем — использовать системный вызов
sigaction (см. раздел 9-1-6) и забыть про signal.
Далее следует описание пяти системных вызовов, призванных упростить
обработку сигналов. Однако вы должны избегать применения этих вызовов в своих
программах, поскольку они не несут ничего нового по сравнению с предыдущими
системными вызовами (например sigaction). Тем не менее, я расскажу о них в двух
словах.
Установить реакцию приложения на сигнал (в стиле sigaction) можно с помощью
системного вызова sigset:
sigset — устанавливает
#include <signal.h>
void (*sigset(
int signum,
реакцию приложения
/*
void (*act)(int) /*
))(int);
/* Возвращает старое
в переменной еггпо)
номер оигнала
действие */
действие или SIG.
*/
•/
ERR в
на
сигнал
случае
ошибки
(код
ошибки -
Этот системный вызов так же прост, как и signal, но по своему поведению
напоминает sigaction, т. е. на время работы обработчика сигнал маскируется, а реакция
приложения на сигнал не изменяется. Кроме того, он предусматривает еще одно
действие по сигналу — SIG_H0LD, которое просто добавляет заданный сигнал к мае-
ГЛАВА 9 Сигналы и таймеры 627
ке сигналов. В отличие от sigaction, если вы вызываете sigset без действия SIG_H0LD,
в виде побочного эффекта он разблокирует заданный сигнал (удалит его из маски
сигналов).
Главная причина, по которой следует избегать использования sigset (и всех
остальных системных вызовов, которые последуют ниже), состоит в том, что с его
помощью довольно трудно выразить свои желания, в то время как с sigaction вы будете
делать это без особого напряжения, даже не изучая его возможности во всех
деталях. Дальше — больше! Еще одна причина, по которой стоит избегать применения
sigset и нижеследующих системных вызовов — они не предназначены для работы
в многопоточных приложениях. Представьте, сколько мук придется претерпеть, если
однопоточную программу позднее надо будет превратить в многопоточную!
Системный вызов sigrelse снимает блокировку с сигнала (удаляет его из маски
сигналов):
sigrelse — снимает блокировку с сигнала
«include <signal.h>
int sigrelse(
int signum, /* номер
);
/* Возвращает 0 в случае
в переменной еггпо) */
сигнала */
успеха или -1 в
случае
ошибки
(код
ошибки -
Далее следует упрощенная версия системных вызовов sigsuspend и sigpause,
которая удаляет сигнал из маски, ожидает доставки любого незаблокированного
сигнала и восстанавливает маску в первоначальное состояние:
sigpause — изменяет маску
((include <signal.h>
int sigpause(
int signum, /* номер
);
/* Всегда возвращает -1 (
и ожидает доставки сигнала
сигнала */
код ошибки в
переменной
еггпо)
V
И в заключение — два избыточных системных вызова, которые делают то же
самое, что и sigset, которому передаются действия SIG_H0LD и S1G_I6N:
sighold — блокирует сигнал
«include <signal.h>
int sighold(
int signum, /* номер
);
/* Возвращает 0 в случае
в переменной еггпо) */
сигнала
успеха
*/
или
-1 в случае
ошибки
(код
ошибки -
628 ГЛАВА 9 Сигналы и таймеры
sigignore — устанавливает реакцию на
«include <signal.h>
int sigignore(
int signum, /* номер
);
/* Возвращает 0 в случае
в переменной errno) */
сигнала */
успеха или -1
сигнал
SIG.IGN
в случае ошибки
(код ошибки -
9.5 Сигналы реального времени
Так называемый стандарт POSIX.4 описывает новый механизм сигналов RTS (от англ.
Realtime Signals — сигналы реального времени), который должен улучшить
системные вызовы, описанные ранее в этой главе. Благодаря новому стандарту:
• увеличено число сигналов, которые могут быть использованы для внутренних
нужд приложения;
• сигналы могут помещаться в очередь, чтобы новый сигнал не был потерян, если
на ожидании уже стоит сигнал с тем же номером;
• определен порядок доставки сигналов;
• вместе с сигналом поставляется дополнительная информация, которая
описывает — от кого прибыл сигнал и почему, и возможно некоторые данные,
сопровождающие сигнал.
Новые возможности были просто добавлены к существующим. Поэтому вы
по-прежнему можете использовать классические 28 сигналов (см. раздел 913), с теми же
самыми действиями и с теми же самыми системными вызовами генерации
искусственных сигналов, такими как kill. Стандарты не оговаривают возможность
использования нововведений для «старых» сигналов (например, постановку в очередь
или передачу данных вместе с сигналом), поэтому, если вы желаете сохранить
переносимость своих приложений, вы должны избегать применения новых
возможностей для старых сигналов, хотя многие системы это допускают.
Проверить наличие новых возможностей можно с помощью макросов из раздела
1.5.4. ОСНОВНОЙ среди НИХ — _POSIX_REALTIHE_SIGNALS.
В последующих подразделах мы детально рассмотрим основные особенности
сигналов реального времени. Описания некоторых дополнительных свойств вы
найдете в разделах 97.5 и 9.7.6.
9.5.1 Обработчики сигналов реального времени
Прежде чем продолжить чтение этого раздела, я рекомендую освежить в памяти
полученные ранее сведения и еще раз прочитать раздел 9.1.6.
Если в структуре, которая передается системному вызову sigaction,
устанавливается флаг SA.SIGINFO, то указатель на функцию-обработчик должен заноситься в поле
ГЛАВА 9 Сигналы и таймеры 629
sa_sigacticn, а не в sa_handler.* Функции-обработчики сигналов реального
времени получают при вызове значительно больше информации и объявляются как:
void rts_handler(lnt signum, slginfc_t *infc, void *ccntext);
Аргумент inf с указывает на область памяти, где располагается структура типа siginfc_t,
несущая в себе сведения о сигнале. Мы уже встречались с ней, когда
рассматривали системный вызов waitid в разделе 5.8:
siginfo_t — структура для
typedef struct {
int si_signc;
int si_errnc;
int si_ccde;
pid_t si_pid;
uid_t si_uid;
void *si_addr;
Int si_status;
long si_band;
union sigval si.
} siginfc_t;
системного вызова sigaction
/* номер сигнала */
/* значение переменней еггпс, */
/* ассоциированное с сигналом */
/* кед сигнала (см. ниже) */
/* идентификатор прсцесса-стправителя */
/* реальный идентификатор пользователя */
/* прсцесса-стправителя */
/* адрес обнаруженной сшибки */
/* кед завершения или сигнал */
/* событие для SIGP0LL */
.value; /* значение, передаваемое с сигналом */
union sigval — объединение, представляющее значение, передаваемое с
сигналом"
unicn sigval {
int sival_int;
void *sival_ptr;
};
/*
/*
целее значение */
указатель */
Поле si.signc просто дублирует содержимое входного аргумента signum.
Порядок использования поля si_errno зависит от реализации — может
использоваться для передачи кода ошибки, породившей сигнал.
Поле si_ocde указывает на причину появления сигнала. Если в нем содержится 0 или
отрицательное число — сигнал поступил от процесса, идентификатор которого
находится в поле si_pid, а реальный идентификатор пользователя — в поле
si.uid. В этом случае оно может содержать одно из следующих значений:
SI_USER Сигнал был отправлен системным вызовом kill или, в
зависимости от реализации, другим системным вызовом (например
raise).
' SUS не уточняет, можно ли использовать значения SIG_IGN и SIG_0FL для записи в поле
sa.sigaction, поэтому безопаснее записывать их только в поле sa.handler.
" ОС FreeBSD (возможно и другие системы) определяет эти поля под именами sigval.int и
sigval_ptr, но она не претендует на поддержку сигналов реального времени.
630 ГЛАВА 9 Сигналы и таймеры
SI_0UEUE Сигнал отправлен системным вызовом sigqueue
(см. раздел 9.5.4).
SI_TIMER Истекло время, установленное системным вызовом
timer_settime (см. раздел 9-7.6).
SI_ASYNCI0 Завершилась операция асинхронного ввода-вывода (раздел 3.9).
SI_MESGQ Получено сообщение (раздел 7.7).
В четырех последних случаях через поле si_value вместе с сигналом может быть
передано некоторое значение.
Если поле si_ccde содержит положительное число — сигнал был послан ядром.
В этом случае содержимое поля зависит от типа сигнала. Например, для сигнала
SIGFPE поле si_ccde может содержать FPE_INTDIV — деление на ноль целого числа,
FPE_INTOVF — выход за пределы целого числа (переполнение), FPE_FLTDIV — деление
на ноль числа с плавающей точкой. Полный список кодов для сигнала SIGFPE и других
вы найдете в [SUS2002] и в системной документации. Кроме того, коды для сигнала
SIGCHLD вы найдете в разделе 5.8.
Порядок использования остальных полей в структуре зависит от типа сигнала.
Описание назначения полей для сигнала SIGCHLD вы найдете в разделе 5-8.
Сигналы SIGILL и SIGSEGV используют поле si_addr для передачи адреса машинной
инструкции, которая вызвала ошибку.
Ниже приводится пример программы, которая отображает некоторые сведения,
поставляемые сигналом реального времени в обработчик сигнала:
int main(vcid)
{
struct sigacticn act;
unicn sigval val;
memset(&act, 0, sizecf(act));
act.sa_flags = SA_SIGINF0;
act.sa_sigacticn = handler;
ec_neg1( sigaction(SIGUSR1, &act, NULL) )
ec_neg1( sigaction(SIGRTMIN, &act, NULL) )
ec_neg1( kill(getpid(), SIGUSR1) )
val.sival_int = 1234;
ec_neg1( sigqueue(getpid(), SIGRTMINl val) )
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
static void handler(int signum, siginfc_t «info, void «context)
{
printf("HCMep сигнала: Xd\n", info->si_signo);
ГЛАВА 9 Сигналы и таймеры 631
printf("Получен от процеооа: Xld\n", (long)info->si_pid);
printf("Реальный идентификатор пользователя: Xld\n", (long)info->si_uid);
switoh (info->si_oode) {
oase SIJJSER:
printf("Сигнал получен от пользователя\п");
break;
oase SI_QUEUE:
printf("Сигнал получен от sigqueue; значение = Xd\n",
info->si_value.sival_int);
break;
oase SI_TIMER:
printfC'HoTeioio время таймера; значение = Xd\n",
info->si_value.sival_int);
break;
oase SI_ASYNCIO:
printf("Завершен аоинхронный ввод-вывод; значение = Xd\n",
info->si_value.sival_int);
break;
oase SI_MESGQ:
printf("Прибыло новое оообщение; значение = Xd\n",
info->si_value.sival_int);
break;
default:
printf("Другой оигнал\п");
}
}
Сравните этот пример с первым примером из этой главы, и вы сразу же увидите
разницу: при подготовке структуры sigaotion устанавливается флаг SA.SIGINFO и
указатель на функцию-обработчик записывается в поле sa_sigaotion, а не в
sa_sighandler. О сигнале SIGRTMIN я расскажу в следующем разделе, а пока считайте,
что это сигнал SIGUSR2.
В данном примере вполне допустимо использовать функцию printf внутри
обработчика сигналов, потому что сигналы порождаются системными вызовами kill и
sigqueue, которые относятся к категории безопасных, в контексте обработки
асинхронных сигналов.
В результате работы программы я получил следующее:
Номер оигнала: 10
Получен от процеооа: 29501
Реальный идентификатор пользователя: 500
Сигнал получен от пользователя
Номер оигнала: 32
Получен от процеооа: 29501
Реальный идентификатор пользователя: 500
Сигнал получен от sigqueue; значение = 1234
21 Зак. 4030
632 ГЛАВА 9 Сигналы и таймеры
Третий аргумент функции-обработчика — context — это указатель на объект типа
ucontext_t, который описывает контекст процесса в момент прерывания
пришедшим сигналом. Причина, по которой аргумент имеет тип void», а не ucontext_t»
заключается в том, что к тому моменту, когда был утвержден прототип функции,
тип ucontext_t еще не был стандартизован. Этот указатель используется не везде и
очень редко. Дополнительные сведения по этой теме вы найдете в [SUS2002] и в
системной документации.
9.5.2 Сигналы реального времени
Сигналы реального времени представляют собой группу новых сигналов, номера
которых находятся в диапазоне от SIGRTMIN до SIGRTMAX. Общее количество
сигналов реального времени должно быть, по крайней мере, RTSIG.MAX. Фактические
значения этих идентификаторов вы можете получить с помощью sysconf (см. раздел
1.5.5), но как минимум должно быть определено 8 сигналов (это число задано в моей
версии Solaris, в Linux их больше — 32).
Для сигналов реального времени (RTS) специальных идентификаторов не
предусмотрено (кроме первого и последнего) поэтому к ним следует обращаться как к
SIGRTMIN, SIGRTMIN+1, SIGRTMIN+2 и так далее, вплоть до SIGRTMAX. Разумеется, вы
можете определить свои идентификаторы сигналов, примерно таким образом:
«define DB_I0_D0NE SIGRTMIN + 4
К сожалению, проблема двух библиотек, использующих одни и те же имена
сигналов, не была решена группой POSIX.4, поэтому вы должны проявлять большую
осторожность при выборе имен для своих сигналов.
Идентификаторы SIGRTMIN и SIGRTMAX не обязательно должны быть константами целого
типа, поэтому вы не всегда сможете использовать их в качестве вариантов case в
операторе выбора switch, а также в условных директивах препроцессора
(например в #if).
Если в ожидании обработки находится более одного сигнала RTS, первым будет
доставлен сигнал с наименьшим номером, поэтому номера сигналов могут
рассматриваться как своего рода уровни приоритетов. Это обстоятельство вы должны
учитывать при проектировании своих приложений, использующих сигналы RTS. Действие
по умолчанию для всех сигналов реального времени — завершение приложения.
9.5.3 Очереди сигналов
Как правило, вы лишены возможности точно подсчитать, сколько раз был
сгенерирован тот или иной сигнал. Если система не предусматривает организации
очередей сигналов, а хранит их только в виде флагов, то в случае, когда сигнал
находится в ожидании обработки, все последующие сигналы с тем же номером будут
утеряны. Поэтому вы никогда не должны выстраивать свои алгоритмы,
основываясь на подсчете доставленных сигналов.
ГЛАВА 9 Сигналы и таймеры 633
Однако сигналы RTS помещаются в очередь, если в вызове sigacticn был
установлен флаг SA_SIGINFO. Тем не менее, вы не должны исходить из предположения, что
в очередь будут помещаться и классические сигналы. В большинстве реализаций
сигналы реального времени заносятся в очередь даже без флага SA_SIGINFO, но вы
все-таки должны устанавливать этот флаг для переносимости своих программ.
Если вы предполагаете принимать сигналы RTS вызовом sigwait (см. раздел 92.2),
будет безопаснее создать пустую функцию-обработчик и записать указатель на нее
в поле sa_sigacticn потому что в SUS никак не оговаривается реакция процесса на
заблокированный сигнал RTS с реакцией SIG_DFL.
Максимальный размер очереди сигналов не может быть меньше 32. Фактическое
значение в каждой конкретной системе вы можете определить с помощью sysccnf
(см. раздел 1.5.5).
9.5.4 Вызов sigqueue
sigqueue — посылает сигнал процессу
«include <signal.h>
int sigqueue(
pid_t pid,
int signum,
/*
/*
const union sigval value /*
/* Возвращает О в случае
в переменней еггпс) */
успеха
идентификатор процесса
номер
данные
или -1
зигнала */
•/
передаваемые вместе
в случае сшибки
с сигналом
(кед сшибки -
*/
Мы уже встречались с системным вызовом sigqueue в примере из раздела 9.5.1. Первые
два аргумента соответствуют аргументам вызова kill, за исключением того, что
аргумент pid определяет только идентификатор процесса, sigqueue не может
посылать широковещательные сигналы. В третьем аргументе вы можете передать
некоторые данные, которые попадут в обработчик сигнала, при условии, что
установлен флаг SA.SIGINFO. Вы должны заранее определиться с типом данных — будет ли
это целое число или указатель, потому что никакой дополнительной информации,
идентифицирующей тип передаваемых данных, отправляться не будет.
Если сигнал отправляется другому процессу, вероятнее всего вы не сможете
передать ему указатель, потому что процессы в UNIX исполняются в изолированных друг
от друга адресных пространствах. Хотя, с другой стороны, указатель может ссылаться
на данные, находящиеся в общей памяти, в этом случае вполне возможно
обеспечить передачу значительных объемов информации вместе с сигналом.
Как я уже упоминал в предыдущем разделе, если сигнал с номером signum уже
находится на ожидании обработки, новый сигнал не будет потерян, а будет
поставлен в очередь.
634 ГЛАВА 9 Сигналы и таймеры
Подсчет количества доставленных сигналов может производиться только для
сигналов реального времени (см. раздел 9.2.2).
Если новый сигнал не может быть помещен в очередь, sigqueue возвращает
признак ошибки с кодом EAGAIN. В следующем разделе вы найдете еще один пример
использования этого системного вызова.
9.5.5 Вызовы sigwaitinfo и sigtimedwait
Системный вызов sigwait не является частью расширений реального времени POSIX,
тем не менее, он прекрасно справляется с обслуживанием сигналов RTS. Он
извлекает из очереди очередной сигнал, ожидающий обработки, и возвращает его в
вызывающую программу. Если в очереди имеются сигналы с тем же номером, они
остаются в очереди. Если в очереди находится несколько разнотипных сигналов RTS,
первым извлекается сигнал с наивысшим приоритетом (с наименьшим номером).
Самая большая проблема sigwait заключается в том, что с его помощью
невозможно получить структуру siginfo_t, также как и данные, передаваемые вместе с
сигналом. Поэтому для сигналов реального времени лучше использовать системный
вызов sigwaitinfo:
sigwaitinfo — ожидает появления сигнала
«inolude <signal.h>
int sigwaitinfo(
const sigset_t *set,
siginfo_t «info
);
/• Возвращаетоя номер ои
в переменной errno) */
/•
/•
набор
ожидаемых оигналов
возвращаемая информация
гнала или
-1 в олучае ошибки
•/
•/
(код
ошибки -
Допускается передавать в аргументе info значение NULL, но в этом случае
системный вызов не будет возвращать информацию о принятом сигнале. В отличие от
sigwait, вызов sigwaitinfo передает номер принятого сигнала не через параметр, а
в виде возвращаемого значения. Если в аргументе info передается указатель, по
заданному адресу будет принято содержимое структуры siginfo_t, которая также
передается обработчику сигнала. Как и в случае с sigwait, вы должны блокировать
ожидаемые сигналы, в противном случае реакция приложения может оказаться
непредсказуемой.
Ниже приводится пример работы с очередью сигналов. В двух первых строках
находятся объявления сигналов RTS, которые будут использованы в примере:
«define MYSIG_COUNT SIGRTHIN
«define MYSIG.STOP SIGRTMIN + 1
ГЛАВА 9 Сигналы и таймеры 635
Далее следует функция потока, которая ожидает доставки сигналов. Если
доставленный сигнал — MYSIG.COUNT, функция выводит строку, принятую вместе с
сигналом. Если сигнал — MYSIG.STOP, функция возвращает управление в вызывающую
программу (завершает работу потока):
static void *sig_thread(vcid *arg)
{
int signum;
siginfc.t infc;
dc {
signura = sigwaitinfc((sigset_t *)arg, &info);
if (signura == MYSIG.CCUNT)
printf("Получен сигнал MYSIG.CCUNT; ее строкой: Xs\n",
(char *)infсsi.vaiue.sivai.ptr);
eise if (signura == MYSIG.STCP) {
printf("Получен сигнал MYSIG.STCP; работа завершена\п");
return (void *)true;
}
eise
printf("Получен сигнал с номером Xd\n", signum);
} whiie (signura != -1 || errnc == EINTR);
EC.FAIL
EC.CLEANUP.BGN
EC_FLUSH("sig_thread")
return (void *)faise;
EC.CLEANUP.END
}
Обратите внимание: в примере не использован оператор выбора switch, потому что,
как уже говорилось ранее (раздел 9.5-2), макросы MYSIG.COUNT и MYSIG.STOP могут и
не быть целочисленными константами.
И наконец — функция main:
int raain(void)
{
sigset.t set;
struct sigacticn act;
union sigval vaiue;
pthread.t tid;
ec_neg1( sigeraptyset(&set) )
ec_neg1( sigaddset(4set, MYSIG.COUNT) )
ec_neg1( sigaddset(4set, MYSIG.STCP) )
ec_rv( pthread_sigmask(SIG_SETMASK, iset, NULL) )
636 ГЛАВА 9 Сигналы и таймеры
memset(&act, 0, sizecf(act));
act.sa_flags = SA_S1G1NF0;
act.sa_slgactlcn = dummy.handler;
ec_neg1( slgactlcn(MYSlG_COUNT, «.act, NULL) )
ec_neg1( slgactlcn(MYSlG_STOP, iact, NULL) )
value.slval_ptr = "Один";
ec_neg1( sigqueue(getpld(), MYSIG_COUNT, value) )
value.slval.ptr = "Два";
ec_neg1( slgqueue(getpld(), MYSIG.COUNT, value) )
value.sival.ptr = "Три";
ec_neg1( sigqueue(getpld(), MYSIG.COUNT, value) )
value.slval_ptr = NULL;
ec.negK slgqueue(getpid(), MYSIG.STOP, value) )
ec_rv( pthread_create(&tid, NULL, slg.thread, &set) )
ec_rv( pthread_jcin(tid, NULL) )
exit(EXlT_SUCCESS);
EC_CLEANUP_BGN
exit(EXlT.FAlLURE);
EC.CLEANUP.END
>
static vcid dummy_handler(lnt signum, siginfc.t *infc, veld «context)
{
}
Обратите внимание: все четыре сигнала посылаются еще до того, как поток будет
запущен, чтобы продемонстрировать, что они действительно помещаются в
очередь. Сигналы MYSIG.COUNT будут восприняты потоком в первую очередь, так как они
имеют наивысший приоритет. Кроме того, в самом начале сигналы блокируются и
остаются заблокированными до окончания работы программы. Пустая функция-
обработчик нужна лишь для того, чтобы имелась возможность установить флаг
SA.SIG1NF0. Для сигналов RTS в Solaris делать это не обязательно, но вы не должны
рассчитывать на это при написании переносимых программ.
В результате работы примера я получил следующее:
Пслучен сигнал MYSIG.COUNT; ее стрскей: Один
Получен сигнал MYSIG.COUNT; ее стрскей: Два
Пслучен сигнал MYSIG.COUNT; cc стрскей: Три
Пслучен сигнал MYSIG.STOP; работа завершена
Если необходимо ожидать прибытия сигнала только в течение определенного
времени, используйте системный вызов sigtimedwait, который, по сравнению с sigwaitinfc,
имеет дополнительный аргумент, задающий предельное время ожидания:
ГЛАВА 9 Сигналы и таймеры
637
sigtimedwait — ожидает появления сигнала
«include <signal.h>
int sigtimedwait(
const sigset_t *set,
siginfo_t *info,
const struct timespec
v
♦ts
/» Возвращается нсиер сигнала
в переменней еггпс) •/
/*
h
h
или
набер
ожидаемых сигналов
возвращаемая
макс.
-1 в
время
случае
информация
ожидания •/
*/
*/
сшибки (кед сшибки -
Системный вызов sigtimedwait ожидает появления сигнала из набора set в течение
заданного аргументом ts количества наносекунд. Если время ожидания истекло до
появления сигнала, в вызывающую программу возвращается признак ошибки с
кодом EAGAIN. В аргументе ts нельзя передать значение NULL. Если оба поля в
структуре timespec равны нулю и в очереди нет ни одного сигнала из набора set,
управление возвращается немедленно, но это не какой-то особый случай — просто
истекает время ожидания!
9.5.6 Структура sigevent и SIGEV_THREAD
Остальные функции для работы с сигналами реального времени будут описаны
позднее. Все они используют структуру sigevent, которая характеризует сигнал и
содержит сопровождающие его данные. Здесь я опишу только структуру sigevent:
struct sigevent — структура, используемая функциями обслуживания сигналов
RTS
struct sigevent {
int sigev_nctify;
int sigev_signo;
union sigval sigev_value;
ниже) */
/« тип уведомления (см.
/* номер сигнала */
/« данные, сопровождающие сигнал <
void (*sigev_nctify_function)(union sigval); /* функция потека <
pthread_attr_t *sigev_nctify_attributes; /* атрибуты потока
};
Номер посылаемого сигнала (пусть это будет SIGRTMIN) заносится в поле sigev.signc,
а данные, сопровождающие его — в поле sigev_value. Поле sigev_nctify задает
порядок передачи сигнала:
SIGEV_SIGNAL Сигнал генерируется так, как будто было обращение к
системному вызову sigqueue.
SIGEv_THREAD Запускается новый поток так, как будто было обращение к
системному вызову pthread_create.
SIGEV_NONE Уведомление не производится.
Самый интересный способ передачи сигнала — SIGEV_THflEAD. Каждый раз, когда
происходит событие, запускается функция, указатель на которую находится в поле
sigev_nctify_functicn. Она становится функцией запуска потока. Обычно функции
638 ГЛАВА 9 Сигналы и таймеры
запуска потока принимают аргумент типа void*. В данном случае, в функцию
передается аргумент типа union sigval, который представляет собой практически то же
самое, так как один из элементов объединения имеет тип void*. При желании
можно задать атрибуты потока через поле sigev_notify_attributes. Но у вас не будет
возможности подождать окончания работы потока и получить от него код завершения.
Накладные расходы на создание нового потока достаточно велики, поэтому не
рекомендуется использовать тип извещения SIGEV.THBEAD в случаях, когда события
следуют достаточно часто или когда цена запуска потока несопоставимо велика по
сравнению с объемом работы, выполняемой по сигналу. Например, будет
неразумным запускать новый поток всякий раз, когда пользователь нажимает очередную
клавишу на клавиатуре.
Потоки, создаваемые по сигналу, совершенно не похожи на потоки, ожидающие
прибытия сигнала с помощью системного вызова sigwait. Единственное, что у них
общее — это то, что они оба являются потоками.
9.6 Далекие, или глобальные переходы
Обычно функции возвращают управление в вызывающую программу с помощью
оператора return. Даже функции, не имеющие возвращаемого значения, передают
управление в точку вызова по достижении конца внешнего блока (оператор return
вызывается неявно). Возможен и другой способ выхода из функции — дальний
переход в произвольную, но запланированную заранее точку программы с
помощью стандартных функций языка С — setjmp и longjmp:
setjmp — устанавливает точку перехода
#inolude <setjmp.h>
int setjmp(
jmp_buf loo.info /* буфер для оохранения информации о местоположении */
);
/* Возвращает 0, если вызывается напрямуп, ненулевое значение - еоли
возврат произошел из longjmp */
longjmp — выполняет переход в точку, заданную с помощью setjmp
#inolude <setjmp.h>
void longjmp(
jrap_buf loo_info, /* буфер о информацией о местоположении */
int val /* значение, которое будет возвращено setjmp */
);
Функция setjmp служит своего рода меткой точки перехода. Она сохраняет в
буфере loc.info всю необходимую информацию (например содержимое регистра
указателя стека, адрес перехода и т. п.). После того, как точка перехода будет
установлена, можно вызвать функцию longjmp, передав ей в качестве аргументов буфер с
ГЛАВА 9 Сигналы и таймеры 639
информацией о точке перехода и ненулевое целое число, при этом глубина
вложения функции, вызывающей longjmp, не имеет никакого значения. Функция longjmp
восстанавливает значения из аргумента loo.info в результате — выполнение
программы продолжается так, как будто соответствующий вызов setjmp только что вернул
значение val, которое не может быть нулем, если в longjmp в аргументе val было
передано число 0, то setjmp вернет 1.
Функция, которая обращается к longjmp, может помещать в стек некоторые данные
— обращение к longjmp автоматически вытолкнет их со стека. Следует заметить, что
функция, содержащая setjmp, не вызывается повторно и здесь не возникает
рекурсии, хотя она могла иметь место до обращения к longjmp.
Ниже приводится пример использования функций setjmp и longjmp. Если вы еще не
встречались с ними, то он может показаться вам довольно странным:
statio jmp_buf loo_info;
statio void fon2(void)
{
printf("Bxofl в функцию fon2\n");
longjmp(loo_info, 1234);
printf("Bbixofl из функции fon2\n");
}
statio void fonl(vold)
{
printf("Bxofl в функцию fon1\n");
fon2();
printf("Bbixofl из функции fon1\n");
}
int main(void)
{
int rtn;
rtn = setjmp(loo_info);
printf("Функция setjmp вернула чиоло Xd\n", rtn);
if (rtn == 0)
fon1();
printf("Конец.\n");
exit(EXIT_SUCCESS);
Результат работы программы:
Функция setjmp вернула чиоло 0
Вход в функцию fcnl
Вход в функцию fon2
640 ГЛАВА 9 Сигналы и таймеры
Функция setjmp вернула число 1234
Конец.
Как видите, функция setjmp была вызвана единожды, но вернула управление
дважды, а так как переход был выполнен прямо изнутри функции fcn2, на экране не
появилось ни одной строчки, содержащей текст «Выход из функции...».
Если вы вообще ничего не поняли из вышесказанного, не стоит беспокоиться,
потому что дальше я скажу следующее: «Никогда не используйте longjmp в своих
программах*, и вот почему:
• хотя стек и приводится в порядок, но кроме него существует еще кое-что
(например зарезервированные области динамической памяти, открытые файлы и
т. п.);
• имеется ряд существенных ограничений, накладываемых на порядок
использования setjmp/longjmp. Если вы о них забудете — проблемы не заставят себя ждать;
• многие (включая и меня) не любят даже простые операторы goto внутри
функций, что уж тут говорить о longjmp! Применение этой «парочки» сильно
усложняет как понимание текста программы, так и ее сопровождение;
• в основном longjmp используется внутри обработчиков сигналов, но эта
функция не относится к разряду безопасных в контексте обработки асинхронных
сигналов, таким образом, вы не сможете воспользоваться ею без гарантий того,
что обработчик сигнала был вызван изнутри безопасной функции, такой как
системный вызов kill, запущенный из того же самого процесса (см. раздел 9-1-7).
Если у вас начинают появляться мысли о том, что без longjmp никак не обойтись,
значит вы плохо продумали свои функции, чтобы позволить им возвращать
управление обычным способом, возможно с признаком ошибки. Подумайте как следует,
и у вас все получится.
Функция longjmp может восстанавливать, а может не восстанавливать маску
сигналов потока. Если вам нужно, чтобы маска сигналов восстанавливалась —
используйте разновидности функций setjmp/longjmp, которые умеют сохранять и
восстанавливать маску сигналов:
sigsetjmp — устанавливает точку перехода
«include <setjmp.
int sigsetjrap(
h>
sigjmp_buf loc.info, /• буфер для
int savemask
•/
);
/* Возвращает О,
возврат произошел
/* ненулевое
с сохранением маски
информации
значение -
если вызывается напрямую,
из siglongjmp «/
о местоположении
сохранить маску
сигналов
•/
сигналов
ненулевое значение - если
ГЛАВА 9 Сигналы и таймеры 641
siglongjmp — выполняет
переход в точ
восстанавливает маску сигналов
«include <setjmp.h>
vcid siglcngjrapC
sigjmp_buf lcc_infc
int val
);
ку, заданную
с помощью
/* буфер с информацией с местоположении
/* значение,
которое будет
sigsetjmp
V
возвращено sigsetjmp
и
V
Восстанавливаемая маска сигналов на самом деле представляет собой маску,
которая существовала в момент обращения к sigsetjmp. Если siglongjmp вызывается из
обработчика сигнала, восстановленная маска может отличаться от той, что
получилась бы при нормальном завершении обработчика сигнала.
Поскольку функция siglongjmp не более «безопасна» чем lcngjmp, как правило, она
также не может быть использована внутри обработчиков сигналов, поэтому ее
способность восстанавливать маску сигналов, в большинстве случаев оказывается
бесполезной.
Если по тем или иным причинам вы не желаете сохранять маску сигналов, тогда
просто передайте в аргументе savemask функции sigsetjmp значение 0. Кроме того,
вы можете использовать еще одну пару функций:
_setjmp — устанавливает точку перехода
«include <setjmp.h>
int _setjmp(
jmp_buf lcc_infc /* буфер для информации с местоположении */
);
/* Возвращает 0, если вызывается напрямую, ненулевое значение - если
возврат произошел из _lcngjmp */
longjmp -
«include
- выполняет
<setjmp
vcid _lcngjmp(
Ji»P.
int
);
.buf lcc_
val
. h>
info,
переход
/*
/•
буфер
в точку, заданную
с помощью
с информацией с местоположении
значение, которое будет
_setjmp
•/
возвращено _setjmp »/
Разумеется, функция „lcngjmp также не является безопасной в контексте обработки
асинхронных сигналов. Фактически, эта пара функций являет собой остатки
прежних стандартов.
i
9.7 Часы и таймеры
В этом разделе будут описаны системные часы и таймеры, которые посылают
сигналы через установленные интервалы времени.
I
642 ГЛАВА 9 Сигналы и таймеры
9.7.1 Системный вызов alarm
alarm — планирует выдачу сигнала
«include <unistd.h>
unsigned alarm(
unsigned sees /* через какое ксл-
);
/* Возвращает количестве секунд, сета
предыдущим сбращением к вызеву или 0,
*/
вс секунд
вшееся
если
ДС
выдать
сигнала
сигнал
•/
, запланированного
выдача сигнала не
запланирсвана
Каждый процесс получает в свое распоряжение один будильник, предназначенный
для использования системным вызовом alarm. Когда заданное время истекает,
процесс получает сигнал SIGALRM. Дочерний процесс наследует от предка «включенный»
будильник, но сами часы не разделяются процессами. Время, оставшееся до
появления сигнала SIGALRM, передается и через системный вызов exec.
Системный вызов alarm устанавливает интервал времени, который передается в
аргументе sees, и возвращает количество секунд, установленное предыдущим
обращением к нему. Значение 0 возвращается в том случае, если время уже истекло
или если выдача сигнала не была запланирована. Возвращаемое значение
используется для того, чтобы восстановить значение, имевшееся до обращения к вызову
alarm.
Если в аргументе sees передается число 0, будильник выключается. Очень важно не
забывать делать это. Например, допустим, что у нас имеется блокируемый
системный вызов, пусть это будет read, и нужно, чтобы он находился в заблокированном
состоянии не более определенного времени. Тогда можно назначить перехват
сигнала SIGALRM (пустой функцией-обработчиком) и вызвать alarm, передав ему число
5 (5 секунд), после чего вызвать системный вызов read, который будет
заблокирован. По истечении заданного интервала времени сигнал SIGALRM прервет
исполнение вызова read, который вернет в вызывающую программу признак ошибки с
кодом EINTR. Но если вызов read выполнит свою работу намного раньше и вы
забудете отключить будильник, сигнал будет доставлен процессу позже, много позже,
поскольку 5 секунд — огромное время по меркам компьютеров и какой-нибудь
другой системный вызов окажется прерванным весьма таинственным образом!
Рассмотрим пример программы, которая использует будильник:
int main(vcid)
{
struct sigacticn act;
char buf[100];
ssize_t rtn;
memset(&act, 0, sizecf(act));
act.sa_handler = handler;
ГЛАВА 9 Сигналы и таймеры 643
ec_neg1( sigacticn(SIGALRM, &act, NULL) )
alarm(5);
if ((rtn = read(STDIN_FILENO, buf, sizecf(buf) - 1)) == -1) {
if (errnc == EINTR)
printf("Время истекло... в следующий раз печатайте быстрее!\п");
else
EC.FAIL
}
alarm(O);
if (rtn == О)
printf("Пслучен признак кснца файла\п");
else If (rtn > 0) {
buf[rtn] = '\0';
printf("Получена строка Xs", buf);
}
exit(EXIT_SUCCESS);
EC_CLEANUP_BGN
exit(EXIT_FAILURE);
EC_CLEANUP_END
}
static vcid handler(int slgnum)
{
}
В следующем примере взаимодействия с программой, в первый раз я ничего не
набирал на клавиатуре. При втором запуске программы я напечатал слово «быстрее»:
$ alarm_test
Время истекло... в следующий раз печатайте быстрее!
$ alarra_test
быстрее
Получена строка быстрее
$
В простых случаях, как в приведенном выше примере, alarm прекрасно подходит
для прерывания заблокированного вызова, но в более сложных ситуациях, вместо
прямого обращения к read, лучше использовать системный вызов select или pell.
Если вам нужно ограничение по времени — пользуйтесь системным вызовом pselect,
если конечно он доступен в вашей системе.
Основное ограничение вызова alarm в том, что процесс обладает единственным
будильником. Более гибкий подход предлагают системные таймеры, которые
будут описаны в разделах 9-7.4 и 9-7.6.
644 ГЛАВА 9 Сигналы и таймеры
9.7.2 Системный вызов sleep
Системный вызов sleep уже хорошо нам знаком. Основное его назначение —
блокировать исполнение потока на заданное количество секунд:
sleep — приостанавливает работу потока до истечения заданного времени
или пока он не будет прерван сигналом
«include <unistd.h>
unsigned sleep(
unsigned sees /* заданный интервал времени в секундах */
);
/* Возвращает количество секунд, оставшееся до конца заданного интервала */
Правила реализации вызова sleep в SUS содержат массу формулировок и оговорок,
которые позволяют реализовать sleep как функцию, в терминах alarm и pause, а еще
лучше — в терминах alarm и sigsuspend. Ниже приводится упрощенный пример
реализации функции sleep:
unsigned aup_sleep(unsigned sees)
{
struct sigaction act;
unsigned unslept;
memset(&act, 0, sizeof(act));
act.sa_handler = slp_handler;
ec_neg1( sigaction(SIGALRM, iact, NULL) )
alarm(secs);
pause();
unslept = alarm(O);
return unslept;
EC_CLEANUP_BGN
EC_FLUSH("aup_sleep")
return 0;
EC_CLEANUP_END
}
static void slp_handler(int signum)
{
}
Эта версия вполне работоспособна, но имеет ряд недостатков.
• Если сигнал SIGALRM не был заблокирован, то его доставка до обращения к sigaction
может завершить работу процесса.
• Если был заблокирован, функция вообще не будет работать.
ГЛАВА 9 Сигналы и таймеры 645
• Если сигнал SIGALRM будет доставлен процесс между обращениями к ala rm и pause,
пауза может затянуться навечно.
• Прежняя реакция на сигнал SIGALRM не восстанавливается.
• Сделано предположение, что alarm и sleep являются совместимыми между собой.
Последнее замечание означает, что следующая последовательность действий:
alarm(10);
sleep(20);
предполагает прерывание функции sleep по сигналу от будильника. А такая
последовательность:
alarm(20);
sleep(10);
предполагает, что сигнал SIGALRM будет доставлен процессу через 10 секунд после
возврата из sleep.
Корректное взаимодействие alarm и sleep должно препятствовать ошибочному
сигналу SIGALRM завершать приложение, предотвращать затягивание паузы до
бесконечности, а также восстанавливать предыдущую реакцию приложения на сигнал и
маску сигналов. Это в принципе возможно, но сопряжено с определенными
трудностями, так как функция должна обрабатывать три возможных ситуации:
• будильник не был установлен в момент обращения к sleep;
• время, оставшееся до срабатывания будильника меньше или равно времени,
заданному в обращении к sleep;
• время, оставшееся до срабатывания будильника больше времени, заданного в
обращении к sleep.
Ниже приводится работоспособная версия функции:"
unsigned aup_sleep(unsigned sees)
{
sigset_t set, oset;
struct sigaction act, oact;
unsigned prev_alarra, slept, unslept, effectlve_secs;
ec_neg1( sigeraptyset(iset) )
ec_neg1( sigaddset(4set, SIGALRM) )
ec_neg1( sigprocraask(SIG_BLOCK, iset, ioset) )
prev_alarm = alarm(O);
if (prev_alarm != 0 && prev_alarm <= sees)
effective_secs = prev_alarm;
else {
• Джеф Клэр (Geoff Clare) внес свой вклад в исправление моей первоначальной версии.
646 ГЛАВА 9 Сигналы и таймеры
meraset(&act, 0, sizecf(act));
act.sa_handler = slp_handler;
ec_neg1( sigacticn(SlGALRM, &act, &cact) )
effective.secs = sees;
}
alarm(effective_secs);
set = cset;
ec_neg1( sigdelset(&set, S1GALRM) )
if (sigsuspend(&set) == -1 && errnc != EINTR)
EC.FA1L
unslept = alarra(O);
slept = effective_secs - unslept;
ec_neg1( sigacticn(SlGALRM, &cact, NULL) )
if (prev_alarm > slept)
alarm(prev_alarm - slept);
ec_neg1( sigprccraask(SlG_SETMASK, &cset, NULL) )
return unslept;
EC_CLEANUP_BGN
EC_FLUSH("aup_sleep")
return 0;
EC_CLEANUP_END
>
static vcid slp_handler(int signum)
{
}
А теперь подробно, шаг за шагом, рассмотрим, что делает эта функция:
1. В первую очередь блокируется сигнал S1GALRH, чтобы он нас больше не
беспокоил. Попутно сохраняется прежняя маска сигналов.
2. Затем отключается будильник, при этом сохраняется время, оставшееся до его
срабатывания.
3. Рассчитывается время ожидания (effective.secs), из двух интервалов времени
выбирается наименьший и будильник взводится на срабатывание через
полученный интервал.
4. Если будильник не был установлен или время ожидания меньше оставшегося
интервала до срабатывания будильника — устанавливается пустая
функция-обработчик и сохраняется предыдущая реакция на сигнал S1GALRM.
5. Чтобы обеспечить атомарность последовательности операций по снятию
блокировки с сигнала S1GALRM и постановки на ожидание, вместо pause вызывается
sigsuspend. Остальные биты маски остаются такими, какими они были на входе
в aup.sleep.
6. После возврата из sigsuspend мы особо не беспокоимся о причине возврата (это
может быть сигнал S1GALRM, которого мы ожидаем, но может быть и другой
сигнал). Нам нужно лишь время, оставшееся до срабатывания будильника — мы
получаем его, выключая будильник.
ГЛАВА 9 Сигналы и таймеры 647
7. Затем рассчитывается время, которое было проведено в состоянии ожидания.
8. Восстанавливается прежняя реакция на сигнал.
9. Если время ожидания было меньше, чем прежнее время срабатывания будильника
— будильник взводится на срабатывание через оставшийся интервал времени.
10. Восстанавливается маска сигналов.
11. Возвращается время, оставшееся до конца заказанного времени.
Если вы оказались в состоянии понять эту функцию, значит вы на 90% усвоили все,
что говорилось в этой главе и смело можете поставить себе пятерку. Если вы
найдете ошибку и отправите сообщение по адресу aup@basepath.com — ставьте себе
пять с плюсом!
9.7.3 Приостановка с высокой точностью измерения
интервалов времени
Системный вызов sleep может приостановить исполнение потока на интервал
времени с точностью до секунды, но для многих применений такой точности бывает
недостаточно. Задать время ожидания с более высокой точностью позволяют
следующие системные вызовы:
usleep — приостанавливает работу потока на интервал времени,
измеряемый с точностью до микросекунды, либо пока не будет доставлен сигнал
«include <unistd.h>
int usleep(
useccnds_t usees /* интервал времени в микросекундах */
);
/» Возвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
Системный вызов usleep очень похож на sleep, но он не должен использоваться для
измерения интервалов длиннее одной секунды. Это означает, что параметром usees
следует передавать значения меньше 1 000 000. Отмерять еще более точные
интервалы времени позволяет системный вызов nancsleep:
nanosleep — приостанавливает работу потока на интервал времени,
измеряемый с точностью до наносекунды, либо пока не будет доставлен сигнал
«include <time.h>
int nancsleep(
censt struct timespec *nsecs, /•
struct timespec * remain /«
);
/* Возвращает О в случае успеха или
в переменней еггпс) */
интервал времени
сставшееся время
-1 в случае сшибки
в наносекундах
или NULL •/
(кед сшибки -
*/
648 ГЛАВА 9 Сигналы и таймеры
Системный вызов nanosleep получает время ожидания через структуру tiitiespeo,
которую мы обсуждали в разделе 1.7.2. Однако напомню еще раз, что в поле tv_seo
этой структуры передается число секунд, а в поле tv.nseo — число наносекунд.
Если вызов был прерван сигналом, он возвращает признак ошибки с кодом EINTR,
как и любой другой системный вызов, при этом по адресу remain записывает
оставшееся время. В случае успешного завершения или когда в аргументе remain
передается пустой указатель — ничего не возвращается.
В разделе 9.7.5 мы рассмотрим еще одну функцию, которая приостанавливает
исполнение потока — oiook.nanosieep.
9.7.4 Основные системные вызовы для работы с таймерами
Системный вызов alarm использует единый таймер для всего процесса и он
присутствует во всех версиях UNIX. Кроме того, все SUS-совместимые системы имеют
в своем распоряжении три дополнительных таймера, которые могут использоваться
независимо друг от друга:
ITIHER.REAL Отслеживает реальное время системы и генерирует сигнал
SIGALRM по истечении заданного интервала.
ITIHER_VIRTUAL Отслеживает виртуальное время процесса и генерирует сигнал
SIGVTALRH по истечении заданного интервала.
ITIHER_PR0F Отслеживает как время работы в процессе, так и время работы
в ядре от имени процесса. Генерирует сигнал SIGPR0F по
истечении заданного интервала времени. Используется
программами-профилировщиками, которые позволяют определить
участки, в которых программа проводит наибольшее время.
В следующих двух системных вызовах аргумент whioh должен быть одним из трех
макросов, перечисленных выше:
getitimer — возвращает значение таймера
«inoiude <sys/time.h>
int getitimer(
int whioh, /*
struot itimervai *val /*
выбранный таймер •/
возвращаемое значение
•/
><
/* Возвращает О в олучае уопеха или -1 в олучае ошибки
в переменной еггпо) */
(код
ошибки -
setitimer — устанавливает значение для таймера
«inoiude <sys/time.h>
int setitimer(
int whioh, /* выбранный таймер »/
oonst struot itimervai »vai, /* начальное значение интервала »/
struot itimervai »ovai /» прежняя величина интервала */
);
/* Возвращает 0 в олучае успеха или -1 в случае ошибки (код ошибки -
в переменной еггпо) »/
ГЛАВА 9 Сигналы и таймеры 649
struct itimerval — структура для системных вызовов geti
struct itimerval {
struct timeval
struct timeval
>;
it
it.
.interval;
.value;
/*
/*
начальное значение «/
текущее значение •/
timer
и
setitimer
В поле it.interval структуры itimerval находится время, которое будет
установлено вновь, по его истечении, а в поле it_value — время, оставшееся до
срабатывания таймера. В отличие от будильника (системный вызов alarm), который
срабатывает один раз, таймеры могут автоматически переустанавливаться на измерение
очередного интервала времени. Чтобы остановить таймер, нужно вызвать setitimer со
значением 0 в поле it.value. Чтобы остановить таймер после ближайшего
срабатывания, нужно вызвать setitimer со значением 0 в поле it_interval. Структура timeval
рассматривалась нами в разделе 1.7.1, она состоит из двух полей — tv_sec
(секунды) и tv_usec (микросекунды).
По адресу oval вызов setitimer возвращает начальное значение таймера, установленное
предыдущим вызовом setitimer. Некоторые примеры использования таймеров:
/* сднскратнсе срабатывание таймера через 3.5 секунды */
itv.it_interval.tv_sec = 0;
itv.it_interval.tv_usec = 0;
itv.it_value.tv_sec = 3;
itv.it_value.tv_usec = 500000;
ec_neg1( setitimer(ITIMER_REAL, &itv, NULL) )
/* первое срабатывание таймера через 3.5 сек.,
каждое последующее - через 2.25 сек. */
itv.it_interval.tv_sec = 2;
itv.it_interval.tv_usec = 250000;
itv.it_value.tv_sec = 3;
itv.it_value.tv_usec = 500000;
ec_neg1( setitimer(ITIMER_REAL, &itv, NULL) )
/* останавливает таймер; интервал срабатывания не имеет значения */
itv.it_value.tv_sec = 0;
itv.it_value.tv_usec = 0;
ec_neg1( setitimer(ITIMER_REAL, &itv, NULL) )
Во втором примере таймер сработает первый раз через 35 секунды, потом через
5.75 секунды, потом через 8 секунд и так далее.
Следующий пример программы должен выводить символ «X» через каждые две
секунды. Обратите внимание: после установки таймера программа может
продолжать заниматься своими делами, в данном случае — читать данные с устройства
стандартного ввода и записывать в устройство стандартного вывода:
650 ГЛАВА 9 Сигналы и таймеры
void timer_try1(void)
{
struot sigaotion aot;
struot itimerval itv;
ohar buf[100];
ssize_t nread;
memset(&aot, 0, sizeof(aot));
aot.sa_handler = handler;
eo_neg1( sigaotion(SIGALRM, &aot, NULL) )
memset(&itv, 0, sizeof(itv));
itv.it_interval.tv_seo = 2;
itv.it_value.tv_seo = 2;
eo_neg1( setitiraer(ITIMER_REAL, &itv, NULL) )
while (true) {
switoh( nread = read(ST0IN_FILEN0, buf, sizeof(buf) - 1) ) {
oase -1:
EC_FAIL
oase 0:
printf("Признак конца файла\п");
break;
default:
if (nread > 0)
buf[nread] = '\0';
eo_neg1( write(ST00UT_FILENO, buf, strlen(buf)) )
oontinue;
}
break;
}
return;
EC_CLEANUP_BGN
EC_FLUSH("tiraer_try1")
EC_CLEANUP_EN0
}
void handler(int signura)
{
write(ST00UT_FILEN0. "\nX\n", 3);
}
В результате работы программы было получено следующее:
X
ERROR: 0: tm1 [/aup/o9/tmr.o:26] 0
*** EINTR (4: "Interrupted system oall") ***
ГЛАВА 9 Сигналы и таймеры 651
Вы удивлены? Здесь нет ничего удивительного: через две секунды был выведен символ
«X», но при этом сигналом SIGALRM было прервано исполнение системного вызова
read. Чтобы этого не происходило, нужно установить флаг SA_RESTART:
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
act. sa_flags = SA_RESTART;
ec_neg1( sigaction(SIGALRM, &act, NULL) )
Теперь все работает правильно. В процессе работы я ввел с клавиатуры слово
«привет», которое было выведено программой:
X
X
привет
X
привет
X
Существует еще один, уже устаревший системный вызов с именем ualarra, который
работает с таймером.
9.7.5 Часы реального времени
Любая UNIX-система имеет в своем распоряжении обычные системные часы,
показания которых можно получить с помощью системных вызовов time и gettiraeof day
(см. раздел 1.7.1). Но стандарт POSIX предусматривает наличие дополнительных
часов (при условии поддержки _POSIX_TIMERS), количество которых определяется
программной и аппаратной поддержкой. Доступ к этим часам осуществляется
через их идентификаторы.
Все системы, которые поддерживают _POSIX_TIMERS, поддерживают как минимум один
идентификатор — CLOCK.REALTIME, который соответствует часам, отслеживающим
время суток. Поддерживаются ли другие идентификаторы, относятся ли они к
процессу или ко всей системе — зависит от конкретной реализации. Например, ОС Solaris
поддерживает еще одни общесистемные часы — CL0CK_HIGHRES, которые
отсчитывают время, начиная с определенного момента. Возвращаемое ими время не
обязательно должно быть временем суток, поскольку вы можете не знать, с какого
момента начат отсчет, но они отлично подходят для определения длительности
интервала времени между двумя обращениями.
Чтобы получить время в наносекундах, вы должны обратиться к системному
вызову clock_gettime. Результат будет возвращен в виде структуры timespec, которая
рассматривалась нами в разделе 1.7.2 и 973:
652 ГЛАВА 9 Сигналы и таймеры
clock_gettime — возвращает время из заданных часов
«include <time.h>
int clcck_gettime(
clcckid_t clcck_id, /* идентификатор чассв (CLOCK_REALTIME, и т. п.) »/
struct timespec *tp /« время «/
);
/« Возвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
Чтобы получить точность измерения времени, следует обратиться к системному
вызову clcck.getres:
clock_getres — возвращает точность измерения времени
«include <tirae.h>
int clcck_getres(
clcckid_t clcck_id, /* идентификатор чассв «/
struct timespec «res /» течнесть */
);
/« Возвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки
в переменней еггпс) «/
Установить время на часах можно с помощью системного вызова clcck_settime (при
наличии прав доступа суперпользователя для CLOCK_REALTIME):
clock_settime — устанавливает часы
«include <time.h>
int clcck_settime(
clcckid_t clcck_id,
censt struct timespec
);
/« Возвращает О в случае
в переменней errnc) */
/•
*tp /*
успеха
идентификатор
время «/
или -1
в случае
часов «/
сшибки (кед сшибки -
Ниже приводится исходный код функции, которая получает время и точность его
измерения, а затем выводит их вместе с результатом исполнения системного
вызова time:
void clccks(vcid)
{
struct timespec ts;
time_t tm;
ec_neg1( time(&tm) )
printf("time() Время: Xld сек.\п", (lcng)tm);
p rintf("CLOCK.REALTIME:\n");
ГЛАВА 9 Сигналы и таймеры 653
ec_neg1( clcck_gettime(CLOCK_REALTIME, &ts) )
printfC'BpeMfl: Ud.X091d сек.\п", (lcng)ts.tv_sec, (lcng)ts.tv_nsec);
ec_neg1( clcck_getres(CLOCK_REALTIME, &ts) )
printf("Тсчнссть: Ud.X091d сек.\п", (lcng)ts.tv_sec, (lcng)ts.tv_nsec);
return;
EC_CLEANUP_BGN
EC_FLUSH( "decks")
EC_CLEANUP_ENO
}
В ОС FreeBSD я получил следующее:
time() Время: 1051646В7В сек.
CLOCK_REALTIME:
Время: 1051646В7В.56В62В061 сек.
Тсчнссть: О.ООООООВЗВ сек.
Отсюда видно, что точность измерения времени составляет менее одной
микросекунды. На другой платформе — ОС Solaris — было получено:
time() Время: 1051646409 сек.
CLOCK_REALTIME:
Время: 1051646409.6В6В696ВЗ сек.
Тсчнссть: 0.010000000 сек.
На этот раз точность измерения времени составила одну сотую секунды. Формат
отображения времени с 9 знаками после десятичной точки может вводить в
заблуждение. Можете попробовать выводить время в более удобоваримом виде.
Однако эти часы не предназначены для показа времени, их основное назначение —
измерение интервалов времени.
Существует версия системного вызова nancsleep, которая использует для измерения
времени задержки определенные часы:
clock_nanosleep — приостанавливает работу потока на интервал времени,
измеряемый с точностью до наносекунд, либо пока не будет доставлен сигнал
«include <time.h>
int clcck_nancsleep(
clcckid_t clcck_id, /* идентификатор чассв */
int flags, /* TIMER_ABSTIME или нсль */
censt struct timespec «nsecs, /* задержка в наносекундах */
struct timespec «remain /* оставшееся время или NULL */
);
/* Возвращает 0 в случае успеха или код ошибки «/ И1
Первый аргумент — идентификатор часов, последние два — соответствуют
аргументам вызова nanosleep. Если в аргументе flags передается значение 0, задержка
654 ГЛАВА 9 Сигналы и таймеры
будет длиться указанное число наносекунд, точно так же как и в системном вызове
nanosleep. Но если передается значение TIMER.ABSTIME, задержка будет
продолжаться до тех пор, пока часы не достигнут времени, указанного в аргументе nseos.
Последний аргумент используется только в случае, когда flags имеет значение 0.
И наконец, с помощью системного вызова olook.getopuolookid, вы можете получить
идентификатор часов процессорного времени для процесса (при условии
поддержки _P0SIX_CPUT1HE):
clock_getcpuclockid — возвращает идентификатор часов процессорного
времени
#inolude <tirae.h>
int olook_getopuolookid(
pid_t pid, ./* идентификатор процеооа */
olookid.t *olook_id /* возвращаемый идентификатор чаоов */
);
/* Возвращает 0 в олучае уопеха или код ошибки */
9.7.6 Дополнительные системные вызовы
для работы с таймерами
Как для обычных системных часов существует альтернатива в виде часов
реального времени, так и для обычных таймеров (обсуждавшихся в разделе 9-7.4) существуют
альтернативные, дополнительные таймеры. Вместо нескольких общесистемных
таймеров, обсуждаемые здесь системные вызовы позволят вам иметь несколько
независимых таймеров внутри процесса, основывающихся на одних и тех же
часах — если вы помните по предыдущему разделу, в системе гарантированно
имеются ТОЛЬКО часы CLOCK.REALTIME.
Начнем с создания таймера:
timer_create — создает таймер внутри процесса
#inolude <signal.h>
«inolude <time.h>
int tiraer_oreate(
olookid_t olookid,
struot sigevent *sig
timer.t *timer_id
);
/* Возвращает О в олучае
в переменной errno) */
/* идентификатор чаоов */
/* NULL или генерируемый оигнал */
1* возвращаемый идентификатор таймера «/
уопеха или -1 в олучае ошибки (код ошибки -
В случае успешного завершения, системный вызов timer.create возвращает
идентификатор таймера в аргументе timer_id. Если аргумент sig не является пустым
указателем, по истечении времени таймера будет сгенерирован заданный сигнал.
ГЛАВА 9 Сигналы и таймеры 655
Порядок доставки сигнала описан в разделе 9-5.6. Если в аргументе sig передается
пустой указатель, процессу будет послан сигнал SIGALRM, вместе с которым будет
передан идентификатор таймера в виде данных, сопровождающих сигнал.
Удаляется таймер системным вызовом timer_delete:
timer_delete — удаляет таймер процесса
«include <time.h>
int timer_delete(
timer_t timer_id /•
);
/* Возвращает О в случае
в переменней еггпс) */
идентификатор таймера */
успеха или -1 в
случае сшибки
(кед сшибки -
Далее следуют два системных вызова, которые напоминают getitimer и setitimer (см.
раздел 9-7.4), за исключением того, что точность измерения времени в структуре
itimerspec задается не в микросекундах, а в наносекундах:
timer_gettime — возвращает значение таймера процесса
«include <time.h>
int timer_gettime(
timer_t timer_id, /* идентификатор таймера */
struct itimerspec «val /* возвращаемое значение */
);
/* Возвращает 0 в случае успеха или -1 в случае сшибки (кед сшибки -
в переменней еггпс) */
timer_settime — устанавливает значение таймера процесса
«include <time.h>
int timer_settime(
timer_t timer_id,
int flags,
censt struct itimerspec *val
struct itimerspec *cval
);
/* Возвращает О в случае успеха
в переменней еггпс) */
/•
/*
, /*
/•
или
идентификатор таймера */
флаги */
начальное значение таймера •/
прежнее значение таймера или NULL •/
-1 в случае сшибки (кед сшибки -
struct itimerspec — структура для функций timer.
struct itimerspec {
struct timespec it_interval;
struct timespec it_value;
};
/•
/•
начальное значение таймера */
текущее значение */
I
656 ГЛАВА 9 Сигналы и таймеры
Если в аргументе flags функции timer.settime передается значение 0, она ведет себя
в точности как setitimer. Но если flags имеет значение TIMER.ABSTIME, значение поля
it.value интерпретируется точно так же, как аргумент nsecs в системном вызове
clcck_nancsleep — как абсолютное время, в которое должен сработать таймер.
В очередь сигналов может быть помещен только один сигнал от таймера. Если
значение it.interval достаточно мало, то между порождением сигнала и его
доставкой приложению может быть послано еще несколько сигналов. Чтобы узнать
количество срабатываний таймера, можно использовать системный вызов:
timer_getovermn — возвращает количество срабатываний таймера
«include <time.h>
int timer_getcverrun(
timer_t timer_id /*
);
/* Возвращает количестве
идентификатор
срабатываний
ошибки - в переменной еггпо) */
таймера
таймера
*/
или
-1 в случае
сшибки
(кед
Упражнения
9.1. Определите, какие еще сигналы существуют в вашей системе, за
исключением тех, что перечислены в разделе 9-1-3-
9.2. Напишите программу, которая вызывает появление сигнала sigpipe, когда
производится обращение к системному вызову write (подсказка:
используйте неименованные каналы). Затем заставьте программу игнорировать сигнал
SIGPIPE и посмотрите, что возвращает write в этом случае. Какое значение имеет
переменная errnc и каков его смысл?
9-3. Реализуйте системный вызов pause в терминах sigwait и любого другого
системного вызова, какой только захотите.
9.4. Попробуйте реализовать системный вызов signal (раздел 94) с помощью
sigacticn. Возможно вам поможет в этом обработчик сигнала.
9.5. Реализуйте системный вызов sigset (раздел 9-4) с помощью sigacticn.
9-6. Напишите обработчик сигнала, который выполняет дальний переход по
прибытии сигнала (раздел 9.6) и убедитесь, что исполнение программы
продолжается с точки вызова setjmp или sigsetjmp. Почему longjmp и siglcngjmp не
попали в разряд функций, безопасных в контексте обработки асинхронных
сигналов?
9.7. Функции lcngjmp и siglcngjmp приводят в порядок только стек — они не
обслуживают области динамической памяти, выделенные функциями malice и
reallco. Обсудите эту проблему. Возможно ли разрешить ее? Действительно ли
стоит ее решать? Предложите возможные изменения для функций setjmp,
sigsetjmp, lcngjmp, siglcngjmp, malice, reallco и free, которые решили бы эту
проблему.
ГЛАВА 9 Сигналы и таймеры 657
9.8. Реализуйте системный вызов sleep с помощью usleep, учитывая, что usleep не
может выполнить задержку дольше чем на 1 секунду. То есть вы должны
предусмотреть возможность организации более длительных задержек, например
на 3 секунды.
9-9- Используя любые системные вызовы, реализуйте команду с именем alarmclock,
которая принимает в качестве аргументов время и текст сообщения, а по
достижении заданного времени выводит сообщение на устройство
стандартного вывода и подает звуковой сигнал. Команда должна использовать
функциональные возможности ядра для организации задержки и не выполнять
никаких действий, пока не наступит заданное время. Спроектируйте приложение
так, чтобы оно запускалось в виде фонового процесса, даже если
пользователь забудет напечатать в командной строке символ амперсанда.
9.10. Объясните (ничего писать не надо), как можно улучшить команду alarmclock
из предыдущего упражнения, чтобы она запоминала время срабатывания и
могла восстановить его после перезагрузки системы. Вы также должны подумать
о том, как запустить команду в момент загрузки системы. Есть ли в UNIX
команды, подобные этой?
9.11. Усовершенствуйте программу из упражнения 5.14 так, чтобы она могла
выводить атрибуты процесса (из Приложения А), о которых вы узнали в этой главе.
Приложение А
Атрибуты процессов
Это приложение содержит таблицу атрибутов процессов, на которые влияют
вызовы fork и exec; помимо прочего в ней указано, в каком разделе обсуждается
каждый из атрибутов.
Обратите внимание, что следующие элементы не являются атрибутами процессов,
потому что, как объясняется в главе 7, они существуют независимо от любого
процесса. Однако в отличие от файлов длительность их существования обычно равна
времени работы системы, т. е. при перезагрузке они утрачиваются. Вот эти элементы:
Очереди сообщений POSIX (раздел 7.7)
Именованные семафоры POSIX (раздел 7.10)
Общие сегменты памяти POSIX (раздел 7.14)
Идентификаторы очередей сообщений System V (раздел 7.5)
Очереди сообщений System V (раздел 7.5)
Идентификаторы наборов семафоров System V (раздел 7.9)
Наборы семафоров System V (раздел 7.9)
Идентификаторы общих сегментов памяти System V (раздел 7.13)
Общие сегменты памяти System V (раздел 7.13)
Таблица А.1. Атрибуты процессов
Атрибут
Что происходит с атрибутом
при вызове fork
Что происходит с атрибутом
при вызове exec
Раздел
асинхронные
операции
ввода/вывода
функции,
зарегистрированные
при помощи atexit
управляющий
терминал
атрибут не копируется
атрибут копируется
атрибут копируется
операции отменяются 3.9
или завершаются
без уведомления
регистрация отменяется 1.34
терминал остается прежним 4.3
(см. след. стр.)
660 Приложение А Атрибуты процессов
Таблица А.1 (продолжение)
Атрибут
Что происходит с атрибутом
при вызове fork
Что происходит с атрибутом Раздел
при вызове exec
позиция в потоке
каталога
потоки каталогов
текущий каталог
корневой каталог
статус выхода'
описания файлов
дескрипторы
файлов
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут используется
совместно
атрибут копируется
атрибут недоступен
3.6.1
блокировки файлов атрибут не копируется
атрибут копируется
атрибут копируется
маска создания
режима файла
среда операций
с плавающей точкой"
действующий идеи- атрибут копируется
тификатор группы
действующий
идентификатор
пользователя
идентификатор
родительского
процесса
идентификатор
процесса
идентификатор
группы процессов
реальный
идентификатор группы
реальный
идентификатор пользователя
сохраненный
установленный
идентификатор группы
атрибут копируется
атрибут изменяется
создается новый
идентификатор
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
потоки закрываются 3.6.1
используется тот же атрибут 3.6.2
используется тот же атрибут 5.14
5.7
атрибут не изменяется 2.2
используется тот же атрибут, 2.2
если не установлен
FD_CLOEXEC
используется тот же атрибут, 7.11
если не был закрыт
дескриптор файла
используется тот же атрибут 2.4.2
используется среда
по умолчанию
происходящее зависит 5.12
от бита установки
идентификатора группы
происходящее зависит 5.12
от бита установки
идентификатора пользователя
используется тот же атрибут 513
используется тот же атрибут 5.13
используется тот же атрибут 4.3
используется тот же атрибут 5.12
используется тот же атрибут 5.12
происходящее зависит 5.12
от бита установки
идентификатора группы
Не устанавливается, пока процесс не выполнит выход.
Среда операций с плавающей точкой включает флаги статуса операций с плавающей
точкой и соответствующие режимы управления.
Приложение А Атрибуты процессов 661
Таблица А.1 {продолжение)
Атрибут
Что происходит с атрибутом
при вызове fork
Что происходит с атрибутом Раздел
при вызове exec
сохраненный
установленный
идентификатор пользователя
идентификатор
потока
дополнительные
идентификаторы
групп
лимит ресурсов
(в том числе
размер файлов)
сегмент данных
в памяти
сегмент данных,
аргументы
сегмент данных,
среда
сегмент команд
в памяти
блокировки
доступа к памяти*
отображения
областей памяти
(POSIX и System V)
стек потока
описания очередей
сообщений (POSIX)
дескрипторы
очередей сообщений
(POSIX)
каналы (FIFO)"
каналы
(неименованные)
планирование.пока-
затель вежливости
планирование,
политики SCHED_FIFO
и SCHED RR'"
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут не копируется
атрибут копируется
атрибут копируется
атрибут используется
совместно
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
происходящее зависит 5.12
от бита установки
идентификатора пользователя
значение атрибута 5.17.1
утрачивается
используется тот же 1.1.8
атрибут
используется тот же атрибут 5.16
используется новый сегмент 1.1.5
используются новые 5.3
аргументы
используется новая среда 5.2
используется новый сегмент 1.1.5
блокировки снимаются
отображения отменяются 7.12
используется новый стек 517
ничего не происходит 7.5
дескрипторы закрываются 7.5
используется тот же атрибут 7.2
используется тот же атрибут 6.2
используется тот же атрибут 5.15
используется тот же атрибут -
Блокировки устанавливаются и снимаются при помощи вызовов mlock и munlock,
входящих в состав опции Range Memory Locking.
" Сами каналы FIFO хранятся столько же, сколько и другие файлы, но содержащиеся в них
данные утрачиваются, если только канал не открыт для чтения каким-либо процессом.
'" Атрибут задается при помощи sched.setscheduler; является частью опции Process Scheduling.
(см. след. стр.)
662 Приложение А Атрибуты процессов
Таблица А.1 (продолжение)
Атрибут
Что происходит с атрибутом
при вызове fork
Что происходит с атрибутом Раздел
при вызове exec
указатель на
именованный семафор
(POSIX)
семафоры, память
(POSIX)
семафоры, значения
semadj (System V)
членство в сеансе
действия,
выполняемые при сигналах
контекст
альтернативного стека
маска сигналов
(поток)
способ обработки
сигналов
сокеты
атрибуты потоков,
их приоритет и т. д.
барьеры потоков
переменные
состояния потоков
мьютексы потоков
блокировки потоков
на чтение-запись
спин-блокировки
потоки
данные,
специфичные для потоков
время процессора
счетчик времени
процессора
счетчик времени
процессора,
принадлежащий потоку"
атрибут копируется
атрибут копируется
значения обнуляются
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут не копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
атрибут копируется
один поток копируется
данные копируются
значение обнуляется
значение обнуляется
значение обнуляется
атрибут закрывается 7.10
атрибут закрывается 7.10
используется тот же атрибут 7.9
используется тот же атрибут 4.3
используется тот же атрибут, 9.1.6
если перехваченный сигнал
не изменяется на значение
по умолчанию (SIGCHLD —
исключение)
атрибут утрачивается; 9-3
все флаги SA.ONSTACK
очищаются
используется тот же атрибут 9-1-5
используется тот же атрибут 9-1-2
используется тот же атрибут 8.1
атрибут утрачивается 517
атрибут утрачивается 5.17.3
атрибут утрачивается 5.17.4
атрибут утрачивается 5.17.3
атрибут утрачивается 5.17.3
атрибут утрачивается 5.17.3
потоки завершаются 5.17
атрибут утрачивается 5-17.1
используется тот же атрибут 1.7.2
используется тот же атрибут 9-7.5
значение обнуляется 5.17.1
Атрибут задается при помощи pthread_getcpuclockid; является частью опции Thread CPU-
Time Clocks.
Приложение А Атрибуты процессов 663
Таблица А.1 (окончание)
Атрибут
Что происходит с атрибутом
при вызове fork
Что происходит с атрибутом Раздел
при вызове exec
сигнал таймера сигнал отменяется,
таймер обнуляется
интервалы таймеров интервалы сбрасываются
таймеры процесса атрибут не копируется
атрибуты возможны разные варианты"
трассировки
используется тот же атрибут 9.7.1
используется тот же атрибут 9.7.4
атрибут удаляется 9.7.6
как правило, используется 1.1.10
тот же атрибут
Происходящее зависит от политики наследования потока (stream) трассировки.
22 Зак. 4030
л Ш ■ ■ ■ ■ Приложение D
Ux: оболочка C++
для стандартных
функций UNIX
В этой книге я часто жаловался на то, что стандартный API UNIX страдает от
несогласованности обработки ошибок, странного именования функций и
избыточности. Как было сказано в разделе 1.4.3, гораздо лучше обрабатывать ошибки с
использованием исключений C++. А если уж я собрался сделать то, что для этого
нужно, я могу переименовать функции и организовать их в классы,
руководствуясь категориями из приложения D.
Созданная мной оболочка C++ называется Ux. При ее разработке я старался
достичь следующих целей:
1. Обработка ошибок должна быть абсолютно унифицированной для всех функций.
2. Заключенные в оболочку функции должны сохранить всю функциональность.
3. Организация функций в классы должна соответствовать архитектуре UNIX.
4. Если есть такая возможность, избыточные, устаревшие или некорректные
функции (например readdir, signal, mktemp) должны быть устранены.
5. По быстродействию оболочка должна минимально отличаться от
естественного интерфейса С.
Вот принципы, которые были соблюдены при разработке Ux для достижения этих
целей:
1. Оболочка соответствует уровню Standard UNIX Specification v3 (не выше) и имеет
идентичную семантику (если не считать обработку ошибок). В этом смысле она
отличается от iostream, библиотеки для работы с потоками Boost' и даже более
' Библиотека C++ для работы с потоками {tvtmv.boost.org).
Приложение Б Ux: оболочка C++ для стандартных функций UNIX 665
сложных пакетов вроде АСЕ.' Тем не менее, в оболочку могут быть включены
дополнительные функции, не определенные в стандарте (например setblcck,
find_and_cpen_master).
2. Полностью реализованы примерно 290 интерфейсов из 1108, определенных в
SUS. Исключены функции стандартного С, функции работы с потоками POSIX,
функции пользовательского уровня, такие как bsearch, а также функции учета,
порождения потоков, трассировки, устаревшие функции и некоторые другие.
3. Информация об ошибках никогда не возвращается — при любой ошибке
генерируется исключение. Генерируемый при исключении объект включает errnc и
другие коды ошибок (например из getaddrinfc).
4. Память динамически не выделяется, если только в имени функции-члена нет
слова «alloc», но таких функций очень мало.
5. По объему и быстродействию код очень близок к естественному С SUS, если не
считать затраты на вызов функций-членов.
6. Объекты соответствуют организации объектов UNIX и SUS (например File, Dir,
DirStream, Process). Иначе говоря, в Ux реализованы абстракции UNIX, а не
какие-то другие.
7. Если определены реентерабельная и нереентерабельная версии функции
(например readdir и readdir_r), обычно используется реентерабельная версия, а буфер
находится в объекте. Если нужно, с целью выделения памяти для объекта
предоставляется функция-член «alloc» (см. пункт 4).
8. Никакая дополнительная обработка ошибок кроме той, что обеспечивают
функции SUS, не выполняется. Например, если в функцию File:: cpen передается
указатель NULL, он передается непосредственно лежащему в ее основе вызову среп.
Это обусловлено пунктом 5.
9. Оболочка автоматически выбирает, например, вызов fchcwn, а не chcwn, если объект
открыт. В обоих случаях используется одна функция-член File::chcwn.
10. В некоторых случаях оболочка автоматически обнаруживает ошибку. Например,
если pathccnf возвращает -1, это является ошибкой только в том случае, если
изменилось значение errnc.
11. Некоторые функции объединены с помощью необязательных аргументов.
В качестве примеров можно привести read/pread, write/pwrite, fsync/fdatasync.
12. Никаких конструкторов копий или операторов присваивания кроме тех, что
используются по умолчанию, нет. Это частично следует из пункта 4.
Вот иерархия классов Ux:
Ux::Base
Ux::Aio
Ux::Clock
Ux::DirStream
Ux::ExitStatus
ADAPTIVE Communication Environment {www.cs.wustl.edu/~schmidt/ACEbttnt).
666 Приложение Б Ux: оболочка C++ для стандартных функций UNIX
Ux:
File
Ux::Dir
Ux::PosixShm
Ux::Socket
Ux::Terminal
Ux::Pty
Ux::Netdb
Ux::PosixMsg
Ux::PosixSem
Ux::Process
Ux::Sigset
Ux::SockAddr
Ux::SockAddrIn
Ux::SockAddrUn
Ux:
Ux:
Ux:
Ux:
SockIPv4
SockIPv6
System
SysVIPC
Ux::SysVMsg
Ux::SysVSem
Ux::SysVShm
Ux::Termios
Ux::TimeMsec
Ux::TimeNsec
Ux::TimeParts
Ux::Timer
Ux::IntervalTimer
Ux::RealtimeTimer
Ux::TimeSec
Ux::Timestr
Ux::TimeString
Ux::Timet
Ux::Timetm
Ux::Timeval
Ux::Error
mq_attr
Ux::semun
Более подробную информацию о Ux, в том числе полную документацию и
исходные файлы, вы можете найти по адресу www.basepath.com/aup. Хотя эта оболочка
кажется перспективной, она использовалась только в тривиальных примерах, так
что жюри, членами которого являетесь и вы, еще не вынесло свое решение. Буду
признателен, если вы сообщите мне свое мнение (aup@basepath.com).
И ■ ■ ■ ■ Приложение В
Jtux: интерфейс
Java/Jython
для стандартных
функций UNIX
Тридцать лет назад, когда UNIX делала первые шаги, почти все приложения UNIX
создавались на С, и С все еще является «языком UNIX» в том смысле, что в
основных стандартах UNIX используется С, ядра UNIX реализованы на С, а в
большинстве книг по программированию для UNIX, в том числе в этой, все примеры
написаны на С. Однако сегодня при программировании для UNIX разработчики очень
часто, если не в большинстве случаев, используют все, что угодно, только не С: C++,
Java, Perl, Python, Jython," PHP и т. д.
C++ — особый случай, потому что вы можете использовать непосредственно
интерфейс С или почти прозрачную библиотеку классов C++, такую как Ux, которая
была описана в приложении Б. Другие языки обычно предоставляют интерфейсы,
позволяющие использовать некоторые классические системные вызовы UNIX,
определенные в POSIX1990, но не более новые вызовы, такие как getaddrinfo или msgsnd.
Из-за этого серьезным студентам, желающим изучить UNIX, приходится осваивать
С или C++, даже если для всего остального они используют Java, а разработчики не
имеют непосредственного доступа к нужным системным вызовам.
Jython — это реализация Python, которая написана на Java и работает в среде Java,
позволяя, таким образом, непосредственно обращаться к любому классу Java. Например, при работе
с Jython вы пишете код пользовательского интерфейса, используя AWT или Swing, а не Tkinter.
Вот почему Jtux работает с Jython так же хорошо, как и с Java. Более подробную
информацию о Jython можно найти по адресу www.jython.org.
668 Приложение В Jtux: интерфейс Java/Jython для стандартных функций UNIX
Чтобы решить эту проблему для Java, я создал интерфейс Java-to-Unix (Jtux).
Разрабатывая Jtux, я преследовал следующие цели:
• создать инструмент обучения программированию для UNIX на Java или Jython;
• предоставить доступ к возможностям UNIX, которые в настоящее время не
поддерживаются пакетами Java;
• обеспечить согласованную обработку ошибок. Что бы ни являлось признаком
ошибки (возврат -1, возврат NULL, возврат кода ошибки и т. д.), ошибка при
выполнении системного вызова всегда приводит к генерированию UErrorExoeption.
Однако имейте в виду следующее.
• Jtux не является объектно-ориентированным вариантом API UNIX.
• В отличие от Ux (см. приложение Б), в Jtux не исправлены никакие
несоответствия в именовании или аргументах системных вызовов UNIX и почти не
снижена степень избыточности. Фактически, почти каждый метод Jtux имеет то же
имя и те же аргументы, что и соответствующий вызов POSIX/SUS, потому что в
противном случае Jtux не помогал бы студентам изучать программирование для
UNIX (несоответствия в обработке ошибок в Jtux исправлены).
• Код Jtux не является более портируемым, чем код POSIX/SUS. Jtux не работает в
средах Java/Jython, хостом которых является система, не соответствующая POSIX/
SUS, такая как Windows.
• Jtux не работает в апплетах, потому что это не реализовано в Java Native Interface
(JNI), который был использован для связи Java с API-интерфейсом С.
Если вам нужен объектно-ориентированный подход к возможностям UNIX,
который более соответствует духу Java, вам лучше использовать другие пакеты Java.
Например, используйте java.net.Socket вместо jtux.UNetwork. Однако Jtux является
гораздо более полным пакетом UNIX для Java или Jython.
Jtux поддерживает следующие 186 системных вызовов:
abort
aooept
aooess
alarm
bind
ohdir
ohmod
ohown
oh root
oiook
oiose
oiosedir
oonneot
с re at
dup
getppid
getrlimit
getrusage
getsid
getsookopt
getuid
htoni
htons
inet.ntop
inet_pton
kill
iohown
link
listen
iookf
poll
pread
pseieot
pthread.signask
putenv
pwrite
read
readdir
readlink
readv
reov
reovfrou
reovusg
renane
rewinddir
setpgid
setriinit
setsid
setsookopt
setuid
shn.open
shn_uniink
shnat
shucti
shmdt
shnget
sigaotion
sigaddset
sigaitstaok
sigdeiset
Приложение В Jtux: интерфейс Java/Jython для стандартных функций UNIX 669
dup2
execvp
_exlt
exit
fchdlr
fchmcd
fchcwn
fcntl
FD_CLR
FD.1SSET
FD.SET
FD.ZERO
fdatasync
fork
freeaddrlnfc
fstat
fstatvfs
fsync
ftck
ftruncate
gal.strerrcr
getaddrlnfc
getcwd
getegld
getenv
geteuld
getgld
gethcstld
gethcstname
getnamelnfc
getpgld
getpld
lseek
lstat
mkdlr
mkflfc
mkncd
mkstemp
mmap
mq.clcse
mq_getattr
niq_nctlfy
nq.cpen
mq_cpen
mq.recelve
mq_send
mq_setattr
mq_tlmed receive
mq_tlmedsend
mq.unllnk
nsgctl
nsgget
msgrcv
nsgsnd
munmap
nanosleep
nice
ntchl
ntchs
open
open
cpendlr
pause
pipe
rradlr
S.1SBLK
S.ISCHR
S.1SDIR
S.1SF1F0
S.1SLNK
S.1SREG
S.ISSOCK
seekdlr
select
sem_clcse
sen.destrcy
sem.getvalue
sen.lnlt
sem_cpen
sem_cpen
sem.pcst
sem.tliaedwalt
sen.trywalt
sen_unllnk
sem_walt
semctl
senget
semcp
send
sendmsg
sendtc
setegld
setenv
seteuld
setgld
slgemptyset
slgflllset
slglnterrupt
slglsnenber
slgpendlng
slgprccmask
slgqueue
slgsuspend
slgtlnedwalt
slgwalt
slgwaltlnfc
sleep
scckatmark
sccket
stat
statvfs
symllnk
sync
system
telldlr
times
truncate
unask
unlink
unsetenv
usleep
utlme
wait
waltpld
write
writev
Это все вызовы POSIX/SUS, работающие с файлами, файловыми системами,
каталогами, сокетами, каналами, процессами, сообщениями/семафорами/общей памятью
System V, сообщениями/семафорами/общей памятью POSIX, сигналами, средой и
не только. Я сделал все, чтобы обеспечить полноту Jtux: например, Jtux
поддерживает не только write, но и pwrite и writev, не только send, но и sendmsg и sendto. Jtux
поддерживает каждый аргумент каждой входящей в его состав функции и каждое
поле каждой структуры, если это технически возможно. Функции ввода/вывода с
использованием терминалов, работы с псевдотерминалами, таймерами интервалов
(кроме alarm) и часами реального времени не поддерживаются, но вы можете
добавить их, если хотите.
670 Приложение В Jtux: интерфейс Java/Jython для стандартных функций UNIX
Использовать системные вызовы UNIX в Jython крайне неудобно. Чтобы показать
вам, на что похож код Jython и Jtux, я включил в книгу программу, в которой два
процесса взаимодействуют с использованием дейтаграмм (см. раздел 8.6):
# Пример Jython, демонстрирующий использование сокетов AF_INET с
# дейтаграммами SOCK_DGRAM.
# Используемые системные вызовы: bind, close, _exit, fork, getaddrinfo,
# recvfrom, sendto, setsockopt, sleep, socket, waitpid
import jtux.UClock as UClock
import jtux.UConstant as UConstant
import jtux.UErrorException as UErrorException
import jtux.UFile as UFile
import jtux.UNetwork as UNetwork
import jtux.UProcess as UProcess
import jtux.UUtil as UUtil
import java.lang
import jarray
def b_to_s(ba): # преобразование массива байтов в строку
s = ""
for b in ba:
if b == 0:
break
s += chr(b)
return s
nodename = "localhost"
servnamel = "5431" # неиспользуемый порт для процесса 1
servname2 = "5432" # неиспользуемый порт для процесса 2
msgsize = 300
hint = UNetwork.s_addrinfo()
hint.ai_family = UConstant.AF_INET
hint.ai_socktype = UConstant.S0CK_DGRAM
infop = UNetwork. AddrlnfoListHeadO
UNetwork.getaddrinfo(nodename, servname2, hint, infop)
sa_peer2 = infop.ai_next.ai_addr; # этот адрес сокета нужен обоим процессам
int_opt = UNetwork.SockOptValue_int() # так в Jtux обрабатывается
# значения setsockopt
int_opt.value = 1
pid = UProcess.fork()
if pid == 0: # процесс 1
msg = jarray.zeros(msgsize, 'b') # jarray - это стандартный модуль Jython
UNetwork.getaddrinfo(nodename, servnamel, hint, infop)
sa_peer1 = infop.ai_next.ai_addr
UClock.sleep(1); # пусть процесс 2 запустится первым - не лучший подход
fd_skt = UNetwork.socket(UConstant.AF_INET, UConstant.S0CK_DGRAM, 0)
UNetwork.setsockopt(fd_skt, UConstant.S0L_S0CKET, UConstant.S0_REUSEADDR,
int_opt, 0)
UNetwork.bind(fd_skt, sa_peer1, 0)
sa_sender = UNetwork.s_sockaddr_in()
Приложение В Jtux: интерфейс Java/Jython для стандартных функций UNIX 671
sa_len = UUtil.IntHolderO; # класс Jtux для передачи типов int no ссылке
maxmsgs = 4
for i in xrange(maxmsgs +1):
if i == maxmsgs:
m = "Stop"
else:
m = "Message #" + str(i)
UNetwork.sendto(fd_skt, m, len(m), 0, sa_peer2, 0)
if i == maxmsgs:
break
nrcv = UNetwork.recvfrom(fd_skt, msg, len(msg), 0, sa_sender, sa_len)
print "Peer 1 got \"" + b_to_s(msg) + "\" from " + str(sa_sender)
UFile.close(fd_skt)
print "Peer 1 exiting"
UProcess._exit(UConstant.EXIT_SUCCESS)
else: # процесс 2
msg = jarray.zeros(msgsize, ' b') # jarray - это стандартный модуль Jython
fd_skt = UNetwork.socket(UConstant.AF_INET, UConstant.SOCK_DGRAM, 0)
UNetwork.setsockopt(fd_skt, UConstant.S0L_S0CKET, UConstant.SO_REUSEADDR,
int_opt, 0)
UNetwork.bind(fd_skt, sa_peer2, 0)
sa_sender = UNetwork.s_sockaddr_in()
sa_len = UUtil.IntHolderO
while 1:
nrcv = UNetwork.recvfrom(fd_skt, msg, len(msg), 0, sa_sender, sa_len)
if b_to_s(msg[:4]) == "Stop":
break
print "Peer 2 got \"" + b_to_s(msg[:nrcv]) + "\" from " + str(sa_sender)
msg[0] = ord('m')
UNetwork.sendto(fd_skt, msg, len(msg), 0, sa_sender, sa_len.value)
UFile.close(fd_skt)
UProcess.waitpid(pid, None, 0)
print "Peer 2 exiting"
А вот пример, написанный на Java:
// Пример на Java, выполняющий рекурсивный спуск по дереву каталогов,
import jtux.*;
class TreeList {
static void list(String entry, int level) {
int dir_fd = -1;
try {
String tabs = "";
for (int i = 0; i < level; i++)
tabs += "\t";
long dir = -1;
try {
dir = UOir.opendir(entry);
}
672 Приложение В Jtux: интерфейс Java/Jython для стандартных функций UNIX
catch (UErrorException e) <
System.out.println(tabs + entry + " - " + e);
return;
}
dir_fd = UFile.open(".", UConstant.O_RDONLY);
UProcess.chdi r(entry);
UDir.s_dirent dirent = new UDir.s_dirent();
UFile.s_stat sbuf = new UFile.s_stat();
while ((dirent = UDir.readdir(dir)) != null) {
UFile.lstat(dirent.d_name, sbuf);
if (UFile.S_ISDIR(sbuf.st_mode) && !dirent.d_name.equals(".") &&
!dirent.d_name.equals("..")) <
System.out.println(tabs + dirent.d_name + ":");
if (UFile.S_ISLNK(sbuf.st_mode)) {
System.out.println(tabs + "[symbolic link - skipping]");
>
else
list(dirent.d_name, level + 1);
>
else
System.out.println(tabs + dirent.d_name);
}
UDir.closedir(dir);
UProcess.fchdir(dir_fd);
UFile.close(dir_fd);
dir_fd = -1;
}
catch (UErrorException e) {
try {
if (dir_fd != -1)
UProcess.fchdir(dir_fd);
>'
catch (Exception edummy) {
>
System.out.println(e);
}
}
public static void main(String args[]) <
list(7", 0);
>
}
Сопряжение API-интерфейса UNIX С с Java связано с некоторыми проблемами:
например, в Java отсутствуют указатели, а константы, зависящие от реализации, такие
как EN0SYS и 0.CREAT, в программах Java невидимы. Чтобы узнать, как были
преодолены эти и другие сложности, как собрать Jtux в единое целое и установить его и,
конечно, чтобы загрузить сам исходный код, посетите сайт wtmv.basepath.com/aup.
1IIIII Приложение Г
Список функций
по алфавиту
и по категориям
В данном приложении приведен список из 307 функций, описанных в этой книге.
Функции перечислены в алфавитном порядке и по категориям с указанием номе-
abort — посылает сигнал SIGABRT (9.1.9)
accept — принимает новое соединение
и создает новый сокет (8.1.2)
access — определяет доступность
файла (3.8.1)
aiocancel — отменяет запрос на
асинхронную операцию ввода-вывода (3-9-5)
aio_error — возвращает отчет о
выполнении асинхронной операции ввода-
вывода (3-9.4)
aio_fsync — инициирует выталкивание
буферов для одного файла (3-9-6)
aioread — выполняет асинхронное
чтение из файла (3-9-3)
aio_return — возвращает статус
асинхронной операции ввода-вывода (3-9.4)
aiosuspend — ожидает завершения
асинхронной операции
ввода-вывода (3.9-7)
ра раздела, в котором они описываются.
Функции в алфавитном
порядке
FD_CLR — очищает дескриптор файла в
наборе fdset (4.2.3)
FD_ISSET — тестирует дескриптор
файла из набора fdset (4.2.3)
FD_SET — задает дескриптор файла в
наборе fd_set (4.2.3)
FD ZERO — очищает весь набор fd_set
(4.2.3)
_Exit — завершает процесс без
обращения к коду сборки мусора (5.7)
_exit — завершает процесс без
обращения к коду сборки мусора (5.7)
_longjmp — выполняет переход в точку,
заданную с помощью _setjmp (9.6)
_setjmp — устанавливает точку
перехода (9-6)
674 Приложение Г Список функций по алфавиту и по категориям
aio_write— выполняет асинхронную
запись в файл (393)
alarm — планирует выдачу сигнала (9-7.1)
asctlme — преобразует время,
разделенное на компоненты, в строчное
местное время (17.1)
atexlt — регистрирует функцию,
вызываемую при завершении работы
процесса (1.3-4)
bind — присваивает адрес сокету (8.1.2)
cfgetispeed — получает скорость ввода
из структуры termios (4.5.3)
cfgetospeed — получает скорость вывода
из структуры termios (4.5.3)
cfsetispeed — задает скорость ввода в
структуре termios (4.5.3)
cfsetospeed — задает скорость вывода в
структуре termios (4.5.3)
chdir — делает заданный каталог
текущим (36.2)
chmod — изменяет режимы доступа по
имени файла (3-7.1)
chown — изменяет владельца и группу
файла по его имени (37.2)
chroot — изменяет корневой каталог (5.14)
clock — возвращает время выполнения
(1.7.2)
clockgetcpuclockid — возвращает
идентификатор часов
процессорного времени (9-7-5)
clock_getres — возвращает точность изт
мерения времени (9.7.5)
clockgettime — возвращает время из
заданных часов (9.7.5)
clock_nanosleep — приостанавливает
работу потока на интервал времени,
измеряемый с точностью до
наносекунд, либо пока не будет доставлен
сигнал (9-7.5)
clock settime — устанавливает часы
(9.7.5)
close — закрывает дескриптор файла
(2.11)
closedlr — закрывает каталог (3.6.1)
confstr — получает конфигурационную
строку (1.5.7)
connect — устанавливает соединение
(8.1.2)
creat — создает новый или очищает
существующий файл и открывает его на
запись (2.4.2)
ctermid — получает путь к
управляющему терминалу (4.7)
ctime — преобразует тип time_t в
строчное местное время (1.7.1)
difftime — вычитает одно значение
time.t из другого (1.7.1)
dup — дублирует дескриптор файла (6.3)
dup2 — дублирует дескриптор файла
(6.3)
endhostent — завершает обзор базы
данных хостов (8.8.1)
endnetent — завершает обзор базы
данных (8.8.2)
endprotoent — завершает обзор базы
данных протоколов (8.8.3)
endservent — завершает обзор базы
данных служб (8.8.4)
execl — запускает программу, входные
аргументы передаются в виде списка
(5.3)
execle — запускает программу,
аргументы передаются в виде списка, также
передается среда окружения (5.3)
execlp — запускает программу,
аргументы передаются в виде списка, поиск
файла ведется с использованием
переменной PATH (5.3)
execv — запускает программу,
аргументы передаются в виде массива (5.3)
execve — запускает программу,
аргументы передаются в виде массива, также
передается среда окружения (53)
Приложение Г Список функций по алфавиту и по категориям 675
execvp — запускает программу,
аргументы передаются в виде массива, поиск
файла ведется с использованием
переменной PATH (5.3)
exit — завершает процесс с
обращением к коду сборки мусора (5.7)
fchdir — делает текущим каталог по
дескриптору (3.6.2)
fchmod — изменяет режимы доступа, по
дескриптору файла (3.7.1)
fchown — изменяет владельца и группу
файла по дескриптору (3.7.2)
fcntl — выполняет управляющие
операции над открытым файлом (383)
fdatasync — принудительно выталкивает
из кэша только данные файла (2.16.2)
fork — создает новый процесс (5.5)
fpathconf — получает информацию о
поддержке опции или предельное
значение, опираясь на дескриптор
файла (1.5.6)
freeaddrinfo — освобождает память,
занимаемую списком структур (8.2.6)
fstat — возвращает сведения о файле по
дескриптору (3-5.1)
fstatvfs — возвращает сведения о
файловой системе по дескриптору
файла (3.2.3)
fsync — планирует или принудительно
выталкивает буферы из кэша для
заданного файла (2.16.2)
ftok — генерирует ключ System V IPC
(7.4.2)
ftruncate — изменяет размер файла,
заданного дескриптором (2.17)
gai_strerror — возвращает строку с
сообщением об ошибке (8.2.6)
getaddrinfo — возвращает информацию
об адресе сокета (8.2.6)
getcwd — возвращает полное имя
текущего каталога (3.4.2)
getdate — преобразует строку во время,
разделенное на компоненты, по
заданным правилам (1.7.1)
getegid — возвращает действующий
идентификатор группы (5.11)
getenv — возвращает значение
переменной окружения (5.2)
geteuid — возвращает действующий
идентификатор пользователя (5.11)
getgid — возвращает реальный
идентификатор группы (5.11)
getgrgid — возвращает запись из файла
со списком групп (3-5.2)
gethostbyaddr — возвращает
информацию о хосте по его IP-адресу (8.8.1)
gethostbyname — возвращает
информацию о хосте по его имени (8.8.1)
gethostent — получить следующую
запись из базы данных хостов (8.8.1)
gethostid — возвращает идентификатор
локального хоста (8.8.1)
gethostname — возвращает сетевое имя
(8.2.7)
getitimer — возвращает значение
таймера (9.7.4)
getlogin — возвращает имя, под которым
пользователь зарегистрировался в
системе (35.2)
getnameinfo — возвращает
информацию о хосте по его IP-адресу (8.8.1)
getnetbyaddr — возвращает
информацию о сети по ее номеру (8.8.2)
getnetbyname — возвращает
информацию о сети по ее имени (8.8.2)
getnetent — получить очередную запись
из базы данных (8.8.2)
getpeername — возвращает адрес сокета
с противоположной стороны
соединения (8.9.2)
getpgid — получает идентификатор
группы процессов (4.3-3)
676 Приложение Г Список функций по алфавиту и по категориям
getpid — возвращает идентификатор
процесса (5.13)
getppid — возвращает идентификатор
родительского процесса (5.13)
getprotobyname — возвращает
сведения о протоколе по его имени (8.8.3)
getprotobynumber — возвращает
сведения о протоколе по его номеру (8.8.3)
getprotoent — получает очередную
запись из базы данных (8.8.3)
getpwuid — возвращает запись из
файла паролей (3.5.2)
getrlimit — возвращает значение
ограничения ресурса (5.16)
getrusage — возвращает
использованный объем ресурсов (5.16)
getservbyname — возвращает сведения
о службе по ее имени (8.8.4)
getservbyport — возвращает сведения о
службе по номеру порта (8.8.4)
getservent — получает очередную
запись из базы данных (8.8.4)
getsid — получает идентификатор сеанса
(4.3.2)
getsockname — возвращает адрес сокета
(8.9.2)
getsockopt — возвращает параметры
сокета (8.3)
gettimeofday — возвращает текущие
дату и время как тип timeval (1.7.1)
getuid—возвращает реальный
идентификатор пользователя (5.11)
gmtime — преобразует тип time_t во
время UTC, разделенное на
компоненты (1.7.1)
grantpt — предоставляет доступ к
подчиненной стороне псевдотерминала
(4.10.1)
htonl — преобразует 32-битное число в
аппаратно-независимое
представление (8.1.4)
htons — преобразует 16-битное число в
аппаратно-независимое
представление (8.1.4)
if_freenameindex — освобождает
память, занимаемую массивом (8.8.5)
If Indextoname — возвращает имя
интерфейса по его порядковому
номеру (8.8.5)
ifnameindex — возвращает сведения
обо всех сетевых интерфейсах и их
порядковых номерах (8.8.5)
Ifnametoindex — возвращает
порядковый номер интерфейса по его имени
(8.8.5)
inetaddr — преобразует строку с
IP-адресом, записанным в точечной
нотации, в целое число (8.2.3)
inetntoa — преобразует целое число,
представляющее адрес IPv4, в строку
(8.2.3)
inet_ntop — преобразует адрес IPv4 или
IPv6 из двоичного представления в
строку (8.9.5)
inet_pton — преобразует адрес IPv4 или
IPv6 из строки в двоичное
представление (8.9.5)
ioctl — позволяет управлять символьным
устройством (4.4)
isatty — проверяет, открыт ли
дескриптор файла для терминала (4.7)
kill — посылает сигнал процессу (9.1.9)
killpg — посылает сигнал группе
процессов (9.1.9)
lchown — изменяет владельца и группу
символической ссылки по ее имени
(3.7.2)
link — создает жесткую ссылку (3-3-1)
lio_listio — выполняет пакетный ввод-
вывод для списка блоков управления
(3.9.9)
Приложение Г Список функций по алфавиту и по категориям 677
listen — подготавливает сокет к приему
запросов на соединение и
устанавливает ограничение на размер очереди
запросов (8.1.2)
localtime — преобразует тип time.t в
местное время, разделенное на
компоненты (1.7.1)
lockf — блокирует доступ к
определенному участку файла (7.11.3)
longjmp — выполняет переход в точку,
заданную с помощью setjmp (9.6)
lseek — устанавливает и возвращает
текущую позицию в файле (2.13)
lstat — возвращает сведения о файле по
его имени, без разыменования
символических ссылок (3-5.1)
mkdir — создает каталог (3.6.3)
mkfifo — создает именованный канал
(7.2.1)
mknod — создает файл (3-8.2)
mkstemp — создает и открывает файл
с уникальным именем (2.7)
mktime — преобразует местное время,
разделенное на компоненты, в тип
time.t (1.7.1)
mmap — отображает страницы памяти
в адресное пространство процесса
(7.14.1)
mount — монтирует файловую систему
(не стандартизован) (3-2.4)
mq_close — закрывает очередь
сообщений (7.7.1)
mq getattr — возвращает атрибуты
очереди сообщений (7.7.1)
mq_notify — включает или выключает
режим асинхронных уведомлений
(7.7.1)
mq_open — открывает очередь
сообщений (7.7.1)
mq_receive — извлекает сообщение из
очереди (7.7.1)
mq_send — помещает сообщение в
очередь (7.7.1)
mq_setattr — изменяет атрибуты
очереди сообщений (7.7.1)
mq_timedreceive — извлекает
сообщение из очереди с предельным
временем ожидания (7.7.1)
mq_timedsend — помещает сообщение
в очередь с предельным временем
ожидания (7.7.1)
mq_unlink — удаляет очередь
сообщений (7.7.1)
msgctl — управляет очередью
сообщений (7.5.1)
msgget — возвращает идентификатор
очереди сообщений (7.5.1)
msgrcv — извлекает сообщения из
очереди (7.5.1)
msgsnd — помещает сообщение в
очередь (7.5.1)
munmap — отключает отображения
страниц в память процесса (7.14.1)
nanosleep — приостанавливает работу
потока на интервал времени,
измеряемый с точностью до наносекунды,
либо пока не будет доставлен сигнал
(9-7.3)
nice — изменяет значение параметра
nice (5.15)
ntohl — преобразует 32-битное число из
аппаратно-независимого
представления (8.1.4)
ntohs — преобразует 16-битное число из
аппаратно-независимого
представления (8.1.4)
open — открывает или создает файл (2.4)
opendir — открывает каталог (3-6.1)
pathconf — получает информацию о
поддержке опции или предельное
значение, опираясь на путь (1.5-6)
pause — ожидает доставки сигнала (9-2.1)
678 Приложение Г Список функций по алфавиту и по категориям
pipe — создает канал (6.2.1)
poll — ожидает готовности
ввода-вывода (4.2.4)
posix_openpt — открывает
псевдотерминал (4.10.1)
pread — выполняет чтение из файла,
начиная с заданной позиции (2.14)
pselect — ожидает готовности
ввода-вывода (4.2.3)
pthread_cancel — принудительно
завершает работу потока (5.17.5)
pthread_cleanup_pop — изымает
обработчик принудительного завершения
(5.17.5)
pthread_cleanup_push —
устанавливает обработчик принудительного
завершения (5.17.5)
pthreadcondsignal — сигнализирует
о наступлении состояния (5.17.4)
pthread_cond_wait — ожидает
наступления состояния (5.17.4)
pthreadcreate — создает новый поток
(5.17.1)
pthreadjoin — ожидает завершения
потока (5.17.2)
pthread_kill — посылает сигнал потоку
(9.1.9)
pthread_mutex_lock — захватывает
мьютекс (5.17.3)
pthread_mutex_unlock — отпускает
мьютекс (5.17.3)
pthreadsigmask — изменяет маску
сигналов для потока (91.5)
pthreadtestcancel — проверяет
наличие команды на принудительное
завершение (5.17.5)
ptsname — получает имя подчиненной
стороны псевдотерминала (4.10.1)
putenv — изменяет или добавляет
переменные в среду окружения (5-2)
pwrite — выполняет запись в файл, в
заданную позицию (2.14)
raise — посылает сигнал тому же
потоку, из которого был вызван (9.1.9)
read — выполняет чтение из файлового
дескриптора (2.10)
readdir — читает запись из каталога (3-6.1)
readdir r — читает запись из каталога
(3.6. Г)
readlink — читает содержимое файла
символической ссылки (333)
readv — чтение вразброс (2.15)
recv — принимает данные из сокета
(8.9.1)
recvfrom — принимает сообщение из
сокета (8.6.2)
recvmsg — принимает сообщение из
сокета с помощью структуры msghdr
(8.6.3)
rename — переименовывает файл (33-2)
rewinddir — переход в начало
каталога (3.6.1)
rmdir — удаляет каталог (3-6.3)
seekdir — переход к требуемому
местоположению (3-6.1)
select — ожидает готовности
ввода-вывода (4.2.3)
sem_close — закрывает именованный
семафор (7.10.1)
sem_destroy — разрушает
неименованный семафор (7.10.2)
sem_getvalue — возвращает значение
семафора (7.10.1)
sem_init — инициализирует
неименованный семафор (7.10.2)
sem_open — открывает именованный
семафор (7.10.1)
sem_post — увеличивает значение
семафора (7.10.1)
sem_timedwait — уменьшает значение
семафора (7.10.1)
Приложение Г Список функций по алфавиту и по категориям 679
sem_trywait — уменьшает значение
семафора, если это возможно (7.10.1)
semunlink — удаляет именованный
семафор (7.10.1)
sem_wait — уменьшает значение
семафора (7.10.1)
semctl — управляет набором семафоров
(7.9.1)
semget — возвращает идентификатор
набора семафоров (7.9.1)
semop — управляет набором семафоров
(7.9.2)
send — передает данные в сокет (89.1)
sendmsg — передает сообщение сокету
с помощью структуры msghdr (8.6.3)
sendto — передает сообщение по
заданному адресу (8.6.2)
setegid — устанавливает действующий
идентификатор группы (5.12)
setenv — изменяет или добавляет
переменные в среду окружения (52)
seteuid — устанавливает действующий
идентификатор пользователя (5.12)
setgid — устанавливает реальный
идентификатор группы (5.12)
sethostent — запускает обзор базы
данных хостов (8.8.1)
setitimer — устанавливает значение для
таймера (9.7.4)
setjmp — устанавливает точку перехода
(9.6)
setnetent — запускает обзор базы
данных сетей (8.8.2)
setpgid — задает или создает группу
процессов (4.33)
setprotoent — запускает обзор базы
данных протоколов (8.8.3)
setrlimit — изменяет значение
ограничения ресурса (5.16)
setservent — запускает обзор базы
данных служб (8.8.4)
setsid — создает сеанс и группу
процессов (4.3.2)
setsockopt — устанавливает параметры
сокета (8.3)
setuid — устанавливает реальный
идентификатор пользователя (5.12)
shm_open — открывает объект общей
памяти (7.14.1)
shm_unlink — удаляет объект общей
памяти (7.14.1)
shmat — присоединяет сегмент общей
памяти (7.13.1)
shmctl — управляет сегментом общей
памяти (7.13.1)
shmdt — отсоединяет сегмент общей
памяти (7.13.1)
shmget — возвращает идентификатор
сегмента общей памяти (7.13.1)
shutdown — прекращает выполнение
операций приема/передачи над соке-
том (8.9.4)
sigaction — устанавливает реакцию
приложения на сигнал (9.1.6)
sigaddset — добавляет сигнал в набор
(9.1-5)
sigaltstack — устанавливает и возвращает
контекст альтернативного стека (93)
sigdelset — удаляет сигнал из набора
(9-1.5)
sigemptyset — инициализирует пустой
набор сигналов (9.1.5)
sigfillset — инициализирует полный
набор сигналов (9.1.5)
sighold — блокирует сигнал (94)
sigignore — устанавливает реакцию на
сигнал SIG_IGN (9.4)
siginterrupt — устанавливает или
сбрасывает флаг SA.RESTART (9-3)
sigismember — проверяет наличие
сигнала в наборе (915)
680 Приложение Г Список функций по алфавиту и по категориям
siglongjmp — выполняет переход в
точку, заданную с помощью sigsetjmp и
восстанавливает маску сигналов (9.6)
signal — устанавливает реакцию
приложения на сигнал (9.4)
sigpause — изменяет маску и ожидает
доставки сигнала (9.4)
sigpending — возвращает набор
сигналов, ожидающих обработки (93)
sigprocmask — изменяет маску сигналов
для потока (только если процесс
имеет единственный поток) (9-1.5)
sigqueue — посылает сигнал процессу
(9.5.4)
sigrelse — снимает блокировку с
сигнала (9.4)
sigset — устанавливает реакцию
приложения на сигнал (9.4)
sigsetjmp — устанавливает точку
перехода, с сохранением маски сигналов
(9.6)
sigsuspend — изменяет маску сигналов
и ожидает доставки сигнала (9-2.3)
sigtimedwait — ожидает появления
сигнала (9-5.5)
sigwait — ожидает доставки сигнала
(9.2.2)
sigwaitinfo — ожидает появления
сигнала (9.5.5)
sleep — приостанавливает работу
потока до истечения заданного времени
или пока он не будет прерван
сигналом (9.7.2)
sockatmark — проверяет наличие
метки первоочередных данных (8.7)
socket — создает объект для
организации сетевых взаимодействий (8.1.2)
socketpair — создает пару сокетов (8.9.3)
stat — возвращает сведения о файле по
его имени (3-5.1)
statvfs — возвращает сведения о
файловой системе по имени каталога (3-2.3)
strftime — преобразует время,
разделенное на компоненты, в
отформатированную строку (1.7.1)
strptime — преобразует строку во
время, разделенное на компоненты, с
использованием формата (1.7.1)
symlink — создает символическую
ссылку (3-3-3)
sync — планирует выталкивание
буферов из кэша (2.16.2)
sysconf — получает информацию о
поддержке опции или предельное
значение (1.5.5)
system — запускает команду (5.5)
tcdrain — ожидает вывод на терминал
(4.6)
tcflow — приостанавливает или
возобновляет терминальный ввод или
вывод (4.6)
tcflush — отбрасывает ввод с
терминала, вывод на терминал или и то и
другое (4.6)
tcgetattr — получает атрибуты
терминала (4.5.1)
tcgetpgrp — получает идентификатор
группы процессов переднего плана
(4.3.4)
tcgetsid — получает идентификатор
сеанса (4.3.4)
tcsendbreak — посылает терминалу
команду перерыва (4.6)
tcsetattr — задает атрибуты терминала
(4.5.1)
tcsetpgrp — задает идентификатор
группы процессов переднего плана (4.3-4)
telldir — получает текущую позицию в
каталоге (3-6.1)
time — возвращает текущие дату и
время как тип time_t (1.7.1)
Приложение Г Список функций по алфавиту и по категориям 681
timer_create — создает таймер внутри
процесса (9-7.6)
timer deiete — удаляет таймер процесса
(9.7.6)
timer_getoverrun — возвращает
количество срабатываний таймера (9.7.6)
timer_gettime — возвращает значение
таймера процесса (9-7.6)
timer_settime — устанавливает
значение для таймера процесса (9-7.6)
times — возвращает информацию о
времени выполнения процесса и
дочернего процесса (1.7.2)
truncate — изменяет размер файла,
заданного по имени (2.17)
ttyname — определяет путь к
терминалу (4.7)
ttynamejr — определяет путь к
терминалу (4.7)
tzset — задает информацию о часовом
поясе (1.7.1)
uiimit — возвращает и изменяет
значения ограничений (5.16)
umask — устанавливает и возвращает
маску прав доступа для вновь
создаваемых файлов (2.5)
umount — отмонтирует файловую
систему (не стандартизован) (3.2.4)
uname — возвращает сведения о
системе (8.8.1)
uniink — удаляет запись в каталоге (2.6)
uniockpt — разблокирует
псевдотерминал (4.10.1)
unsetenv — удаляет переменную среды
окружения (5.2)
usieep — приостанавливает работу
потока на интервал времени,
измеряемый с точностью до микросекунды,
либо пока не будет доставлен сигнал
(9.7.3)
utime — устанавливает время
последнего обращения и изменения файла
(3.7.3)
vfork — создает новый процесс с
разделением памяти (устарел) (5.5)
wait — ожидает завершения дочернего
процесса (5-8)
waitid — ожидает изменения состояния
дочернего процесса [Х/Ореп] (5.8)
waitpid — ожидает изменения состояния
дочернего процесса (5.8)
wcsftime — преобразует время,
разделенное на компоненты, в
отформатированную строку широких символов
(1.7.1)
write — выполняет запись в файловый
дескриптор (2.9)
writev — запись со слиянием (2.15)
Функции по категориям
Конфигурация
confstr — получает конфигурационную
строку (1.5-7)
fpathconf — получает информацию о
поддержке опции или предельное
значение, опираясь на дескриптор
файла (1.5.6)
gethostid — возвращает идентификатор
локального хоста (8.8.1)
gethostname — возвращает сетевое имя
(8.2.7)
pathconf — получает информацию о
поддержке опции или предельное
значение, опираясь на путь (1.5.6)
sysconf — получает информацию о
поддержке опции или предельное
значение (1.5.5)
uname — возвращает сведения о
системе (8.8.1)
682 Приложение Г Список функций по алфавиту и по категориям
Каталоги
closedir — закрывает каталог (3.6.1)
opendir — открывает каталог (3-6.1)
readdir — читает запись из каталога (3.6.1)
readdir_r — читает запись из каталога
(3.6.1)
rewinddir — переход в начало
каталога (3.6.1)
seekdir — переход к требуемому
местоположению (3.6.1)
telldir — получает текущую позицию в
каталоге (3-6.1)
Управление каталогами
link — создает жесткую ссылку (3-3.1)
mkdir — создает каталог (3-6.3)
readlink — читает содержимое файла
символической ссылки (3-3-3)
rename — переименовывает файл (3-3-2)
rmdir — удаляет каталог (3-6.3)
symlink — создает символическую
ссылку (3.3.3)
unlink — удаляет запись в каталоге (2.6)
Файловые атрибуты
access — определяет доступность
файла (3.8.1)
chmod — изменяет режимы доступа по
имени файла (3.7.1)
chown — изменяет владельца и группу
файла по его имени (37.2)
fchmod — изменяет режимы доступа по
дескриптору файла (3-7.1)
fchown — изменяет владельца и группу
файла по дескриптору (3-7.2)
fstat — возвращает сведения о файле по
дескриптору (3-5.1)
lchown — изменяет владельца и группу
символической ссылки по ее имени
(3.7.2)
lstat — возвращает сведения о файле по
его имени, без разыменования
символических ссылок (3-5.1)
stat — возвращает сведения о файле по
его имени (3.5.1)
utime — устанавливает время
последнего обращения и изменения файла
(3-7.3)
Файловый ввод-вывод
close — закрывает дескриптор файла (2.11)
creat — создает новый или очищает
существующий файл и открывает его на
запись (2.4.2)
dup — дублирует дескриптор файла (6.3)
dup2 — дублирует дескриптор файла
(6.3)
fcntl — выполняет управляющие
операции над открытым файлом (3-8.3)
fdatasync — принудительно выталкивает
из кэша только данные файла (2.16.2)
fsync — планирует или принудительно
выталкивает буферы из кэша для
заданного файла (2.16.2)
ftruncate — изменяет размер файла,
заданного дескриптором (2.17)
lseek — устанавливает и возвращает
текущую позицию в файле (2.13)
mkstemp — создает и открывает файл
с уникальным именем (2.7)
open — открывает или создает файл (2.4)
pipe — создает канал (6.2.1)
poll — ожидает готовности
ввода-вывода (4.2.4)
pread — выполняет чтение из файла,
начиная с заданной позиции (2.14)
pselect — ожидает готовности
ввода-вывода (4.2.3)
pwrite — выполняет запись в файл, в
заданную позицию (2.14)
Приложение Г Список функций по алфавиту и по категориям 683
read — выполняет чтение из файлового
дескриптора (2.10)
readv — чтение вразброс (2.15)
select — ожидает готовности
ввода-вывода (4.2.3)
sync — планирует выталкивание
буферов из кэша (2.16.2)
truncate — изменяет размер файла,
заданного по имени (2.17)
write — выполняет запись в файловый
дескриптор (2.9)
writev — запись со слиянием (2.15)
Асинхронный файловый
ввод-вывод
aiocancel — отменяет запрос на
асинхронную операцию ввода-вывода
(3.9.5)
aio_error — возвращает отчет о
выполнении асинхронной операции ввода-
вывода (3-9-4)
aio_fsync — инициирует выталкивание
буферов для одного файла (3-9-6)
aio_read — выполняет асинхронное
чтение из файла (3-9-3)
aio_return — возвращает статус
асинхронной операции ввода-вывода (394)
aio_suspend — ожидает завершения
асинхронной операции
ввода-вывода (3-9-7)
aio_write— выполняет асинхронную
запись в файл (3-9-3)
lio_listio — выполняет пакетный ввод-
вывод для списка блоков управления
(3.9-9)
Управление файлами
lockf — блокирует доступ к
определенному участку файла (7.11.3)
mkfifo — создает именованный канал
(7.2.1)
mknod — создает файл (3-8.2)
Файловая система
fstatvfs — возвращает сведения о
файловой системе по дескриптору
файла (3-2.3)
mount — монтирует файловую систему
(не стандартизован) (3.2.4)
statvfs — возвращает сведения о
файловой системе по имени каталога (3-2.3)
umount — отмонтирует файловую
систему (не стандартизован) (3.2.4)
Файловые дескрипторы
FD_CLR — очищает дескриптор файла в
наборе fd.set (4.2.3)
FD_ISSET — тестирует дескриптор
файла из набора fd_set (4.2.3)
FD_SET — задает дескриптор файла в
наборе fd_set (4.2.3)
FD ZERO — очищает весь набор fd_set
"(4.2.3)
IPC — очереди сообщений
POSIX
mq_close — закрывает очередь
сообщений (7.7.1)
mq getattr — возвращает атрибуты
очереди сообщений (7.7.1)
mq_notify — включает или выключает
режим асинхронных уведомлений
(7.7.1)
mq_open — открывает очередь
сообщений (7.71)
mq_receive — извлекает сообщение из
очереди (7.7.1)
mq_send — помещает сообщение в
очередь (7.7.1)
mq_setattr — изменяет атрибуты
очереди сообщений (7.7.1)
684 Приложение Г Список функций по алфавиту и по категориям
mq_timedreceive — извлекает
сообщение из очереди с предельным
временем ожидания (7.7.1)
mq_timedsend — помещает сообщение
в очередь с предельным временем
ожидания (7.7.1)
mq_unlink — удаляет очередь
сообщений (7.7.1)
IPC — семафоры POSIX
sem_close — закрывает именованный
семафор (7.10.1)
semdestroy — разрушает
неименованный семафор (7.10.2)
sem_getvalue — возвращает значение
семафора (7.10.1)
sem_init — инициализирует
неименованный семафор (7.10.2)
sem_open — открывает именованный
семафор (7.10.1)
sem_post — увеличивает значение
семафора (7.10.1)
sem_timedwait — уменьшает значение
семафора (7.10.1)
sem_trywait — уменьшает значение
семафора, если это возможно (7.10.1)
sem_unlink — удаляет именованный
семафор (7.10.1)
sem_wait — уменьшает значение
семафора (7.10.1)
IPC — общая память POSIX
mmap — отображает страницы памяти
в адресное пространство процесса
(7.14.1)
munmap — отключает отображения
страниц в память процесса (7.14.1)
shm_open — открывает объект общей
памяти (7.14.1)
shm_unlink — удаляет объект общей
памяти (7.14.1)
IPC — очереди сообщений
System V
ftok — генерирует ключ System V IPC
(7.4.2)
msgctl — управляет очередью
сообщений (7.5.1)
msgget — возвращает идентификатор
очереди сообщений (7.5.1)
msgrcv — извлекает сообщения из
очереди (7.5.1)
msgsnd — помещает сообщение в
очередь (7.5.1)
IPC — семафоры System V
semctl — управляет набором семафоров
(7.9.1)
semget — возвращает идентификатор
набора семафоров (7.9-1)
semop — управляет набором семафоров
(7.9-2)
IPC — общая память System V
shmat — присоединяет сегмент общей
памяти (7.131)
shmctl — управляет сегментом общей
памяти (7.131)
shmdt — отсоединяет сегмент общей
памяти (7.131)
shmget — возвращает идентификатор
сегмента общей памяти (7.13-1)
Сетевая БД
endhostent — завершает обзор базы
данных хостов (8.8.1)
endnetent — завершает обзор базы
данных (8.8.2)
endprotoent — завершает обзор базы
данных протоколов (8.8.3)
endservent — завершает обзор базы
данных служб (8.8.4)
Приложение Г Список функций по алфавиту и по категориям 685
gethostent — получает следующую
запись из базы данных хостов (8.8.1)
getnetbyaddr — возвращает
информацию о сети по ее номеру (8.8.2)
getnetbyname — возвращает
информацию о сети по ее имени (8.8.2)
getnetent — получает очередную запись
из базы данных (8.8.2)
getprotobyname — возвращает
сведения о протоколе по его имени (8.8.3)
getprotobynumber — возвращает
сведения о протоколе по его номеру (8.8.3)
getprotoent — получает очередную
запись из базы данных (8.8.3)
getservbyname — возвращает сведения
о службе по ее имени (8.8.4)
getservbyport — возвращает сведения о
службе по номеру порта (8.8.4)
getservent — получает очередную
запись из базы данных (8.8.4)
htonl — 32-битное число в аппаратно-
независимое представление (8.1.4)
htons — преобразует 16-битное число в
аппаратно-независимое
представление (8.1.4)
iffreenameindex — освобождает
память, занимаемую массивом (8.8.5)
ifindextoname — возвращает имя
интерфейса по его порядковому
номеру (8.8.5)
ifnameindex — возвращает сведения
обо всех сетевых интерфейсах и их
порядковых номерах (8.8.5)
if_nametoindex — возвращает
порядковый номер интерфейса по его имени
(8.8.5)
ntohl — преобразует 32-битное число из
аппаратно-независимого
представления (8.1.4)
ntohs — преобразует 16-битное число из
аппаратно-независимого
представления (8.1.4)
sethostent — запустить обзор базы
данных хостов (8.8.1)
setnetent — запускает обзор базы
данных сетей (8.8.2)
setprotoent — запускает обзор базы
данных протоколов (8.8.3)
setservent — запускает обзор базы
данных служб (8.8.4)
Атрибуты процессов
chdir — делает заданный каталог
текущим (3.6.2)
chroot — изменяет корневой каталог
(5-14)
fchdir — делает текущим каталог по
дескриптору (3-6.2)
getcwd — возвращает полное имя
текущего каталога (34.2)
getrusage — возвращает
использованный объем ресурсов (5.16)
nice — изменяет значение параметра
nice (5.15)
Управляющая логика
процессов
_longjmp — выполняет переход в точку,
заданную с помощью _setjmp (9.6)
_setjmp — устанавливает точку
перехода (9.6)
longjmp — выполняет переход в точку,
заданную с помощью setjmp (9.6)
pause — ожидает доставки сигнала (9-2.1)
setjmp — устанавливает точку перехода
(9-6)
siglongjmp — выполняет переход в
точку, заданную с помощью sigsetjmp и
восстанавливает маску сигналов (9.6)
sigsetjmp — устанавливает точку
перехода, с сохранением маски сигналов
(9-6)
686 Приложение Г Список функций по алфавиту и по категориям
Среда процессов
getenv — возвращает значение
переменной окружения (5.2)
putenv — изменяет или добавляет
переменные в среду окружения (5.2)
setenv — изменяет или добавляет
переменные в среду окружения (5.2)
unsetenv — удаляет переменную среды
окружения (5.2)
Ограничения процессов
getrlimit — возвращает значение
ограничения ресурса (5.16)
setrlimit — изменяет значение
ограничения ресурса (5-16)
ulimit — возвращает и изменяет
значения ограничений (516)
Управление процессами
_Exit — завершает процесс без
обращения к коду сборки мусора (5.7)
_exit — завершает процесс без
обращения к коду сборки мусора (5.7)
abort — посылает сигнал SIGABRT (9.1.9)
atexit — регистрирует функцию,
вызываемую при завершении работы
процесса (1.3-4)
execl — запускает программу, входные
аргументы передаются в виде списка
(5.3)
execle — запускает программу,
аргументы передаются в виде списка, также
передается среда окружения (5.3)
execlp — запускает программу,
аргументы передаются в виде списка, поиск
файла ведется с использованием
переменной PATH (5.3)
execv — запускает программу,
аргументы передаются в виде массива (5.3)
execve — запускает программу,
аргументы передаются в виде массива, так же
передается среда окружения (5.3)
execvp — запускает программу,
аргументы передаются в виде массива, поиск
файла ведется с использованием
переменной PATH (5.3)
exit — завершает процесс с
обращением к коду сборки мусора (5.7)
fork — создает новый процесс (5-5)
system — запускает команду (5.5)
vfork — создает новый процесс с
разделением памяти (устарел) (5.5)
wait — ожидает завершения дочернего
процесса (5.8)
waitid — ожидает изменения состояния
дочернего процесса [Х/Ореп] (5.8)
waitpid — ожидает изменения состояния
дочернего процесса (58)
Разрешения процессов
getegid — возвращает действующий
идентификатор группы (5.11)
geteuid — возвращает действующий
идентификатор пользователя (5.11)
getgid — возвращает реальный
идентификатор группы (511)
getuid—возвращает реальный
идентификатор пользователя (5.11)
setegid — устанавливает действующий
идентификатор группы (5.12)
seteuid — устанавливает действующий
идентификатор пользователя (512)
setgid — устанавливает реальный
идентификатор группы (5.12)
setuid — устанавливает реальный
идентификатор пользователя (512)
umask — устанавливает и возвращает
маску прав доступа для вновь
создаваемых файлов (2.5)
Приложение Г Список функций по алфавиту и по категориям 687
Ресурсы процессов
getpgid — получает идентификатор
группы процессов (433)
getpid — возвращает идентификатор
процесса (5.13)
getppid — возвращает идентификатор
родительского процесса (5.13)
getsid — получает идентификатор сеанса
(4-3.2)
setpgid — задает или создает группу
процессов (4.33)
setsid — создает сеанс и группу
процессов (4.3.2)
Сигналы
kill — посылает сигнал процессу (9.1.9)
killpg — посылает сигнал группе
процессов (9.1.9)
raise — посылает сигнал тому же
потоку, из которого был вызван (9.19)
sigaction — устанавливает реакцию
приложения на сигнал (9.1.6)
sigaltstack — устанавливает и
возвращает контекст альтернативного стека
(9.3)
sighold — блокирует сигнал (9.4)
sigignore — устанавливает реакцию на
сигнал SIG.IGN (9.4)
siginterrupt — устанавливает или
сбрасывает флаг SA.RESTART (9-3)
sigismember — проверяет наличие
сигнала в наборе (9.1-5)
signal — устанавливает реакцию
приложения на сигнал (9.4)
sigpause — изменяет маску и ожидает
доставки сигнала (9.4)
sigpending — возвращает набор
сигналов, ожидающих обработки (9-3)
sigqueue — посылает сигнал процессу
(9.54)
sigrelse — снимает блокировку с
сигнала (9.4)
sigset — устанавливает реакцию
приложения на сигнал (9.4)
sigsuspend — изменяет маску сигналов
и ожидает доставки сигнала (9-2.3)
sigtimedwait — ожидает появления
сигнала (9.5.5)
sigwait — ожидает доставки сигнала (9-2.2)
sigwaitinfo — ожидает появления
сигнала (9.5.5)
Маски сигналов
sigaddset — добавляет сигнал в набор
(9.1.5)
sigdelset — удаляет сигнал из набора
(9.1.5)
sigemptyset — инициализирует пустой
набор сигналов (9.1-5)
sigflllset — инициализирует полный
набор сигналов (9.1.5)
sigprocmask — изменяет маску сигналов
для потока (только если процесс
имеет единственный поток) (9-1.5)
Сокеты
accept — принимает новое соединение
и создает новый сокет (8.1.2)
bind — присваивает адрес сокету (8.1.2)
connect — устанавливает соединение
(8.1.2)
getpeername — возвращает адрес сокета
с противоположной стороны
соединения (8.9.2)
getsockname — возвращает адрес сокета
(8.9.2)
getsockopt — возвращает параметры
сокета (8.3)
listen — подготавливает сокет к приему
запросов на соединение и устанавли-
688 Приложение Г Список функций по алфавиту и по категориям
вает ограничение на размер очереди
запросов (8.1.2)
recv — принимает данные из сокета
(8.9.1)
recvfrom — принимает сообщение из
сокета (8.6.2)
recvmsg — принимает сообщение из
сокета с помощью структуры msghdr
(8.6.3)
send — передает данные в сокет (8.9.1)
sendmsg — передает сообщение сокету
с помощью структуры msghdr (8.6.3)
sendto — передает сообщение по
заданному адресу (8.6.2)
setsockopt — устанавливает параметры
сокета (8.3)
shutdown — прекращает выполнение
операций приема/передачи над
шкетом (8.9.4)
sockatmark — проверяет наличие
метки первоочередных данных (8.7)
socket — создает объект для
организации сетевых взаимодействий (8.1.2)
socketpair — создает пару сокетов (8.9.3)
Адресация сокетов
freeaddrinfo — освобождает память,
занимаемую списком структур (8.2.6)
gai_strerror — возвращает строку с
сообщением об ошибке (8.2.6)
getaddrinfo — возвращает информацию
об адресе сокета (8.2.6)
gethostbyaddr — возвращает
информацию о хосте по его IP-адресу (8.8.1)
gethostbyname — возвращает
информацию о хосте по его имени (8.8.1)
getnameinfo — возвращает
информацию о хосте по его IP-адресу (8.8.1)
inet_addr — преобразует строку с
IP-адресом, записанным в точечной
нотации, в целое число (8.2.3)
inet_ntoa — преобразует целое число,
представляющее адрес IPv4, в строку
(8.2.3)
inet_ntop — преобразует адрес IPv4 или
IPv6 из двоичного представления в
строку (8.9.5)
inet_pton — преобразует адрес IPv4 или
IPv6 из строки в двоичное
представление (8.9.5)
Терминал
cfgetispeed — получает скорость ввода
из структуры termios (4-5.3)
cfgetospeed — получает скорость вывода
из структуры termios (4.53)
cfsetispeed — задает скорость ввода в
структуре termios (4.5.3)
cfsetospeed — задает скорость вывода в
структуре termios (4.5.3)
ctermid — получает путь к
управляющему терминалу (4.7)
ioctl — позволяет управлять символьным
устройством (4.4)
isatty — проверяет, открыт ли
дескриптор файла для терминала (4.7)
tcdrain — ожидает вывод на терминал
(4-6)
tcflow — приостанавливает или
возобновляет терминальный ввод или
вывод (4.6)
tcflush — отбрасывает ввод с
терминала, вывод на терминал или и то и
другое (4.6)
tcgetattr — получает атрибуты
терминала (4.5.1)
tcgetpgrp — получает идентификатор
группы процессов переднего плана
(4-3.4)
tcgetsid — получает идентификатор
сеанса (4.3-4)
Приложение Г Список функций по алфавиту и по категориям 689
tcsendbreak — посылает терминалу
команду перерыва (4.6)
tcsetattr — задает атрибуты терминала
(4.5.1)
tcsetpgrp — задает идентификатор
группы процессов переднего плана (4.3.4)
ttyname — определяет путь к
терминалу (47)
ttyname_r — определяет путь к
терминалу (4.7)
Терминал (Pty)
grantpt — предоставляет доступ к
подчиненной стороне псевдотерминала
(4.10.1)
posix_openpt — открывает
псевдотерминал (4.10.1)
ptsname — получает имя подчиненной
стороны псевдотерминала (4.10.1)
unlockpt — разблокирует
псевдотерминал (4.10.1)
Потоки
pthread_cancel — принудительно
завершает работу потока (5.17.5)
pthread_cleanup_pop — изымает
обработчик принудительного завершения
(5.17.5)
pthread_cleanup_push —
устанавливает обработчик принудительного
завершения (5.17.5)
pthreadcondsignal — сигнализирует
о наступлении состояния (5174)
pthread_cond_wait — ожидает
наступления состояния(517.4)
pthread_create — создает новый поток
(5.17.1)
pthread join — ожидает завершения
потока (5.17.2)
pthread kill — посылает сигнал поток)'
(9.1.9)
pthreadmutexlock — захватывает
мьютекс (5.17.3)
pthreadmutexunlock — отпускает
мьютекс (5.17.3)
pthreadsigmask — изменяет маску
сигналов для потока (915)
pthread_testcancel — проверяет
наличие команды на принудительное
завершение (5.17.5)
Время
asctime — преобразует время,
разделенное на компоненты, в строчное
местное время (17.1)
clock — возвращает время выполнения
(1.7.2)
clockgetcpuclockid — возвращает
идентификатор часов
процессорного времени (9-7.5)
clock_getres — возвращает точность
измерения времени (975)
clock_gettime — возвращает время из
заданных часов (9-7.5)
clock_nanosleep — приостанавливает
работу потока на интервал времени,
измеряемый с точностью до
наносекунды, либо пока не будет доставлен
сигнал (9-7.5)
clock settime — устанавливает часы
(9.7.5)
ctime — преобразует тип time_t в
строчное местное время (1.7.1)
difftime — вычитает одно значение
time.t из другого (1.7.1)
getdate — преобразует строку во время,
разделенное на компоненты, по
заданным правилам (1.7.1)
gettimeofday — возвращает текущие
дату и время как тип timeval (1.7.1)
gmtime — преобразует тип time_t во
время UTC, разделенное на компоненты
(1.7.1)
690 Приложение Г Список функций по алфавиту и по категориям
localtime — преобразует тип time.t в
местное время, разделенное на
компоненты (1.7.1)
mktime — преобразует местное время,
разделенное на компоненты, в тип
time_t (1.7.1)
nanosleep — приостанавливает работу
потока на интервал времени,
измеряемый с точностью до наносекунды,
либо пока не будет доставлен сигнал
(9.7.3)
sleep — приостанавливает работу
потока до истечения заданного времени
или пока он не будет прерван
сигналом (9.7.2)
strftime — преобразует время,
разделенное на компоненты, в
отформатированную строку (1.7.1)
strptime — преобразует строку во
время, разделенное на компоненты, с
использованием формата (1.7.1)
time — возвращает текущие дату и
время как тип time_t (1.7.1)
times — возвращает информацию о
времени выполнения процесса и
дочернего процесса (1.7.2)
tzset — задает информацию о часовом
поясе (1.7.1)
usleep — приостанавливает работу
потока на интервал времени,
измеряемый с точностью до микросекунды,
либо пока не будет доставлен сигнал
(9.7.3)
wcsftime — преобразует время,
разделенное на компоненты, в
отформатированную строку широких символов
(1.7.1)
Таймер
alarm — планирует выдачу сигнала
(9.7.1)
getitimer — возвращает значение
таймера (9.7.4)
setitimer — устанавливает значение для
таймера (97.4)
timer_create — создает таймер внутри
процесса (9.7.6)
timer delete — удаляет таймер процесса
(9.7.6)
timer_getoverrun — возвращает
количество срабатываний таймера (9-7.6)
timer_gettime — возвращает значение
таймера процесса (9.7.6)
timer_settime — устанавливает
значение для таймера процесса (9-7.6)
Информация о пользователе
getgrgid — возвращает запись из файла
со списком групп (35.2)
getlogin — возвращает имя, под которым
пользователь зарегистрировался в
системе (3.5.2)
getpwuid — возвращает запись из
файла паролей (35.2)
ш ■ ■ ■ ■ исылки
[Abr 1996] Abrahams, Paul W and Bruce R. Larson, UNIX for the Impatient, 2nd Ed., Addison-Wesley Longman,
1996.
[AUP2003] Rochkind, Marc, Advanced UNIX Programming Web Site, www.basepath.com/aup
[Вас198б] Bach, Maurice J., The Design of the UNIX Operating System , Prentice Hall, 1986.
[Bov2001] Bovet, Daniel P. and Marco Cesati, Understanding the Linux Kernel, O'Reilly & Associates, 2001.
[Butl997] Butenhof, David R., Programming with POS1X Threads, Addison-Wesley Longman, 1997.
[Dre2003] Drepper, Ulrich, "POSIX Option Groups," http://people.redhat.com/~drepper/posix-
optiongroups.html
[Har2002] Harbison III, Samuel P. and Guy L. Steele Jr., C: A Reference Manual, 5,h Ed, Prentice Hall, 2002.
[Keg2003] Kegel, Dan, «The C10K Problem», www.kegel.com/clOk.html
[Kerl984] Kernighan, Brian W and Rob Pike, The UNIX Programming Environment, Prentice Hall Computer
Books, 1984.
[Mau2001] Mauro, Jim and Richard McDougall, Solaris Internals, Prentice Hall PTR, 2001.
[McK1996] McKusick, Marshall Kirk, Keith Bostic, Michael J. Karels, and John S. Quarterman, The Design
and Implementation of the 4.4BSD Operating System, Addison-Wesley, 1996.
[Norl997] Norton, Scott J. and Mark D. DiPasquale, Thread Time: The Multithreaded Programming Guide,
Prentice-Hall PTR, 1997.
[RFC] IETF RFC Page, www.ietf.org/rfc
[RFC854] Postel, J. and J. Reynolds, «Telnet Protocol Specification», www.ietf.org/rfc/rfc0854.txtFnum ber=854
[RFC1288] Zimmerman, D., «The Finger User Information Protocol», www.ietf.org/rfc/rfcl288.txt7num
ber=1288
[Stel999] Stevens, Richard P, UNIX Network Programming, Vol. 2, 2nd Ed., Prentice Hall PTR, 1999-
[Ste2003] Stevens, Richard P., Bill Fenner, and Andrew M. Rudoff, UNIX Network Programming, Vol. 1,
3rd Ed, Addison-Wesley, 2003.
[Str2000] Stroustrup, Bjarne, The C++ Programming Language (Special 3rd Edition), Addison-Wesley, 2000.
[SUS2002] The Open Group, «The Single UNIX Specification, Version 3», www.unix.org/version3
Другие рекомендации
Bell Labs, «The Creation of the UNIX' Operating System», www.bell-labs.com/history/unix/.
Интересный сайт, спонсируемый Bell Labs.
Gallmeister, Bill O, POS1X.4: Programming for the Real World, O'Reilly & Associates, 1995.
Единственная книга по расширениям реального времени POS1X, написанная вице-президентом группы,
разрабатывавшей этот стандарт.
Garfinkel, Simson, Daniel Weise, and Steven Strassmann, The UNIX-Haters Handbook, IDG Books
Worldwide, 1994. Фанаты UNIX ненавидят эту книгу, но их критицизм неоправдан. Электронную
версию можно найти на http://web.mit.edu/~slmsong/www/ugh.pdf.
Google Groups, www.google.com/grphp. Здесь вы найдете форумы, например comp.unix.programmer.
Libes, Don and Sandy Ressler, Life With JJNrX^ Prentice Hall, 1989- История UNIX и другая интересная
информация. /
Nemeth, Evi, Garth Snyder, Scott Seebass, and Trent R. Hein, UNIX System Administration Handbook,
3rd Ed, Prentice Hall PTR, 2001. Лучшая книга по администрированию UNIX. В ней
рассказывается об особенностях Solaris, HP-UX, Red Hat Linux и FreeBSD, но по большей части говорится
о UNIX в целом.
Salus, Peter H, A Quarter Century of UNIX, Addison-Wesley, 1994. Исключительно полная история UNIX.
Taylor, Christopher С, "Unix Is a Four Letter Word," http://unix.t-a-y-l-o-r.com. Интерактивное
знакомство с UNIX и vi.
Unix Heritage Society, www.tuhs.org. Отличная подборка ссылок по историческим материалам UNIX.