КулЛиб электронная библиотека
Всего книг - 591335 томов
Объем библиотеки - 896 Гб.
Всего авторов - 235367
Пользователей - 108115

Впечатления

Serg55 про Берг: Танкистка (Попаданцы)

похоже на Поселягина произведение, почитаем продолжение про 14 год, когда автор напишет. А так, фантази оно и есть фантази...

Рейтинг: 0 ( 0 за, 0 против).
Влад и мир про Михайлов: Трещина (Альтернативная история)

Я такие доклады не читаю.

Рейтинг: 0 ( 0 за, 0 против).
Stribog73 про Гиндикин: Рассказы о физиках и математиках (Физика)

Не ставьте галочку "Добавить в список OCR" если есть слой. Галочка означает "Требуется OCR".

Рейтинг: 0 ( 0 за, 0 против).
lopotun про Гиндикин: Рассказы о физиках и математиках (Физика)

Благодаря советам и помощи Stribog73 заменил кривой OCR-слой в книге на правильный. За это ему огромное спасибо.

Рейтинг: +1 ( 1 за, 0 против).
kiyanyn про Ананишнов: Ходоки во времени. Освоение времени. Книга 1 (Научная Фантастика)

Научная фантастика, как написано в аннотации?

Скорее фэнтези с битвами на мечах во времени :) Научностью здесь и не пахнет...

Рейтинг: 0 ( 0 за, 0 против).
kiyanyn про Никитин: Происхождение жизни. От туманности до клетки (Химия)

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

Лично меня книга заставила скатиться вниз по кривой Даннинга-Крюгера, так что теперь я лучше понимаю не то, как работает биология клетки, а психологию креационистов :)

Рейтинг: 0 ( 0 за, 0 против).
kiyanyn про Лонэ: Большой роман о математике. История мира через призму математики (Математика)

После перлов типа

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

читать уже и не хочется. "Составные числа" назвать "сложными цифрами"... Или

"Когда Тарталья передал свой метод решения уравнений третьей степени Кардано, тот опубликовал его на итальянском и

подробнее ...

Рейтинг: +1 ( 1 за, 0 против).

Системное программирование в UNIX средствами Free Pascal [А. Полищук] (doc) читать онлайн

-  Системное программирование в UNIX средствами Free Pascal  716 Кб (скачать doc) (скачать doc+fbd)  (читать)  (читать постранично) - А. П. Полищук - С. А. Семериков

Книга в формате doc! Изображения и текст могут не отображаться!


Настройки текста:



А.П. Полищук, С.А. Семериков

Системное программирование в UNIX
средствами Free Pascal

Содержание

Предисловие 8
О книге 8
Назначение этой книги 8
Спецификация Х/Open 9
Структура книги 10
Что вы должны знать 10
Соглашения 12
Глава 1. Основные понятия и терминология 13
1.1. Файл 13
1.1.1. Каталоги и пути 13
1.1.2. Владелец файла и права доступа 14
1.1.3. Обобщение концепции файла 14
1.2. Процесс 15
1.2.1. Межпроцессное взаимодействие 15
1.3. Системные вызовы и библиотечные подпрограммы 15
Глава 2. Файл 17
2.1. Примитивы доступа к файлам в системе UNIX 17
2.1.1. Введение 17
2.1.2. Системный вызов fdopen 18
2.1.3. Создание файла при помощи вызова fdopen 20
2.1.4. Системный вызов fdcreat 22
2.1.5. Системный вызов fdclose 22
2.1.6. Системный вызов fdread 23
2.1.7. Системный вызов fdwrite 26
2.1.8. Пример copyfile 27
2.1.9. Эффективность вызовов fdread и fdwrite 29
2.1.10. Вызов fdseek и произвольный доступ 30
2.1.11. Пример: гостиница 31
2.1.12. Дописывание данных в конец файла 33
2.1.13. Удаление файла 34
2.1.14. Системный вызов fcntl 34
2.2. Стандартный ввод, стандартный вывод и стандартный вывод диагностики 35
2.2.1. Основные понятия 35
2.2.2. Программа io 36
2.2.3. Использование стандартного вывода диагностики 38
2.3. Стандартная библиотека ввода/вывода: взгляд в будущее 38
2.4. Системные вызовы и переменная linuxerror 41
2.4.7. Подпрограмма perror 41
Глава 3. Работа с файлами 43
3.1. Файлы в многопользовательской среде 43
3.1.1. Пользователи и права доступа 43
3.1.2. Права доступа и режимы файлов 44
3.1.3. Дополнительные права доступа для исполняемых файлов 45
3.1.4. Маска создания файла и системный вызов umask 46
3.1.5. Вызов fdopen и права доступа к файлу 48
3.1.6. Определение доступности файла при помощи вызова access 48
3.1.7. Изменение прав доступа при помощи вызова chmod 49
3.1.8. Изменение владельца при помощи вызова chown 50
3.2. Файлы с несколькими именами 50
3.2.1. Системный вызов link 51
3.2.2. Системный вызов unlink 51
3.2.3. Системный вызов frename 52
3.2.4. Символьные ссылки 52
3.2.5. Еще об именах файлов 54
3.3. Получение информации о файле: вызов fstat 54
3.3.1. Подробнее о вызове chmod 60
Глава 4. Каталоги, файловые системы и специальные файлы 62
4.1. Введение 62
4.2. Каталоги с точки зрения пользователя 62
4.3. Реализация каталогов 64
4.3.1. Снова о системных вызовах link и unlink 65
4.3.2. Точка и двойная точка 65
4.3.3. Права доступа к каталогам 66
4.4. Использование каталогов при программировании 67
4.4.1. Создание и удаление каталогов 67
4.4.2. Открытие и закрытие каталогов 68
4.4.3. Чтение каталогов: вызовы readdir и rewinddir 69
4.4.4. Текущий рабочий каталог 72
4.4.5. Смена рабочего каталога при помощи вызова chdir 72
4.4.6. Определение имени текущего рабочего каталога 73
4.4.7. Обход дерева каталогов 74
4.5. Файловые системы UNIX 78
4.5.1. Кэширование: вызовы sync и fsync 80
4.6. Имена устройств UNIX 81
4.6.1. Файлы блочных и символьных устройств 82
4.6.2. Структура tstat 82
4.6.3. Информация о файловой системе 83
4.6.4. Ограничения файловой системы: процедуры pathconf и fpathconf 84
Глава 5. Процесс 86
5.1. Понятие процесса 86
5.2. Создание процессов 87
5.2.1. Системный вызов fork 87
5.3. Запуск новых программ при помощи вызова ехес 89
5.3.1. Семейство вызовов ехес 89
5.3.2. Доступ к аргументам, передаваемым при вызове exec 92
5.4. Совместное использование вызовов ехес и fork 93
5.5. Наследование данных и дескрипторы файлов 96
5.5.1. Вызов fork, файлы и данные 96
5.5.2. Вызов ехес и открытые файлы 97
5.6. Завершение процессов при помощи системного вызова halt 98
5.7. Синхронизация процессов 98
5.7.1. Системный вызов wait 98
5.7.2. Ожидание завершения определенного потомка: вызов waitpid 100
5.8. Зомби-процессы и преждевременное завершение программы 102
5.9. Командный интерпретатор smallsh 102
5.10. Атрибуты процесса 108
5.10.1. Идентификатор процесса 108
5.10.2. Группы процессов и идентификаторы группы процессов 110
5.10.3. Изменение группы процесса 110
5.10.4. Сеансы и идентификатор сеанса 110
5.10.5. Переменные программного окружения 111
5.10.6. Текущий рабочий каталог 113
5.10.7. Текущий корневой каталог 113
5.10.8. Идентификаторы пользователя и группы 113
5.10.9. Ограничения на размер файла: вызов ulimit 114
5.10.10. Приоритеты процессов 115
Глава 6. Сигналы и их обработка 117
6.1. Введение 117
6.1.1. Имена сигналов 118
6.1.2. Нормальное и аварийное завершение 121
6.2. Обработка сигналов 122
6.2.1. Наборы сигналов 123
6.2.2. Задание обработчика сигналов: вызов sigaction 124
6.2.3. Сигналы и системные вызовы 128
6.2.4. Процедуры sigsetjmp и siglongjmp 129
6.3. Блокирование сигналов 130
6.4. Посылка сигналов 131
6.4.1. Посылка сигналов другим процессам: вызов kill 131
6.4.2. Посылка сигналов самому процессу: вызовы sigraise и alarm 134
6.4.3. Системный вызов pause 136
6.4.4. Системные вызовы sigpending и sigsuspend 138
Глава 7. Межпроцессное взаимодействие при помощи программных каналов 139
7.1. Каналы 139
7.1.1. Каналы на уровне команд 139
7.1.2. Использование каналов в программе 140
7.1.3. Размер канала 144
7.1.4. Закрытие каналов 146
7.1.5. Запись и чтение без блокирования 146
7.1.6. Использование системного вызова select для работы с несколькими каналами 149
7.1.7. Каналы и системный вызов ехес 154
7.2. Именованные каналы, или FIFO 157
7.2.1. Программирование при помощи каналов FIFO 159
Глава 8. Дополнительные методы межпроцессного взаимодействия 163
8.1. Введение 163
8.2. Блокировка записей 163
8.2.1. Мотивация 163
8.2.2. Блокировка записей при помощи вызова fcntl 164
8.3. Дополнительные средства межпроцессного взаимодействия 171
8.3.1. Введение и основные понятия 171
8.3.2. Очереди сообщений 173
8.3.3. Семафоры 183
8.3.4. Разделяемая память 189
8.3.5. Команды ipcs и ipcrm 194
Глава 9. Терминал 196
9.1. Введение 196
9.2. Терминал UNIX 198
9.2.1. Управляющий терминал 198
9.2.2. Передача данных 199
9.2.3. Эхо-отображение вводимых символов и опережающий ввод с клавиатуры 199
9.2.4. Канонический режим, редактирование строки и специальные символы 200
9.3. Взгляд с точки зрения программы 202
9.3.1. Системный вызов fdopen 202
9.3.2. Системный вызов fdread 203
9.3.3. Системный вызов fdwrite 205
9.3.4. Функции ttyname и isatty 205
9.3.5. Изменение свойств терминала: структура termios 205
9.3.6. Параметры MIN и TIME 212
9.3.7. Другие системные вызовы для работы с терминалом 213
9.3.8. Сигнал разрыва соединения 214
9.4. Псевдотерминалы 215
9.5. Пример управления терминалом: программа tscript 218
Глава 10.Сокеты 224
10.1. Введение 224
10.2. Типы соединения 224
10.3. Адресация 225
10.3.1. Адресация Internet 225
10.3.2. Порты 225
10.4. Интерфейс сокетов 226
10.4.1. Создание сокета 226
10.5. Программирование в режиме TCP-соединения 227
10.5.1. Связывание 228
10.5.2. Включение приема TCP-соединений 228
10.5.3. Прием запроса на установку TCP-соединения 228
10.5.4. Подключение клиента 230
10.5.5. Пересылка данных 231
10.5.6. Закрытие TCP-соединения 233
10.6. Программирование в режиме пересылок UDP-дейтаграмм 235
10.6.1. Прием и передача UDP-сообщений 236
10.7. Различия между двумя моделями 238
Глава 11. Стандартная библиотека ввода/вывода 239
11.1. Введение 239
11.2. Структура TFILE 239
11.3. Открытие и закрытие потоков: процедуры fopen и fclose 240
11.4. Посимвольный ввод/вывод: процедуры getc и putc 242
11.5. Возврат символов в поток: процедура ungetc 243
11.6. Стандартный ввод, стандартный вывод и стандартный вывод диагностики 245
11.7. Стандартные процедуры опроса состояния 246
11.8. Построчный ввод и вывод 247
11.9. Ввод и вывод бинарных данных: процедуры fread и fwrite 249
11.10. Произвольный доступ к файлу: процедуры fseek, rewind и ftell 252
11.11. Форматированный вывод: семейство процедур printf 252
11.12. Форматированный ввод: семейство процедур scanf 257
11.13. Запуск программ при помощи библиотек стандартного ввода/вывода 260
11.14. Вспомогательные процедуры 265
11.14.1. Процедуры freopen и fdopen 265
11.14.2. Управление буфером: процедуры setbuf и setvbuf 265
Глава 12. Разные дополнительные системные вызовы и библиотечные процедуры 267
12.1. Введение 267
12.2. Управление динамическим распределением памяти 267
12.3. Ввод/вывод с отображением в память и работа с памятью 272
12.4. Время 276
12.5. Работа со строками и символами 278
12.5.1. Семейство процедур strings 278
12.5.2. Преобразование строк в числовые значения 279
12.5.3. Проверка и преобразование символов 280
12.6. Дополнительные средства 281
12.6.1. Дополнение о сокетах 282
12.6.2. Потоки управления 282
12.6.3. Расширения режима реального времени 284
12.6.4. Получение параметров локальной системы 284
12.6.5. Интернационализация 285
12.6.6. Математические функции 286
12.6.7. Работа с портами ввода вывода 286
Глава 13. Задачи с решениями 287
13.1. Введение 287
13.2. Обработка текста 287
13.3. Бинарные файлы 305
13.4. Каталоги 309
13.5. Файловые системы 327
13.6. Файловая система proc 329
13.7. Управление файлами 333
13.8. Управление процессами 342
13.9. Программные каналы 351
13.10. Управление терминалом 355
13.11. Дата и время 355
13.12. Генератор лексических анализаторов lex 362
Приложение 1. Коды ошибок переменной linuxerror и связанные с ними сообщения 368
Введение 368
Список кодов и сообщений об ошибках 368
Приложение 2. История UNIX 375
Основные стандарты 375
Приложение 3. Модуль stdio 377
Приложение 4. Замечания о компиляции во Free Pascal 2.0 395
Литература 418

Предисловие
О книге
В основу данной книги положено второе издание руководства программиста UNIX System Programming: A programmer’s guide to software development by Keith Haviland, Dina Gray, Ben Salama. Очень удачное по структуре и подбору примеров, это руководство является одним из лучших учебников по системному программированию в UNIX, поэтому с самого начала мы посчитали уместным сохранить их, исправив и дополнив в соответствии с новыми возможностями Linux/BSD и компилятора Free Pascal.
На первом этапе нашей работы был создан модуль stdio, необходимый для совместимости со стандартной библиотекой языка Си. В модуль вошли множество структур данных, процедур и функций, не входящих в библиотечные модули Free Pascal, но существенно облегчающие жизнь программиста.
На втором этапе примеры из книги Кейт Хэвиленд, Даны Грей и Бена Саламы были переведены с Си на Паскаль. Это потребовало модификации значительной части текста книги, посвященной описанию используемых библиотечных функций и системных вызовов.
Наконец, книга была дополнена описанием структур данных, процедур и функций библиотечных модулей linux, ipc и sockets, специфичных для ОС Linux/BSD.
В результате проделанной работы была получена данная книга, в которой сохранилось часть исходного текста из книги Кейт Хэвиленд, Даны Грей и Бена Саламы. Разумеется, при необходимости эти фрагменты могут быть заменены на другие, но результатом этого будет всего лишь изложение известной справочной информации иными словами.
Назначение этой книги
Со времени своего появления в Bell Laboratories в 1969 г. операционная система UNIX становилась все более популярной, вначале получив признание в академическом мире, а затем уже в качестве стандартной операционной системы для нового поколения многопользовательских микро- и миникомпьютеров в 80-х годах. И этот рост, по-видимому, продолжается в момент написания данной книги.
Операционная система UNIX оправдала возлагавшиеся на нее надежды и теперь является ключевой деталью технологического пейзажа на рубеже XXI века. Не говоря уже о том, что UNIX всегда занимала сильные позиции в научном и техническом сообществах, в настоящее время существует множество крупномасштабных систем управления данными и обработки транзакций на платформе UNIX. Но, самое главное, ОС UNIX, безусловно, является ядром серверов магистральной сети Internet.
Основное внимание в книге уделяется программному интерфейсу между ядром UNIX (частью UNIX, которая делает ее операционной системой) и прикладным программным обеспечением, которое выполняется в среде UNIX. Этот интерфейс часто называется интерфейсом системных вызовов UNIX (хотя разница между такими вызовами и обычными библиотечными процедурами теперь уже не столь очевидна, как это было раньше). В дальнейшем мы увидим, что системные вызовы – это примитивы, на которых в конечном счете построены все программы UNIX – и поставляемые вместе с операционной системой, и разрабатываемые независимо. Целевая аудитория книги состоит из программистов, уже знакомых с UNIX, которые собираются разрабатывать программное обеспечение для ОС UNIX на языке Pascal. Эта книга в равной мере подойдет разработчикам системного программного обеспечения и прикладных или деловых приложений – фактически всем, кто серьезно интересуется разработкой программ для ОС UNIX.
Кроме системных вызовов мы также рассмотрим некоторые из наиболее важных библиотек подпрограмм, которые поставляются с системой UNIX. Эти библиотеки, конечно же, написаны с использованием системных вызовов и во многих случаях делают то же самое, но на более высоком уровне, или более удобны для использования программистами. Надеемся, что при исследовании как системных вызовов, так и библиотеки подпрограмм вы получите представление о том, когда можно пользоваться существующими достижениями, не «изобретая велосипед», а также лучше поймете внутреннее устройство этой все еще прекрасной операционной системы.
Спецификация Х/Open
ОС UNIX имеет долгую историю, и существовало множество ее официальных и фактических стандартов, а также коммерческих и учебных вариантов. Тем не менее ядро системы UNIX, находящееся в центре внимания в данной книге, остается необычайно стабильным.
Мы положили в основу текста и примеров документы (все датированные 1994 годом) System Interface Definitions (Описания системного интерфейса) и System Interfaces and Headers (Системные интерфейсы и заголовки) из 4-й версии второго выпуска Х/Open, а также часть связанного с ними документа Networking Services (Сетевые сервисы). Для удобства мы будем применять сокращение XSI – от Х/Open System Interfaces (Системные интерфейсы Х/Open). При необходимости особенности различных реализации системы будут обсуждаться отдельно.
Здесь нужно сделать небольшое отступление. Консорциум Х/Open первоначально объединил производителей аппаратного обеспечения, серьезно заинтересованных в открытых операционных системах и платформе UNIX, но со временем число его членов возросло. Одна из главных задач консорциума состояла 3 в выработке практического стандарта UNIX, и руководство по обеспечению мобильности программ, называемое XPG, послужило базовым документом для проекта нескольких основных производителей (включая Sun, IBM, Hewlett Packard, Novell и Open Software Foundation), обычно называемого Spec 1170 Initiative (1170 – это число охватываемых этим документом вызовов, заголовков, команд и утилит). Целью этого проекта было создание единой унифицированной спецификации системных сервисов UNIX, включая системные вызовы, которые являются основой этого документа. В результате получился удобный набор спецификации, объединивший многие конфликтующие направления в стандартизации UNIX, главную часть которых составляют вышеупомянутые документы. Другие представленные разработки охватывали основные команды UNIX и обработку вывода на экран.
С точки зрения системного программирования, документы XSI формируют практическую базу, и множество примеров из книги будет выполняться на большинстве существующих платформ UNIX. Стандарт Х/Open объединяет ряд соответствующих и дополняющих друг друга стандартов с их практической реализацией. Он объединил в себе ANSI/ISO стандарт языка С, важный базовый стандарт POSIX (IEEE 1003.1-1990) и стандарт SVID, а также позаимствовал элементы спецификаций Open Software Foundation (Организации открытого программного обеспечения) и некоторые известные функции из системы Berkeley UNIX, оказавшей большое влияние на развитие UNIX систем в целом.
Конечно, стандартизация продолжилась и в дальнейшем. В 1996 г. в результате слияния Х/Open и OSF (Open Software Foundation) образовалась Группа открытых стандартов (The Open Group). Последние разработки (на момент написания книги) из стандарта POSIX, с учетом опыта практической реализации, Группа открытых стандартов называет второй версией Single UNIX Specification (Единой спецификации UNIX, далее по тексту – SUSV2), которая, в свою очередь, содержит пятый выпуск System Interface Definitions, System Interfaces and Headers и Networking Services. Эти важные, хотя и специализированные, дополнения охватывают такие области, как потоки, расширения реального времени и динамическая компоновка.
В заключение заметим, что:
• все стандарты являются очень обширными, охватывая альтернативные методы реализации и редко используемые, но все же важные функциональные возможности. Основное внимание в этой книге уделяется основам программирования в среде UNIX, а не полному описанию системы в соответствии с базовыми стандартами;
• при необходимости создания программы, строго следующей стандартам, необходимо предусмотреть в ней установку (и проверку) соответствующих флагов, таких как _XOPEN_SOURCE или _POSIX_SOURCE.
Структура книги
Книга состоит из тринадцати глав.
Глава 1 представляет собой обзор основных понятий и терминологии. Два наиболее важных из обсуждаемых терминов – это файл (file) и процесс (process). Мы надеемся, что большинство читателей книги уже хотя бы частично знакомы с приведенным в главе материалом (см. в следующем разделе предпосылки для изучения книги).
В главе 2 описаны системные примитивы доступа к файлам, включая открытие и создание файлов, чтение и запись данных в них, а также произвольный доступ к содержимому файлов. Представлены также способы обработки ошибок, которые могут генерироваться системными вызовами.
В главе 3 изучается контекст работы с файлами. В ней рассмотрены вопросы владения файлами, управления системными привилегиями в системе UNIX и оперирования атрибутами файлов при помощи системных вызовов.
Глава 4 посвящена концепции дерева каталогов (directories) с точки зрения программиста. В нее также включено краткое обсуждение файловых систем (file systems) и специальных файлов (special files), используемых для представления устройств.
Глава 5 посвящена природе процессов UNIX и методам работы с ними. В ней представляются и подробно объясняются системные вызовы fork и ехес. Приводится пример простого командного интерпретатора (command processor).
Глава 6 – первая из трех глав, посвященных межпроцессному взаимодействию. Она охватывает сигналы (signals) и обработку сигналов (signal handling) и весьма полезна для перехвата ошибок и обработки аномальных ситуаций.
В главе 7 рассматривается наиболее полезный метод межпроцессного взаимодействия в системе UNIX – программные каналы, или конвейеры (pipes), позволяющие передавать выход одной программы на вход другой. Будет исследовано создание каналов, чтение и запись с их помощью, а также выбор из множества каналов.
Глава 8 посвящена методам межпроцессного взаимодействия, которые были впервые введены в ОС System V. В ней описаны блокировка записей (record locking), передача сообщений (message passing), семафоры (semaphores) и разделяемая память (shared memory).
В главе 9 рассматривается работа терминала на уровне системных вызовов. Представлен пример использования псевдотерминалов (pseudo terminals).
В главе 10 дается краткое описание сетевой организации UNIX и рассматриваются сокеты (sockets), которые могут использоваться для пересылки данных между компьютерами.
В главе 11 мы отходим от системных вызовов и начинаем рассмотрение основных библиотек. В этой главе приведено систематическое изложение стандартной библиотеки ввода/вывода (Standard I/O Library), содержащей намного больше средств для работы с файлами, чем системные примитивы, представленные в главе 2.
Глава 12 дает обзор дополнительных системных вызовов и библиотечных процедур, многие из которых очень важны при создании реальных программ. Среди обсуждаемых тем – обработка строк, функции работы со временем и функции управления памятью.
И, наконец, глава 13 содержит задачи с решениями, обобщающими весь предыдущий материал.
Что вы должны знать
Эта книга не является учебником по системе UNIX или языку программирования Паскаль, а подробно исследует интерфейс системных вызовов UNIX. Чтобы использовать ее наилучшим образом, необходимо хорошо изучить следующие темы:
• вход в систему UNIX;
• создание файлов при помощи одного из стандартных редакторов системы;
• древовидную структуру каталогов UNIX;
• основные команды работы с файлами и каталогами;
• создание и компиляцию простых программ на языке Паскаль (включая программы, текст которых находится в нескольких файлах);
• процедуры ввода/вывода;
• использование аргументов командной строки;
• применение man-системы (интерактивного справочного руководства системы). К сожалению, сейчас уже нельзя давать общие советы для работы со справочным руководством в различных системах, поскольку в формат руководства, прежде считавшийся стандартным, были внесены изменения несколькими производителями. Традиционно руководство было разбито на восемь разделов, каждый из которых был структурирован по алфавитному принципу. Наиболее важными являются три из них: раздел 1, описывающий команды; раздел 2, в котором представлены системные вызовы, и раздел 3, охватывающий функции стандартных библиотек.
Тем из вас, кто не знаком с какими-либо темами из приведенного списка, следует выполнить приведенные упражнения. Если потребуется дополнительная помощь, вы можете найти подходящее руководство, воспользовавшись библиографией в конце книги.
Наконец, стоит отметить, что для изучения информатики недостаточно простого чтения, поэтому на протяжении всей книги делается акцент на упражнения и примеры. Для выполнения упражнений вы должны иметь доступ к компьютеру с системой UNIX.
Упражнение 1. Объясните назначение следующих команд UNIX:
ls cat rm cp mv mkdir fpc
Упражнение 2. Создайте небольшой текстовый файл в вашем любимом текстовом редакторе. Создайте другой файл, содержащий пятикратно повторенный первый файл при помощи команды cat.
Подсчитайте число слов и символов в обоих файлах при помощи команды wc. Объясните полученный результат. Создайте подкаталог и поместите в него оба файла.
Упражнение 3. Создайте файл, содержащий список файлов в вашем начальном каталоге и в каталоге /bin.
Упражнение 4. Выведите при помощи одной команды число пользователей, находящихся в данный момент в системе.
Упражнение 5. Напишите, откомпилируйте и запустите на выполнение программу на языке Паскаль, которая выводит какое-либо приветствие.
Упражнение 6. Напишите, откомпилируйте и запустите на выполнение программу на языке Паскаль, которая печатает свои аргументы.
Упражнение 7. Напишите программу, которая подсчитывает и выводит число вводимых слов, строк и символов при помощи функций.
Упражнение 8. Создайте файл, содержащий процедуру на языке Паскаль, которая выводит сообщение 'hello, world'. Создайте отдельный файл основной программы, который вызывает эту процедуру. Откомпилируйте и выполните полученную программу, назвав ее hw.
Упражнение 9. Найдите в руководстве системы разделы, посвященные команде cat, процедуре printf и системному вызову write.
Соглашения
В книге приняты следующие выделения:
• моноширинным шрифтом набраны листинги, параметры командной строки, пути к файлам и значения переменных;
• также моноширинным шрифтом набран вывод на терминал, при этом курсивом выделены символы, вводимые пользователем;
• полужирным шрифтом отмечены названия элементов интерфейса, а также клавиши и их комбинации;
• курсивом выделены слова и утверждения, на которые следует обратить особое внимание, а также точки входа указателя.
Глава 1. Основные понятия и терминология
В этой главе сделан краткий обзор основных идей и терминологии, которые будут использоваться в книге. Начнем с понятия файл (file).
1.1. Файл
В системе UNIX информация находится в файлах. Типичные команды UNIX, работающие с файлами, включают в себя следующие:
$ vi my_test.pas
которая вызовет редактор vi для создания и редактирования файла my_test.pas;
$ cat my_test.pas
которая выведет на терминал содержимое файла my_test.pas;
$ fpc my_test.pas
которая вызовет компилятор языка Паскаль для создания программы my_test из исходного файла my_test.pas, если файл my_test.pas не содержит ошибок.
Большинство файлов будет принадлежать к некоторой логической структуре, заданной пользователем, который их создает. Например, документ может состоять из слов, строк, абзацев и страниц. Тем не менее, с точки зрения системы, все файлы UNIX представляют собой простые неструктурированные последовательности байтов или символов. Предоставляемые системой примитивы позволяют получить доступ к отдельным байтам последовательно или в произвольном порядке. Не существует встроенных в файлы символов конца записи или конца файла, а также различных типов записей, которые нужно было бы согласовывать.
Эта простота является концептуальной для философии UNIX. Файл в системе UNIX является ясным и общим понятием, на основе которого могут быть сконструированы более сложные и специфические структуры (такие как индексная организация файлов). При этом безжалостно устраняются излишние подробности и особые случаи. Например, в обычном текстовом файле символ перехода на следующую строку (обычно символ перевода строки ASCII), определяющий конец строки текста, в системе UNIX представляет собой всего лишь один из символов, который может читаться и записываться системными утилитами и пользовательскими программами. Только программы, предполагающие, что на их вход подается набор строк, должны заботиться о семантике символа перевода строки.
Кроме этого, система UNIX не различает разные типы файлов. Файл может заключать в себе текст (например, файл, содержащий список покупок, или абзац, который вы сейчас читаете) или содержать «двоичные» данные (такие как откомпилированный код программы). В любом случае для оперирования файлом могут использоваться одни и те же примитивы или утилиты. Вследствие этого, в UNIX отсутствуют формальные схемы присваивания имен файлам, которые существуют в других операционных системах (тем не менее некоторые программы, например cc, следуют определенным простым условиям именования файлов). Имена файлов в системе UNIX совершенно произвольны и в системе SVR4 (System V Release 4) могут включать до 255 символов. Тем не менее, для того чтобы быть переносимыми в соответствии со спецификацией XSI, длина имен не должна превышать 14 символов – предела, заложенного в ранних версиях UNIX.
1.1.1. Каталоги и пути
Важное понятие, связанное с файлами, – каталог (directory). Каталоги представляют собой набор файлов, позволяя сформировать некоторую логическую структуру содержащейся в системе информации. Например, каждый пользователь обычно имеет начальный каталог, в котором он работает, а команды, системные библиотеки и программы администрирования обычно расположены в своих определенных каталогах. Кроме файлов, каталоги также могут содержать произвольное число подкаталогов, которые, в свою очередь, также могут содержать подкаталоги, и так далее. Фактически каталоги могут иметь любую глубину вложенности. Таким образом, файлы UNIX организованы в иерархической древовидной структуре, в которой каждый узел, кроме конечных, соответствует каталогу. Вершиной этого дерева является один каталог, которая обычно называется корневым каталогом (root directory).

/

|

usr

keith

ben

|

|

book

file1

file2

book

chap1

chap2

chap1

chap2
Рис. 1.1. Пример дерева каталогов

Подробное рассмотрение структуры каталогов системы UNIX содержится в главе 4. Тем не менее, поскольку в книге постоянно будет идти речь о файлах UNIX, стоит отметить, что полные их имена, которые называются путями (pathnames), отражают эту древовидную структуру. Каждый путь задает последовательность ведущих к файлу каталогов. Например, полное имя
/usr/keith/book/chap1
можно разбить на следующие части: первый символ / означает, что путь начинается с корневого каталога, то есть путь дает абсолютное положение файла (absolute pathname). Затем идет usr – подкаталог корневого каталога. Каталог keith находится еще на один уровень ниже и поэтому является подкаталогом /usr. Аналогично каталог book является подкаталогом /usr/keith. Последняя часть, chap1, может быть и каталогом, и обычным файлом, поскольку каталоги именуются точно так же, как и обычные файлы. На рис. 1.1 показан пример дерева каталогов, содержащих этот путь.
Путь, который не начинается с символа /, называется относительным путем (relative pathname) и задает маршрут к файлу от текущего рабочего каталога (current working directory) пользователя. Например, полное имя
chap1/intro.txt
описывает файл intro.txt, который находится в подкаталоге chap1 текущего каталога. В самом простом случае имя
intro.txt
просто обозначает файл intro.txt в текущем каталоге. Снова заметим: для того чтобы программа была действительно переносимой, каждая из частей полного имени файла должна быть не длиннее 14 символов.
1.1.2. Владелец файла и права доступа
Файл характеризуется не только содержащимися в нем данными: существует еще ряд других простых атрибутов, связанных с каждым файлом UNIX. Например, для каждого файла задан определенный пользователь – владелец файла (file owner). Владелец файла обладает определенными правами, в том числе возможностью изменять права доступа (permissions) к файлу. Как показано в главе 3, права доступа определяют, кто из пользователей может читать или записывать информацию в файл либо запускать его на выполнение, если файл является программой.
1.1.3. Обобщение концепции файла
В UNIX концепция файлов расширена и охватывает не только обычные файлы (regular files), но и периферийные устройства, а также каналы межпроцессного взаимодействия. Это означает, что одни и те же примитивы могут использоваться для записи и чтения из текстовых и двоичных файлов, терминалов, накопителей на магнитной ленте и даже оперативной памяти. Данная схема позволяет рассматривать программы как обобщенные инструменты, способные использовать любые типы устройств. Например,
$ cat file > /dev/rmt0
представляет грубый способ записи файла на магнитную ленту (путь /dev/rmt0 обычно обозначает стример).
1.2. Процесс
В терминологии UNIX термин процесс (process) обычно обозначает экземпляр выполняющейся программы. Простейший способ создания процесса - передать команду для исполнения оболочке (shell), которая также называется командным интерпретатором (command processor). Например, если пользователь напечатает:
$ ls
то процесс оболочки создаст другой процесс, в котором будет выполняться программа ls, выводящая список файлов в каталоге. UNIX – многозадачная система, поэтому несколько процессов могут выполняться одновременно. Фактически для каждого пользователя, работающего в данный момент с системой UNIX, выполняется хотя бы один процесс.
1.2.1. Межпроцессное взаимодействие
Система UNIX позволяет процессам, выполняемым одновременно, взаимодействовать друг с другом, используя ряд методов межпроцессного взаимодействия.
Одним из таких методов является использование программных каналов (pipes). Они обычно связывают выход одной программы с входом другой без необходимости сохранения данных в промежуточном файле. Пользователи опять же могут применять эти средства при помощи командного интерпретатора. Командная строка
$ ls | wc -l
организует конвейер из двух процессов, в одном из которых будет выполняться программа ls, а в другом – одновременно программа подсчета числа слов wc. При этом выход ls будет связан с входом wc. В результате будет выведено число файлов в текущем каталоге.
Другими средствами межпроцессного взаимодействия UNIX являются сигналы (signals), которые обеспечивают модель взаимодействия по принципу программных прерываний. Дополнительные средства предоставляют семафоры (semaphores) и разделяемая память (shared memory). Для обмена между процессами одной системы могут также использоваться сокеты (sockets), используемые обычно для взаимодействия между процессами в сети.
1.3. Системные вызовы и библиотечные подпрограммы
В предисловии уже упомянули, что книга сконцентрирована на интерфейсе системных вызовов (system call interface). Тем не менее понятие системного вызова требует дополнительного определения.
Системные вызовы фактически дают разработчику программного обеспечения доступ к ядру (kernel). Ядро, с которым познакомились в предисловии, – это блок кода, который постоянно находится в памяти и занимается планированием системных процессов UNIX и управлением вводом/выводом. По существу ядро является частью UNIX, которое и делает ее операционной системой. Все управление, выделение ресурсов и контроль над пользовательскими процессами, а также весь доступ к файловой системе осуществляется через ядро.
Системные вызовы осуществляются так же, как и вызовы обычных подпрограмм и функций Паскаля. Например, можно считать данные из файла, используя библиотечную подпрограмму Паскаля read:
read(fileptr, inputbuf);
или при помощи низкоуровневого системного вызова fdread:
nread := fdread(filedes, inputbuf, BUFSIZE);
Основное различие между подпрограммой и системным вызовом состоит в том, что при вызове подпрограммы исполняемый код является частью объектного кода программы, даже если он был скомпонован из библиотеки; при системном вызове основная часть исполняемого кода в действительности является частью ядра, а не вызывающей программы. Другими словами, вызывающая программа напрямую вызывает средства, предоставляемые ядром. Переключение между ядром и пользовательским процессом обычно осуществляется при помощи механизма программных прерываний.
Неудивительно, что большинство системных вызовов выполняет операции либо над файлами, либо над процессами. Фактически системные вызовы составляют основные примитивы, связанные с обоими типами объектов.
При работе с файлами эти операции могут включать передачу данных в файл и из файла, произвольный поиск в файле или изменение связанных с файлом прав доступа.

Код программы

Адресное пространство пользователя (данные и программы пользователя)

Библиотечная процедура read

Пользовательский код для fdread

Адресное пространство ядра (системные ресурсы)

Код ядра для вызова fdread

Рис. 1.2. Связь между кодом программы, библиотечной подпрограммой и системным вызовом

При выполнении действий с процессами системные вызовы могут создавать новые процессы, завершать существующие, получать информацию о состоянии процесса и создавать канал взаимодействия между двумя процессами.
Небольшое число системных вызовов не связано ни с файлами, ни с процессами. Обычно системные процессы, входящие в эту категорию, относятся к управлению системой в целом или запросам информации о ней. Например, один из системных вызовов позволяет запросить у ядра текущее время и дату, а другой позволяет переустановить их.
Кроме интерфейса системных вызовов, системы UNIX также предоставляют обширную библиотеку стандартных процедур, одним из важных примеров которых является Стандартная библиотека ввода/вывода (Standard I/O Library). Подпрограммы этой библиотеки обеспечивают средства преобразования форматов данных и автоматическую буферизацию, которые отсутствуют в системных вызовах доступа к файлам. Хотя процедуры стандартной библиотеки ввода/вывода гарантируют эффективность, они сами, в конечном счете, используют интерфейс системных вызовов. Их можно представить, как дополнительный уровень средств доступа к файлам, основанный на системных примитивах доступа к файлам, а не отдельную подсистему. Таким образом, любой процесс, взаимодействующий со своим окружением, каким бы незначительным не было это взаимодействие, должен использовать системные вызовы.
На рис. 1.2 показана связь между кодом программы и библиотечной процедурой, а также связь между библиотечной процедурой и системным вызовом. Из рисунка видно, что библиотечная процедура read в конечном итоге является интерфейсом к лежащему в его основе системному вызову fdread.
Упражнение 1.1. Объясните значение следующих терминов: ядро, системный вызов, подпрограмма Pascal, процесс, каталог, путь.
Глава 2. Файл
2.1. Примитивы доступа к файлам в системе UNIX
2.1.1. Введение
В этой главе будут рассмотрены основные примитивы для работы с файлами, предоставляемые системой UNIX. Эти примитивы состоят из небольшого набора системных вызовов, которые обеспечивают прямой доступ к средствам ввода/вывода, обеспечиваемым ядром UNIX. Они образуют строительные блоки для всего ввода/вывода в системе UNIX, и многие другие механизмы доступа к файлам в конечном счете основаны на них. Названия этих примитивов приведены в табл. 2.1. Дублирование функций, выполняемых различными вызовами, соответствует эволюции UNIX в течение последнего десятилетия.
Типичная программа UNIX вызывает для инициализации файла вызов fdopen (или fdcreat), а затем использует вызовы fdread, fdwrite или fdseek для работы с данными в файле. Если файл больше не нужен программе, она может вызвать fdclose, показывая, что работа с файлом завершена. Наконец, если пользователю больше не нужен файл, его можно удалить из системы при помощи вызова unlink.
Следующая программа, читающая начальный фрагмент некоторого файла, более ясно демонстрирует эту общую схему. Так как это всего лишь вступительный пример, мы опустили некоторые необходимые детали, в частности обработку ошибок. Заметим, что такая практика совершенно недопустима в реальных программах.
Таблица 2.1. Примитивы UNIX
Имя
Функция
fdopen
Открывает файл для чтения или записи либо создает пустой файл
fdcreat
Создает пустой файл
fdclose
Закрывает открытый файл
fdread
Считывает информацию из файла
fdwrite
Записывает информацию в файл
fdseek
Перемещается в заданную позицию в файле
unlink
Удаляет файл
fcntl
Управляет связанными с файлом атрибутами
(* элементарный пример *)

uses linux;

var
fd:integer;
nread:longint;
buf:array [0..1024-1] of char;
begin
(* Открыть файл 'data' для чтения *)
fd := fdopen ('data', Open_RDONLY);
(* Прочитать данные *)
nread := fdread (fd, buf, 1024);
(* Закрыть файл *)
fdclose (fd);
end.

Первый системный вызов программа примера делает в строке
fd := fdopen ('data', Open_RDONLY);
Это вызов функции fdopen, он открывает файл data в текущем каталоге. Второй аргумент функции, Open_RDONLY, является целочисленной константой, определенной в модуле linux. Это значение указывает на то, что файл должен быть открыт в режиме только для чтения (read only). Другими словами, программа сможет только читать содержимое файла и не изменит файл, записав в него какие-либо данные.
Результат вызова fdopen крайне важен, в данном примере он помещается в переменную fd. Если вызов fdopen был успешным, то переменная fd будет содержать так называемый дескриптор файла (file descriptor) – неотрицательное целое число, значение которого определяется системой. Оно определяет открытый файл при передаче его в качестве параметра другим примитивам доступа к файлам, таким как fdread, fdwrite, fdseek и fdclose. Если вызов fdopen завершается неудачей, то он возвращает значение -1 (большинство системных вызовов возвращает это значение в случае ошибки). В реальной программе нужно выполнять проверку возвращаемого значения и в случае возникновения ошибки предпринимать соответствующие действия.
После открытия файла программа делает системный вызов fdread:
nread := fdread (fd, buf, 1024);
Этот вызов требует считать 1024 символа из файла с идентификатором fd, если это возможно, и поместить их с символьный массив buf. Возвращаемое значение nread дает число считанных символов, которое в нормальной ситуации должно быть равно 1024, но может быть и меньше, если длина файла оказалась меньше 1024 байт. Так же, как и fdopen, вызов fdread возвращает в случае ошибки значение -1.
Переменная nread имеет тип longint, определенный в модуле linux.
Этот оператор демонстрирует еще один важный момент: примитивы доступа к файлам имеют дело с простыми линейными последовательностями символов или байтов. Вызов fdread, например, не будет выполнять никаких полезных преобразований типа перевода символьного представления целого числа в форму, используемую для внутреннего представления целых чисел. Не нужно путать системные вызовы fdread и fdwrite с операторами более высокого уровня в таких языках, как Fortran или Pascal. Системный вызов fdread типичен для философии, лежащей в основе интерфейса системных вызовов: он выполняет одну простую функцию и представляет собой строительный блок, с помощью которого могут быть реализованы другие возможности.
В конце примера файл закрывается:
fdclose(fd);
Этот вызов сообщает системе, что программа закончила работу с файлом, связанным с идентификатором fd. Легко увидеть, что вызов fdclose противоположен вызову fdopen. В действительности, так как программа на этом завершает работу, вызов fdclose не является необходимым, поскольку все открытые файлы автоматически закрываются при завершении процесса. Тем не менее обязательное использование вызова fdclose считается хорошей практикой.
Этот короткий пример должен дать представление о примитивах UNIX для доступа к файлам. Теперь каждый из этих примитивов будет рассмотрен более подробно.
2.1.2. Системный вызов fdopen
Для выполнения операций записи или чтения данных в существующем файле его следует открыть при помощи системного вызова fdopen. Ниже приведено описание этого вызова. Для ясности и согласованности с документацией системы все описания системных вызовов будут использовать структуру прототипов функций ANSI. В них также будут приводиться заголовочные модули, в которых декларируются прототипы и определяются все важные постоянные:
Описание
uses linux;

Function fdOpen(PathName:String;flags:longint):longint;
Function fdOpen(PathName:Pchar;flags:longint):longint;
Function fdOpen(PathName:String;flags,mode:longint):longint;
Function fdOpen(PathName:Pchar;flags,mode:longint):longint;
Первый аргумент, pathname, является указателем на строку маршрутного имени открываемого файла. Значение pathname может быть абсолютным путем, например:
/usr/keith/junk
Данный путь задает положение файла по отношению к корневому каталогу. Аргумент pathname может также быть относительным путем, задающим маршрут от текущего каталога к файлу, например:
keith/junk
или просто:
junk
В последнем случае программа откроет файл junk в текущем каталоге. В общем случае, если один из аргументов системного вызова или библиотечной процедуры – имя файла, то в качестве него можно задать любое допустимое маршрутное имя файла UNIX.
Второй аргумент системного вызова fdopen, который в нашем описании называется flags, имеет целочисленный тип и определяет метод доступа. Параметр flags принимает одно из значений, заданных постоянными в модуле linux при помощи директивы const (fcntl является сокращением от file control, «управление файлом»). Так же, как и большинство стандартных модулей, файл linux.ppu может быть включен в программу при помощи директивы:
uses linux;
В модуле linux определены три постоянных, которые сейчас представляют для нас интерес:
Open_RDONLY Открыть файл только для чтения
Open_WRONLY Открыть файл только для записи
Open_RDWR Открыть файл для чтения и записи
В случае успешного завершения вызова fdopen и открытия файла возвращаемое вызовом fdopen значение будет содержать неотрицательное целое число – дескриптор файла. Значение дескриптора файла будет наименьшим целым числом, которое еще не было использовано в качестве дескриптора файла выполнившим вызов процессом – знание этого факта иногда может понадобиться. Как отмечено во введении, в случае ошибки вызов fdopen возвращает вместо дескриптора файла значение -1. Это может произойти, например, если файл не существует. Для создания нового файла можно использовать вызов fdopen с параметром flags, равным Open_CREAT, – эта операция описана в следующем разделе.
Необязательный (optional) третий параметр mode, используемый только вместе с флагом Open_CREAT, также будет обсуждаться в следующем разделе – он связан с правами доступа к файлу. Следует обратить внимание на квадратные скобки в описании, которые обозначают, что параметр mode является необязательным.
Следующий фрагмент программы открывает файл junk для чтения и записи и проверяет, не возникает ли при этом ошибка. Этот последний момент особенно важен: имеет смысл устанавливать проверку ошибок во все программы, которые используют системные вызовы, поскольку каким бы простым не было приложение, иногда может произойти сбой. В этом примере используются библиотечные процедуры writeln для вывода сообщения и halt – для завершения процесса. Обе эти процедуры являются стандартными в любой системе.
uses linux;

const
workfile = 'junk'; (* задать имя рабочего файла *)

var
filedes:integer;
begin
(* Открыть, используя постоянную Open_RDWR из модуля linux *)
(* Файл открывается для чтения/записи *)
filedes := fdopen (workfile, Open_RDWR);
if filedes = -1 then
begin
writeln('Невозможно открыть файл ', workfile);
halt(1); (* выход по ошибке *)
end;
writeln('Файл ', workfile, ' успешно открыт, дескриптор равен ', filedes);
(*
* Остальная программа
*)
halt(0); (* нормальный выход *)
end.
Обратите внимание, что используется halt с параметром 1 в случае ошибки, и 0 – в случае удачного завершения. Это соответствует соглашениям UNIX и является правильной практикой программирования. Как будет показано в следующих главах, после завершения программы можно получить передаваемый вызову halt аргумент (program’s exit status – код завершения программы).
Предостережение
Здесь необходимо сделать несколько предостережений. Во-первых, существует предельное число файлов, которые могут быть одновременно открыты программой, – в стандарте POSIX (а следовательно, и в спецификации XSI) не менее двадцати1. Чтобы обойти эту проблему, требуется использовать системный вызов fdclose, тем самым сообщая системе, что работа с файлом закончена. Вызов fdclose будет рассмотрен в разделе 2.1.5. Может также существовать предел суммарного числа файлов, открытых всеми процессами, определяемый размером таблицы в ядре. Во-вторых, в ранних версиях UNIX и в параметре flags непосредственно задавались численные значения. Это все еще достаточно распространенный, хотя и не совсем удовлетворительный прием – задание численных значений вместо имен постоянных, определенных в модуле linux. Поэтому может встретиться оператор типа
filedes := fdopen(filename, 0);
который в обычных условиях открывает файл только для чтения и эквивалентен следующему оператору:
filedes := fdopen (filename, Open_RDONLY);
Упражнение 2.1. Создайте небольшую программу, описанную выше. Проверьте ее работу при условии, что файл junk не существует. Затем создайте файл junk с помощью текстового редактора и снова запустите программу. Содержимое файла junk не имеет значения.
2.1.3. Создание файла при помощи вызова fdopen
Вызов fdopen может использоваться для создания файла, например:
filedes := fdopen('/tmp/newfile', Open_WRONLY or Open_CREAT, octal(0644));
Здесь объединены флаги Open_CREAT и Open_WRONLY, задающие создание файла /tmp/newfile при помощи вызова fdopen. Если /tmp/newfile не существует, то будет создан файл нулевой длины с таким именем и открыт только для записи.
В этом примере вводится третий параметр mode вызова fdopen, который нужен только при создании файла. Не углубляясь в детали, заметим, что параметр mode содержит число, определяющее права доступа (access permissions) к файлу, указывающие, кто из пользователей системы может осуществлять чтение, запись или выполнение файла. В вышеприведенном примере используется восьмеричное значение 0644. При этом пользователь, создавший файл, может выполнять чтение из файла и запись в него. Остальные пользователи будут иметь доступ только для чтения файла. В следующей главе показано, как вычисляется это значение. Для простоты оно будет использовано во всех примерах этой главы.
Следующая программа создает файл newfile в текущем каталоге:
uses linux;

const
PERMS=0644; (* права доступа при открытии с Open_CREAT *)
filename='newfile';

var
filedes:integer;

begin
filedes := fdopen (filename, Open_RDWR or Open_CREAT, octal(PERMS));
if filedes = -1 then
begin
writeln('Не могу создать ', filename);
halt(1); (* выход по ошибке *)
end;

writeln('Файл ', filename, ' успешно создан (открыт для записи), дескриптор равен ',filedes);

(* Остальная программа *)

halt(0);
end.
Что произойдет, если файл newfile уже существует? Если позволяют права доступа к нему, то он будет открыт на запись, как если бы флаг Open_CREAT не был задан. В этом случае параметр mode не будет иметь силы. С другой стороны, объединение флагов Open_CREAT и Open_EXCL (exclusive – исключительный) приведет к ошибке во время вызова fdcreat, если файл уже существует. Например, следующий вызов
fd := fdopen('lock', Open_WRONLY or Open_CREAT or Open_EXCL, octal(0644));
означает, что если файл lock не существует, его следует создать с правами доступа 0644. Если же он существует, то в переменную fd будет записано значение -1, свидетельствующее об ошибке. Имя файла lock (защелка) показывает, что он создается для обозначения исключительного доступа к некоторому ресурсу.
Еще один полезный флаг – флаг Open_TRUNC. При его использовании вместе с флагом Open_CREAT файл будет усечен до нулевого размера, если он существует, и права доступа к файлу позволяют это. Например:
fd := fdopen ('file', Open_WRONLY or Open_CREAT or Open_TRUNC, octal(0644));
Это может понадобиться, если вы хотите, чтобы программа писала данные поверх данных, записанных во время предыдущих запусков программы.
Упражнение 2.2. Интересно, что флаг Open_TRUNC может использоваться и без флага Open_CREAT. Попытайтесь предугадать, что при этом получится, а затем проверьте это при помощи программы в случаях, когда файл существует и не существует.
Существует возможность установить размер файла не только в 0, но и в любое заданное количество байт. Это позволяет сделать функция fdTruncate.
Описание
uses linux;

Function fdTruncate (fd,size:longint): boolean;
fdTruncate устанавливает длину файла, заданного дескриптором fd, в size байт, где size должен быть меньше либо равен текущей длине файла fd. Функция возвращает True, если вызов был успешен, и false в случае ошибки.
2.1.4. Системный вызов fdcreat
Другой способ создания файла заключается в использовании системного вызова fdcreat. В действительности это исходный способ создания файла, но сейчас он в какой-то мере является излишним и предоставляет меньше возможностей, чем вызов fdopen. Мы включили его для полноты описания. Так же, как и вызов fdopen, он возвращает либо ненулевой дескриптор файла, либо -1 в случае ошибки. Если файл успешно создан, то возвращаемое значение является дескриптором этого файла, открытого для записи. Этот вызов не входит в модуль linux, поэтому для его использования в программе потребуется предварительное описание:
Описание
uses stdio;

function Fdcreat(PathName:Pchar;mode:longint):longint;cdecl;external 'c';
Первый параметр PathName указывает на маршрутное имя файла UNIX, определяющее имя создаваемого файла и путь к нему. Так же, как в случае вызова fdopen, параметр mode задает права доступа. При этом, если файл существует, то второй параметр также игнорируется. Тем не менее, в отличие от вызова fdopen, в результате вызова fdcreat файл всегда будет усечен до нулевой длины. Пример использования вызова fdcreat:
filedes := fdcreat('/tmp/newfile', octal(0644));
что эквивалентно вызову
filedes := fdopen('/tmp/newfile', Open_WRONLY or Open_CREAT or Open_TRUNC, octal(0644));
Ключевое слово cdecl определяет для данной функции стиль вызова, характерный для языка С. Это необходимо для доступа к функциям в объектных файлах, сгенерированных компилятором языка С, таким, как функции стандартной библиотеки языка С (это указывается с помощью ключевого слова external, параметром которого является имя библиотеки – 'c').
Следует отметить, что вызов fdcreat всегда открывает файл только для записи. Например, программа не может создать файл при помощи fdcreat, записать в него данные, затем вернуться назад и попытаться прочитать данные из файла, если предварительно не закроет его и не откроет снова при помощи вызова fdopen.
Упражнение 2.3. Напишите небольшую программу, которая вначале создает файл при помощи вызова fdcreat, затем, не вызывая fdclose, сразу же, открывает его при помощи системного вызова fdopen для чтения в одном случае и записи в другом. В обоих случаях выведите сообщение об успешном или неуспешном завершении, используя writeln.
2.1.5. Системный вызов fdclose
Системный вызов fdclose противоположен вызову fdopen. Он сообщает системе, что вызывающий его процесс завершил работу с файлом. Необходимость этого вызова определяется тем, что число файлов, которые могут быть одновременно открыты программой, ограничено.
Описание
uses linux;

Function fdClose(fd:longint):boolean;
Системный вызов fdclose имеет всего один аргумент – дескриптор закрываемого файла, обычно получаемый в результате предыдущего вызова fdopen или fdcreat. Следующий фрагмент программы поясняет простую связь между вызовами fdopen и fdclose:
filedes := fdopen('file', Open_RDONLY);
.
.
.
fdclose(filedes);
Системный вызов fdclose возвращает true в случае успешного завершения и false – в случае ошибки (которая может возникнуть, если целочисленный аргумент не является допустимым дескриптором файла).
При завершении работы программы все открытые файлы закрываются автоматически.
2.1.6. Системный вызов fdread
Системный вызов fdread используется для копирования произвольного числа символов или байтов из файла в буфер. Буфер формально устанавливается как ссылка на бестиповую переменную; это означает, что он может содержать элементы любого типа. Хотя обычно буфер является массивом данных типа char, он также может быть массивом структур, определенных пользователем.
Заметим, что программисты на языке Паскаль часто любят использовать термины «символ» и «байт» как взаимозаменяемые. Байт является единицей памяти, необходимой для хранения символа, и на большинстве машин имеет длину восемь бит. Термин «символ» обычно описывает элемент из набора символов ASCII, который является комбинацией всего из семи бит. Поэтому обычно байт может содержать больше значений, чем число символов ASCII; такая ситуация возникает при работе с двоичными данными. Тип char языка С представляет более общее понятие байта, поэтому название данного типа является не совсем правильным.
Описание
uses linux;

Function fdRead(filedes:longint;var buffer;size:longint):longint;
Первый параметр, filedes, является дескриптором файла, полученным во время предыдущего вызова fdopen или fdcreat. Второй параметр, buffer, – это ссылка на массив или структуру, в которую должны копироваться данные. Во многих случаях в качестве этого параметра будет выступать просто имя массива, например:
var
fd:integer;
nread:longint;
buffer:array [0..SOMEVALUE-1] of char;

(* Дескриптор файла fd получен в результате вызова fdopen *)
.
.
.
nread := fdread(fd, buffer, SOMEVALUE);
Как видно из примера, третьим параметром вызова fdread является положительное число (имеющее тип longint), задающее число байтов, которое требуется считать из файла.
Возвращаемое вызовом fdread число (присваиваемое в примере переменной nread) содержит число байтов, которое было считано в действительности. Обычно это число запрошенных программой байтов, но, как будет показано в дальнейшем, – не всегда, и значение переменной nread может быть меньше. Кроме того, в случае ошибки вызов fdread возвращает значение -1. Это происходит, например, если передать fdread недопустимый дескриптор файла.
Указатель чтения-записи
Достаточно естественно, что программа может последовательно вызывать fdread для просмотра файла. Например, если предположить, что файл foo содержит не менее 1024 символов, то следующий фрагмент кода поместит первые 512 символов из файла foo в массив buf1, а вторые 512 символов – в массив buf2.
var
fd:integer;
n1,n2:longint;
buf1, buf2 : array [0..511] of char;
.
.
.
fd := fdopen('foo', Open_RDONLY);
if fd = -1 then
halt(-1);
n1 := fdread(fd, buf1, 512);
n2 := fdread(fd, buf2, 512);
Система отслеживает текущее положение в файле при помощи объекта, который называется указателем ввода/вывода (read-write pointer), или указателем файла (file pointer). По существу, в этом указателе записано положение очередного байта в файле, который должен быть считан (или записан) следующим для определенного дескриптора файла; следовательно, указатель файла можно себе представить в виде закладки. Его значение отслеживает система, и программисту нет необходимости выделять под него переменную. Произвольный доступ, при котором положение указателя ввода/вывода изменяется явно, может осуществляться при помощи системного вызова fdseek, который описан в разделе 2.1.10. В случае вызова fdread система просто перемещает указатель ввода/вывода вперед на число байтов, считанных в результате данного вызова.
Поскольку вызов fdread может использоваться для просмотра файла с начала и до конца, программа должна иметь возможность определять конец файла. При этом становится важным возвращаемое вызовом fdread значение. Если число запрошенных во время вызова fdread символов больше, чем оставшееся число символов в файле, то система передаст только оставшиеся символы, установив соответствующее возвращаемое значение. Любые последующие вызовы fdread будут возвращать значение 0. При этом больше не останется данных, которые осталось бы прочитать. Обычным способом проверки достижения конца файла в программе является проверка равенства нулю значения, возвращаемого вызовом fdread, по крайней мере, для программы, использующей вызов fdread.
Следующая программа count иллюстрирует некоторые из этих моментов:

(* Программа count подсчитывает число символов в файле *)

uses linux;

const
BUFSIZE=512;

var
filedes:integer;
nread:longint;
buffer:array [0..BUFSIZE-1] of byte;
total:longint;
begin
total := 0;

(* Открыть файл 'anotherfile' только для чтения *)
filedes := fdopen ('anotherfile', Open_RDONLY);
if filedes=-1 then
begin
writeln('Ошибка при открытии файла anotherfile');
halt(1);
end;

(* Повторять до конца файла, пока nread не будет равно 0 *)
nread := fdread (filedes, buffer, BUFSIZE);
while nread > 0 do
begin
inc(total,nread); (* увеличить total на nread *)
nread := fdread (filedes, buffer, BUFSIZE);
end;

writeln('Число символов в файле anotherfile: ', total);
fdclose(filedes);
halt(0);
end.
Эта программа будет выполнять чтение из файла anotherfile блоками по 512 байт. После каждого вызова fdread значение переменной total будет увеличиваться на число символов, действительно скопированных в массив buffer. Почему total объявлена как переменная типа longint?
Здесь использовано для числа считываемых за один раз символов значение 512, поскольку система UNIX сконфигурирована таким образом, что наибольшая производительность достигается при перемещении данных блоками, размер которых кратен размеру блока на диске, в этом случае 512. (В действительности размер блока зависит от конкретной системы и может составлять до и более 8 Кбайт.) Тем не менее мы могли бы задавать в вызове fdread произвольное число, в том числе единицу. Введение определенного значения, соответствующего вашей системе, не дает выигрыша в функциональности, а лишь повышает производительность программы, но, как мы увидим в разделе 2.1.9, это улучшение может быть значительным.
Упражнение 2.4. Если вы знаете, как это сделать, перепишите программу count так, чтобы вместо использования постоянного имени файла она принимала его в качестве аргумента командной строки. Проверьте работу программы на небольшом файле, состоящем из нескольких строк.
Упражнение 2.5. Измените программу count так, чтобы она также выводила число слов и строк в файле. Определите слово как знак препинания или алфавитно-цифровую строку, не содержащую пробельных символов, таких как пробел, табуляция или перевод строки. Строкой, конечно же, будет любая последовательность символов, завершающаяся символом перевода строки.
Следующая программа демонстрирует функции fdread и fdtrucate.
Uses linux;

Const Data : string[10] = '12345687890';

Var FD : Longint;
l : longint;

begin
FD:=fdOpen('test.dat',open_wronly or open_creat,octal(666));
if fd>0 then
begin
{ Fill file with data }
for l:=1 to 10 do
if fdWrite (FD,Data[1],10)<>10 then
begin
writeln ('Error when writing !');
halt(1);
end;
fdClose(FD);
FD:=fdOpen('test.dat',open_rdonly);
{ Read data again }
If FD>0 then
begin
For l:=1 to 5 do
if fdRead (FD,Data[1],10)<>10 then
begin
Writeln ('Error when Reading !');
Halt(2);
end;
fdCLose(FD);
{ Truncating file at 60 bytes }
{ For truncating, file must be open or write }
FD:=fdOpen('test.dat',open_wronly,octal(666));
if FD>0 then
begin
if not fdTruncate(FD,60) then
Writeln('Error when truncating !');
fdClose (FD);
end;
end;
end;
end.
2.1.7. Системный вызов fdwrite
Системный вызов fdwrite противоположен вызову fdread. Он копирует данные из буфера программы, рассматриваемого как массив, во внешний файл.
Описание
uses linux;

Function fdWrite (fd:longint; var buf; size:longint):longint;
Так же, как и вызов fdread, вызов fdwrite имеет три аргумента: дескриптор файла filedes, указатель на записываемые данные buffer и n – положительное число записываемых байтов. Возвращаемое вызовом значение является либо числом записанных символов, либо кодом ошибки -1. Фактически, если возвращаемое значение не равно -1, то оно почти всегда будет равно n. Если оно меньше n, значит, возникли какие-то серьезные проблемы. Например, это может произойти, если в процессе вызова fdwrite было исчерпано свободное пространство на выходном носителе. (Если носитель уже был заполнен до вызова fdwrite, то вызов вернет значение -1.)
Вызов fdwrite часто использует дескриптор файла, полученный при создании нового файла. Легко увидеть, что происходит в этом случае. Изначально файл имеет нулевую длину (он только что создан или получен усечением существующего файла до нулевой длины), и каждый вызов fdwrite просто дописывает данные в конец файла, перемещая указатель чтения-записи на позицию, следующую за последним записанным байтом. Например, в случае удачного завершения фрагмент кода
var
fd:integer;
w1, w2 : longint;
header1: array [0..511] of char;
header2: array [0..1023] of char;
.
.
.
fd := fdopen('newfile', Open_WRONLY or Open_CREAT or Open_EXCL, octal(0644));
if fd = -1 then
exit;
w1 := fdwrite(fd, header1, 512);
w2 := fdwrite(fd, header2, 1024);
дает в результате файл длиной 1536 байт, с содержимым массивов header1 и header2.
Что произойдет, если программа откроет существующий файл на запись и сразу же запишет в него что-нибудь? Ответ очень прост: старые данные в файле будут заменены новыми, символ за символом. Например, предположим, что файл oldhat имеет длину 500 символов. Если программа откроет файл oldhat для записи и выведет в него 10 символов, то первые 10 символов в файле будут заменены содержимым буфера записи программы. Следующий вызов fdwrite заменит очередные 10 символов и так далее. После достижения конца исходного файла в процессе дальнейших вызовов fdwrite его длина будет увеличиваться. Если нужно избежать переписывания файла, можно открыть файл с флагом Open_APPEND. Например:
filedes := fdopen(filename, Open_WRONLY or Open_APPEND);
Теперь в случае успешного вызова fdopen указатель чтения-записи будет помещен, сразу же за последним байтом в файле, и вызов fdwrite будет добавлять данные в конец файла. Этот прием более подробно будет объяснен в разделе 2.1.12.
Программа, демонстрирующая fdOpen, fdwrite и fdCLose.
Uses linux;

Const Line : String[80] = 'This is easy writing !';

Var FD : Longint;

begin
FD:=fdOpen ('Test.dat',Open_WrOnly or Open_Creat);
if FD>0 then
begin
if length(Line)<>fdwrite (FD,Line[1],Length(Line)) then
Writeln ('Error when writing to file !');
fdClose(FD);
end;
end.
2.1.8. Пример copyfile
Теперь можем закрепить материал на практике. Задача состоит в написании функции copyfile, которая будет копировать содержимое одного файла в другой. Возвращаемое значение должно быть равно нулю в случае успеха или отрицательному числу – в случае ошибки.
Основная логика действий понятна: открыть первый файл, затем создать второй и выполнять чтение из первого файла и запись во второй до тех пор, пока не будет достигнут конец первого файла. И, наконец, закрыть оба файла.
Окончательное решение может выглядеть таким образом:
(* Программа copyfile: скопировать файл name1 в файл name2 *)

uses linux;

const
BUFSIZE=512; (* Размер считываемого блока *)
PERM=0644; (* Права доступа для нового файла в форме,
похожей на восьмеричную *)

(* Скопировать файл name1 в файл name2 *)
function copyfile(name1, name2: string):integer;
var
infile, outfile: integer; (*дескрипторы файлов*)
nread: longint;
buffer: array [0..BUFSIZE-1] of byte; (*буфер для чтения/записи*)
begin

infile := fdopen (name1, Open_RDONLY);
if infile=-1 then
begin
copyfile:=-1; (*ошибка открытия первого файла*)
exit;
end;

outfile := fdopen (name2, Open_WRONLY or Open_CREAT or Open_TRUNC, octal(PERM));
if outfile=-1 then
begin
fdclose(infile);
copyfile:=-2; (*ошибка открытия второго файла*)
exit;
end;

(* Чтение из файла name1 по BUFSIZE символов *)
nread := fdread (infile, buffer, BUFSIZE);
while nread > 0 do
begin
(* Записать buffer в выходной файл. *)
if fdwrite (outfile, buffer, nread) < nread then
begin
fdclose (infile);
fdclose (outfile);
copyfile:=-3; (* ошибка записи *)
exit;
end;
nread := fdread (infile, buffer, BUFSIZE);
end;

(*закрываем прочитанные файлы*)
fdclose (infile);
fdclose (outfile);

if (nread = -1) then
copyfile := -4 (* ошибка при последнем чтении *)
else
copyfile := 0; (* все порядке *)
end;

(* Программа для тестирования функции copyfile *)
var
retcode:integer;
begin
if paramcount<2 then
begin
writeln('Используйте: ',paramstr(0),' файл-источник файл-приемник');
exit;
end;
retcode := copyfile(paramstr(1), paramstr(2));
case retcode of
0: writeln('Файл ',paramstr(1),' успешно скопирован в файл ',paramstr(2));
-1: writeln('Ошибка открытия файла ',paramstr(1),' для чтения');
-2: writeln('Ошибка открытия файла ',paramstr(2),' для записи');
-3: writeln('Ошибка записи в файл ',paramstr(2));
-4: writeln('Ошибка чтения из файла ',paramstr(1));
end;
end.
Теперь функцию copyfile можно вызывать так:
retcode := copyfile('squarepeg', 'roundhole');
Упражнение 2.6. Измените функцию copyfile так, чтобы в качестве ее параметров могли выступать дескрипторы, а не имена файлов. Проверьте работу новой версии программы.
Упражнение 2.7. Если вы умеете работать с аргументами командной строки, используйте одну из процедур copyfile для создания программы mycp, копирующей первый заданный в командной строке файл во второй.
2.1.9. Эффективность вызовов fdread и fdwrite
Процедура copyfile дает возможность оценить эффективность примитивов доступа к файлам в зависимости от размера буфера. Один из методов заключается просто в компиляции copyfile с различными значениями BUFSIZE, а затем в измерении времени ее выполнения при помощи команды UNIX time. Мы сделали это, используя программу
(* Программа для тестирования функции copyfile *)
begin
copyfile('test.in', 'test.out');
end.
и получили при копировании одного и того же большого файла (68307 байт) на компьютере с системой SVR4 UNIX для диска, разбитого на блоки по 512 байт, результаты, приведенные в табл. 2.2.
Таблица 2.2. Результаты тестирования функции copyfile
BUFSIZE
Real time
User time
System time
1
0:24.49
0:3.13
0:21.16
64
0:0.46
0:0.12
0:0.33
512
0:0.12
0:0.02
0:0.08
4096
0:0.07
0:0.00
0:0.05
8192
0:0.07
0:0.01
0:0.05
Формат данных в таблице отражает вывод команды time. В первом столбце приведены значения BUFSIZE, во втором – действительное время выполнения процесса в минутах, секундах и десятых долях секунды. В третьем столбце приведено «пользовательское» время, то есть время, занятое частями программы, не являющимися системными вызовами. Из-за дискретности используемого таймера одно из значений в таблице ошибочно записано как нулевое. В последнем, четвертом, столбце приведено время, затраченное ядром на обслуживание системных вызовов. Как видно из таблицы, третий и четвертый столбцы в сумме не дают действительное время выполнения. Это связано с тем, что в системе UNIX одновременно выполняется несколько процессов. Не все время тратится на выполнение ваших программ!
Полученные результаты достаточно убедительны – чтение и запись по одному байту дает очень низкую производительность, тогда как увеличение размера буфера значительно повышает производительность. Наибольшая производительность достигается, если BUFSIZE кратно размеру блока диска на диске, как видно из результатов, для значений BUFSIZE 512, 4096 и 8192 байта.
Следует также отметить, что большая часть прироста (но не всего) эффективности получается просто от уменьшения числа системных вызовов. Переключение между программой и ядром может обойтись достаточно дорого. В общем случае, если нужна максимальная производительность, следует минимизировать число генерируемых программой системных вызовов.
Функция fdFlush позволяет опустошить файловый буфер ядра UNIX для того, чтобы файл был действительно записан на диск.
Описание
uses linux;

Function fdFlush(fd:Longint):boolean;
2.1.10. Вызов fdseek и произвольный доступ
Системный вызов fdseek позволяет пользователю изменять положение указателя чтения-записи, то есть изменять номер байта, который будет первым считан или записан в следующей операции ввода/вывода. Таким образом, использование вызова fdseek позволяет получить произвольный доступ к файлу.
Описание
uses linux;

Function fdSeek(filedes, offset, SeekType:longint):longint;
Первый параметр, filedes, – это дескриптор открытого файла. Второй параметр, offset, обычно определяет новое положение указателя чтения-записи и задает число байтов, которое нужно добавить к начальному положению указателя. Третий целочисленный параметр, SeekType, определяет, что принимается в качестве начального положения, то есть откуда вычисляется смещение offset. Флаг SeekType может принимать одно из символьных значений (определенных в модуле linux), как показано ниже:
SEEK_SET
Смещение offset вычисляется от начала файла, обычно имеет значение = 0
SEEK_CUR
Смещение offset вычисляется от текущего положения в файле, обычное значение = 1
SEEK_END
Смещение offset вычисляется от конца файла, обычное значение = 2
Эти значения показаны в графическом виде на рис. 2.1, на котором представлен файл из 7 байт.


SEEK_SET

a

Текущее

b

положение

c

SEEK_CUR
указателя

d

файла

e

f

g

SEEK_END

Рис. 2.1. Символьные значения флага SeekType

Пример использования вызова fdseek:
var
newpos:longint;
.
.
.
newpos := fdseek(fd, -16, SEEK_END);
который задает положение указателя в 16 байтах от конца файла.
Во всех случаях возвращаемое значение (содержащееся в переменной newpos в примере) дает новое положение в файле. В случае ошибки оно будет содержать стандартный код ошибки -1.
Существует ряд моментов, которые следует отметить. Во-первых, обе переменные newpos и offset имеют тип longint, и должны вмещать смещение для любого файла в системе. Во-вторых, как показано в примере, смещение offset может быть отрицательным. Другими словами, возможно перемещение в обратную сторону от начального положения, заданного флагом SeekType. Ошибка возникнет только при попытке переместиться при этом на позицию, находящуюся до начала файла. В-третьих, можно задать позицию за концом файла. В этом случае, очевидно, не существует данных, которые можно было бы прочитать – невозможно предугадать будущие записи в этот участок (UNIX не имеет машины времени) – но последующий вызов fdwrite имеет смысл и приведет к увеличению размера файла. Пустое пространство между старым концом файла и начальным положением новых данных не обязательно выделяется физически, но для последующих вызовов fdread оно будет выглядеть как заполненное символами null ASCII.
В качестве простого примера мы можем создать фрагмент программы, который будет дописывать данные в конец существующего файла, открывая файл, перемещаясь на его конец при помощи вызова fdseek и начиная запись:
filedes := fdopen(filename, Open_RDWR);
fdseek(filedes, 0, SEEK_END);
fdwrite(filedes, outbuf, OBSIZE);
Здесь параметр направления поиска для вызова fdseek установлен равным SEEK_END для перемещения в конец файла. Так как перемещаться дальше нам не нужно, то смещение задано равным нулю.
Вызов fdseek также может использоваться для получения размера файла, так как он возвращает новое положение в файле.
var
filesize:longint;
filedes:integer;
.
.
.
filesize := fdseek(filedes, 0, SEEK_END);
Упражнение 2.8. Напишите функцию, которая использует вызов fdseek для получения размера открытого файла, не изменяя при этом значения указателя чтения-записи.
2.1.11. Пример: гостиница
В качестве несколько надуманного, но возможно наглядного примера, предположим, что имеется файл residents, в котором записаны фамилии постояльцев гостиницы. Первая строка содержит фамилию жильца комнаты 1, вторая – жильца комнаты 2 и т.д. (очевидно, это гостиница с прекрасно организованной системой нумерации комнат). Длина каждой строки составляет ровно 41 символ, в первые 40 из которых записана фамилия жильца, а 41-й символ является символом перевода строки для того, чтобы файл можно было вывести на дисплей при помощи команды UNIX cat.
Следующая функция getoccupier вычисляет по заданному целому номеру комнаты положение первого байта фамилии жильца, затем перемещается в эту позицию и считывает данные. Она возвращает либо указатель на строку с фамилией жильца, либо нулевой указатель в случае ошибки (мы будем использовать для этого значение nil). Обратите внимание, что мы присвоили переменной дескриптора файла infile исходное значение –1. Благодаря этому мы можем гарантировать, что файл будет открыт всего один раз.
(* Функция getoccupier - получить фамилию из файла residents *)

uses linux;

const
NAMELENGTH=41;

var
namebuf:array [0..NAMELENGTH-1] of char; (* Буфер для фамилии *)
const
infile:integer=-1; (* Для хранения дескриптора файла *)

function getoccupier(roomno:integer):pchar;
var
offset, nread:longint;
begin
(* Убедиться, что файл открывается впервые *)
if infile = -1 then
begin
infile := fdopen ('residents', Open_RDWR);
if infile = -1 then
begin
getoccupier := nil; (* Невозможно открыть файл *)
exit;
end;
end;

offset := (roomno - 1) * NAMELENGTH;

(* Найти поле комнаты и считать фамилию жильца *)
if fdseek (infile, offset, SEEK_SET) = -1 then
begin
getoccupier := nil;
exit;
end;

nread := fdread (infile, namebuf, NAMELENGTH);
if nread <= 0 then
begin
getoccupier := nil;
exit;
end;

(* Создать строку, заменив символ перевода строки на '\0' *)
namebuf[nread - 1] := #0;
getoccupier := namebuf;
end;
Если предположить, что в гостинице 10 комнат, то следующая программа будет последовательно вызывать функцию getoccupier для просмотра файла и выводить каждую найденную фамилию при помощи процедуры writeln из стандартного модуля system:
(* Программа listoc выводит все фамилии жильцов *)

const
NROOMS=10;

var
j:integer;
p:pchar;
begin

for j := 1 to NROOMS do
begin
p := getoccupier (j);
if p<>nil then
writeln('Комната ', j:2, ', ', p)
else
writeln('Ошибка для комнаты ', j);
end;
end.
Упражнение 2.9. Придумайте алгоритм для определения пустых комнат. Измените функцию getoccupier и файл данных, если это необходимо, так, чтобы он отражал эти изменения. Затем напишите процедуру с названием findfree для поиска свободной комнаты с наименьшим номером.
Упражнение 2.10. Напишите процедуру freeroom для удаления записи о жильце. Затем напишите процедуру addguest для внесения новой записи о жильце, с предварительной проверкой того, что выделяемая комната свободна.
Упражнение 2.11. Объедините процедуры getoccupier, freeroom, addguest и findfree в простой программе с названием frontdesk, которая управляет файлом данных. Используйте аргументы командной строки или напишите интерактивную программу, которая вызывает функции writeln и readln. В обоих случаях для вычисления номера комнаты вам потребуется преобразовывать строки в целые числа. Вы можете использовать для этого библиотечную процедуру StrToInt:
i := StrToInt(str);
где string – указатель на строку символов, а i – целое число.
Упражнение 2.12. В качестве обобщенного примера напишите программу на основе системного вызова fdseek, которая копирует в обратном порядке байты из одного файла в другой. Насколько эффективным получилось ваше решение?
Упражнение 2.13. Используя вызов fdseek, напишите процедуры для копирования последних 10 символов, последних 10 слов и последних 10 строк из одного файла в другой.
2.1.12. Дописывание данных в конец файла
Как должно быть ясно из раздела 2.1.10, для дописывания данных в конец файла может использоваться следующий код:
(* Поиск конца файла *)
fdseek(filedes, 0, SEEK_END);
fdwrite(filedes, appbuf, BUFSIZE);
Тем не менее более изящный способ состоит в использовании одного из дополнительных флагов вызова fdopen, Open_APPEND. Если установлен этот флаг, то перед каждой записью указатель будет устанавливаться в конец файла. Это может быть полезно, если нужно лишь дополнить файл, застраховавшись от случайной перезаписи данных в начале файла.
Можно использовать флаг Open_APPEND следующим образом:
filedes := fdopen('yetanother', Open_WRONLY or Open_APPEND);
Каждый последующий вызов fdwrite будет дописывать данные в конец файла. Например:
fdwrite(filedes, appbuf, BUFSIZE);
Упражнение 2.14. Напишите процедуру fileopen, имеющую два аргумента: первый – строку, содержащую имя файла, и второй – строку, которая может иметь одно из следующих значений:
r – открыть файл только для чтения;
w – открыть файл только для записи;
rw – открыть файл для чтения и записи;
а – открыть файл для дописывания.
процедура fileopen должна возвращать дескриптор файла или код ошибки -1.
2.1.13. Удаление файла
Существует один метод удаления файла из системы – при помощи вызова unlink.
Описание
uses linux;

Function UnLink(Var Path): Boolean;
Вызов имеет единственный аргумент – строку с именем удаляемого файла, например:
unlink('/tmp/usedfile');
Вызов возвращает true в случае успешного завершения и false – в случае ошибки.
2.1.14. Системный вызов fcntl
Системный вызов fcntl был введен для управления уже открытыми файлами. Это довольно странная конструкция, которая может выполнять разные функции.
Описание
uses linux;

/* Примечание: тип последнего параметра может меняться */

Function Fcntl(filedes:longint;Cmd:Integer):integer;
Function Fcntl(var filedes:Text;Cmd:Integer):integer;
Procedure Fcntl(Fd:text;Cmd:Integer;Arg:longint);
Procedure Fcntl(Fd:longint;Cmd:longint;Arg:Longint);
Системный вызов fcntl работает с открытым файлом, заданным дескриптором файла filedes. Конкретная выполняемая функция задается выбором одного из значений параметра cmd из модуля linux. Тип третьего параметра зависит от значения параметра cmd. Например, если вызов fcntl используется для установки флагов статуса файла, тогда третий параметр будет целым числом. Если же, как можно будет увидеть позже, вызов fcntl будет использоваться для блокировки файла, то третий параметр будет указателем на структуру lock. Иногда третий параметр вообще не используется.
Некоторые из этих функций относятся к взаимодействию файлов и процессов, и мы не будем рассматривать их здесь; тем не менее две из этих функций, заданные значениями F_GETFL и F_SETFL параметра cmd, представляют для нас сейчас интерес.
При задании параметра F_GETFL вызов fcntl возвращает текущие флаги статуса файла, установленные вызовом fdopen. Следующая функция filestatus использует fcntl для вывода текущего статуса открытого файла.
(*
* Функция filestatus описывает текущий статус файла
*)

uses linux;

function filestatus(filedes:integer):integer;
var
arg1:integer;
begin
arg1 := fcntl (filedes, F_GETFL);
if arg1 = -1 then
begin
writeln('Ошибка чтения статуса файла');
filestatus := -1;
exit;
end;

write('Дескриптор файла ', filedes, ': ');

(*
* Сравнить аргумент с флагами открытия файла.
*)
case (arg1 and Open_ACCMODE) of
Open_WRONLY:
write('Только для записи');
Open_RDWR:
write('Для чтения-записи');
Open_RDONLY:
write('Только для чтения');
else
write('Режим не существует');
end;

if (arg1 and Open_APPEND)<>0 then
write (' - установлен флаг append');
writeln;
filestatus := 0;
end;
Следует обратить внимание на проверку установки определенного бита во флаги статуса файла в переменной arg1 при помощи побитового оператора И, обозначаемого AND. Поле интересующих нас битов вырезается с помощью специальной маски Open_ACCMODE, определенной в модуле linux. Дальнейшие действия осуществляются с учетом того, что в данном поле не может быть выставлено более одного бита, поскольку эти три режима доступа к файлу не совместимы.
Значение F_SETFL используется для переустановки связанных с файлом флагов статуса. Новые флаги задаются в третьем аргументе вызова fcntl. При этом могут быть установлены только некоторые флаги, например, нельзя вдруг превратить файл, открытый только для чтения, в файл, открытый для чтения и записи. Тем не менее с помощью F_SETFL можно задать режим, при котором все следующие операции записи будут только дописывать информацию в конец файла:
if (fcntl(filedes, F_SETFL, Open_APPEND) = -1) then
writeln('Ошибка вызова fcntl');
2.2. Стандартный ввод, стандартный вывод и стандартный вывод диагностики
2.2.1. Основные понятия
Система UNIX автоматически открывает три дескриптора файла для любой выполняющейся программы. Эти дескрипторы называются стандартным вводом (standard input), стандартным выводом (standard output) и стандартным выводом диагностики (standard error). Они всегда имеют значения 0, 1 и 2 соответственно. Недопустимо путать эти дескрипторы с похожими по названию стандартными потоками stdin, stdout и stderr из стандартной библиотеки ввода/ вывода.
По умолчанию вызов fdread для стандартного ввода приведет к чтению данных с клавиатуры. Аналогично запись в стандартный вывод или стандартный вывод диагностики приведет по умолчанию к выводу сообщения на экран терминала. Это первый пример использования примитивов доступа к файлам для ввода/вывода на устройства, отличные от обычных файлов.
Программа, применяющая эти стандартные дескрипторы файлов, тем не менее, не ограничена использованием терминала. Каждый из этих дескрипторов может быть независимо переназначен, если программа вызывается с использованием средств перенаправления, обеспечиваемых стандартным командным интерпретатором UNIX. Например, команда
$ prog_name < infile
приведет к тому, что при чтении из дескриптора со значением 0 программа будет получать данные из файла infile, а не с терминала, обычного источника для стандартного ввода.
Аналогично все данные, записываемые в стандартный вывод, могут быть перенаправлены в выходной файл, например:
$ prog_name > outfile
Возможно, наиболее полезно то, что можно связать стандартный вывод одной программы со стандартным вводом другой при помощи каналов UNIX. Следующая команда оболочки означает, что все данные, записываемые программой prog_1 в ее стандартный вывод, попадут на стандартный ввод программы prog_2 (такие команды называются конвейерами):
$ prog_1 | prog_2
Дескрипторы файлов стандартного ввода и вывода позволяют писать гибкие и совместимые программы. Программа может представлять собой инструмент, способный при необходимости принимать ввод от пользователя, из файла, или даже с выхода другой программы. Программа настроена на чтение из стандартного ввода, использует файловый дескриптор 0, а выбор входного источника данных откладывается до момента запуска программы.
2.2.2. Программа io
В качестве очень простого примера использования стандартных дескрипторов файлов приведем программу io, применяющую системные вызовы fdread и fdwrite и дескрипторы файлов со значениями 0 и 1 для копирования стандартного ввода в стандартный вывод. В сущности, это усеченная версия программы UNIX cat. Обратите внимание на отсутствие вызовов fdopen и fdcreat.
(* Программа io копирует стандартный ввод *)
(* в стандартный вывод *)
uses linux;

const
SIZE=512;
var
nread:longint;
buf:array [0..SIZE-1] of byte;
begin
nread := fdread (0, buf, SIZE);
while nread > 0 do
begin
fdwrite (1, buf, nread);
nread := fdread (0, buf, SIZE);
end;
halt(0);
end.
Предположим, что исходный код этой программы находится в файле iо.pas, который компилируется для получения исполняемого файла io:
$ fpc io.pas
Если теперь запустить программу io на выполнение, просто набрав имя файла программы, то она будет ожидать ввода с терминала. Если пользователь напечатает строку и затем нажмет клавишу Return или Enter на клавиатуре, то программа iо просто выведет на дисплей напечатанную строку, то есть запишет строку в стандартный вывод. При этом диалог с системой в действительности будет выглядеть примерно так:
$ io Пользователь печатает io и нажимает Return
Это строка 1 Пользователь печатает строку и нажимает Return
Это строка 1 Программа io выводит строку на дисплей
.
.
.
После вывода строки на экран программа io будет ожидать дальнейшего ввода. Пользователь может продолжать печатать, и программа io будет послушно выводить каждую строку на экран при нажатии на клавишу Return или Enter.
Для завершения программы пользователь может напечатать строку из единственного символа конца файла. Обычно это символ ^D, то есть Ctrl+D, который набирается одновременным нажатием клавиш Ctrl и D. При этом вызов fdread вернет 0, указывая на то, что достигнут конец файла. Весь диалог с системой мог бы выглядеть примерно так:
$ io
Это строка 1
Это строка 1
Это строка 2
Это строка 2
Пользователь печатает Ctrl+D
$
Обратите внимание, что программа io ведет себя не совсем так, как можно было бы ожидать. Вместо того чтобы считать все 512 символов до начала вывода на экран, как, казалось бы, следует делать, она выводит строку на экран при каждом нажатии клавиши Return. Это происходит из-за того, что вызов fdread, который использовался для ввода данных с терминала, обычно возвращает значение после каждого символа перевода строки для облегчения взаимодействия с пользователем. Если быть еще более точным, это будет иметь место только для обычных настроек терминала. Терминалы могут быть настроены в другом режиме, позволяя осуществлять, например, посимвольный ввод. Дополнительные соображения по этому поводу изложены в главе 9.
Поскольку программа io использует стандартные дескрипторы файлов, к ней можно применить стандартные средства оболочки для перенаправления и организации конвейеров. Например, выполнение команды
$ io < /etc/motd > message
приведет к копированию при помощи программы io сообщения с цитатой дня команды /etc/motd в файл message, а выполнение команды
$ io < /etc/motd | wc
направит стандартный вывод программы io в утилиту UNIX для подсчета числа слов wc. Так как стандартный вывод программы io будет фактически идентичен содержимому /etc/motd, это просто еще один (более громоздкий) способ подсчета слов, строк и символов в файле.
Упражнение 2.15. Напишите версию программы io, которая проверяет наличие аргументов командной строки. Если существует хотя бы один из них, то программа должна рассматривать каждый из аргументов как имя файла и копировать содержимое каждого файла в стандартный вывод. Если аргументы командной строки отсутствуют, то ввод должен осуществляться из стандартного ввода. Как должна действовать программа io, если она не может открыть файл?
Упражнение 2.16. Иногда данные в файле могут медленно накапливаться в течение продолжительного промежутка времени. Напишите версию программы io с именем watch, которая будет выполнять чтение из стандартного ввода до тех пор, пока не встретится символ конца файла, выводя данные на стандартный вывод. После достижения конца файла программа watch должна сделать паузу на пять секунд, а затем снова начать чтение стандартного ввода, чтобы проверить, не поступили ли новые данные, не открывая при этом файл заново и не изменяя положение указателя чтения-записи. Для прекращения работы процесса на заданное время вымажете использовать стандартную библиотечную процедуру delay, которая имеет единственный аргумент – целое число, задающее продолжительность ожидания в миллисекундах. Например, вызов
sleep(5000);
заставляет процесс прекратить работу на 5 секунд. Программа watch аналогична программе readslow, существующей в некоторых версиях UNIX. Посмотрите также в руководстве системы описание ключа -f команды tail.
2.2.3. Использование стандартного вывода диагностики
Стандартный вывод диагностики является особым файловым дескриптором, который по принятому соглашению зарезервирован для сообщений об ошибках и для предупреждений, что позволяет программе отделить обычный вывод от сообщений об ошибках. Например, использование стандартного вывода диагностики позволяет программе выводить сообщения об ошибках на терминал, в то время как стандартный вывод записывается в файл. Тем не менее при необходимости стандартный вывод диагностики может быть перенаправлен аналогично перенаправлению стандартного вывода. Например, часто используется такая форма команды запуска системы make:
$ make > log.out 2>log.err
В результате все сообщения об ошибках работы make направляются в файл log.err, а стандартный вывод направляется в файл log.out.
Можно выводить сообщения в стандартный вывод диагностики при помощи системного вызова write со значением дескриптора файла равным 2:
var
msg:array [0..5] of char='boob'#$a;
.
.
fdwrite(2, msg, 5);
Тем не менее это достаточно грубый и громоздкий способ. Мы приведем лучшее решение в конце этой главы.
2.3. Стандартная библиотека ввода/вывода: взгляд в будущее
Системные вызовы доступа к файлам лежат в основе всего ввода и вывода программ UNIX. Тем не менее эти вызовы действительно примитивны и работают с данными только как с простыми последовательностями байтов, оставляя все остальное на усмотрение программиста. Соображения эффективности также ложатся на плечи разработчика.
Чтобы несколько упростить ситуацию, система UNIX предоставляет стандартную библиотеку ввода/вывода, которая содержит намного больше средств, чем уже описанные системные вызовы. Поскольку книга в основном посвящена интерфейсу системных вызовов с ядром, подробное рассмотрение стандартной библиотеки ввода/вывода отложено до главы 11. Тем не менее для сравнения стоит и в этой главе кратко описать возможности стандартного ввода/вывода.
Возможно, наиболее очевидное отличие между стандартным вводом/выводом и примитивами системных вызовов состоит в способе описания файлов. Вместо целочисленных дескрипторов файлов, процедуры стандартного ввода/вывода явно или неявно работают со структурой FILE. Следующий пример показывает, как открывается файл при помощи процедуры fopen:
uses stdio;

var
stream:PFILE;

begin
stream := fopen ('junk', 'r');
if stream = nil then
begin
printf('Невозможно открыть файл junk'#10,[]);
halt(1);
end;
end.
Первая строка примера
uses stdio;
подключает модуль библиотеки ввода/вывода stdio, описанной в приложении. Этот файл, кроме всего прочего, содержит определение TFILE, PFILE и объявления для таких функций, как fopen.
Настоящее содержание этого примера заключается в операторе:
stream := fopen ('junk', 'r');
if stream = nil then
begin
.
.
end;
Здесь junk – это имя файла, а строка 'r' означает, что файл открывается только для чтения. Строка 'w' может использоваться для усечения файла до нулевой длины или создания файла и открытия его на запись. В случае успеха функция fopen проинициализирует структуру TFILE и вернет ее адрес в переменной stream. Указатель stream может быть передан другим процедурам из библиотеки. Важно понимать, что где-то внутри тела функции fopen осуществляется вызов нашего старого знакомого fdopen. И, естественно, где-то внутри структуры TFILE находится дескриптор файла, привязывающий структуру к файлу. Существенно то, что процедуры стандартного ввода/вывода написаны на основе примитивов системных вызовов. Основная функция библиотеки состоит в создании более удобного интерфейса и автоматической буферизации.
После открытия файла существует множество стандартных процедур ввода/ вывода для доступа к нему. Одна из них – процедура getc, считывающая одиночный символ, другая, putc, выводит один символ. Они используются следующим образом:
Описание
uses stdio;

(* Считать символ из __stream *)
function getc(__stream:pfile):integer;

(* Поместить символ в __stream *)
function putc(__c:integer;__stream:pfile):integer;
Можно поместить обе процедуры в цикл для копирования одного файла в другой:
var
с:integer;
istream, ostream:PFILE;

(* Открыть файл istream для чтения и файл ostream для записи *)
.
.
.
с := getc (istream);
while c<>-1 do
begin
putc (с, ostream);
с := getc (istream);
end;
Значение -1 возвращается функцией getc при достижении конца файла, поэтому тип возвращаемого функцией getc значения определен как integer.
На первый взгляд, функции getc и putc могут вызывать определенное беспокойство, поскольку они работают с одиночными символами, а это, как уже было продемонстрировано на примере системных вызовов, чрезвычайно неэффективно. Процедуры стандартного ввода/вывода избегают этой неэффективности при помощи изящного механизма буферизации, который работает следующим образом: первый вызов функции getc приводит к чтению из файла BUFSIZ символов при помощи системного вызова fdread. Данные находятся в буфере, созданном библиотекой и находящемся в пользовательском адресном пространстве. Функция getc возвращает только первый символ. Все остальные внутренние действия скрыты от вызывающей программы. Последующие вызовы функции getc поочередно возвращают символы из буфера. После того как при помощи функции getc программе будут переданы BUFSIZ символов и будет выполнен очередной вызов getc, из файла снова будет считан буфер целиком. Аналогичный механизм реализован в функции putc.
Такой подход весьма удобен, так как он освобождает программиста от беспокойства по поводу эффективности работы программы. Это также означает, что данные записываются большими блоками, и запись в файл будет производиться с задержкой (для терминалов сделаны специальные оговорки). Поэтому весьма неблагоразумно использовать для одного и того же файла и стандартные процедуры ввода/вывода, и системные вызовы, такие как fdread, fdwrite или fdseek. Это может привести к хаосу, если не представлять четко, что при этом происходит. С другой стороны, вполне допустимо смешивать системные вызовы и процедуры стандартного ввода/вывода для разных файлов.
Кроме механизма буферизации стандартный ввод/вывод предоставляет утилиты для форматирования и преобразования, например, функция printf обеспечивает форматированный вывод:
printf('Целое число %d'#10, [ival]);
Кстати, функция printf неявно осуществляет запись в стандартный вывод.
Вывод сообщений об ошибках при помощи функции writeln
Функция printf может использоваться для вывода диагностических ошибок. К сожалению, она осуществляет запись в стандартный вывод, а не в стандартный вывод диагностики. Тем не менее можно использовать для этого функцию writeln. Следующий фрагмент программы показывает, как можно это сделать:
uses stdio; (* Для определения stderr *)
.
.
writeln (stderr, 'Ошибка номер ', linuxerror);
Отличие между использованием writeln и вызовом printf заключается в параметре stderr, являющемся указателем на текстовый файл, автоматически связанный с потоком вывода стандартной диагностики.
Следующая процедура расширяет возможности использования функции writeln в более общей процедуре вывода сообщения об ошибке:
(* Функция notfound - вывести сообщение об ошибке и выйти *)

uses linux;

function notfound(progname, filename: string):integer;
begin
writeln(stderr, progname, ': файл ', filename, ' не найден');
halt(1);
end;
В последующих примерах для вывода сообщений об ошибках будет использована функция writeln, а не printf. Это обеспечит совместимость с большинством команд и программ, применяющих для диагностики стандартный вывод диагностики.
2.4. Системные вызовы и переменная linuxerror
Из вышеизложенного материала видно, что все описанные до сих пор системные вызовы файлового ввода/вывода могут завершиться неудачей. В этом случае возвращаемое значение всегда равно -1. Чтобы помочь программисту получить информацию о причине ошибки, система UNIX предоставляет глобальную целочисленную переменную, содержащую код ошибки. Значение кода ошибки связано с сообщением об ошибке, таким как no permission (нет доступа) или invalid argument (недопустимый аргумент). Полный список кодов и описаний ошибок приведен в Приложении 1. Текущее значение кода ошибки соответствует типу последней ошибки, произошедшей во время системного вызова.
Переменная, содержащая код ошибки, имеет имя linuxerror (сокращение от linux error number – номер ошибки в Linux). Программист может использовать переменную linuxerror в программе на языке Паскаль, подключив модуль linux.
Следующая программа использует вызов fdopen, и в случае его неудачного завершения использует функцию writeln для вывода значения переменной linuxerror:
(* Программа err1.pas - открывает файл с обработкой ошибок *)
uses linux;

var
fd:integer;
begin

fd := fdopen ('nonesuch', Open_RDONLY);
if fd=-1 then
writeln(stderr, 'Ошибка ', linuxerror);
end.
Если, например, файл nonesuch не существует, то код соответствующей ошибки в стандартной реализации UNIX будет равен 2; Так же, как и остальные возможные значения переменной linuxerror, этот код является значением определенной в модуле linux константы, в данном случае – константы Sys_ENOENT, имя которой является сокращением от no such entry (нет такого файла или каталога). Эти константы можно непосредственно использовать в программе.
При использовании переменной linuxerror следует проявлять осторожность, так как при следующем системном вызове ее значение не сбрасывается. Поэтому наиболее безопасно использовать linuxerror сразу же после неудачного системного вызова.
2.4.7. Подпрограмма perror
Кроме linuxerror UNIX обеспечивает библиотечную процедуру (не системный вызов) perror. Для большинства традиционных команд UNIX использование perror является стандартным способом вывода сообщений об ошибках. Она имеет единственный аргумент строчного типа и при вызове отправляет на стандартный вывод диагностики сообщение, состоящее из переданного ей строчного аргумента, двоеточия и дополнительного описания ошибки, соответствующей текущему значению переменной linuxerror. Важно, что сообщение об ошибке отправляется на стандартный вывод диагностики, а не в стандартный вывод.
В вышеприведенном примере можно заменить строку, содержащую вызов writeln на:
perror('Ошибка при открытии файла nonesuch');
Если файл nonesuch не существует, то функция perror выведет сообщение:
Ошибка при открытии файла nonesuch: No such file or directory
Упражнение 2.17. Напишите процедуры, выполняющие те же действия, что и примитивы доступа к файлам, описанные в этой главе, но вызывающие функцию perror при возникновении ошибок или исключительных ситуаций.
Глава 3. Работа с файлами
Файлы не определяются полностью содержащимися в них данными. Каждый файл UNIX содержит ряд простых дополнительных свойств, необходимых для администрирования этой сложной многопользовательской системы. В данной главе будут изучены дополнительные свойства и оперирующие ими системные вызовы.
3.1. Файлы в многопользовательской среде
3.1.1. Пользователи и права доступа
Для каждого файла в системе UNIX задан его владелец (owner – один из пользователей системы; обычно пользователь, создавший файл). Истинный идентификатор пользователя представлен неотрицательным числом user-id, сокращенно uid, которое связывается с файлом при его создании.
В типичной системе UNIX связанный с определенным именем пользователя идентификатор uid находится в третьем поле записи о пользователе в файле паролей, то есть в строке файла /etc/passwd/, которая идентифицирует пользователя в системе. Типичная запись
keith:x:35:10::/usr/keith:/bin/ksh
указывает, что пользователь keith имеет uid 35.
Поля в записи о пользователе в файле паролей разделяются двоеточием. Первое поле задает имя пользователя. Второе, в данном случае х, – это маркер пароля пользователя. В отличие от ранних версий UNIX сам зашифрованный пароль обычно находится в другом файле, отличающемся для разных систем. Как уже было показано, третье поле содержит идентификатор пользователя uid. В четвертом поле находится идентификатор группы пользователя по умолчанию – group-id, сокращенно gid; подробнее он будет рассмотрен ниже. Пятое поле – это необязательное поле комментария. Шестое задает домашний каталог пользователя. Последнее поле – полное имя программы, которая запускается после входа пользователя в систему. Например, /bin/ksh – одна из стандартных оболочек UNIX.
Фактически для идентификации пользователя в системе UNIX нужен только идентификатор user-id. Каждый процесс UNIX обычно связывается с идентификатором пользователя, который запустил его на выполнение. При этом процесс является просто экземпляром выполняемой программы. При создании файла система устанавливает его владельца на основе идентификатора uid, создающего файл процесса.
Владелец файла позже может быть изменен, но только суперпользователем или владельцем файла. Следует отметить, что суперпользователь имеет имя root и его идентификатор uid всегда равен 0.
Помимо владельца файлы могут быть связаны с группами пользователей (groups) которые представляют произвольный набор пользователей, благодаря чему становится возможным простой способ управления проектами, включающими несколько человек. Каждый пользователь принадлежит как минимум к одной группе.
Группы пользователей определяются в файле /etc/group. Каждая из них определена своим идентификатором gid, который, как и uid, является неотрицательным числом. Группа пользователя по умолчанию задается четвертым полем записи о нем в файле паролей.
Так же, как идентификатор пользователя uid, идентификатор группы gid пользователя наследуется процессом, который запускает пользователь. Поэтому при создании файла связанный с создающим его процессом идентификатор группы gid записывается наряду с идентификатором пользователя uid.
Действующие идентификаторы пользователей и групп
Необходимо сделать одно уточнение: создание файла определяется связанным с процессом действующим идентификатором пользователя euid (effective user-id). Хотя процесс может быть запущен одним пользователем (скажем, keith), при определенных обстоятельствах он может получить права доступа другого пользователя (например, dina). Вскоре будет показано, как это можно осуществить. Идентификатор пользователя, запустившего процесс, называется истинным идентификатором пользователя (real user-id, сокращенно ruid) этого процесса. Разумеется, в большинстве случаев действующий и истинный идентификаторы пользователя совпадают.
Аналогично с процессом связывается действующий идентификатор группы (effective group-id, сокращенно egid), который может отличаться от истинного идентификатора группы (real group-id, сокращенно rgid).
3.1.2. Права доступа и режимы файлов
Владелец обладает исключительными правами обращения с файлами. В частности, владелец может изменять связанные с файлом права доступа (permissions). Права доступа определяют возможности доступа к файлу других пользователей. Они затрагивают три группы пользователей:
1) владелец файла;
2) все пользователи, кроме владельца файла, принадлежащие к связанной с файлом группе;
3) все пользователи, не входящие в категории 1 или 2.
Для каждой категории пользователей существуют три основных типа прав доступа к файлам. Они определяют, может ли пользователь определенной категории выполнять:
• чтение из файла;
• запись в файл;
• запуск файла на выполнение. В этом случае файл обычно является программой или последовательностью команд оболочки.
Как обычно, суперпользователь выделен в отдельную категорию и может оперировать любыми файлами, независимо от связанных с ними прав чтения, записи или выполнения.
Система хранит связанные с файлом права доступа в битовой маске, называемой кодом доступа к файлу (file mode). Хотя модуль linux и определяет символьные имена для битов прав доступа, большинство программистов все еще предпочитает использовать восьмеричные постоянные, приведенные в табл. 3.1 – при этом символьные имена являются относительно недавним и весьма неудобным нововведением. Следует обратить внимание, что в языке С восьмеричные постоянные всегда начинаются с нуля, иначе компилятор будет расценивать их как десятичные. В Паскале же для записи восьмеричных чисел удобно использовать функцию octal, параметром которой является восьмеричное число, записанное десятичными числами, а результатом – значение в десятеричной системе:
Описание
uses linux;

Function Octal(l:longint):longint;
Таблица 3.1. Восьмеричные значения для прав доступа к файлам
Восьмеричное значение
Символьное обозначение
Значение
0400
STAT_IRUSR
Владелец имеет доступ для чтения
0200
STAT_IWUSR
Владелец имеет доступ для записи
0100
STAT_IXUSR
Владелец может выполнять файл
0040
STAT_IRGRP
Группа имеет доступ для чтения
0020
STAT_IWGRP
Группа имеет доступ для записи
0010
STAT_IXGRP
Группа может выполнять файл
0004
STAT_IROTH
Другие пользователи имеют доступ для чтения
0002
STAT_IWOTH
Другие пользователи имеют доступ для записи
0001
STAT_IXOTH
Другие пользователи могут выполнять файл
Из таблицы легко увидеть, что можно сделать файл доступным для чтения всем типам пользователей, сложив 0400 (доступ на чтение для владельца), 040 (доступ на чтение для членов группы файла) и 04 (доступ на чтение для всех остальных пользователей). В итоге это дает код доступа к файлу 0444. Такой код может быть получен и при помощи побитовой операции ИЛИ (or) для соответствующих символьных представлений; например, 0444 эквивалентен выражению:
STAT_IRUSR or STAT_IRGRP or STAT_IROTH
Поскольку все остальные значения из таблицы не включены, код доступа 0444 также означает, что никто из пользователей, включая владельца файла, не может получить доступ к файлу на запись или выполнение.
Чтобы устранить это неудобство, можно использовать более одного восьмеричного значения, относящегося к одной категории пользователей. Например, сложив 0400, 0200 и 0100, получим в сумме значение 0700, которое показывает, что владелец файла может читать его, производить в него запись и запускать файл на выполнение.
Поэтому чаще встречается значение кода доступа:
0700 + 050 + 05 = 0755
Это означает, что владелец файла может читать и писать в файл или запускать файл на выполнение, в то время как права членов группы, связанной с файлом, и все остальных пользователей ограничены только чтением или выполнением файла.
Легко понять, почему программисты UNIX предпочитают использовать восьмеричные постоянные, а не имена констант из модуля linux, когда просто значение 0755 представляется выражением:
STAT_IRUSR or STAT_IWUSR or STAT_IXUSR or STAT_IRGRP or STAT_IXGRP or STAT_IROTH or STAT_IXOTH
Рассказ о правах доступа еще не закончен. В следующем подразделе будет продемонстрировано, как три других типа прав доступа влияют на файлы, содержащие исполняемые программы. В отношении доступа к файлам важно то, что каждый каталог UNIX, почти как обычный файл, имеет набор прав доступа, которые влияют на доступность файлов в каталоге. Этот вопрос будет подробно рассмотрен в главе 4.
Упражнение 3.1. Что означают следующие значения прав доступа: 0761, 0777, 0555, 0007 и 0707?
Упражнение 3.2. Замените восьмеричные значения из упражнения 3.1 эквивалентными символьными выражениями.
Упражнение 3.3. Напишите процедуру lsoct, которая переводит набор прав доступа из формы, получаемой на выходе команды ls (например, rwxr-xr-x) в эквивалентные восьмеричные значения. Затем напишите обратную процедуру octls.
3.1.3. Дополнительные права доступа для исполняемых файлов
Существуют еще три типа прав доступа к файлам, задающие особые атрибуты и обычно имеющие смысл только в том случае, если файл содержит исполняемую программу. Соответствующие восьмеричные значения и символьные имена также соответствуют определенным битам в коде доступа к файлу и обозначают следующее:
04000 STAT_ISUID Задать user-id при выполнении
02000 STAT_ISGID Задать group-id при выполнении
01000 STAT_ISVTX Сохранить сегмент кода (бит фиксации)
Если установлен флаг доступа STAT_ISUID, то при запуске на выполнение находящейся в файле программы система задает в качестве действующего идентификатора пользователя полученного процесса не идентификатор пользователя, запустившего процесс (как обычно), а идентификатор владельца файла. Процессу при этом присваиваются права доступа владельца файла, а не пользователя, запустившего процесс.
Подобный механизм может использоваться для управления доступом к критическим данным. Конфиденциальная информация может быть защищена от публичного доступа или изменения при помощи стандартных прав доступа на чтение/запись/выполнение. Владелец файла создает программу, которая будет предоставлять ограниченный доступ к файлу. Затем для файла программы устанавливается флаг доступа STAT_ISUID, что позволяет другим пользователям получать ограниченный доступ к файлу только при помощи данной программы. Очевидно, программа должна быть написана аккуратно во избежание случайного и умышленного нарушения защиты.1
Классический пример этого подхода представляет программа passwd. Администратора системы ожидают неприятности, если он позволит всем пользователям выполнять запись в файл паролей. Тем не менее все пользователи должны иногда изменять этот файл при смене своего пароля. Решить проблему позволяет программа passwd, так как ее владельцем является суперпользователь и для нее установлен флаг STAT_ISUID.
Не столь полезна установка флага STAT_ISGID, которая выполняет те же функции для идентификатора группы файла group-id. Если указанный флаг установлен, то при запуске файла на выполнение получившийся процесс получает действующий идентификатор группы egid владельца файла, а не пользователя, который запустил программу на выполнение.
Исторически бит STAT_ISVTX обычно использовался для исполняемых файлов и назывался флагом сохранения сегмента кода (save-text-image), или битом фиксации (sticky bit). В ранних версиях системы, если для файла был установлен этот бит, при его выполнении код программы оставался в файле подкачки до выключения системы. Поэтому при следующем запуске программы системе не приходилось искать файл в структуре каталогов системы, а можно было просто и быстро переместить программу в память из файла подкачки. В современных системах UNIX указанный бит является избыточным, и в спецификации XSI бит STAT_ISVTX определен только для каталогов. Использование STAT_ISVTX будет подробнее рассмотрено в главе 4.
Упражнение 3.4. Следующие примеры показывают, как команда ls выводит на экран права доступа set-user-id и group-id, соответственно:
r-sr-xr-x
r-xr-sr-x
При помощи команды ls -l найдите в каталогах /bin, /etc и /usr/bin файлы с необычными правами доступа (если это командные файлы оболочки и у вас есть право на чтение этих файлов, посмотрите, что они делают и надежно ли они защищены). Более опытные читатели могут ускорить поиск, воспользовавшись программой grep. Если вам не удастся найти файлы с необычными правами доступа, объясните, почему это произошло.
3.1.4. Маска создания файла и системный вызов umask
Как уже было отмечено в главе 2, первоначально права доступа к файлу задаются в момент его создания при помощи вызова fdcreat или fdopen в расширенной форме, например:
filedes := fdopen('datafile', Open_CREAT, ocatl(0644));
С каждым процессом связано значение, называемое маской создания файла (file creation mask). Эта маска используется для автоматического выключения битов прав доступа при создании файлов, независимо от режима, заданного соответствующим вызовом fdcreat или fdopen. Это полезно для защиты всех создаваемых за время существования процесса файлов, так как предотвращается случайное включение лишних прав доступа.
Основная идея просматривается четко: если в маске создания файлов задан какой-либо бит доступа, то при создании файлов он всегда остается выключенным. Биты в маске могут быть установлены при помощи тех же восьмеричных постоянных, которые были описаны ранее для кода доступа к файлам, хотя при этом могут использоваться только основные права доступа на чтение, запись и выполнение. Экзотические права доступа, такие как STAT_ISUID, не имеют смысла для маски создания файла.
Таким образом, с точки зрения программиста, оператор
filedes := fdopen(pathname, Open_CREAT, mode);
эквивалентен оператору
filedes := fdopen(pathname, Open_CREAT, (not mask) and mode);
где переменная mask содержит текущее значение маски создания файла, not – это оператор побитового отрицания, а and – оператор побитового И.
Например, если значение маски равно 04+02+01=07, то права доступа, обычно задаваемые этими значениями, при создании файла выключаются. Поэтому файл, создаваемый при помощи оператора
filedes := fdopen(pathname, Open_CREAT, octal(0644));
в действительности будет иметь код доступа 0640. Это означает, что владелец файла и пользователи из связанной с файлом группы смогут использовать файл, а пользователи всех остальных категорий не будут иметь доступа к нему.
Маску создания файла можно изменить при помощи системного вызова umask.
Описание
uses linux;

Function Umask(Mask:Integer):Integer;
Например:
var
oldmask:integer;
.
.
.
oldmask := umask(octal(022));
Значение octal(022) запрещает присваивание файлу прав доступа на запись всем, кроме владельца файла. После вызова в переменную oldmask будет помещено предыдущее значение маски.
Поэтому, если вы хотите быть абсолютно уверены, что файлы создаются именно с кодами доступа, заданными в вызовах fdcreat или fdopen, вам следует вначале вызвать umask с нулевым аргументом. Так как все биты в маске создания файла будут равны нулю, ни один из битов в коде доступа, передаваемом вызовам fdopen или fdcreat, не будет сброшен. В следующем примере этот подход используется для создания файла с заданным кодом доступа, а затем восстанавливается старая маска создания файла. Программа возвращает дескриптор файла, полученный в результате вызова fdopen.
uses linux,stdio;

function specialcreat(pathname:string;mode:longint):integer;
var
oldu,filedes:integer;
begin
(* Установить маску создания файла равной нулю *)
oldu:=umask(0);
if oldu = -1 then
begin
perror('Ошибка сохранения старой маски');
specialcreat:=-1;
exit;
end;
(* Создать файл *)
filedes:=fdopen(pathname, Open_WRONLY or Open_CREAT or Open_EXCL, mode);
if (filedes = -1) then
perror ('Ошибка открытия файла');
(* Восстановить прежний режим доступа к файлу *)
if (umask (oldu) = -1) then
perror ('Ошибка восстановления старой маски');
(* Вернуть дескриптор файла *)
specialcreat:=filedes;
end;
3.1.5. Вызов fdopen и права доступа к файлу
Если вызов fdopen используется для открытия существующего файла на чтение или запись, то система проверяет, разрешен ли запрошенный процессом режим доступа (только для чтения, только для записи или для чтения-записи), проверяя права доступа к файлу. Если режим не разрешен, вызов fdopen вернет значение -1, указывающее на ошибку, а переменная linuxerror будет содержать код ошибки Sys_EACCES, означающий: нет доступа (permission denied).
Если для создания файла используется расширенная форма вызова fdopen, то использование флагов Open_CREAT, Open_TRUNC и Open_EXCL позволяет по-разному работать с существующими файлами. Примеры использования вызова fdopen с заданием прав доступа к файлу:
filedes := fdopen(pathname, Open_WRONLY or Open_CREAT or Open_TRUNC, octal(0600));
и:
filedes := fdopen(pathname, Open_WRONLY or Open_CREAT or Open_EXCL, octal(0600));
В первом примере, если файл существует, он будет усечен до нулевой для в случае, когда права доступа к файлу разрешают вызывающему процессу доступ на запись. Во втором – вызов fdopen завершится ошибкой, если файл существу независимо от заданных прав доступа к нему, а переменная linuxerror будет содержать код ошибки Sys_EEXIST.
Упражнение 3.5.
А. Предположим, что действующий идентификатор пользовать euid процесса равен 100, а его действующий идентификатор группы egid равен 200. Владельцем файла testfile являет пользователь с идентификатором 101, а идентификатор группы файла gid равен 200. Для каждого возможного режима доступа (только для чтения, только для записи, для записи-чтения) определите, будет ли успешным вызов open, если файл testfile имеет следующие права доступа:
rwxr-xrwx r-xrwxr-x rwx--x--- rwsrw-r--
--s--s--x ---rwx--- ---r-x--x
В. Что произойдет, если real user-id (действующий идентификатор пользователя) процесса равен 101, a real group-id (действующий идентификатор группы) равен 201?
3.1.6. Определение доступности файла при помощи вызова access
Системный вызов access определяет, может ли процесс получить доступ к файлу в соответствии с истинным (real), а не действующим (effective) идентификатором пользователя (и группы) процесса. Такой вызов позволяет процессу, получившему права при помощи бита STAT_ISUID, определить настоящие права пользователя, запустившего это процесс, что облегчает написание безопасных программ.
Описание
uses linux;

Function Access(PathName:Pathstr; AMode:integer):Boolean;
Как мы уже видели, существует несколько режимов доступа к файлу, поэтому параметр amode содержит значение, указывающее на интересующий нас метод доступа. Параметр amode может принимать следующие значения, определенные в модуле linux:
R_OK – имеет ли вызывающий процесс доступ на чтение;
W_ОК – имеет ли вызывающий процесс доступ на запись;
Х_ОК – может ли вызывающий процесс выполнить файл.
Аргумент amode не конкретизирует, к какой категории пользователей относится вопрос, так как вызов access сообщает права доступа к файлу конкретного пользователя, имеющего ruid и rgid текущего процесса. Переменная amode также может принимать значение F_OK, в этом случае проверяется лишь существование файла. Как обычно, параметр pathname задает имя файла.
Значение, возвращаемое вызовом access, либо равно нулю (доступ разрешен) или -1 (доступ не разрешен). В последнем случае переменная linuxerror будет содержать значение кода ошибки. Значение Sys_EACCES, например, означает, что запрошенный режим доступа к файлу не разрешен, а значение Sys_ENOENT показывает, что указанного файла просто не существует.
Следующий пример программы использует вызов access для проверки, разрешено ли пользователю чтение файла при любом значении бита STAT_ISUID исполняемого файла этой программы:
(* Пример использования вызова access *)
uses linux,stdio;

const
filename = 'afile';

begin
if not access (filename, R_OK) then
begin
writeln(stderr, 'Пользователь не имеет доступа на чтение к файлу ', filename);
halt(1);
end;
writeln(filename, ' доступен для чтения, продолжаем');
(* Остальная программа *)
end.
Упражнение 3.6. Напишите программу whatable, которая будет сообщать, можете ли вы выполнять чтение, запись или выполнение заданного файла. Если доступ невозможен, программа whatable должна сообщать почему (используйте коды, ошибок, возвращаемых в переменной linuxerror).
3.1.7. Изменение прав доступа при помощи вызова chmod
Описание
uses linux;

Function Chmod(PathName:Pathstr; NewMode:Longint):Boolean;
Для изменения прав доступа к существующему файлу применяется системный вызов chmod. Вызов разрешен владельцу файла или суперпользователю.
Параметр pathname указывает имя файла. Параметр newmode содержит новый код доступа файла, образованный описанным в первой части главы способом.
Пример использования вызова chmod:
if not chmod(pathname, octal(0644)) then
perror('Ошибка вызова chmod');
Упражнение 3.7. Напишите программу setperm, которая имеет два аргумента командной строки. Первый – имя файла, второй – набор прав доступа в восьмеричной форме или в форме, выводимой команда ls. Если файл существует, то программа setperm должна попытаться поменять права доступа к файлу на заданные. Используйте процедуру lsoct, которую вы разработали в упражнении 3.3.
3.1.8. Изменение владельца при помощи вызова chown
Вызов chown используется для изменения владельца и группы файла.
Описание
uses linux;

Function Chown(PathName:Pathstr; Owner_id,Group_id:Longint):Boolean;
Например:
Uses linux;

Var UID,GID : Longint;
F : Text;

begin

Writeln ('This will only work if you are root.');
Write ('Enter a UID : ');readln(UID);
Write ('Enter a GID : ');readln(GID);
Assign (f,'test.txt');
Rewrite (f);
Writeln (f,'The owner of this file should become : ');
Writeln (f,'UID : ',UID);
Writeln (f,'GID : ',GID);
Close (F);
if not Chown ('test.txt',UID,GID) then
if LinuxError=Sys_EPERM then
Writeln ('You are not root !')
else
Writeln ('Chmod failed with exit code : ',LinuxError)
else
Writeln ('Changed owner successfully !');
end.
Вызов имеет три аргумента: pathname, указывающий имя файла, owner_id, задающий нового владельца, и group_id, задающий новую группу. Возвращаемое значение retval равно true в случае успеха и false – в случае ошибки.
В системе, удовлетворяющей спецификации XSI, вызывающий процесс должен быть процессом суперпользователя или владельца файла (точнее, действующий идентификатор вызывающего процесса должен либо совпадать с идентификатором владельца файла, либо быть равным 0). При несанкционированной попытке изменить владельца файла выставляется код ошибки Sys_EPERM.
Поскольку вызов chown разрешен текущему владельцу файла, обычный пользователь может передать свой файл другому пользователю. При этом пользователь не сможет впоследствии отменить это действие, так как идентификатор пользователя уже не будет совпадать с идентификатором пользователя файла. Следует также обратить внимание на то, что при смене владельца файла в целях предотвращения неправомерного использования вызова chown для получения системных полномочий, сбрасываются права доступа set-user-id и set-group-id. (Что могло бы произойти, если бы это было не так?)
3.2. Файлы с несколькими именами
Любой файл UNIX может иметь несколько имен. Другими словами, один и тот же набор данных может быть связан с несколькими именами UNIX без необходимости создания копий файла. Поначалу это может показаться странным, но для экономии свободного пространства на диске и увеличения числа пользователей, использующих один и тот же файл, – весьма полезно.
Каждое такое имя называется жесткой ссылкой (hard link). Число связанных с файлом ссылок называется счетчиком ссылок (link count).
Новая жесткая ссылка создается при помощи системного вызова link, а существующая жесткая ссылка может быть удалена при помощи системного вызова unlink.
Следует отметить полную равноправность жестких ссылок на файл и настоящего имени файла. Нет способа отличить настоящее имя файла от созданной позднее жесткой ссылки. Это становится очевидным, если рассмотреть организацию файловой системы, – см. главу 4.
3.2.1. Системный вызов link
Описание
uses linux;

Function Link(original_path, New_Path:pathstr):Boolean;
Первый параметр, original_path, является указателем на массив символов, содержащий полное имя файла в системе UNIX. Он должен задавать существующую ссылку на файл, то есть фактическое имя файла. Второй параметр, new_path, задает новое имя файла или ссылку на файл, но файл, заданный параметром new_path, еще не должен существовать.
Системный вызов link возвращает значение true в случае успешного завершения и false – в случае ошибки. В последнем случае новая ссылка на файл не будет создана.
Например, оператор
link('/usr/keith/chap.2', '/usr/ben/2.chap');
создаст новую ссылку /usr/ben/2.chap на существующий файл /usr/keith/chap.2. Теперь к файлу можно будет обратиться, используя любое из имен. Пример показывает, что ссылка не обязательно должна находиться в одном каталоге с файлом, на который она указывает.
3.2.2. Системный вызов unlink
В разделе 2.1.13 мы представили системный вызов unlink в качестве простого способа удаления файла из системы. Например:
unlink('/tmp/scratch');
удалит файл /tmp/scratch.
Фактически системный вызов unlink просто удаляет указанную ссылку и уменьшает счетчик ссылок (link count) файла на единицу. Данные в файле будут безвозвратно потеряны только после того, как счетчик ссылок на него станет равным нулю, и он не будет открыт ни в одной программе. В этом случае занятые файлом блоки на диске добавляются к поддерживаемому системой списку свободных блоков. Хотя данные могут еще существовать физически в течение какого-то времени, восстановить их будет невозможно. Так как многие файлы иметь лишь одну ссылку – принятое имя файла, удаление файла является обычным результатом вызова unlink. И наоборот, если счетчик ссылок не уменьшится до нуля, то данные в файле останутся нетронутыми, и к ним можно будет обратиться при помощи других ссылок на файл.
Следующая короткая программа переименовывает файл, вначале создавая на него ссылку с новым именем и удаляя в случае успеха старую ссылку на файл. Это упрощенная версия стандартной команды UNIX mv:
(* Программа move - переименование файла *)
uses linux,stdio;

const
usage = 'Применение: move файл1 файл2';

(*
* Программа использует аргументы командной строки,
* передаваемые обычным способом.
*)
begin
if (paramcount <> 2) then
begin
writeln(stderr, usage);
halt(1);
end;
if not link(paramstr(1), paramstr(2)) then
begin
perror('Ошибка в вызове link');
halt(1);
end;
if not unlink (argv[1]) then
begin
perror('Ошибка в вызове unlink');
unlink(paramstr(2));
halt(1);
end;
writeln('Успешное завершение');
halt(0);
end.
До сих пор не было упомянуто взаимодействие вызова unlink и прав доступа к файлу, связанных с аргументом, задающим имя файла. Это объясняется тем, что права просто не влияют на вызов unlink. Вместо этого успешное или неуспешное завершение вызова unlink определяется правами доступа к содержащему файл каталогу. Эту тема будет рассмотрена в главе 4.
3.2.3. Системный вызов frename
Фактически задачу предыдущего примера можно выполнить гораздо легче, используя системный вызов frename, который был добавлен в систему UNIX сравнительно недавно. Системный вызов frename может использоваться для переименования как обычных файлов, так и каталогов.
Описание
uses linux;

Function Frename(oldpathname, newpathname:Pchar):Boolean;
Function Frename(oldpathname, newpathname:String):Boolean;
Файл, заданный аргументом oldpathname, получает новое имя, заданное вторым параметром newpathname. Если файл с именем newpathname уже существует, то перед переименованием файла oldpathname он удаляется.
Упражнение 3.8. Напишите свою версию команды rm, используя вызов unlink. Ваша программа должна проверять, имеет ли пользователь право записи в файл при помощи вызова access и в случае его отсутствия запрашивать подтверждение перед попыткой удаления ссылки на файл. (Почему?) Будьте осторожны при тестировании программы!
3.2.4. Символьные ссылки
Существует два важных ограничения на использование вызова link. Обычный пользователь не может создать ссылку на каталог (в некоторых версиях UNIX и суперпользователь не имеет права этого делать), и невозможно создать ссылку между различными файловыми системами (file systems). Файловые системы являются основными составляющими всей файловой структуры UNIX и будут изучаться более подробно в главе 4.
Для преодоления этих ограничений спецификация XSI поддерживает понятие символьных ссылок (symbolic links). Символьная ссылка в действительности представляет собой файл, содержащий вместо данных путь к файлу, на который указывает ссылка. Можно сказать, что символьная ссылка является указателем на другой файл.
Для создания символьной ссылки используется системный вызов symlink:
Описание
uses linux;

Function SymLink(realname, symname:pathstr):Boolean;
После завершения вызова symlink создается файл symname, указывающий на файл realname. Если возникает ошибка, например, если файл с именем symname уже существует, то вызов symlink возвращает значение false. В случае успеха вызов возвращает истинное значение.
Пример использования SymLink:
Uses linux;

Var F : Text;
S : String;

begin
Assign (F,'test.txt');
Rewrite (F);
Writeln (F,'This is written to test.txt');
Close(f);
{ new.txt and test.txt are now the same file }
if not SymLink ('test.txt','new.txt') then
writeln ('Error when symlinking !');
{ Removing test.txt still leaves new.txt
Pointing now to a non-existent file ! }
If not Unlink ('test.txt') then
Writeln ('Error when unlinking !');
Assign (f,'new.txt');
{ This should fail, since the symbolic link
points to a non-existent file! }
{$i-}
Reset (F);
{$i+}
If IOResult=0 then
Writeln ('This shouldn''t happen');
{ Now remove new.txt also }
If not Unlink ('new.txt') then
Writeln ('Error when unlinking !');
end.
Если файл символьной ссылки открывается при помощи fdopen, то системный вызов fdopen корректно прослеживает путь к файлу realname. Если необходимо считать данные из самого файла symname, то нужно использовать системный вызов readlink.
Описание
uses linux;

Function ReadLink(sympath, buffer:pchar; bufsize:longint):longint;
Function ReadLink(name:pathstr):pathstr;
Системный вызов readlink вначале открывает файл sympath, затем читает его содержимое в переменную buffer, и, наконец, закрывает файл sympath. К сожалению, спецификация XSI не гарантирует, что строка в переменной buffer будет заканчиваться нулевым символом. Возвращаемое вызовом readlink значение равно числу символов в буфере или -1 – в случае ошибки.
Следует сделать предупреждение, касающееся использования и прослеживания символьных ссылок. Если файл, на который указывает символьная ссылка, удаляется, то при попытке доступа к файлу при помощи символьной ссылки выдается ошибка, которая может ввести вас в заблуждение. Программа все еще сможет «видеть» символьную ссылку, но, к сожалению, вызов fdopen не сможет проследовать по указанному в ссылке пути и вернет ошибку, установив значение переменной linuxerror равным Sys_EEXIST.
3.2.5. Еще об именах файлов
Для выделения из имени файла его частей можно воспользоваться функциями:
Описание
uses linux;

Function BaseName(Const Path;Const Suf:Pathstr):Pathstr;
Function DirName(Const Path:Pathstr):Pathstr;
Procedure FSplit(const Path:PathStr; var Dir:DirStr; Var Name:NameStr;
Var Ext:ExtStr);
BaseName выделяет из полного пути Path имя файла, обрезая окончание Suf, если оно существует. Для каталогов завершающий слэш всегда убирается (за исключением корневого каталога).
DirName возвращает часть пути, соответствующую имени каталога. Это будет часть параметра Path до завершающего слэша, или ничего в его отсутствие.
FSplit разбивает полное имя файла на 3 части: путь Path, имя Name и расширение ext. Расширением считаются все символы, следующие за последней точкой.
Например:
uses Linux;

var
Path,Name,Ext : string;

begin
FSplit(ParamStr(1),Path,Name,Ext);
WriteLn('Split ',ParamStr(1),' in:');
WriteLn('Path : ',Path);
WriteLn('Name : ',Name);
WriteLn('Extension: ',Ext);
end.
3.3. Получение информации о файле: вызов fstat
До сих пор были лишь рассмотрены вопросы, как можно установить или изменить основные связанные с файлами свойства. Системный вызов fstat позволяет процессу определить значения этих свойств в существующем файле.
Описание
uses linux;

Function FStat(Path:Pathstr;Var Info:tstat):Boolean;
Function FStat(Fd:longint;Var Info:tstat):Boolean;
Function FStat(var F:Text;Var Info:tstat):Boolean;
Function FStat(var F:File;Var Info:tstat):Boolean;
Function LStat(Path:Pathstr; Var Info:tstat):Boolean;
Системный вызов fstat имеет два аргумента: первый из них – path, как обычно, указывает на полное имя файла. Второй аргумент info является ссылкой на структуру tstat (stat). Эта структура после успешного вызова будет содержать связанную с файлом информацию. Вместо имени файла может также использоваться его дескриптор или файловая переменная.
.
.
.
var
s:tstat;
filedes:integer;
retval:boolean;

filedes := fdopen('/tmp/dina', Open_RDWR);

(* Структура s может быть заполнена при помощи вызова ... *)
retval := fstat('/tmp/dina', s);

/* ... или */
retval := fstat(filedes, @s);
Определение структуры tstat находится в модуле linux и включает следующие элементы:
stat=record
dev : word;
pad1 : word;
ino : longint;
mode : word;
nlink : word;
uid : word;
gid : word;
rdev : word;
pad2 : word;
size : longint;
blksze : Longint;
blocks : Longint;
atime : Longint;
unused1 : longint;
mtime : Longint;
unused2 : longint;
ctime : Longint;
unused3 : longint;
unused4 : longint;
unused5 : longint;
end;
Системный вызов lstat получает информацию о символьной ссылке. Например:
uses linux;

var f : text;
i : byte;
info : stat;

begin
{ Make a file }
assign (f,'test.fil');
rewrite (f);
for i:=1 to 10 do writeln (f,'Testline # ',i);
close (f);
{ Do the call on made file. }
if not fstat ('test.fil',info) then
begin
writeln('Fstat failed. Errno : ',linuxerror);
halt (1);
end;
writeln;
writeln ('Result of fstat on file ''test.fil''.');
writeln ('Inode : ',info.ino);
writeln ('Mode : ',info.mode);
writeln ('nlink : ',info.nlink);
writeln ('uid : ',info.uid);
writeln ('gid : ',info.gid);
writeln ('rdev : ',info.rdev);
writeln ('Size : ',info.size);
writeln ('Blksize : ',info.blksze);
writeln ('Blocks : ',info.blocks);
writeln ('atime : ',info.atime);
writeln ('mtime : ',info.mtime);
writeln ('ctime : ',info.ctime);

If not SymLink ('test.fil','test.lnk') then
writeln ('Link failed ! Errno :',linuxerror);

if not lstat ('test.lnk',info) then
begin
writeln('LStat failed. Errno : ',linuxerror);
halt (1);
end;
writeln;
writeln ('Result of fstat on file ''test.lnk''.');
writeln ('Inode : ',info.ino);
writeln ('Mode : ',info.mode);
writeln ('nlink : ',info.nlink);
writeln ('uid : ',info.uid);
writeln ('gid : ',info.gid);
writeln ('rdev : ',info.rdev);
writeln ('Size : ',info.size);
writeln ('Blksize : ',info.blksze);
writeln ('Blocks : ',info.blocks);
writeln ('atime : ',info.atime);
writeln ('mtime : ',info.mtime);
writeln ('ctime : ',info.ctime);
{ Remove file and link }
erase (f);
unlink ('test.lnk');
end.
Элементы структуры stat имеют следующие значения:
– dev, ino
Первый из элементов структуры описывает логическое устройство, на котором находится файл, а второй задает номер индексного дескриптора (inode number), который вместе с dev однозначно определяет файл. Фактически и dev, и ino относятся к низкоуровневому управлению структурой файлов UNIX. Эти понятия будут рассмотрены в следующей главе.
– mode
Этот элемент задает режим доступа к файлу и позволяет программисту вычислить связанные с файлом права доступа. Здесь следует сделать предостережение. Значение, содержащееся в переменной mode, также дает информацию о типе файла, и только младшие 12 бит относятся к правам доступа. Это станет очевидно в главе 4.
– nlink
Число ссылок, указывающих на этот файл (другими словами, число различных имен файла, так как жесткие ссылки неотличимы от «настоящего» имени). Это значение обновляется при каждом системном вызове link и unlink.
– uid, gid
Идентификаторы пользователя uid и группы gid файла. Первоначально устанавливаются вызовом fdcreat и изменяются системным вызовом chown
– rdev
Этот элемент имеет смысл только в случае использования файла для описания устройства. На него пока можно не обращать внимания.
– size
Текущий логический размер файла в байтах. Нужно понимать, что способ хранения файла определяется реальными параметрами устройства, и поэтому физический размер занимаемого пространства может быть больше, чем логический размер файла. Элемент size изменяется при каждом вызове fdwrite в конце файла.
– atime
Содержит время последнего чтения из файла (хотя первоначальные вызов fdcreat и fdopen устанавливают это значение).
– mtime
Указывает время последней модификации файла – изменяется при каждом вызове fdwrite для файла.
– ctime
Содержит время последнего изменения информации, возвращаемой в структуре stat. Это время изменяется системными вызовами link (меняется элемент nlink), chmod (меняется mode) и fdwrite (меняется mtime и, возможно, size).
– blksize
Содержит размер блока ввода/вывода, зависящий от настроек системы. Для некоторых систем этот параметр может различаться для разных файлов.
– blocks
Содержит число физических блоков, занимаемых определенным файлом.
Системный вызов utime позволяет установить время доступа и модификации файла. Структура utimbuf содержит два поля, actime и modtime, оба типа Longint. Они должны быть заполнены значениями времени в секундах с 1.1.1970 г. относительно последнего времени доступа и последнего времени модификации.
Описание
uses linux;

Function Utime(path:pathstr; utim:utimbuf):Boolean;
Например:
Uses linux;

Var utim : utimbuf;
year,month,day,hour,minute,second : Word;

begin
{ Set access and modification time of executable source }
GetTime (hour,minute,second);
GetDate (year,month,day);
utim.actime:=LocalToEpoch(year,month,day,hour,minute,second);
utim.modtime:=utim.actime;
if not Utime('ex25.pp',utim) then
writeln ('Call to UTime failed !')
else
begin
Write ('Set access and modification times to : ');
Write (Hour:2,':',minute:2,':',second,', ');
Writeln (Day:2,'/',month:2,'/',year:4);
end;
end.
Следующий пример – процедура filedata выводит данные, связанные с файлом, определяемым переменной pathname. Пример сообщает размер файла, идентификатор пользователя, группу файла, а также права доступа к файлу.
Чтобы преобразовать права доступа к файлу в удобочитаемую форму, похожую на результат, выводимый командой ls, был использован массив octarray чисел типа integer, содержащий значения для основных прав доступа, и массив символов perms, содержащий символьные эквиваленты прав доступа.
(* Процедура filedata выводит данные о файле *)
uses linux;

(*
* Массив octarray используется для определения
* установки битов прав доступа.
*)
const
octarray:array[0..8] of integer= (
0400, 0200, 0100,
0040, 0020, 0010,
0004, 0002, 0001);

(*
* Мнемонические коды для прав доступа к файлу,
* длиной 10 символов, включая нулевой символ в конце строки.
*)
const
perms:pchar = 'rwxrwxrwx';

function filedata(pathname:string):integer;
var
statbuf:tstat;
descrip:array [0..9] of char;
j:integer;
begin
if not fstat (pathname, statbuf) then
begin
writeln('Ошибка вызова stat для ', pathname);
filedata:=-1;
exit;
end;

(* Преобразовать права доступа в удобочитаемую форму *)
for j:=0 to 8 do
begin
(*
* Проверить, установлены ли права доступа
* при помощи побитового И
*)
if (statbuf.mode and octal(octarray[j]))<>0 then
descrip[j] := perms[j]
else
descrip[j] := '-';
end;
descrip[9] := #0; (* задать строку *)
(* Вывести информацию о файле *)
writeln(#10'Файл ', pathname, ':');
writeln('Размер ',statbuf.size,' байт');
writeln('User-id ',statbuf.uid,', Group-id ',statbuf.gid,#10);
writeln('Права доступа: ', descrip);
filedata:=0;
end;
Более полезным инструментом является следующая программа lookout. Она раз в минуту проверяет, изменился ли какой-либо из файлов из заданного списка, опрашивая время модификации каждого из файлов (mtime). Это утилита, которая предназначена для запуска в качестве фонового процесса.1
(* Программа lookout сообщает об изменении файла *)

uses linux, stdio;

const
MFILE=10;

var
sb:tstat;
j:integer;
last_time:array [1..MFILE] of longint;

procedure sleep(t:longint);cdecl;external 'c';

procedure cmp(name:string;last:longint);
begin
(*
* Проверять время изменения файла,
* если можно считать данные о файле.
*)
if not fstat(name,sb) or (sb.mtime <> last) then
begin
writeln('lookout: файл ',name,' изменился');
halt(0);
end;
end;

begin
if (paramcount < 1) then
begin
writeln('Применение: lookout имя_файла ...');
halt(1);
end;
if (paramcount > MFILE) then
begin
writeln('lookout: слишком много имен файлов');
halt (1);
end;
(* Инициализация *)
for j:=1 to paramcount do
begin
if not fstat(paramstr(j), sb) then
begin
writeln ('lookout: ошибка вызова stat для ', paramstr(j));
halt(1);
end;
last_time[j]:=sb.mtime;
end;
(* Повторять до тех пор, пока файл не изменится *)
while true do
begin
for j:=1 to paramcount do
cmp(paramstr(j), last_time[j]);
(*
* Остановиться на 60 секунд.
* Функция 'sleep' стандартная
* библиотечная процедура UNIX.
*)
sleep (60);
end;
end.
Упражнение 3.9. Напишите программу, которая проверяет и записывает изменения размера файла в течение часа. В конце работы она должна строить простую гистограмму, демонстрирующую изменения размера во времени.
Упражнение 3.10. Напишите программу slowwatch, которая периодически проверяет время изменения заданного файла (она не должна завершаться ошибкой, если файл изначально не существует). При изменении файла программа slowwatch должна копировать его на свой стандартный вывод. Как можно убедиться (или предположить), что обновление файла закончено до того, как он будет скопирован?
3.3.1. Подробнее о вызове chmod
Системный вызов fstat расширяет использование вызова chmod, поскольку позволяет предварительно узнать значение кода доступа к файлу, что дает возможность изменять отдельные биты, а не менять весь код доступа целиком.
Следующая программа addx демонстрирует сказанное. Она вначале вызывает fstat для получения режима доступа к файлу из списка аргументов вызова программы. В случае успешного завершения вызова программа изменяет существующие права доступа так, чтобы файл был доступен для выполнения его владельцем. Эта программа может быть полезна для придания командным файлам, составленным пользователем, статуса исполняемых файлов.
(* Программа addx разрешает доступ на выполнение файла *)
uses linux,stdio;

const XPERM=0100; (* Право на выполнение для владельца *)

var
k:integer;
statbuf:tstat;

begin
(* Выполнить для всех файлов в списке аргументов *)
for k := 1 to paramcount do
begin
(* Получить текущий код доступа к файлу *)
if not fstat(paramstr(k), statbuf) then
begin
writeln('addx: ошибка вызова stat для ',paramstr(k));
continue;
end;
(*
Попытаться разрешить доступ на выполнение
при помощи оператора побитового ИЛИ
*)
statbuf.mode := statbuf.mode or octal(XPERM);
if not chmod (paramstr(k), statbuf.mode) then
writeln('addx: ошибка изменения прав доступа для файла ', paramstr(k));
end; (* Конец цикла *)
halt(0);
end.
Наиболее интересный момент заключается здесь в способе изменения кода доступа файла при помощи побитового оператора ИЛИ. Это гарантирует, что устанавливается бит, заданный определением XPERM. Фактически мы могли бы расписать этот оператор в виде:
statbuf.mode := statbuf.mode or octal(XPERM);
Для ясности использована более короткая форма. Можно было бы также использовать вместо XPERM предусмотренную в системе постоянную STAT_IXUSR.
Упражнение 3.11. Приведенную задачу можно решить проще. Если вы знаете, как это сделать, напишите эквивалент этой программы при помощи командного интерпретатора.
Упражнение 3.12. Напишите свою версию команды chmod, используя ее описание в справочном руководстве вашей системы UNIX.
Глава 4. Каталоги, файловые системы и специальные файлы
4.1. Введение
В двух предыдущих главах внимание было сконцентрировано на основном компоненте файловой структуры UNIX – обычных файлах. В этой главе будут рассмотрены другие компоненты файловой структуры, а именно:
• каталоги. Каталоги выступают в качестве хранилища имен файлов и, следовательно, позволяют пользователям группировать произвольные наборы файлов. Понятие каталогов должно быть знакомо большинству пользователей UNIX и многим «эмигрантам» из других операционных систем. Далее будет показано, что каталоги UNIX могут быть вложенными, это придает структуре файлов древовидную иерархическую форму;
• файловые системы. Файловые системы представляют собой набор каталогов и файлов и являются подразделами иерархического дерева каталогов и файлов, образующих общую файловую структуру UNIX. Файловые системы обычно соответствуют физическим разделам (partitions) дискового устройства или всему дисковому устройству. При решении большинства задач файловые системы остаются невидимыми для пользователя;
• специальные файлы. Концепция файла получила в системе UNIX дальнейшее развитие и включает в себя присоединенные к системе периферийные устройства. Эти периферийные устройства, такие как принтеры, дисковые накопители и даже системная память, представляются в файловой структуре именами файлов. Файл, представляющий устройство, называется специальным файлом (special file). К устройствам можно получить доступ при помощи обычных системных вызовов доступа к файлам, описанных в главах 2 и 3 (например, вызовов fdopen, fdread и fdwrite). Каждый такой вызов задействует код драйвера устройства в ядре системы, отвечающий за управление заданным устройством. Тем не менее программе не нужно ничего знать об этом, так как система позволяет обращаться со специальными файлами почти как с обычными.
4.2. Каталоги с точки зрения пользователя
Даже случайный пользователь системы UNIX будет иметь некоторое представление о том, как выглядит структура каталогов системы. Тем не менее для полноты изложения кратко опишем обычное расположение файлов с точки зрения пользователя.
В сущности каталоги являются просто списками имен файлов, которые обеспечивают способ разбиения файлов на логически связанные группы. Например, каждый пользователь имеет домашний каталог (home directory), в который попадает при входе в систему и где может создавать файлы и работать с ними. Смысл этого, очевидно, состоит в том, чтобы разделить файлы отдельных пользователей. Аналогично программы, доступные всем пользователем, такие как cat или ls, помещаются в общеизвестные каталоги, обычно /bin или /usr/bin. Используя общепринятую метафору, каталоги можно сравнить с ящиками в шкафу, в которых хранятся папки файлов с документами.
Вместе с тем у каталогов есть некоторые преимущества по сравнению с ящиками в шкафу, так как кроме файлов они также могут включать другие каталоги, которые называются подкаталогами (subdirectories) и позволяют организовать следующие уровни классификации. Подкаталоги, в свою очередь, могут содержа другие подкаталоги и так далее. Допустим любой уровень вложенности, хотя может быть ограничение на длину абсолютного пути файла (см. пункт 4.6.4).
Фактически файловую структуру UNIX можно представить в виде иерархической структуры, напоминающей перевернутое дерево. Упрощенное дерево каталогов показано на рис. 4.1 (это тот же пример, что и рисунок в главе 1). Конечно же, любая реальная система будет иметь более сложную структуру.

/

|

usr

keith

ben

|

|

book

file1

file2

book

chap1

chap2

chap1

chap2
Рис. 4.1. Пример дерева каталогов

На вершине этого дерева, так же как и на вершине любого дерева каталогов UNIX, находится единственный каталог, который называется корневым каталогом (root directory) и имеет очень короткое имя /. Все узлы дерева, кроме конечных, например узлы keith или ben, всегда являются каталогами. Конечные узлы, например узлы file1 или file2, являются файлами или пустыми каталогами. В настоящее время в большинстве систем UNIX имена каталогов могут содержать до 255 символов, но, так же как и в случае с файлами, для обеспечения совместимости со старыми версиями системы их длина не должна превышать 14 символов.
В нашем примере узлы keith и ben являются подкаталогами родительского каталога usr. В каталоге keith находятся три элемента: два обычных файла file1 и file2 и подкаталог book. Каталог keith является родительским для каталога book. В свою очередь каталог book содержит два файла chap1 и chap2. Как было показано в главе 1, положение файла в иерархии может быть задано заданием пути к нему. Например, полное имя файла chap2 в каталоге keith, включающее путь к нему, будет /usr/keith/book/chap2. Аналогично можно указать и полное имя каталога. Полное имя каталога ben будет /usr/ben.
Следует обратить внимание, что каталог /usr/ben/book также содержит два файла с именами chap1 и chap2. Они не обязательно связаны со своими тезками в каталоге /usr/keith/book, так как только полное имя файла однозначно идентифицирует его. Тот факт, что в разных каталогах могут находиться файлы с одинаковыми именами, означает, что пользователям нет необходимости изобретать странные и уникальные имена для файлов.
Текущий рабочий каталог
После входа в систему пользователь находится в определенном месте файловой структуры, называемом текущим рабочим каталогом (current working directory) или иногда просто текущим каталогом (current directory). Это будет, например, каталог, содержимое которого выведет команда ls при ее запуске без параметров. Первоначально в качестве текущего рабочего каталога для пользователя выступает его домашний каталог, заданный в файле паролей системы. Можно перейти в другой каталог при помощи команды cd, например, команда
$ cd /usr/keith
сделает /usr/keith текущим каталогом. Имя текущего каталога можно при необходимости узнать при помощи команды вывести рабочий каталог (print working directory, сокращенно pwd):
$ pwd
/usr/keith
В отношении текущего каталога основной особенностью является то, что с него система начинает поиск при задании относительного пути – то есть такого, который не начинается с корня /. Например, если текущий рабочий каталог /usr/keith, то команда
$ cat book/chap1
эквивалентна команде
$ cat /usr/keith/book/chap1
а команда
$ cat file1
эквивалентна команде
$ cat usr/keith/file1
4.3. Реализация каталогов
На самом деле каталоги UNIX – не более чем файлы. Во многих аспектах система обращается с ними точно так же, как и с обычными файлами. Они имеют владельца, группу, размер и связанные с ними права доступа. Многие из системных вызовов для работы с файлами, которые были рассмотрены в предыдущих главах, могут использоваться и для работы с каталогами, хотя так делать и не рекомендуется. Например, каталоги можно открывать на чтение при помощи системного вызова open, и возвращенный этим вызовом дескриптор файла может использоваться для последующих вызовов fdread, fdseek, fstat и fdclose.
Тем не менее между каталогами и обычными файлами существуют некоторые важные различия, налагаемые системой. Каталоги не могут быть созданы при помощи системных вызовов fdcreat или fdopen. Системный вызов fdopen также не будет работать с каталогом, если установлен любой из флагов Open_WRONLY или Open_RDWR (только для записи или чтение/запись). При этом вызов вернет ошибку и запишет в переменную linuxerror код ошибки Sys_EISDIR. Эти ограничения делают невозможным изменение каталога при помощи системного вызова fdwrite. Фактически из-за особой природы каталогов для работы с ними гораздо лучше использован выделенное семейство системных вызовов, которое будет далее изучено.
Структура каталогов состоит из набора элементов каталогов, по одному элементу для каждого содержащегося в них файла или подкаталога. Каждый элемент каталога состоит, по крайней мере, из одного положительного числа, номера индексного дескриптора (inode number), и символьного поля, содержащего имя файла. Когда имена файлов были длиной не более 14 символов, элементы каталога имели фиксированную длину и большинство систем UNIX использовали один и тот же метод их реализации (исключение составлял Berkeley UNIX). Тем не менее после введения длинных имен файлов элементы каталога стали иметь различную длину, и реализация каталогов стала зависеть от файловой системы. Поэтому при разработке программ не следует полагаться на формат каталога, и для того, чтобы сделать их действительно переносимыми, необходимо использовать для работы с каталогами системные вызовы из спецификации XSI.
Часть каталога, содержащая три файла, может выглядеть примерно так, как показано на рис. 4.2. (Информация, необходимая для управления свободным пространством в файле каталога, исключена.) Этот каталог содержит имена трех файлов fred, bookmark и abc, которые могут быть и подкаталогами. Номера индексных дескрипторов для этих файлов равны соответственно 120, 207 и 235. На рис. 4.2 представлена логическая структура каталога; в действительности же каталог представляет собой непрерывный поток байтов.
120
f
r
e
d
\0

207
b
o
o
k
m
a
r
k
\0
235
a
b
c
\0

Рис. 4.2. Часть каталога

Номер индексного дескриптора однозначно идентифицирует файл (на самом деле номера индексных дескрипторов уникальны только в пределах одной файловой системы, но подробнее об этом будет рассказано ниже). Номер индексного дескриптора используется операционной системой для поиска в дисковой структуре данных структуры индексного дескриптора (inode structure), содержащей всю информацию, необходимую для обращения файлом: его размер, идентификатор владельца и группы, права доступа, время последнего доступа, последнего изменения и адреса блоков на диске, в которых хранятся данные файла. Большая часть информации, получаемой системным вызовом fstat, описанными в предыдущей главе, фактически получается напрямую из структуры индексного дескриптора. Более подробно структура индексного дескриптора будет рассмотрена в разделе 4.5.
Важно понимать, что представление каталога является только логической картиной. Просмотр содержимого каталога при помощи команды cat может завершиться выводом «мусора» на экран терминала. Более удобно исследовать каталог при помощи команды восьмеричного вывода od с параметром -с. Например, для вывода содержимого текущей директории следует набрать в командной строке:
$ od -с .
Символ «точка» (.) в этой команде является стандартным способом задания текущего рабочего каталога.
4.3.1. Снова о системных вызовах link и unlink
В предыдущей главе было описано, как можно использовать системный вызов link для создания различных имен для одного и того же физического файла, поэтому уже должно быть понятно, как работает этот вызов. Каждая ссылка просто представляет собой еще одну позицию в каталоге с тем же самым номером индексного дескриптора, что и исходный, но с новым именем.
Если в каталоге с рис. 4.2 создать ссылку на файл abc с именем xyz при помощи следующего вызова
link('abc', 'xyz');
то рассматриваемый участок каталога будет выглядеть примерно так, как показано на рис. 4.3. При удалении ссылки при помощи системного вызова unlink соответствующие байты, содержащие имя файла, освобождаются для дальнейшего использования. Если имя файла представляет последнюю ссылку на этот файл, то вся связанная с ним структура индексных дескрипторов стирается. Связанные с файлом блоки на диске, которые содержали данные файла, добавляются к поддерживаемому системой списку свободных блоков и становятся пригодными для дальнейшего использования. В большинстве систем UNIX восстановление удаленных файлов невозможно.
120
f
r
e
d
\0

207
b
o
o
k
m
a
r
k
\0
235
a
b
c
\0

235
x
y
z
\0

Рис. 4.3. Пример каталога с новым файлом

4.3.2. Точка и двойная точка
В каждом каталоге всегда присутствуют два странных имени файлов: точка (.) и двойная точка (..). Точка является стандартным для системы UNIX способом обозначения текущего рабочего каталога, как в команде
$ cat ./fred
которая выведет на экран файл fred в текущем каталоге, или
$ ls .
которая выведет список файлов в текущем каталоге. Двойная точка является стандартным способом ссылки на родительский каталог текущего рабочего каталог, то есть каталог, содержащий текущий каталог. Поэтому команда
$ cd ..
позволяет пользователю переместиться на один уровень вверх по дереву каталогов.
Фактически имена «точка» (.) и «двойная точка» (..) просто являются ссылками на текущий рабочий каталог и родительский каталог соответственно, и любой каталог UNIX содержит в первых двух позициях эти два имени. Другими словами, во время создания каталога в него автоматически добавляются эти два имени.
Можно более ясно это представить себе, рассмотрев участок дерева каталогов, приведенный на рис. 4.4.

ben

book

memos

|

|

chap1
chap2
chap3

kd
kh
mv
Рис. 4.4. Часть дерева каталогов

Если рассмотреть каждый из каталогов ben, book и memos, то откроется картина, похожая на рис. 4.5. Нужно обратить внимание на то, что в каталоге book номер записи с именем . равен 260, а номер записи с именем .. равен 123, и эти номера соответствуют элементам book и . в родительском каталоге ben. Аналогично имена . и .. в каталоге memos (с номерами узлов 401 и 123) соответствуют каталогу memos и имени . в каталоге ben.
4.3.3. Права доступа к каталогам
Так же как и с обычными файлами, с каталогами связаны права доступа, определяющие возможность доступа к ним различных пользователей.
Права доступа к каталогам организованы точно так же, как и права доступа к обычным файлам, разбиты на три группы битов rwx, определяющих права владельца файла, пользователей из его группы и всех остальных пользователей системы.
Тем не менее, хотя эти права доступа представлены так же, как и у обычных файлов, интерпретируются они по-другому:
• право доступа к каталогу на чтение показывает, что соответствующий класс пользователей может выводить список содержащихся в каталоге файлов и подкаталогов. Однако это не означает, что пользователи могут читать содержащуюся в файлах информацию – это определяется правами доступа к отдельным файлам;
• право доступа к каталогу на запись позволяет пользователю создавать в каталоге новые файлы и удалять существующие. И снова это не дает пользователю права изменять содержимое существующих файлов, если это не разрешено правами доступа к отдельным файлам. Вместе с тем при этом можно удалить существующий файл и создать новый с тем же самым именем, и это, по сути, означает то же самое, что и изменение содержимого исходного файла;
• право доступа к каталогу на выполнение, называемое также правом выполнения, или правом прохождения, поиска (search permission), позволяет пользователю перейти в каталог при помощи команды cd или системного вызова chdir в программе (который будет рассмотрен позже). Кроме этого, чтобы иметь возможность открыть файл или выполнить программу, пользователь должен иметь право доступа на выполнение для всех ведущих к файлу каталогов, входящих в абсолютный путь файла;
• бит фиксации, STAT_ISVTX, позволяет установить дополнительную защиту файлов, находящихся в каталоге. Из такого каталога пользователь может удалить только те файлы, которыми он владеет или на которые он имеет явное право доступа на запись, даже при наличии права на запись в каталог. Примером является каталог /tmp.
Каталог ben
123
.
\0

247
.
.
\0

260
b
o
o
k
\0

401
m
e
m
o
s
\0
Каталог book
260
.
\0

123
.
.
\0

566
c
h
a
p
1
\0
567
c
h
a
p
2
\0
590
c
h
a
p
3
\0
Каталог memos
401
.
\0

123
.
.
\0

800
k
h
\0

810
k
d
\0

077
m
w
\0

Рис. 4.5. Каталоги ben, book и memos

На уровне командного интерпретатора связанные с каталогами права доступа можно вывести при помощи команды ls с параметром -l. Подкаталоги будут обозначаться буквой d в первой позиции, например:
$ ls -l
total 168
-rw-r----- 1 ben other 39846 Oct 12 21:21 dir_t
drwxr-x--- 2 ben other 32 Oct 12 22:02 expenses
-rw-r----- 1 ben other 46245 Oct 13 10:34 new
-rw-r----- 1 ben other 3789 Sep 2 18:40 pwd_text
-rw-r----- 1 ben other 1310 Sep 13 10:38 test.с
Здесь строка, описывающая подкаталог expenses, помечена буквой d в начале строки. Видно, что владелец этого каталога (пользователь ben) имеет права на чтение, запись и выполнение (поиск), пользователи группы файла (называющейся other) имеют права на чтение и выполнение (переход в каталог), а для всех остальных пользователей доступ полностью запрещен.
Если требуется получить информацию о текущем каталоге, можно задать в команде ls кроме параметра -l еще и параметр -d, например:
$ ls -ld
drwxr-x--- 3 ben other 128 Oct 12 22:02 .
Помните, что имя . (точка) в конце листинга обозначает текущий каталог.
4.4. Использование каталогов при программировании
Как уже упоминалось, для работы с каталогами существует особое семейство системных вызовов. Главным образом эти вызовы работают со структурой dirent, которая определена в модуле linux и содержит следующие элементы:
PDirent = ^Dirent;
Dirent = Record
ino, (* Номер индексного дескриптора *)
off : longint;
reclen : word;
name : string[255] (* Имя файла *)
end;
Спецификация XSI не определяет размер name, но гарантирует, что число байтов, предшествующих нулевому символу, будет меньше, чем число, хранящееся в переменной _PC_NAME_MAX, определенной в заголовочном файле . Обратите внимание, что нулевое значение переменной ino обозначает пустую запись в каталоге.
4.4.1. Создание и удаление каталогов
Как уже упоминалось ранее, каталоги нельзя создать при помощи системных вызовов fdcreat или fdopen. Для выполнения этой задачи существует специальный системный вызов mkdir.
Описание
uses stdio;

function mkdir(pathname:pchar;mode:integer):integer;
Первый параметр, pathname, указывает на строку символов, содержащую имя создаваемого каталога. Второй параметр, mode, является набором прав доступа к каталогу. Права доступа будут изменяться с учетом значения umask процесса, например:
var
retval:integer;
retval := mkdir('/tmp/dir1', octal(0777));
Как обычно, системный вызов mkdir возвращает нулевое значение в случае успеха, и -1 – в случае неудачи. Обратите внимание, что mkdir также помещает две ссылки (. и ..) в создаваемый новый каталог. Если бы этих элементов не было, работать с полученным каталогом было бы невозможно.
Если каталог больше не нужен, то его можно удалить при помощи системного вызова rmdir.
Описание
uses stdio;

function rmdir(pathname:pchar):integer;
Параметр pathname определяет путь к удаляемому каталогу. Этот вызов завершается успехом, только если удаляемый каталог пуст, то есть содержит только записи «точка» (.) и «двойная точка (..).
4.4.2. Открытие и закрытие каталогов
Для открытия каталога UNIX спецификация XSI определяет особую функцию opendir.
Описание
uses linux;

Function OpenDir(dirname:pchar):pdir;
Function OpenDir(dirname:string):pdir;
Передаваемый вызову opendir параметр является именем открываемого каталога. При успешном открытии каталога dirname вызов opendir возвращает указатель на переменную типа TDIR. Определение типа TDIR, представляющего дескриптор открытого каталога, находится в модуле linux. Это определение аналогично определению типа TFILE, используемого в стандартной библиотеке ввода/вывода, описанной в главах 2 и 11. Указатель позиции ввода/вывода в полученном от функции opendir дескрипторе установлен на первую запись каталога. Если вызов завершился неудачно, то функция возвращает nil. Всегда следует проверять возвращаемое значение, прежде чем это значение может быть использовано.
После того, как программа закончит работу с каталогом, она должна закрыть его. Это можно сделать при помощи функции closedir.
Описание
uses linux;

Function CloseDir(dirptr:pdir):integer;
Функция closedir закрывает дескриптор открытого каталога, на который указывает аргумент dirptr. Обычно его значение является результатом предшествующего вызова opendir, что демонстрирует следующий пример:
uses linux;

var
dp:pdir;
begin
dp := opendir ('/tmp/dir1');
if dp = nil then
begin
writeln('Ошибка открытия каталога /tmp/dir1');
halt(1);
end;
(*
Код, работающий с каталогом
.
.
.
*)
closedir (dp);
end.
4.4.3. Чтение каталогов: вызовы readdir и rewinddir
После открытия каталога из него можно начать считывать записи.
Описание
uses linux;

Function ReadDir(dirptr:pdir):pdirent;
Функции readdir должен передаваться допустимый указатель на дескриптор открытого каталога, обычно возвращаемый предшествующим вызовом opendir. При первом вызове readdir в структуру dirent будет считана первая запись в каталоге. В результате успешного вызова указатель каталога переместится на следующую запись.
Когда в результате последующих вызовов readdir достигнет конца каталога, то вызов вернет нулевой указатель. Если в какой-то момент потребуется начать чтение каталога с начала, то можно использовать системный вызов rewinddir, определенный следующим образом:
Описание
uses stdio;

procedure rewinddir(dirptr:pdir);
Следующий после вызова rewinddir вызов readdir вернет первую запись в каталоге, на который указывает переменная dirptr.
В приведенном ниже примере функция my_double_ls дважды выведет на экран имена всех файлов в заданном каталоге. Она принимает в качестве параметра имя каталога и в случае ошибки возвращает значение -1.
uses linux,stdio;

function my_double_ls(name:pchar):integer;
var
dp:PDIR;
d:pdirent;
begin
(* Открытие каталога с проверкой ошибок *)
dp:=opendir (name);
if dp=nil then
begin
my_double_ls:=-1;
exit;
end;

(* Продолжить обход каталога,
* выводя записи в нем, если
* индекс остается допустимым
*)
d:=readdir(dp);
while d<>nil do
begin
if d^.ino<>0 then
writeln(d^.name);
d:=readdir(dp);
end;

(* Вернуться к началу каталога ... *)
rewinddir(dp);
(* ... и снова вывести его содержимое *)
d:=readdir(dp);
while d<>nil do
begin
if d^.ino<>0 then
writeln(d^.name);
d:=readdir(dp);
end;
closedir(dp);
my_double_ls:=0;
end;
Порядок выводимых функцией my_double_ls имен файлов будет совпадать с порядком расположения файлов в каталоге. Если вызвать функцию my_double_ls в каталоге, содержащем три файла abc, bookmark и fred, то ее вывод может выглядеть так:
.
..
fred
bookmark
abc
.
..
fred
bookmark
abc
Второй пример: процедура find_entry
Процедура find_entry будет искать в каталоге следующий файл (или подкаталог), заканчивающийся определенным суффиксом. Она имеет три параметра: имя каталога, в котором будет выполняться поиск, строка суффикса и флаг, определяющий, нужно ли продолжать дальнейший поиск после того, как искомый элемент будет найден.
Процедура find_entry использует процедуру проверки совпадения строк match с целью определения, заканчивается ли файл заданным суффиксом. Процедура match, в свою очередь, вызывает две процедуры из стандартной библиотеки C системы UNIX: функцию strlen, возвращающую длину строки в символах, и функцию strcmp, которая сравнивает две строки, возвращая нулевое значение в случае их совпадения.
uses linux,strings;

function match(s1, s2: pchar):boolean;forward;

function find_entry(dirname:pchar;suffix:pchar;cont:integer):pchar;
const
dp:pdir=nil;
var
d:pdirent;
begin
if (dp = nil) or (cont = 0) then
begin
if dp <> nil then
closedir (dp);
dp:=opendir(dirname);
if dp = nil then
begin
find_entry:=nil;
exit;
end;
end;

d := readdir (dp);
while d <> nil do
begin
if d^.ino = 0 then
continue;
if match (d^.name, suffix) then
begin
find_entry:=d^.name;
exit;
end;
d := readdir (dp);
end;

closedir (dp);
dp := nil;
find_entry:=nil;
end;

function match(s1, s2: pchar):boolean;
var
diff:integer;
begin
diff := strlen (s1) - strlen (s2);

if strlen (s1) > strlen (s2) then
match:=(strcomp (@s1[diff], s2) = 0)
else
match:=false;
end;
Упражнение 4.1. Измените функцию my_double_ls из предыдущего примера так, чтобы она имела второй параметр – целочисленную переменную skip. Если значение skip равно нулю, то функция my_double_ls должна выполняться так же, как и раньше. Если значение переменной skip равно 1, функция my_double_ls должна пропускать все имена файлов, которые начинаются сточки (.).
Упражнение 4.2. В предыдущей главе мы познакомились с использованием системного вызова fstat для получения информации о файле. Структура tstat, возвращаемая вызовом fstat, содержит поле mode, режим доступа к файлу. Режим доступа к файлу образуется при помощи выполнения побитовой операции ИЛИ значения кода доступа с константами, определяющими, является ли этот файл обычным файлом, каталогом, специальным файлом, или механизмом межпроцессного взаимодействия, таким как именованный канал. Наилучший способ проверить, является ли файл каталогом – использовать макрос S_ISDIR:
(* Переменная buf получена в результате вызова fstat *)
if S_ISDIR(buf.mode) then
writeln('Это каталог')
else
writeln('Это не каталог');
Измените процедуру my_double_ls так, чтобы она вызывала fstat для каждого найденного файла и выводила звездочку после каждого имени каталога.
В дополнение к упражнению приведем пример, демонстрирующий остальные S_-функции:
Uses linux;

Var Info : Stat;

begin
if LStat (paramstr(1),info) then
begin
if S_ISLNK(info.mode) then
Writeln ('File is a link');
if S_ISREG(info.mode) then
Writeln ('File is a regular file');
if S_ISDIR(info.mode) then
Writeln ('File is a directory');
if S_ISCHR(info.mode) then
Writeln ('File is a character device file');
if S_ISBLK(info.mode) then
Writeln ('File is a block device file');
if S_ISFIFO(info.mode) then
Writeln ('File is a named pipe (FIFO)');
if S_ISSOCK(info.mode) then
Writeln ('File is a socket');
end;
end.
4.4.4. Текущий рабочий каталог
Как уже было рассмотрено в разделе 4.2, после входа в систему пользователь работает в текущем рабочем каталоге. Фактически каждый процесс UNIX, то есть каждый экземпляр выполняемой программы, имеет свой текущий рабочий каталог, который используется в качестве начальной точки при поиске относительных путей в вызовах fdopen и им подобных. Текущий рабочий каталог пользователя на самом деле является текущим рабочим каталогом процесса оболочки, интерпретирующего команды пользователя.
Первоначально в качестве текущего рабочего каталога процесса задается текущий рабочий каталог запустившего его процесса, обычно оболочки. Процесс может поменять свой текущий рабочий каталог при помощи системного вызова chdir.
4.4.5. Смена рабочего каталога при помощи вызова chdir
Описание
uses stdio;

function chdir(path:pchar):integer;
После выполнения системного вызова chdir каталог path становится текущим рабочим каталогом вызывающего процесса. Важно отметить, что эти изменения относятся только к процессу, который выполняет вызов chdir. Смена текущего каталога в программе не затрагивает запустивший программу командный интерпретатор, поэтому после выхода из программы пользователь окажется в том же рабочем каталоге, в котором он находился перед запуском программы, независимо от перемещений программы.
Системный вызов chdir завершится неудачей и вернет значение -1, если путь path не является корректным именем каталога или если вызывающий процесс не имеет доступ на выполнение (прохождение) для всех каталогов в пути.
Системный вызов может успешно использоваться, если нужно получить доступ к нескольким файлам в заданном каталоге. Смена каталога и задание имен файлов относительно нового каталога будет более эффективной, чем использование абсолютных имен файлов. Это связано с тем, что системе приходится поочередно проверять все каталоги в пути, пока не будет найдено искомое имя файла, поэтому уменьшение числа составляющих в пути файла сэкономит время. Например, вместо использования следующего фрагмента программы
fd1 := fdopen('/usr/ben/abc', Open_RDONLY);
fd2 := fdopen('/usr/ben/xyz', Open_RDWR);
можно использовать:
chdir('/usr/ben');
fd1 := fdopen('abc', Open_RDONLY);
fd2 := fdopen('xyz', Open_RDWR);
4.4.6. Определение имени текущего рабочего каталога
Спецификация XSI определяет функцию (а не системный вызов) getcwd, которая возвращает имя текущего рабочего каталога.
Описание
uses stdio;

function getcwd(name:pchar; size:longint):pchar;

uses linux;

Function TellDir(p:pdir):longint;
Функция getcwd возвращает указатель на имя текущего каталога. Следует помнить, что значение второго аргумента size должно быть больше длины имени возвращаемого пути не менее чем на единицу. В случае успеха имя текущего каталога копируется в массив, на который указывает переменная name. Если значений size равно нулю или меньше значения, необходимого для возвращения строки имени текущего каталога, то вызов завершится неудачей и вернет нулевой указатель. В некоторых реализациях, если переменная name содержит нулевой указатель, то функция getcwd сама запросит size байтов оперативной памяти; тем не менее, так как эта семантика зависит от системы, не рекомендуется вызывать функцию getcwd с нулевым указателем.
Функция TellDir помещает текущий каталог по указателю p, возвращая 0 в случае успешного завершения и -1 – при ошибке.
Альтернативой getcwd является определенная в модуле sysutils функция GetCurrentDir.
Описание
uses sysutils;

Function GetCurrentDir:String;
Эта короткая программа имитирует команду pwd:
(* Программа my_pwd - вывод рабочего каталога *)

uses sysutils;

procedure my_pwd;
begin
writeln(GetCurrentDir);
end;

begin
my_pwd;
end.
4.4.7. Обход дерева каталогов
Иногда необходимо выполнить операцию над иерархией каталогов, начав от стартового каталога, и обойти все лежащие ниже файлы и подкаталоги. Для этого определим процедуру ftw, выполняющую обход дерева каталогов, начиная с заданного, и вызывающая процедуру, определенную пользователем для каждой встретившейся записи в каталоге.
Описание
uses linux,stdio,strings;

const
FTW_NS =100; (* При ошибке stat(2) *)
FTW_DNR=200; (* При ошибке opendir(3) *)
FTW_F =300; (* Обычный файл *)
FTW_D =400; (* Каталог *)
MAXNAMLEN=4000;

(* Удобное сокращение *)
function EQ(a,b:pchar):boolean;
begin
EQ:=(strcomp(a, b) = 0);
end;

type
func=function(name:pchar; var status:tstat; _type:integer):integer;

function ftw(directory:pchar; funcptr:func; depth:integer):integer;
var
dp:pdir;
p,fullpath:pchar;
i:integer;
e:pdirent;
sb:tstat;
seekpoint:longint;
begin
(* При невозможности выполнения fstat, сообщаем пользователю об этом *)
if not fstat(directory, Sb) then
begin
ftw:=funcptr(directory, Sb, FTW_NS);
exit;
end;

(* Если не каталог, вызываем пользовательскую функцию. *)
if ((Sb.mode and STAT_IFMT) <> STAT_IFDIR) then
(* Сообщение "FTW_F" может быть некорректным (вдруг это символическая ссылка? *)
begin
ftw:=funcptr(directory, Sb, FTW_F);
exit;
end;

(* Открываем каталог; при невозможности - сообщаем пользователю. *)
Dp := opendir(directory);
if dp = nil then
begin
ftw:=funcptr(directory, Sb, FTW_DNR);
exit;
end;

(* Определяем, желает ли пользователь продолжать. *)
i := funcptr(directory, Sb, FTW_D);

if i <> 0 then
begin
closedir(Dp);
ftw:=i;
exit;
end;

(* Готовим место для хранения поного пути. *)
i := strlen(directory);
fullpath := stralloc(i + 1 + MAXNAMLEN + 1);
if fullpath = nil then
begin
closedir(Dp);
ftw:=-1;
exit;
end;
strcopy(fullpath, directory);
p := @fullpath[i];
if (i<>0) and (p[-1] <> '/') then
begin
p^:='/';
inc(p);
end;

(* Читаем все элементы каталога. *)
E := readdir(Dp);
while E <> nil do
begin
if not EQ(E^.name, '.') and not EQ(E^.name, '..') then
begin
if depth <= 1 then
begin
(* Слишком углубились - закрываем этот каталог. *)
seekpoint := telldir(Dp);
closedir(Dp);
Dp := nil;
end;

(* Обработка файла. *)
strcopy(p, E^.name);
i := ftw(fullpath, funcptr, depth - 1);
if i<>0 then
begin
(* Пользователь завершил; оканчиваем работу. *)
strdispose(fullpath);
if Dp<>nil then
closedir(Dp);
ftw:=i;
exit;
end;

(* Повторно отрываем каталог в случае необходимости. *)
if Dp = nil then
begin
Dp := opendir(directory);
if Dp = nil then
begin
(* WTF? *)
strdispose(fullpath);
ftw:=-1;
exit;
end;
seekdir(Dp, seekpoint);
end;
end;
E := readdir(Dp);
end;

(* Завершающие действия. *)
strdispose(fullpath);
closedir(Dp);
ftw:=0;
end;

Первый параметр path определяет имя каталога, с которого должен начаться рекурсивный обход дерева. Параметр depth управляет числом используемых функцией ftw различных дескрипторов файлов. Чем больше значение depth, тем меньше будет случаев повторного открытия каталогов, что сократит общее время отработки вызова. Хотя на каждом уровне дерева будет использоваться только один дескриптор, следует быть уверенным, что значение переменной depth не больше числа свободных дескрипторов файлов. Для определения максимально возможного числа дескрипторов, которые может задействовать процесс, рекомендуется использовать системный вызов getrlimit, обсуждаемый в главе 12.
Второй параметр funcptr – это определенная пользователем функция, вызываемая для каждого файла или каталога, найденного в поддереве каталога path. Как можно увидеть из описания, параметр funcptr передается процедуре ftw как указатель на функцию, поэтому функция должна быть объявлена до вызова процедуры ftw. При каждом вызове функции funcptr будут передаваться три аргумента: заканчивающаяся нулевым символом строка с именем объекта, ссылка на структуру tstat с данными об объекте и целочисленный код. Функция funcptr, следовательно, должна быть построена следующим образом:
function func (name:pchar; var status:tstat; _type:integer):integer;
begin
(* Тело функции *)
end;
Целочисленный аргумент _type может принимать одно из нескольких возможных значений, описывающих тип встретившегося объекта. Вот эти значения:
FTW_F
Объект является файлом
FTW_D
Объект является каталогом
FTW_DNR
Объект является каталогом, который нельзя прочесть
FTW_SL
Объект является символьной ссылкой
FTW_NS
Объект не является символьной ссылкой, и для него нельзя успешно выполнить вызов fstat
Если объект является каталогом, который нельзя прочесть (_type = FTW_DNR), то его потомки не будут обрабатываться. Если нельзя успешно выполнить функцию fstat (_type = FTW_NS), то передаваемая для объекта структура tstat будет иметь неопределенные значения.
Работа вызова будет продолжаться до тех пор, пока не будет завершен обход дерева или не возникнет ошибка внутри функции ftw. Обход также закончится, если определенная пользователем функция возвратит ненулевое значение. Тогда функция ftw прекратит работу и вернет значение, возвращенное функций пользователя. Ошибки внутри функции ftw приведут к возврату значения -1, тогда в переменной linuxerror будет выставлен соответствующий код ошибки.
Следующий пример использует функцию ftw для обхода поддерева каталогов, выводящего имена всех встретившихся файлов (каталогов) и права доступа к ним. Каталоги и символьные ссылки при выводе будут обозначаться дополнительной звездочкой.
Сначала рассмотрим функцию list, которая будет передаваться в качестве аргумента функции ftw.
function list(name:pchar; var status:tstat; _type:integer):integer;
begin
(* Если вызов stat завершился неудачей, просто вернуться *)
if (_type = FTW_NS) then
begin
list:=0;
exit;
end;

(*
* Иначе, вывести имя объекта,
* права доступа к нему и постфикс "*",
* если объект является каталогом или символьной ссылкой.
*)
if (_type = FTW_F) then
printf ('%-30s'#9'0%3o'#$a, [name, status.mode and octal(0777)])
else
printf ('%-30s*'#9'0%3o'#$a, [name, status.mode and octal(0777)]);

list:=0;
end;
Теперь запишем основную программу, которая принимает в качестве параметра путь и использует его в качестве начальной точки для обхода дерева. Если аргументы не заданы, то обход начинается с текущего рабочего каталога:
var
path:array [0..255] of char;
begin
if paramcount=0 then
ftw ('.', @list, 1)
else
begin
strpcopy(path,paramstr(1));
ftw (path, @list, 1);
end;
halt(0);
end.
Вывод программы list для простой иерархии каталогов будет выглядеть так:
$ list
. * 0755
./list * 0755
./filel 0644
./subdir * 0777
./subdir/another 0644
./subdir/subdir2 * 0755
./subdir/yetanother 0644
Обратите внимание на порядок обхода каталогов.
В модуле linux определен ряд специализированных функций для обхода дерева каталогов.
Описание
uses linux;

Function FNMatch(const Pattern, Name:string):Boolean;
Function FSearch(Path:pathstr; DirList:string):Pathstr;
Function Glob(Const Path:Pathstr):PGlob;
Procedure GlobFree(Var P:Pglob);
FNMatch возвращает True, если имя файла в Name совпадает с шаблоном в Pattern. Шаблон может содержать знаки * (совпадение с нулем или более символов) или ? (совпадение с одиночными символом).
FSearch ищет в DirList, списке каталогов, разделенных двоеточием, файл, указанный в Path, возвращаю путь к найденному файлу или пустую строку.
Glob возвращает указатель на структуру tglob, содержащую имена всех файлов, отвечающих шаблону в Path. Возвращает nil при ошибке, устанавливая LinuxError.
GlobFree освобождает память, занятую структурой tglob.
Например:
Uses linux;

Var G1,G2 : PGlob;

begin
G1:=Glob ('*');
if LinuxError=0 then
begin
G2:=G1;
Writeln ('Files in this directory : ');
While g2<>Nil do
begin
Writeln (g2^.name);
g2:=g2^.next;
end;
GlobFree (g1);
end;
end.
4.5. Файловые системы UNIX
Как уже было рассмотрено, файлы могут быть организованы в различные каталоги, которые образуют иерархическую древовидную структуру. Каталоги могут быть сгруппированы вместе, образуя файловую систему (file system). Обычно с файловыми системами имеет дело только системный администратор UNIX. Они позволяют распределять структуру каталогов по нескольким различным физическим дискам или разделам диска, сохраняя однородность структуры с точки зрения пользователя.
Каждая файловая система начинается с каталога в иерархическом дереве. Это свойство позволяет системным администраторам разбивать иерархию файлов UNIX и отводить под ее части отдельные области на диске или даже распределять файловую структуру между несколькими физическими дисковыми устройствами. В большинстве случаев физическое разбиение файловой системы остается невидимым для пользователей.
Файловые системы также называются монтируемыми томами (mountable volumes), поскольку их можно динамически монтировать и демонтировать в виде целых поддеревьев в определенные точки общей древовидной структуры каталогов системы. Демонтирование файловой системы делает все ее содержимое временно недоступным для пользователей. Операционной системе могут быть доступны несколько файловых систем, но не все из них обязательно будут видны как части древовидной структуры.
Информация, содержащаяся в файловой системе, находится на разделе диска, доступном через файл устройства (device file), также называемый специальный файлом (special file). Этот тип файлов будет описан ниже, а пока просто упомянем, что в системе UNIX каждая файловая система однозначно определяется некоторым именем файла.
Реальное расположение данных файловой системы на носителе никак не связано с высокоуровневым иерархическим представлением каталогов с точки зрения пользователя. Кроме того, расположение данных файловой системы не определяется спецификацией XSI – существуют разные реализации. Ядро может поддерживать одновременно несколько типов файловых систем с различной организацией хранения данных. Здесь будет описано только традиционное расположение.
Традиционная файловая система разбита на ряд логических частей. Каждая такая файловая система содержит четыре определенных секции: загрузочная область (bootstrap area), суперблок (superblock), ряд блоков, зарезервированных для структур индексных дескрипторов (inode) файловой системы, и области, отведенной для блоков данных, образующих файлы этой файловой системы. Это расположение схематично представлено на рис. 4.6. Первый из этих блоков (блок с нулевым логическим номером, физически он может быть расположен где угодно внутри раздела диска) зарезервирован для использования в качестве загрузочного блока. Это означает, что он может содержать зависящую от оборудования загрузочную программу, которая используется для загрузки ОС UNIX при старте системы.
Блок 0

Загрузочный блок

Блок 1

Суперблок

Блоки 2…n

Блоки индексных
дескрипторов

Блоки n+1…r

Блоки данных

Рис. 4.6. Расположение традиционной файловой системы

Логический блок 1 в файловой системе называется суперблоком. Он содержит всю жизненно важную информацию о системе, например, полный размер файловой системы (r блоков на приведенном рисунке), число блоков, отведенных для индексных дескрипторов (n–2), дату и время последнего обновления файловой системы. Суперблок содержит также два списка. В первом из них находится часть цепочки номеров свободных блоков секции данных, а во втором – часть цепочки номеров свободных индексных дескрипторов. Эти два списка обеспечивают ускорение доступа к файловой системе при выделении новых блоков на диске для хранения дополнительных данных или при создании нового файла или каталога. Суперблок смонтированной файловой системы находится в памяти для обеспечения быстрого доступа к списку свободных блоков и свободных узлов. Эти списки в памяти пополняются с диска по мере их исчерпания.
Размер структуры индексных дескрипторов зависит от файловой системы; например, в определенных файловых системах она имеет размер 64 байта, а в других – 128 байт. Индексные дескрипторы последовательно нумеруются, начиная с единицы, поэтому для определения положения структуры индексного дескриптора с заданным номером, прочтенным из записи каталога (как это происходит при переходе в подкаталог или при открытии определенного файла каталога), используется совсем простой алгоритм.
Файловые системы создаются при помощи программы mkfs, и при ее запуске задаются размеры области индексных дескрипторов и области данных. В традиционных файловых системах размеры этих областей нельзя изменять динамически, поэтому можно было исчерпать пространство файловой системы одним из двух способов. Во-первых, это может произойти, если были использованы все блоки данных (даже если еще есть доступные номера индексных дескрипторов). Во-вторых, могут быть использованы все номера индексных дескрипторов (при создании большого числа мелких файлов), и, следовательно, дальнейшее создание новых файлов в файловой системе станет невозможным, даже если есть еще свободные блоки данных. В настоящее время современные файловые системы могут иметь переменный размер, и пространство под индексные дескрипторы часто выделяется динамически.
Теперь понятно, что номера индексных дескрипторов являются уникальными только в пределах файловой системы, вот почему невозможно использовать жесткие ссылки между файловыми системами.
4.5.1. Кэширование: вызовы sync и fsync
Из соображений эффективности в традиционной файловой системе копии суперблоков смонтированных систем находятся в оперативной памяти. Их обновление может выполняться очень быстро, без необходимости обращаться к диску. Аналогично все операции между памятью и диском обычно кэшируются в области данных оперативной системы вместо немедленной записи на диск. Операции чтения также буферизуются в кэше. Следовательно, в любой заданный момент времени данные на диске могут оказаться устаревшими по сравнению с данными кэша в оперативной памяти. В UNIX существуют две функции, которые позволяют процессу убедиться, что содержимое кэша совпадает с данными на диске. Системный вызов sync используется для сброса на диск всего буфера памяти, содержащего информацию о файловой системе, а вызов fsync используется для сброса на диск всех данных и атрибутов, связанных с определенным файлом.
Описание
uses stdio;

procedure sync;

function fsync(filedes:integer):integer;
Важное отличие между этими двумя вызовами состоит в том, что вызов fsync не завершается до тех пор, пока все данные не будут записаны на диск. Вызов sync может завершиться, но запись данных при этом может быть не завершена, а только занесена в планировщик (более того, в некоторых реализациях вызов sync может быть ненужным и не иметь эффекта).
Функция sync не возвращает значения. Функция fsync будет возвращать нулевое значение в случае успеха и -1 – в случае ошибки. Вызов fsync может завершиться неудачей, если, например, переменная filedes содержит некорректный дескриптор файла.
Чтобы убедиться, что содержимое файловых систем на диске не слишком надолго отстает от времени, в системе UNIX регулярно производится вызов sync. Обычно период запуска sync равен 30 секундам, хотя этот параметр может изменяться системным администратором.
4.6. Имена устройств UNIX
Подключенные к системе UNIX периферийные устройства (диски, терминале) принтеры, дисковые массивы и так далее) доступны при помощи их имен в файловой системе. Эти файлы называются файлами устройств (device files). Соответствующие файловым системам разделы дисков также относятся к классу объектов, представленных этими специальными файлами.
В отличие от обычных дисковых файлов, чтение и запись в файлы устройств приводит к пересылке данных напрямую между системой и соответствующим периферийным устройством.
Обычно эти специальные файлы находятся в каталоге /dev. Поэтому, например, имена
/dev/tty00
/dev/console
/dev/pts/as (псевдотерминал для сетевого доступа)
могут соответствовать трем портам терминалов системы, а имена
/dev/lp
/dev/rmt0
/dev/rmt/0cbn
могут обозначать матричный принтер и два накопителя на магнитной ленте. Имена разделов диска могут иметь разнообразный формат, например:
/dev/dsk/c0b0t0d0s3
/dev/dsk/hd0d
В командах оболочки и в программах файлы устройств могут использоваться так же, как и обычные файлы, например, команды
$ cat fred > /dev/lp
$ cat fred > /dev/rmt0
выведут файл fred на принтер и накопитель на магнитной ленте соответственно (если это позволяют права доступа). Очевидно, что пытаться таким образом оперировать разделами диска с файловыми системами – огромный риск. Одна неосторожная команда может привести к случайной потере большого объема ценных данных. Кроме того, если бы права доступа к таким файлам устройств были бы не очень строгими, то продвинутые пользователи могли бы обойти ограничения прав доступа, наложенные на файлы в файловой системе. Поэтому системные администраторы должны задавать для файлов дисковых разделов соответствующие права доступа, чтобы иметь уверенность в том, что такие действия невозможны.
Для доступа к файлам устройств в программе могут использоваться вызовы fdopen, fdclose, fdread и fdwrite, например, программа
uses linux;

var
i,fd:integer;
begin

fd := fdopen ('/dev/tty', Open_WRONLY);

for i := 1 to 100 do
fdwrite(fd, 'x', 1);

fdclose(fd);
end.
приведет к выводу 100 символов х на порт терминала tty00. Конечно, работа с терминалом является отдельной важной темой, поэтому она подробнее будет рассмотрена в главе 9.
4.6.1. Файлы блочных и символьных устройств
Файлы устройств UNIX разбиваются на две категории: блочные устройства (block devices) и символьные устройства (character devices):
• семейство файлов блочных устройств соответствует устройствам класса дисковых накопителей (съемных и встроенных) и накопителей на магнитной ленте. Передача данных между ядром и этими устройствами осуществляется блоками стандартного размера. Все блочные устройства обеспечивают произвольный доступ. Внутри ядра доступ к этим устройствам управляется хорошо структурированным набором процедур и структур ядра. Этот общий интерфейс к блочным устройствам означает, что обычно драйверы блочных устройств очень похожи, различаясь только в низкоуровневом управлении заданным устройством;
• семейство файлов символьных устройств соответствует устройствам терминалов, модемных линий, устройствам печати, то есть тем устройствам, которые не используют блочный механизм структурированной пересылки данных. Произвольный доступ для символьных устройств может как поддерживаться, так и не поддерживаться. Данные передаются не блоками фиксированного размера, а в виде потоков байтов произвольной длины.
Важно заметить, что файловые системы могут находиться только на блочных устройствах, и блочные устройства имеют связанные с ними символьные устройства для быстрого и простого доступа, которые называются устройствами прямого доступа (raw device). Утилиты mkfs и fsck используют интерфейс прямого доступа.
ОС UNIX использует две конфигурационные таблицы для связи периферийного устройства с кодом его управления, эти таблицы называются таблицей блочных устройств (block device switch) и таблицей символьных устройств (character device switch). Обе таблицы проиндексированы при помощи значения старшего номера устройства (major device number), который записан в номере индексного дескриптора файла устройства. Последовательность передачи данных к периферийному устройству и от него выглядит так:
1. Системные вызовы fdread или fdwrite обращаются к индексному дескриптору файла устройства обычным способом.
2. Система проверяет флаг в структуре индексного дескриптора и определяет, является ли устройство блочным или символьным. Также извлекается старший номер устройства.
3. Старший номер используется для индексирования соответствующей таблицы устройств и нахождения процедуры драйвера устройства, нужной для непосредственного выполнения передачи данных.
Таким образом, порядок доступа к периферийным устройствам полностью согласуется с порядком доступа к обычным дисковым файлам.
Кроме старшего номера устройства, в индексном дескрипторе также записан второе значение, называемое младшим номером устройства (minor device number) и передаваемое процедурам драйвера устройства для точного задания номера порта на устройствах, которые поддерживают более одного порта, или для обозначения одного из разделов жесткого диска, обслуживаемых одним драйвером. Например, на 8-портовой плате терминала все линии будут иметь один и тот же старший номер устройства и, соответственно, тот же набор процедур драйвера устройства, но каждая конкретная линия будет иметь свой уникальный младший номе устройства в диапазоне от 0 до 7.
4.6.2. Структура tstat
Структура tstat, которую уже была обсуждена в главе 3, позволяет хранить ин формацию о файле устройства в двух полях:
mode
В случае файла устройства это поле содержит права доступа к файлу, к которым прибавлено восьмеричное значение 060000 для блочных устройств или 020000 для символьных устройств. В модуле linux определены константы STAT_IFBLK и STAT_IFCHR, которые могут использоваться вместо этих чисел
rdev
Это поле содержит старший и младший номера устройства
Можно вывести эту информацию при помощи команды ls с параметром -l, например:
$ ls -l /dev/tty3
crw--w--w- 1 ben other 8,3 Sep 13 10:19 /dev/tty3
Обратите внимание на символ с в первой строке вывода, что говорит о том, что /dev/tty3 является символьным устройством. Значения 8 и 3 представляют старший и младший номера устройства соответственно.
Можно получить в программе значение поля mode при помощи методики, введенной в упражнении 4.2:
if S_ISCHR(buf.mode) then
writeln('Символьное устройство')
else
writeln('He символьное устройство');
S_ISCHR – это макрос, определенный в модуле linux.
4.6.3. Информация о файловой системе
Для устройств, которые представляют файловые системы, применимы две функции, сообщающие основную информацию о файловой системе, – полное число блоков, число свободных блоков, число свободных индексных дескрипторов и т.д. Это функции fsstat.
Описание
uses linux;

Function FSStat(Path:Pathstr; Var buf:statfs):Boolean;
Function FSStat(Fd:longint; Var buf:stat):Boolean;
Обе функции возвращают информацию о файловой системе, заданной либо именем файла устройства path, либо дескриптором открытого файла fd. Параметр buf является указателем на структуру statfs, определенную модуле linux. Структура statfs включает, по меньшей мере, следующие элементы:
bsize:longint;
Размер блока данных, при котором система имеет наибольшую производительность. Например, значение bsize может составлять при этом 8 Кбайт, что означает, что система обеспечивает более эффективный ввод/вывод при операциях с такими порциями данных
bfree:longint;
Полное число свободных блоков
bavail:longint;
Число свободных блоков, доступных непривилегированным процессам
files:longint;
Полное число номеров индексных дескрипторов
ffree:longint;
Полное число свободных номеров индексных дескрипторов
fsid:longint;
Идентификатор файловой системы
namelen:longint;
Максимальная длина файла
Следующий пример делает примерно то же самое, что и стандартная команда df. Эта программа использует функцию fsstat для вывода числа свободных блоков и свободных индексных дескрипторов в файловой системе.
(* Программа fsys - вывод информации о файловой системе *)
(* Имя файловой системы передается в качестве аргумента *)

uses linux;

var
buf:statfs;

begin
if paramcount<>1 then
begin
writeln('Применение: fsys имя_файла');
halt(1);
end;

if not fsstat(paramstr(1), buf) then
begin
writeln('Ошибка вызова fsstat');
halt(2);
end;

writeln(paramstr(1),': свободных блоков ', buf.bfree, ', свободных индексов ', buf.ffree);
halt(0);
end.
4.6.4. Ограничения файловой системы: процедуры pathconf и fpathconf
Комитет разработки стандарта POSIX и другие группы разработки стандартов несколько формализовали способ определения системных ограничений. Так как система может поддерживать различные типы файловых систем, определенные ограничения могут различаться для разных файлов и каталогов. Для запроса этих ограничений, связанных с определенным каталогом, могут использоваться две процедуры, pathconf и fpathconf.
Описание
uses stdio;

function pathconf(pathname:pchar;name:longint):integer;

function fpathconf(filedes, name:longint):integer;
Обе эти процедуры работают одинаково и возвращают значение для запрошенного ограничения или переменной. Различие между ними заключается в первом параметре: для процедуры pathconf это имя файла или каталога, а для процедуры fpathconf – дескриптор открытого файла. Второй параметр является значением одной из констант, определенных в файле stdio и обозначающих запрашиваемое ограничение.
Следующая программа lookup может использоваться для вывода системных ограничений для заданного файла/каталога. В этом примере программа lookup выводит наиболее интересные из этих значений для стандартного каталога /tmp:
(* Программа lookup - выводит установки ограничений файлов *)

uses stdio;

type table=record
val:integer;
name:pchar;
end;

var
tb:^table;
const options:array [0..3] of table=(
(val:_PC_LINK_MAX; name:'Максимальное число ссылок'),
(val:_PC_NAME_MAX; name:'Максимальная длина имени файла'),
(val:_PC_PATH_MAX; name:'Максимальная длина пути'),
(val:-1; name:nil)
);
begin
tb:=options;
while tb^.name<>nil do
begin
printf('%-32.31s%ld'#$a, [tb^.name, pathconf ('/tmp', tb^.val)]);
inc(tb);
end;
end.
На одной из систем эта программа вывела следующий результат:
Максимальное число ссылок 32767
Максимальная длина имени файла 256
Максимальная длина пути 1024
Эти значения относятся к каталогу /tmp. Максимально возможное число ссылок является характеристикой самого каталога, а максимальная длина имени файла относится к файлам в каталоге. Существуют также общесистемные ограничения (system-wide limits), они декларируются в файле и их значения могут быть определены при помощи похожей процедуры sysconf.
Глава 5. Процесс
5.1. Понятие процесса
Как было уже рассмотрено в главе 1, процессом в терминологии UNIX является просто экземпляр выполняемой программы, соответствующий определению задачи в других средах. Каждый процесс объединяет код программы, значения данных в переменных программы и более экзотические элементы, такие как значения регистров процессора, стек программы и т.д.1
Командный интерпретатор для выполнения команд обычно запускает один или несколько процессов. Например, командная строка
$ cat file1 file2
приведет к созданию процесса для выполнения команды cat. Немного более сложная команда
$ ls | wc -l
приведет к созданию двух процессов для одновременного выполнения команд ls и wc. (Кроме этого, результат программы ls, вывод списка файлов в каталоге, перенаправляется с помощью программного канала (pipe) на вход программы подсчета числа слов wc.)
Так как процессы соответствуют выполняемым программам, не следует путать их с программами, которые они выполняют. Несколько процессов могут выполнять одну и ту же программу. Например, если несколько пользователей выполняют одну и ту же программу редактора, то каждый из экземпляров программы будет отдельным процессом.
Любой процесс UNIX может, в свою очередь, запускать другие процессы. Это придает среде процессов UNIX иерархическую структуру, подобную дереву каталогов файловой системы. На вершине дерева процессов находится единственный управляющий процесс, экземпляр очень важной программы init, которая является предком всех системных и пользовательских процессов.
Система UNIX предоставляет программисту набор системных вызовов для создания процессов и управления ими. Если исключить различные средства для межпроцессного взаимодействия, то наиболее важными из оставшихся будут:
fork
Используется для создания нового процесса, дублирующего вызывающий. Вызов fork является основным примитивом создания процессов
ехес
Семейство библиотечных процедур и один системный вызов, выполняющих одну и ту же функцию – смену задачи процесса за счет перезаписи его пространства памяти новой программой. Различие между вызовами ехес в основном лежит в способе задания списка их аргументов
wait
Этот вызов обеспечивает элементарную синхронизацию процессов. Он позволяет процессу ожидать завершения другого процесса, обычно логически связанного с ним
halt
Используется для завершения процесса
Далее рассмотрим, что представляют собой процессы UNIX в целом и вышеприведенные четыре важных системных вызова в частности.
5.2. Создание процессов
5.2.1. Системный вызов fork
Основным примитивом для создания процессов является системный вызов fork. Он является механизмом, который превращает UNIX в многозадачную систему.
Описание
uses linux;

Function Fork:Longint;
В результате успешного вызова fork ядро создает новый процесс, который является почти точной копией вызывающего процесса. Другими словами, новый процесс выполняет копию той же программы, что и создавший его процесс, при этом все его объекты данных имеют те же самые значения, что и в вызывающем процессе, за одним важным исключением, которое вскоре обсудим.
Созданный процесс называется дочерним процессом (child process), а процесс, осуществивший вызов fork, называется родителем (parent).
После вызова родительский процесс и его вновь созданный потомок выполняются одновременно, при этом оба процесса продолжают выполнение с оператора, который следует сразу же за вызовом fork.
Идею, заключенную в вызове fork, быть может, достаточно сложно понять тем, кто привык к схеме последовательного программирования. Рис. 5.1 иллюстрирует это понятие. На рисунке рассматриваются три строки кода, состоящие из вызова writeln, за которым следует вызов fork, и еще один вызов writeln.

writeln('One');

pid:=fork;

writeln('Two');
← PC

A

fork

До

После

writeln('One');

pid:=fork;

writeln('One');

pid:=fork;

writeln('Two');
← PC
writeln('Two');
← PC

A

B

Рис. 5.1. Вызов fork

Рисунок разбит на две части: До и После. Часть рисунка До показывает состояние до вызова fork. Существует единственный процесс A (его обозначили буквой А только для удобства, для системы это ничего не значит). Стрелка, обозначенная PC (program counter – программный счетчик), указывает на выполняемый в настоящий момент оператор. Так как стрелка указывает на первый оператор writeln, на стандартный вывод выдается тривиальное сообщение One.
Часть рисунка После показывает ситуацию сразу же после вызова fork. Теперь существуют два выполняющихся одновременно процесса: А и В. Процесс A – это тот же самый процесс, что и в части рисунка До. Процесс В – это новый процесс, порожденный вызовом fork. Этот процесс является копией процесса A за одним важным исключением – он имеет другое значение идентификатора процесса pid, но выполняет ту же самую программу, что и процесс А, то есть те же три строки исходного кода, приведенные на рисунке. В соответствии с введенной выше терминологией процесс А является родительским процессом, а процесс В – дочерним.
Две стрелки с надписью PC в этой части рисунка показывают, что следующим оператором, который выполняется родителем и потомком после вызова fork, является вызов writeln. Другими словами, оба процесса А и В продолжают выполнение с той же точки кода программы, хотя процесс В и является новым процессом для системы. Поэтому сообщение Two выводится дважды.
Идентификатор процесса
Как было уже отмечено в начале этого раздела, вызов fork не имеет аргументов и возвращает идентификатор процесса pid типа longint. Пример вызова:
uses linux;
var
pid:longint;

pid := fork;
Родитель и потомок отличаются значением переменной pid. В родительском процессе значение переменной pid будет ненулевым положительным числом, для потомка же оно равно нулю. Так как возвращаемые в родительском и дочернем процессе значения различаются, то программист может задавать различные действия для двух процессов.
Значение, возвращаемое родительскому процессу в переменной pid, называется идентификатором процесса (process-id) дочернего процесса. Это число идентифицирует процесс в системе аналогично идентификатору пользователя. Поскольку все процессы порождаются при помощи вызова fork, то каждый процесс UNIX имеет уникальный идентификатор процесса.
Следующая короткая программа более наглядно показывает работу вызова fork и использование идентификатора процесса:
(* Программа spawn - демонстрация вызова fork *)
uses linux;

var
pid:longint; (* process-id в родительском процессе *)
begin
writeln ('Пока всего один процесс');
writeln ('Вызов fork...');

pid := fork; (* создание нового процесса *)

if pid = 0 then
writeln ('Дочерний процесс')
else if (pid > 0) then
writeln ('Родительский процесс, pid потомка ', pid)
else
writeln ('Ошибка вызова fork, потомок не создан');
end.
Оператор if, следующий за вызовом fork, имеет три ветви. Первая определяет дочерний процесс, соответствующий нулевому значению переменной pid. Вторая задает действия для родительского процесса, соответствуя положительному значению переменной pid. Третья ветвь неявно соответствует отрицательному, а на самом деле равному -1, значению переменной pid, которое возвращается, если вызову fork не удается создать дочерний процесс. Это может означать, что вызывающий процесс попытался нарушить одно из двух ограничений; первое из них – системное ограничение на число процессов; второе ограничивает число процессов, одновременно выполняющихся и запущенных одним пользователем. В обоих случаях переменная linuxerror содержит код ошибки Sys_EAGAIN. Обратите также внимание на то, что поскольку оба процесса, созданных программой, будут выполняться одновременно без синхронизации, то нет гарантии, что вывод родительского и дочернего процессов не будет смешиваться.
Перед тем как продолжить, стоит обсудить, зачем нужен вызов fork, поскольку сам по себе он может показаться бессмысленным. Существенный момент заключается в том, что вызов fork обретает ценность в сочетании с другими средствами UNIX. Например, возможно, что родительский и дочерний процессы будут выполнять различные, но связанные задачи, организуя совместную работу при помощи одного из механизмов межпроцессного взаимодействия, такого как сигналы или каналы (описываемые в следующих главах). Другим средством, часто используемым совместно с вызовом fork, является системный вызов ехес, позволяющий выполнять другие программы, и который будет рассмотрен в следующем разделе.
Упражнение 5.1. Программа может осуществлять вызов fork несколько раз. Аналогично каждый дочерний процесс может вызывать fork, порождая своих потомков. Чтобы доказать это, напишите программу, которая создает два подпроцесса, а они, в свою очередь, – свой подпроцесс. После каждого вызова fork каждый родительский процесс должен использовать функцию writeln для вывода идентификаторов своих дочерних процессов.
5.3. Запуск новых программ при помощи вызова ехес
5.3.1. Семейство вызовов ехес
Если бы вызов fork был единственным доступным для программиста примитивом создания процессов, то система UNIX была бы довольно скучной, так как в ней можно было бы создавать копии только одной программы. К счастью, для смены исполняемой программы можно использовать функции семейства ехес. На рис. 5.2 показано дерево семейства функций ехес. Основное отличие между разными функциями в семействе состоит в способе передачи параметров. Как видно из рисунка, в конечном итоге все эти функции выполняют один системный вызов execve.
Описание
uses stdio;

(* Для семейства вызовов linuxexecl аргументы должны быть списком,
заканчивающимся NULL *)

function linuxexecl(path:pchar;arg0:pchar;argv:array of const):integer;
function linuxexeclp(fname:pchar;arg0:pchar;argv:array of const):integer;

(* Вызову execl нужно передать полный путь к файлу программы *)
Procedure Execl(Path:pathstr);

(* Вызову execle нужно передать полный путь к файлу программы
и массив указателей на строки окружения *)
Procedure Execle(Path:pathstr; Envp:ppchar);

(* Вызову ехесlp нужно только имя файла программы *)
Procedure Execlp(Path:pathstr);

(* Семейству вызовов execv нужно передать массив аргументов *)

(* Вызову execv нужно передать полный путь к файлу программы *)
Procedure Execv(Path:pathstr;argv:ppchar);

(* Вызову execvp нужно только имя файла программы *)
Procedure Execvp(Path:pathstr;argv:ppchar);

(* Вызову execve нужно передать полный путь к файлу программы
и массив указателей на строки окружения *)
Procedure Execve(Path:pchar;argv:ppchar;envp:ppchar);
Procedure Execve(Path:pathstr;argv,envp:ppchar);
execl
execle
execlp



execv

execvp

execve

Рис. 5.2. Дерево семейства вызовов ехес

Все множество системных вызовов ехес выполняет одну и ту же функцию: они преобразуют вызывающий процесс, загружая новую программу в его пространство памяти. Если вызов ехес завершился успешно, то вызывающая программ полностью замещается новой программой, которая запускается с начала. Результат вызова можно рассматривать как запуск нового процесса, который при этом сохраняет идентификатор вызывающего процесса и по умолчанию наследует файловые дескрипторы (см. пункт 5.5.2).
Важно отметить, что вызов ехес не создает новый подпроцесс, который выполняется одновременно с вызывающим, а вместо этого новая программа загружается на место старой. Поэтому, в отличие от вызова fork, успешный вызов ехеc не возвращает значения.
Для простоты осветим только один из вызовов ехес, а именно linuxexecl.
Все аргументы функции linuxexecl являются указателями строк. Первый из них, аргумент path, задает имя файла, содержащего программу, которая будет запущена на выполнение; для вызова linuxexecl это должен быть полный путь к программе, абсолютный или относительный. Сам файл должен содержать программу или последовательность команд оболочки и быть доступным для выполнения. Система определяет, содержит ли файл программу, просматривая его первые байты (обычно первые два байта). Если они содержат специальное значение, называемое магическим числом (magic number), то система рассматривает файл как программу. Второй аргумент, arg0, является, по соглашению, именем программы или команды, из которого исключен путь к ней. Этот аргумент и оставшееся переменное число аргументов (массив args) доступны в вызываемой программе, аналогично аргументам командной строки при запуске программы из оболочки. В действительности командный интерпретатор сам вызывает команды, используя один из вызовов ехес совместно с вызовом fork. Так как список аргументов имеет произвольную длину, он должен заканчиваться нулевым указателем для обозначения конца списка.
Короткий пример ценнее тысячи слов – следующая программа использует вызов execl для запуска программы вывода содержимого каталога ls:
(* Программа runls - использование "execl" для запуска ls *)
uses linux,stdio;

begin
writeln('Запуск программы ls');
execl('/bin/ls -l');
(* Если execl возвращает значение, то вызов был неудачным *)
perror ('Вызов execl не смог запустить программу ls');
halt(1);
end.
Работа этой демонстрационной программы показана на рис. 5.3. Часть До показывает процесс непосредственно перед вызовом execl. Часть После показывает измененный процесс после вызова execl, который при этом выполняет программу ls. Программный счетчик PC указывает на первую строку программы ls, показывая, что вызов execl запускает программу с начала.

writeln(...);
← PC

execl('/bin/ls -l');

runls

exec

До

После
(* 1-ая строка ls*)
← PC

Команда ls

Рис. 5.3. Вызов exec

Обратите внимание, что в примере за вызовом execl следует безусловный вызов библиотечной процедуры perror. Это отражает то, что успешный вызов функции execl (и других родственных функций) стирает вызывающую программу. Если вызывающая программа сохраняет работоспособность и происходит возврат из вызова execl, значит, произошла ошибка. Поэтому возвращаемое значение execl и родственных функций всегда равно -1.
Вызовы execv, execlp и execvp
Другие формы вызова ехес упрощают задание списков параметров запуска загружаемой программы. Вызов execv принимает два аргумента: первый (path в описании применения вызова) является строкой, которая содержит полное имя и путь к запускаемой программе. Второй аргумент (argv) является массивом строк, определенным как:
argv:ppchar;
Первый элемент этого массива указывает, по принятому соглашению, на имя запускаемой программы (исключая префикс пути). Оставшиеся элементы указывают на все остальные аргументы программы. Так как этот список имеет неопределенную длину, он всегда должен заканчиваться нулевым указателем.
Следующий пример использует вызов execv для запуска той же программы ls, что и в предыдущем примере:
(* Программа runls2 - использует вызов execv для запуска ls *)
uses linux,stdio;

const
av: array [0..2] of pchar=('ls', '-l', nil);
begin
execv ('/bin/ls', av);
(* Если мы оказались здесь, то произошла ошибка *)
perror ('execv failed');
halt(1);
end.
Функции execlp и execvp почти эквивалентны функциям execl и execv. Основное отличие между ними состоит в том, что первый аргумент обоих функций execlp и execvp – просто имя программы, не включающее путь к ней. Путь к файлу находится при помощи поиска в каталогах, заданных в переменной среды PATH. Переменная PATH может быть легко задана на уровне командного интерпретатора с помощью следующих команд:
$ PATH = /bin:/usr/bin:/usr/keith/mybin
$ export PATH
Теперь командный интерпретатор и вызов execvp будут вначале искать команды в каталоге /bin, затем в /usr/bin, и, наконец, в /usr/keith/mybin.
Упражнение 5.2. В каком случае нужно использовать вызов execv вместо execl?
Упражнение 5.3. Предположим, что вызовы execvp и execlp не существуют. Напишите эквиваленты этих процедур, используя вызовы execl и execv. Параметры этих процедур должны состоять из списка каталогов и набора аргументов командной строки.
5.3.2. Доступ к аргументам, передаваемым при вызове exec
Любая программа может получить доступ к аргументам активизировавшего ее вызова exec через параметры, передаваемые ей. Эти параметры описаны в модуле syslinux следующим образом:
var
argc:integer;
argv:ppchar;
envp:ppchar;
Такое описание должно быть знакомо большинству программистов на Си, так как похожий метод используется для доступа к аргументам командной строки при обычном старте программы – еще один признак того, что командный интерпретатор также использует для запуска процессов вызов exec. (Несколько предшествующих примеров и упражнений были составлены с учетом того, что читателям книги известен метод получения программой параметров ее командной строки. Ниже эта тема будет рассмотрена подробнее.)
В вышеприведенном определении значение переменной argc равно числу аргументов, переменная argv указывает на массив самих аргументов, а переменная envp – на массив строк окружения. Поэтому, если программа запускается на выполнение при помощи вызова execvp следующим образом:
const
argin:array [0..3] of pchar = ('команда', 'с', 'аргументами', nil);

execvp('prog', argin);
то в программе prog будут истинны следующие выражения (выражения вида argv[х] = 'ххх' следует считать фигуральным равенством, а не выражением языка Паскаль):
При использовании модуля syslinux
При использовании модуля system
argc = 3
paramcount = 2
argv[0] = 'команда'
paramstr(0) = 'команда'
argv[1] = 'с'
paramstr(1) = 'с'
argv[2] = 'аргументами'
paramstr(2) = 'аргументами'
argv[3] = nil
paramstr(3) = nil
В качестве простой иллюстрации этого метода рассмотрим следующую программу, которая печатает свои аргументы, за исключением нулевого, на стандартный вывод:1
а) с применением модуля system:
(* Программа myecho - вывод аргументов командной строки *)
var
i:integer;
begin
for i:=1 to paramcount do
write(paramstr(i), ' ');
writeln;
end.
б) с применением модуля syslinux:
(* Программа myecho - вывод аргументов командной строки *)
uses syslinux;

var
i:integer;
begin
for i:=1 to argc-1 do
write(argv[i], ' ');
writeln;
end.
Если вызвать эту программу в следующем фрагменте кода
const
argin:array [0..3] of pchar = ('myecho', 'hello', 'world', nil);
execvp(argin[0], argin);
то переменная argc в программе myecho будет иметь значение 3, и в результат на выходе программы получим:
hello world
Тот же самый результат можно получить при помощи команды оболочки:
$ ./myecho hello world
Упражнение 5.4. Напишите программу waitcmd, которая выполняет произвольную команду при изменении файла. Она должна принимать в качестве аргументов командной строки имя контролируемой файла и команду, которая должна выполняться в случае его изменения. Для слежения за файлом можно использовать вызов fstat. Программа не должна расходовать напрасно системные ресурсы, поэтому следует использовать процедуру sleep (представленную в упражнении 2.16), для приостановки выполнения программы waitcmd в течение заданного интервала времени, после того как она проверит файл. Как должна действовать программа, если файл изначально не существует?
5.4. Совместное использование вызовов ехес и fork
Системные вызовы fork и ехес, объединенные вместе, представляют мощный инструмент для программиста. Благодаря ветвлению при использовании вызова ехес во вновь созданном дочернем процессе программа может выполнять другую программу в дочернем процессе, не стирая себя из памяти. Следующий пример показывает, как это можно сделать. В нем мы также представим простую процедуру обработки ошибок fatal и системный вызов wait. Системный вызов wait, описанный ниже, заставляет процесс ожидать завершения работы дочернего процесса.
(* Программа runls3 - выполнить ls как субпроцесс *)
uses linux,stdio;

var
pid:longint;
begin
pid := fork;
case pid of
-1:
fatal ('Ошибка вызова fork');
0:
begin
(* Потомок вызывает exec *)
execl('/bin/ls -l');
fatal('Ошибка вызова exec ');
end;
else
begin
(* Родительский процесс вызывает wait для приостановки
* работы до завершения дочернего процесса.
*)
wait(nil);
writeln('Программа ls завершилась');
halt(0);
end;
end;
end.
Процедура fatal использует функцию perror для вывода сообщения, а затем завершает работу процесса. Процедура fatal реализована следующим образом:
procedure fatal(s:pchar);
begin
perror(s);
halt(1);
end;
Снова графическое представление, в данном случае рис. 5.4, используется для наглядного объяснения работы программы. Рисунок разбит на три части: До вызова fork, После вызова fork и После вызова ехес.
В начальном состоянии, До вызова fork, существует единственный процесс А и программный счетчик PC направлен на оператор fork, показывая, что это следующий оператор, который должен быть выполнен.
После вызова fork существует два процесса, А и В. Родительский процесс A выполняет системный вызов wait. Это приведет к приостановке выполнения процесса А до тех пор, пока процесс В не завершится. В это время процесс В использует вызов execl для запуска на выполнение команды ls.

pid:=fork;
← PC

A

До вызова fork

wait(nil);
← PC

execl('/bin/ls -l');
← PC

A

B

После вызова fork

После вызова exec

(* 1-ая строка ls*)
← PC
wait(nil);
← PC

A

B (теперь

выполняет команду ls)
Рис. 5.4. Совместное использование вызовов fork и ехес

Что происходит дальше, показано в части После вызова ехес на рис. 5.4. Процесс В изменился и теперь выполняет программу ls. Программный счетчик процесса В установлен на первый оператор команды ls. Так как процесс А ожидает завершения процесса В, то положение его программного счетчика PC не изменилось.
Теперь можно увидеть в общих чертах механизмы, используемые командным интерпретатором. Например, при обычном выполнении команды оболочка использует вызовы fork, ехес и wait приведенным выше образом. При фоновом исполнении команды вызов wait пропускается, и оба процесса – команда и оболочка – продолжают выполняться одновременно.
Пример docommand
Модуль stdio предоставляет библиотечную процедуру runshell, которая позволяет выполнить в программе команду оболочки. Для примера создадим упрощенную версию этой процедуры docommand, используя вызовы fork и ехес. В качестве посредника вызовем стандартную оболочку (заданную именем /bin/sh), а не будем пытаться выполнять программу напрямую. Это позволит программе docommand воспользоваться преимуществами, предоставляемыми оболочкой, например, раскрытием шаблонов имен файлов. Задание параметра -с вызова оболочки определяет, что команды передаются не со стандартного ввода, а берутся из следующего строчного аргумента.
(* Программа docommand -- запуск команды оболочки, первая версия *)

uses linux,stdio;

function docommand(command:pchar):integer;
var
pid:longint;
begin
pid := fork;
if pid < 0 then
begin
docommand:=-1;
exit;
end;

if pid = 0 then (* дочерний процесс *)
begin
linuxexecl('/bin/sh', 'sh', ['-c', command, nil]);
perror ('execl');
halt(1);
end;

(* Код родительского процесса *)
(* Ожидание возврата из дочернего процесса *)
wait(nil);

docommand:=0;
end;

begin
docommand('ls -l | wc -l');
end.
Следует сказать, что это только первое приближение к настоящей библиотечной процедуре runshell. Например, если конечный пользователь программы нажмет клавишу прерывания во время выполнения команды оболочки, то и вызывающая программа, и команда остановятся. Существуют способы обойти это ограничение, которые будут рассмотрены в следующей главе.
5.5. Наследование данных и дескрипторы файлов
5.5.1. Вызов fork, файлы и данные
Созданный при помощи вызова fork дочерний процесс является почти точной копией родительского. Все переменные в дочернем процессе будут иметь те же самые значения, что и в родительском (единственным исключением являете значение, возвращаемое самим вызовом fork). Так как данные в дочернем процессе являются копией данных в родительском процессе и занимают другое абсолютное положение в памяти, важно понимать, что последующие изменения в одном процессе не будут затрагивать переменные в другом.
Аналогично все файлы, открытые в родительском процессе, также будут открытыми и в потомке; при этом дочерний процесс будет иметь свою копию связанных с каждым файлом дескрипторов. Тем не менее файлы, открытые до вызова fork, остаются тесно связанными в родительском и дочернем процессах. Это обусловлено тем, что указатель чтения-записи для каждого из таких файлов используется совместно родительским и дочерним процессами благодаря тому, что он поддерживается системой и существует не только в самом процессе. Следовательно, если дочерний процесс изменяет положение указателя в файле, то в родительском процессе он также окажется в новом положении. Это поведение демонстрирует следующая короткая программа, в которой использована процедура fatal, приведенная ранее в этой главе, а также новая процедура printpos. Дополнительно введено допущение, что существует файл с именем data длиной не меньше 20 символов (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx).
(* Программа proc_file -- поведение файлов при ветвлении *)
(* Предположим, что длина файла "data" не менее 20 символов *)
uses linux,stdio;

var
fd:integer;
pid:longint; (* идентификатор процесса *)
buf:array [0..9] of char; (* буфер данных для файла *)
begin
fd := fdopen ('data', Open_RDONLY);
if fd = -1 then
fatal ('Ошибка вызова open ');

fdread (fd, buf, 10); (* переместить вперед указатель файла *)
printpos ('До вызова fork', fd);
(* Создать два процесса *)

pid := fork;
case pid of
1: (* ошибка *)
fatal ('Ошибка вызова fork ');
0: (* потомок *)
begin
printpos ('Дочерний процесс до чтения', fd);
fdread (fd, buf, 10);
printpos ('Дочерний процесс после чтения', fd);
end;
else (* родитель *)
begin
wait(nil);
printpos ('Родительский процесс после ожидания', fd);
end;
end;
end.
Процедура printpos просто выводит текущее положение в файле, а также короткое сообщение. Ее можно реализовать следующим образом:
(* Вывести положение в файле *)
procedure printpos(_string:pchar;filedes:integer);
var
pos:longint;
begin
pos := fdseek (filedes, 0, SEEK_CUR);
if pos=-1 then
fatal ('Ошибка вызова lseek');
writeln(_string,':',pos);
end;
После запуска этого примера получены результаты, которые убедительно подтверждают то, что указатель чтения-записи совместно используется обоими процессами:
До вызова fork:10
Дочерний процесс до чтения:10
Дочерний процесс после чтения:20
Родительский процесс после ожидания:20
Упражнение 5.5. Напишите программу, показывающую, что значения переменных программы в родительском и дочернем процессах первоначально совпадают, но не зависят друг от друга.
Упражнение 5.6. Определите, что происходит в родительском процессе, если дочерний процесс закрывает файл, дескриптор которого он унаследовал после ветвления. Другими словами, останется ли файл открытым в родительском процессе или же будет закрыт?
5.5.2. Вызов ехес и открытые файлы
Дескрипторы открытых файлов обычно сохраняют свое состояние также во время вызова ехес, то есть файлы, открытые в исходной программе, остаются открытыми, когда совершенно новая программа запускается при помощи вызова ехес. Указатели чтения-записи на такие файлы остаются неизменными после вызова ехес. (Очевидно, не имеет смысла говорить о сохранении значений переменных после вызова ехес, так как в общем случае новая программа совершенно отличается от старой.)
Тем не менее есть связанный с файловым дескриптором флаг close-on-exec (закрывать при вызове ехес), который может быть установлен с помощью универсальной процедуры fcntl. Если этот флаг установлен (по умолчанию он сброшен), то файл закрывается при вызове любой функции семейства ехес. Следующий фрагмент показывает, как устанавливается флаг close-on-exec:
uses linux;

.
.
.
var
fd:longint;

fd := fdopen('file', Open_RDONLY);
.
.
.
(* Установить флаг close-on-exec *)
fcntl(fd, F_SETFD, 1);
Флаг close-on-exec можно сбросить так:
fcntl(fd, F_SETFD, 0);
Значение флага можно получить следующим образом:
res := fcntl(fd, F_GETFD);
Целое res будет иметь значение 1, если флаг close-on-exit установлен для дескриптора файла fd, и 0 – в противном случае.
5.6. Завершение процессов при помощи системного вызова halt
Описание
uses system;

procedure halt(status:word);
Системный вызов halt уже известен, но теперь следует дать его правильное описание. Этот вызов используется для завершения процесса, хотя это также происходит, когда управление доходит до конца тела главной программы или до процедуры exit в теле главной программы.
Единственный целочисленный аргумент вызова halt называется статусом завершения (exit status) процесса, младшие восемь бит которого доступны родительскому процессу при условии, если он выполнил системный вызов wait (подробнее об этом см. в следующем разделе). При этом возвращаемое вызовом halt значение обычно используется для определения успешного или неудачного завершения выполнявшейся процессом задачи. По принятому соглашению, нулевое возвращаемое значение соответствует нормальному завершению, а ненулевое значение говорит о том, что что-то случилось.
Кроме завершения вызывающего его процесса, вызов halt имеет еще несколько последствий: наиболее важным из них является закрытие всех открытых дескрипторов файлов. Если, как это было в последнем примере, родительский процесс выполнял вызов wait, то его выполнение продолжится.
Для полноты изложения следует также упомянуть системный вызов _exit, который отличается от вызова halt наличием символа подчеркивания в начале. Он используется в точности так же, как и вызов halt. Тем не менее он не включает описанные ранее действия по очистке. В большинстве случаев следует избегать использования вызова _exit.
Упражнение 5.7. Статус завершения программы можно получить в командном интерпретаторе при помощи переменной $?, например:
$ ls nonesuch
nonesuch: No such file or directory
$ echo $?
2
Напишите программу fake, которая использует целочисленное значение первого аргумента в качестве статуса завершения. Используя намеченный выше метод, выполните программу fake, задавая различные значения аргументов, включая большие и отрицательные. Есть ли польза от программы fake?
5.7. Синхронизация процессов
5.7.1. Системный вызов wait
Описание
uses stdio;

function wait(status:pinteger):longint;
Как было уже обсуждено, вызов wait временно приостанавливает выполнение процесса, в то время как дочерний процесс продолжает выполняться. После завершения дочернего процесса выполнение родительского процесса продолжится. Если запущено более одного дочернего процесса, то возврат из вызова wait произойдет после выхода из любого из потомков.
Вызов wait часто осуществляется родительским процессом после вызова fork, например:
.
.
.
var
status:integer;
cpid:longint;

cpid := fork; (*Создать новый процесс *)

if cpid = 0 then
begin
(* Дочерний процесс *)
(* Выполнить какие-либо действия ... *)
end
else
begin
/* Родительский процесс, ожидание завершения дочернего */
cpid := wait(@status);
writeln('Дочерний процесс ', cpid, ' завершился');
end;
.
.
.
Сочетание вызовов fork и wait наиболее полезно, если дочерний процесс предназначен для выполнения совершенно другой программы при помощи вызова ехеc.
Возвращаемое значение wait обычно является идентификатором дочернего процесса, который завершил свою работу. Если вызов wait возвращает значение -1, это может означать, что дочерние процессы не существуют, и в этом случае переменная linuxerror будет содержать код ошибки Sys_ECHILD. Возможность определить завершение каждого из дочерних процессов по отдельности означает, что родительский процесс может выполнять цикл, ожидая завершения каждого из потомков, а после того, как все они завершатся, продолжать свою работу.
Вызов wait принимает один аргумент, status, – указатель на целое число. Если указатель равен nil, то аргумент просто игнорируется. Если же вызову wait передается допустимый указатель, то после возврата из вызова wait переменная status будет содержать полезную информацию о статусе завершения процесса. Обычно эта информация будет представлять собой код завершения дочернего процесса, переданный при помощи вызова halt.
Следующая программа status показывает, как может быть использован вызов wait:
(* Программа status -- получение статуса завершения потомка *)
uses linux,stdio;

var
pid:longint;
status, exit_status:integer;
begin
pid := fork;
if pid < 0 then
fatal ('Ошибка вызова fork ');
if pid = 0 then (* потомок *)
begin
(* Вызвать библиотечную процедуру sleep
* для временного прекращения работы на 4 секунды
*)
sleep(4);
halt(5); (* выход с ненулевым значением *)
end;
(* Если мы оказались здесь, то это родительский процесс, *)
(* поэтому ожидать завершения дочернего процесса *)
pid := wait(@status);
if pid = -1 then
begin
perror ('Ошибка вызова wait ');
halt(2);
end;
(* Проверка статуса завершения дочернего процесса *)
if WIFEXITED (status) then
begin
exit_status := WEXITSTATUS (status);
writeln ('Статус завершения ',pid,' равен ', exit_status);
end;
halt(0);
end.
Значение, возвращаемое родительскому процессу при помощи вызова halt, записывается в старшие восемь бит целочисленной переменной status. Чтобы оно имело смысл, младшие восемь бит должны быть равны нулю. Функция WIFEXITED (определенная в модуле stdio) проверяет, так ли это на самом деле. Если WIFEXITED возвращает false, то это означает, что выполнение дочернего процесса было остановлено (или прекращено) другим процессом при помощи межпроцессного взаимодействия, называемого сигналом (signal) и рассматриваемого в главе 6.
Упражнение 5.8. Переделайте процедуру docommand так, чтобы она возвращала статус вызова halt выполняемой команды. Что должно происходить, если вызов wait возвращает значение -1?
5.7.2. Ожидание завершения определенного потомка: вызов waitpid
Системный вызов wait позволяет родительскому процессу ожидать завершения любого дочернего процесса. Тем не менее, если нужна большая определенность, то можно использовать системный вызов waitpid для ожидания завершения определенного дочернего процесса.
Описание
uses linux;

Function WaitPid(Pid:longint; Status:pointer; Options:Longint):Longint;
Первый аргумент pid определяет идентификатор дочернего процесса, завершения которого будет ожидать родительский процесс. Если этот аргумент установлен равным -1, а аргумент options установлен равным 0, то вызов waitpid ведет себя в точности так же, как и вызов wait, поскольку значение -1 соответствует любому дочернему процессу. Если значение pid больше нуля, то родительский процесс будет ждать завершения дочернего процесса с идентификатором процесса равным pid. Во втором аргументе status будет находиться статус дочернего процесса после возврата из вызова waitpid.
Последний аргумент, options, может принимать константные значения, определенные в модуле linux. Наиболее полезное из них – константа WNOHANG. Задание этого значения позволяет вызывать waitpid в цикле без блокирования процесса, контролируя ситуацию, пока дочерний процесс продолжает выполняться. Если установлен флаг WNOHANG, то вызов waitpid будет возвращать 0 в случае, если дочерний процесс еще не завершился.
Функциональные возможности вызова waitpid с параметром WNOHANG можно продемонстрировать, переписав предыдущий пример. На этот раз родительски процесс проверяет, завершился ли уже дочерний процесс. Если нет, он выводит сообщение, говорящее о том, что он продолжает ждать, затем делает секундную паузу и снова вызывает waitpid, проверяя, завершился ли дочерний процесс. Обратите внимание, что потомок получает свой идентификатор процесса при помощи вызова getpid. Об этом вызове расскажем в разделе 5.10.1.
(* Программа status2 - получение статуса завершения
* дочернего процесса при помощи вызова waitpid
*)
uses linux,stdio,crt;

var
pid:longint;
status, exit_status:integer;
begin
pid := fork;
if pid < 0 then
fatal ('Ошибка вызова fork ');

if pid = 0 then (* потомок *)
begin
(* Вызов библиотечной процедуры sleep
* для приостановки выполнения на 4 секунды
*)
writeln ('Потомок ',getpid,' пауза...');
sleep(4);
halt(5); (* выход с ненулевым значением *)
end;

(* Если мы оказались здесь, то это родительский процесс *)
(* Проверить, закончился ли дочерний процесс, и если нет, *)
(* то сделать секундную паузу, и потом проверить снова *)
while (waitpid (pid, @status, WNOHANG) = 0) do
begin
writeln ('Ожидание продолжается...\n');
sleep(1);
end;

(* Проверка статуса завершения дочернего процесса *)
if WIFEXITED (status) then
begin
exit_status := WEXITSTATUS (status);
writeln ('Статус завершения ',pid,' равен ', exit_status);
end;

halt(0);
end.
При запуске программы получим следующий вывод:
Ожидание продолжается...
Потомок 12857 пауза...
Ожидание продолжается...
Ожидание продолжается...
Ожидание продолжается...
Статус завершения 12857 равен 5
5.8. Зомби-процессы и преждевременное завершение программы
До сих пор предполагалось, что вызовы halt и wait используются правильно, и родительский процесс ожидает завершения каждого подпроцесса. Вместе с тем иногда могут возникать две другие ситуации, которые стоит обсудить:
• в момент завершения дочернего процесса родительский процесс не выполняет вызов wait;
• родительский процесс завершается, в то время как один или несколько дочерних процессов продолжают выполняться.
В первом случае завершающийся процесс как бы «теряется» и становится зомби-процессом (zombie). Зомби-процесс занимает ячейку в таблице, поддерживаемой ядром для управления процессами, но не использует других ресурсов ядра. В конце концов, он будет освобожден, если его родительский процесс вспомнит о нем и вызовет wait. Тогда родительский процесс сможет прочитать статус завершения процесса, и ячейка освободится для повторного использования. Во втором случае родительский процесс завершается нормально. Дочерние процесс (включая зомби-процессы) принимаются процессом init (процесс, идентификатор которого pid = 1, становится их новым родителем).
5.9. Командный интерпретатор smallsh
В этом разделе создается простой командный интерпретатор smallsh. Этот пример имеет два достоинства. Первое состоит в том, что он развивает понятия, введенные в этой главе. Второе – в том, что подтверждается отсутствие в стандартных командах и утилитах UNIX чего-то особенного. В частности, пример показывает, что оболочка является обычной программой, которая запускается при входе в систему.
Наши требования к программе smallsh просты: она должна транслировать и выполнять команды – на переднем плане и в фоновом режиме – а также обрабатывать строки, состоящие из нескольких команд, разделенных точкой с запятой. Другие средства, такие как перенаправление ввода/вывода и раскрытие имен файлов, могут быть добавлены позднее.
Основная логика понятна:
while не встретится EOF do
begin
получить строку команд от пользователя
оттранслировать аргументы и выполнить
ожидать возврата из дочернего процесса
end;
Дадим имя userin функции, выполняющей «получение командной строки от пользователя». Эта функция должна выводить приглашение, а затем ожидать ввода строки с клавиатуры и помещать введенные символы в буфер программы. Функция userin реализована следующим образом:
uses stdio,linux;

(* Заголовочный файл для примера *)
{$i smallsh.inc}

(* Буферы программы и рабочие указатели *)
var
inpbuf:array [0..MAXBUF-1] of char;
tokbuf:array [0..2*MAXBUF-1] of char;
const
ptr:pchar=@inpbuf[0];
tok:pchar=@tokbuf[0];

(* Вывести приглашение и считать строку *)
function userin(p:pchar):integer;
var
c, count:integer;
begin
(* Инициализация для следующих процедур *)
ptr := inpbuf;
tok := tokbuf;
(* Вывести приглашение *)
write(p);
count := 0;
while true do
begin
c := getchar;
if c = EOF then
begin
userin:=EOF;
exit;
end;
if count < MAXBUF then
begin
inpbuf[count] := char(c);
inc(count);
end;
if (c = $a) and (count < MAXBUF) then
begin
inpbuf[count] := #0;
userin:=count;
exit;
end;
(* Если строка слишком длинная, начать снова *)
if c = $a then
begin
writeln ('smallsh: слишком длинная входная строка');
count := 0;
write (p);
end;
end;
end;
Некоторые детали инициализации можно пока не рассматривать. Главное, что функция userin вначале выводит приглашение ввода команды (передаваемое в качестве параметра), а затем считывает ввод пользователя по одному символу до тех пор, пока не встретится символ перевода строки или конец файла (последний случай обозначается символом EOF).
Функция getchar содержится в стандартной библиотеке ввода/вывода. Она считывает один символ из стандартного ввода программы, который обычно соответствует клавиатуре. Функция userin помещает каждый новый символ (если это возможно) в массив символов inpbuf. После своего завершения функция userin возвращает либо число считанных символов, либо EOF, обозначающий конец файла. Обратите внимание, что символы перевода строки не отбрасываются, а добавляются в массив inpbuf.
Заголовочный файл smallsh.inc, упоминаемый в функции userin, содержит определения для некоторых полезных постоянных (например, MAXBUF). В действительности файл содержит следующее:
(* smallsh.inc - определения для интерпретатора smallsh *)
{ifndef SMALL_H}
{define SMALL_H}

const
EOL=1; (* конец строки *)
ARG=2; (* обычные аргументы *)
AMPERSAND=3; (* символ '&' *)
SEMICOLON=4; (* точка с запятой *)
MAXARG=512; (* макс. число аргументов *)
MAXBUF=512; (* макс. длина строки ввода *)
FOREGROUND=0; (* выполнение на переднем плане *)
BACKGROUND=1; (* фоновое выполнение *)

{endif} (* SMALL_H *)
Другие постоянные, не упомянутые в функции userin, встретятся в следующих процедурах.
Рассмотрим следующую процедуру, gettok. Она выделяет лексемы (tokens) из командной строки, созданной функцией userin. (Лексема является минимальной единицей языка, например, имя или аргумент команды.) Процедура gettok вызывается следующим образом:
toktype := gettok(@tptr);
Целочисленная переменная toktype будет содержать значение, обозначающее тип лексемы. Диапазон возможных значений берется из файла smallsh.inc и включает символы EOL (конец строки), SEMICOLON и так далее. Переменная tptr является символьным указателем, который будет указывать на саму лексему после вызова gettok. Так как процедура gettok сама выделяет пространство под строки лексем, нужно передать адрес переменной tptr, а не ее значение.
Исходный код процедуры gettok приведен ниже. Обратите внимание, что поскольку она ссылается на символьные указатели tok и ptr, то должна быть включена в тот же исходный файл, что и userin. (Теперь должно быть понятно, зачем была нужна инициализация переменных tok и ptr в начале функции userin.)
(* Получить лексему и поместить ее в буфер tokbuf *)
function gettok (outptr:ppchar):integer;
var
_type:integer;
begin
(* Присвоить указателю на строку outptr значение tok *)
outptr^ := tok;
(* Удалить пробелы из буфера, содержащего лексемы *)
while (ptr^ = ' ') or (ptr^ = #9) do
inc(ptr);
(* Установить указатель на первую лексему в буфере *)
tok^ := ptr^;
inc(tok);
(* Установить значение переменной type в соответствии
* с типом лексемы в буфере *)
case ptr^ of
#$a:
begin
_type := EOL;
inc(ptr);
end;
'&':
begin
_type := AMPERSAND;
inc(ptr);
end;
';':
begin
_type := SEMICOLON;
inc(ptr);
end;
else
begin
_type := ARG;
inc(ptr);
(* Продолжить чтение обычных символов *)
while inarg (ptr^) do
begin
tok^ := ptr^;
inc(tok);
inc(ptr);
end;
end;
end;
tok^ := #0;
inc(tok);
gettok:=_type;
end;
Функция inarg используется для определения того, может ли символ быть частью «обычного» аргумента. Пока можно просто проверять, является ли символ особым для командного интерпретатора команд smallsh или нет:
const
special:array [0..5] of char = (' ', #9, '&', ';', #$a, #0);

function inarg(c:char):boolean;
var
wrk:pchar;
begin
wrk := special;
while wrk^<>#0 do
begin
if c = wrk^ then
begin
inarg:=false;
exit;
end;
inc(wrk);
end;
inarg:=true;
end;
Теперь можно составить функцию, которая будет выполнять главную работу нашего интерпретатора. Функция procline будет разбирать командную строку, используя процедуру gettok, создавая тем самым список аргументов процесса. Если встретится символ перевода строки или точка с запятой, то она вызывает для выполнения команды процедуру runcommand. При этом она предполагает, что командная строка уже была считана при помощи функции userin.
{$i smallsh.inc}

function procline:integer; (* обработка строки ввода *)
var
arg:array [0..MAXARG] of pchar; (* массив указателей для runcommand *)
toktype:integer; (* тип лексемы в команде *)
narg:integer; (* число аргументов *)
_type:integer; (* на переднем плане или в фоне *)
begin
narg := 0;
while true do (* бесконечный цикл *)
begin
(* Выполнить действия в зависимости от типа лексемы *)
toktype := gettok (@arg[narg]);
case toktype of
2://ARG
if narg < MAXARG then
inc(narg);
1,3,4://EOL,SEMICOLON, AMPERSAND:
begin
if toktype = AMPERSAND then
_type := BACKGROUND
else
_type := FOREGROUND;
if narg <> 0 then
begin
arg[narg] := nil;
runcommand (arg, _type);
end;
if toktype = EOL then
exit;
narg := 0;
end;
end;
end;
end;
Следующий этап состоит в определении процедуры runcommand, которая в действительности запускает командные процессы. Процедура runcommand в сущности, является переделанной процедурой docommand, с которой встречались раньше. Она имеет еще один целочисленный параметр where. Если параметр where принимает значение BACKGROUND, определенное в файле smallsh.inc, то вызов waitpid пропускается, и процедура runcommand просто выводит идентификатор процесса и завершает работу.
{$i smallsh.inc}

(* Выполнить команду, возможно ожидая ее завершения *)
function runcommand(cline:ppchar;where:integer):integer;
var
pid:longint;
status:integer;
begin
pid := fork;
case pid of
-1:
begin
perror ('smallsh');
runcommand:=-1;
exit;
end;
0:
begin
execvp (cline^, cline, envp);
perror (cline^);
halt(1);
end;
end;

(* Код родительского процесса *)
(* Если это фоновый процесс, вывести pid и выйти *)
if where = BACKGROUND then
begin
writeln ('[Идентификатор процесса ',pid,']');
runcommand:=0;
exit;
end;

(* Ожидание завершения процесса с идентификатором pid *)
if waitpid (pid, @status, 0) = -1 then
runcommand:=-1
else
runcommand:=status;
end;
Обратите внимание, что простой вызов wait из функции docommand был заменен вызовом waitpid. Это гарантирует, что выход из процедуры docommand произойдет только после завершения процесса, запущенного в этом вызове docommand, и помогает избавиться от проблем с фоновыми процессами, которые завершаются в это время. (Если это кажется не совсем ясным, следует вспомнить, что вызов wait возвращает идентификатор первого завершающегося дочернего процесса, а не идентификатор последнего запущенного.)
Процедура runcommand также использует системный вызов execvp. Это гарантирует, что при запуске программы, заданной командой, выполняется ее поиск во всех каталогах, указанных в переменной окружения PATH, хотя, в отличие от настоящего командного интерпретатора, в программе smallsh нет никаких средств для работы с переменной PATH.
Последний шаг состоит в написании программы, которая связывает вместе остальные функции. Это простое упражнение:
(* Программа smallsh - простой командный интерпретатор *)

{$i smallsh.inc}

const
prompt = 'Command> '; (* приглашение ввода командной строки *)
begin
while userin (prompt) <> EOF do
procline;
end.
Эта процедура завершает первую версию программы smallsh. И снова следует отметить, что это только набросок законченного решения. Так же, как в случае процедуры docommand, поведение программы smallsh далеко от идеала, когда пользователь вводит символ прерывания, поскольку это приводит к завершению работы программы smallsh. В следующей главе будет показано, как можно сделать программу smallsh более устойчивой.
Упражнение 5.9. Включите в программу smallsh механизм для выключения с помощью символа \ (escaping) специального значения символов, таких как точка с запятой и символ &, так чтобы они могли входить в список аргументов программы. Программа должна также корректно интерпретировать комментарии, обозначаемые символом # в начале. Что должно произойти с приглашением командной строки, если пользователь выключил таким способом специальное значение символа возврата строки?
Упражнение 5.10. Системный вызов dup2 можно использовать для получения копии дескриптора открытого файла. В этом случае он вызывается следующим образом:
dup2(filedes, reqvalue);
где filedes – это исходный дескриптор открытого файла. Значение переменной reqvalue должно быть небольшим целым числом. Если уже был открыт файл с дескриптором, равным reqvalue, он закрывается. После успешного вызова переменная reqvalue будет содержать дескриптор файла, который ссылается на тот же самый файл, что и дескриптор filedes. Следующий фрагмент программы показывает, как перенаправить стандартный ввод, то есть дескриптор файла со значением 0:
fd := fdopen('somefile', Open_RDONLY);
fdclose (0);
dup2(fd, 0);
Используя этот вызов вместе с системными вызовами fdopen и fdclose, переделайте программу smallsh так, чтобы она поддерживала перенаправление стандартного ввода и стандартного вывода, используя ту же систему обозначений, что и стандартный командный интерпретатор UNIX. Помните, что стандартный ввод и вывод соответствует дескрипторам 0 и 1 соответственно. Обратите внимание, что существует также близкий по смыслу вызов dup.
5.10. Атрибуты процесса
С каждым процессом UNIX связан набор атрибутов, которые помогают системе управлять выполнением и планированием процессов, обеспечивать защиту файловой системы и так далее. Один из атрибутов, с которым мы уже встречались, – это идентификатор процесса, то есть число, которое однозначно идентифицирует процесс. Другие атрибуты простираются от окружения, которое является набором строк, определяемых программистом и находящихся вне области данных, до действующего идентификатора пользователя, определяющего права доступа процесса к файловой системе. В оставшейся части этой главы рассмотрим наиболее важные атрибуты процесса.
5.10.1. Идентификатор процесса
Как уже отмечено в начале этой главы, система присваивает каждому процессу неотрицательное число, которое называется идентификатором процесса. В любой момент времени идентификатор процесса является уникальным, хотя после завершения процесса он может использоваться снова для другого процесса. Некоторые идентификаторы процесса зарезервированы системой для особых процессов. Процесс с идентификатором 0, хотя он и называется планировщиком (scheduler), на самом деле является процессом подкачки памяти (swapper). Процесс с идентификатором 1 – это процесс инициализации, выполняющий программу /etc/init. Этот процесс, явно или неявно, является предком всех других процессов в системе UNIX.
Программа может получить свой идентификатор процесса при помощи следующего системного вызова:
pid := getpid;
Аналогично вызов getppid возвращает идентификатор родителя вызывающего процесса:
ppid := getppid;
Например:
Uses linux;

begin
Writeln ('Process Id = ',getpid,' Parent process Id = ',getppid);
end.
Следующая процедура gentemp использует вызов getpid для формирования уникального имени временного файла. Это имя имеет форму:
/tmp/tmp.
Суффикс номера увеличивается на единицу при каждом вызове процедуры gentemp. Процедура также вызывает функцию access, чтобы убедиться, что файл еще не существует:
uses linux,strings;

const
num:integer=0;
namebuf:array [0..19] of char='';
prefix='/tmp/tmp';

function gentemp:pchar;
var
length:integer;
pid:longint;
begin
pid := getpid; (* получить идентификатор процесса *)

(* Стандартные процедуры работы со строками *)
strcopy (namebuf, prefix);
length := strlen (namebuf);

(* Добавить к имени файла идентификатор процесса *)
itoa (pid, @namebuf[length]);
strcat (namebuf, '.');
length := strlen (namebuf);
repeat
(* Добавить суффикс с номером *)
itoa(num, @namebuf[length]);
inc(num);
until(not access (namebuf, F_OK));
gentemp:=namebuf;
end;
Процедура itoa просто преобразует целое число в эквивалентную строку:
(* Функция itoa - преобразует целое число в строку *)
function itoa(i:integer;str:pchar):integer;
var
power, j : integer;
begin
j := i;
power := 1;
while j >= 10 do
begin
power := power * 10;
j := j div 10;
end;
while power > 0 do
begin
str^ := char(byte('0') + i div power);
inc(str);
i := i mod power;
power := power div 10;
end;
str^ := #0;
end;
Обратите внимание на способ преобразования цифры в ее символьный эквивалент в первом операторе во втором цикле for – он опирается на знание таблицы символов ASCII. Следует также отметить, что большую часть работы можно было бы выполнить гораздо проще при помощи процедуры sprintf. Описание процедуры sprintf смотрите в главе 11.
Упражнение 5.11. Переделайте процедуру gentemp так, чтобы она принимала в качестве аргумента префикс имени временного файла.
5.10.2. Группы процессов и идентификаторы группы процессов
Система UNIX позволяет легко помещать процессы в группы. Например, если в командной строке задано, что процессы связаны при помощи программного канала, они обычно помещаются в одну группу процессов. На рис. 5.5 показана такая типичная группа процессов, установленная из командной строки.

who

awk '{print $1}'

sort -u

Рис. 5.5. Группа процессов

Группы процессов удобны для работы с набором процессов в целом, с помощью механизма межпроцессного взаимодействия, который называется сигналами, о чем будет сказано подробнее в главе 6. Обычно сигнал «посылается» отдельному процессу и может вызывать завершение этого процесса, но можно послать сигнал и целой группе процессов.
Каждая группа процессов (process group) обозначается идентификатором группы процессов (process group-id), имеющим тип pid_t. Процесс, идентификатор которого совпадает с идентификатором группы процессов, считается лидером (leader) группы процессов, и при его завершении выполняются особые действия. Первоначально процесс наследует идентификатор группы во время вызова fork или ехес.
Процесс может получить свой идентификатор группы при помощи системного вызова getpgrp:
Описание
uses stdio;

function getpgrp:longint;
5.10.3. Изменение группы процесса
В оболочке UNIX, поддерживающей управление заданиями, может потребоваться переместить процесс в другую группу процессов. Управление заданиями позволяет оболочке запускать несколько групп процессов (заданий) и контролировать, какие группы процессов должны выполняться на переднем плане и, следовательно, иметь доступ к терминалу, а какие должны выполняться в фоне. Управление заданиями организуется при помощи сигналов.
Процесс может создать новую группу процессов или присоединиться к существующей при помощи системного вызова setpgid:
Описание
uses stdio;

function setpgid(pid, pgid:longint):longint;
Вызов setpgid устанавливает идентификатор группы процесса с идентификатором pid равным pgid. Если pid равен 0, то используется идентификатор вызывающего процесса. Если значения идентификаторов pid и pgid одинаковы, то процесс становится лидером группы процессов. В случае ошибки возвращает значение -1. Если идентификатор pgid равен нулю, то в качестве идентификатора группы процесса используется идентификатор процесса pid.
5.10.4. Сеансы и идентификатор сеанса
В свою очередь, каждая группа процессов принадлежит к сеансу. В действительности сеанс относится к связи процесса с управляющим терминалом (controlling terminal). Когда пользователи входят в систему, все процессы и группы процессов, которые они явно или неявно создают, будут принадлежать к сеансу, связанному с их текущим терминалом. Сеанс обычно представляет собой набор из одной группы процессов переднего плана, использующей терминал, и одной или более групп фоновых процессов. Сеанс обозначается при помощи идентификатора сеанса (session-id), который имеет тип longint.
Процесс может получить идентификатор сеанса при помощи вызова getsid:
Описание
uses stdio;

function getsid(pid:longint):longint;
Если передать вызову getsid значение 0, то он вернет идентификатор сеанса вызывающего процесса, в противном случае возвращается идентификатор сеанса процесса, заданного идентификатором pid.
Понятие сеанса полезно при работе с фоновыми процессами или процессами-демонами (daemon processes). Процессом-демоном называется просто процесс, не имеющий управляющего терминала. Примером такого процесса является процесс cron, запускающий команды в заданное время. Демон может задать для себя сеанс без управляющего терминала, переместившись в другой сеанс при помощи системного вызова setsid.
Описание
uses stdio;

function setsid:longint;cdecl;
Если вызывающий процесс не является лидером группы процессов, то создается новая группа процессов и новый сеанс, и идентификатор вызывающего процесса станет идентификатором созданного сеанса. Он также не будет иметь управляющего терминала. Процесс-демон теперь будет находиться в странном состоянии, так как он будет единственным процессом в группе процессов, содержащейся в новом сеансе, а его идентификатор процесса pid – также идентификатором группы и сеанса.
Функция setsid может завершиться неудачей и возвратит значение -1, если вызывающий процесс уже является лидером группы.
5.10.5. Переменные программного окружения
Программное окружение (environment) процесса – это просто набор строк, заканчивающихся нулевым символом, представленных в программе просто в виде массива указателей на строки. Эти строки называются переменными окружения (environment variables). По принятому соглашению, каждая строка окружения имеет следующую форму:
имя переменной = ее содержание
В модуле dos определены функция envcount, возвращающие количество строк окружения, и функция envstr, возвращаю строку окружения с заданным номером. Можно напрямую использовать программное окружение процесса с помощью массива envp из модуля syslinux.
В качестве простого примера следующая программа просто выводит свое окружение и завершает работу.
а) с использованием модуля dos:
(* Программа showmyenv.pas - вывод окружения *)
uses dos;

var
i:integer;
begin
for i:=1 to envcount do
writeln(envstr(i));
end.
б) с использованием модуля syslinux:
(* Программа showmyenv.pas - вывод окружения *)
uses syslinux;

var
i:integer;
begin
i:=0;
while envp[i]<>nil do
begin
writeln(envp[i]);
inc(i);
end;
end.
При запуске этой программы на одной из машин были получены следующие результаты:
CDPATH=:..:/
HOME=/usr/keith
LOGNAME=keith
MORE=-h -s
PATH=/bin:/etc:/usr/bin:/usr/cbin:/usr/lbin
SHELL=/bin/ksh
TERM=vti00
TZ=GMTOBST
Этот список может быть вам знакомым. Это окружение процесса командного интерпретатора (оболочки), вызвавшего программу showmyenv, и оно включает такие важные переменные, как HOME и PATH.
Пример показывает, что по умолчанию окружение процесса совпадает с окружением процесса, создавшего его при помощи вызова fork или ехес. Поскольку окружение передается указанным способом, то можно записывать в окружении информацию, которую иначе пришлось бы задавать заново для каждого нового процесса. Переменная окружения TERM, в которой записан тип текущего терминала, является хорошим примером того, насколько полезным может быть использование окружения.
Чтобы задать для процесса новое окружение, необходимо использовать для его запуска один из двух вызовов из семейства exec: execle или execve. Они вызываются следующим образом:
execle(path, envp);
и:
execve(path, argv, envp);
Эти вызовы дублируют соответственно вызовы execv и execl. Единственное различие заключается в добавлении параметра envp, который является заканчивающимся нулевым символом массивом строк, задающим окружение новой программы. Следующий пример использует вызов execle для передачи нового программного окружения программе showmyenv:
(* Программа setmyenv.pas - установка окружения программы *)
uses linux,stdio;

const
argv:array [0..1] of pchar=('showmyenv',nil);
envp:array [0..2] of pchar=('foo=bar','bar=foo',nil);
begin
execve ('./showmyen', argv, envp);
perror ('Ошибка вызова execve');
end.
Для поиска в переменной envp имени переменной окружения, заданной в форме name=string, можно использовать стандартную библиотечную функцию getenv.
Описание
uses dos;

Function GetEnv(EnvVar:String):String;
Аргументом функции getenv является имя искомой переменной. При успешном завершении поиска функция getenv возвращает указатель на строку переменной окружения, в противном случае – пустая строка. Следующий код показывает пример использования этой функции:
(* Найти значение переменной окружения PATH *)

uses dos;

begin
writeln('PATH=',getenv('PATH'));
end.
Для изменения окружения существует парная процедура putenv. Она вызывается следующим образом:
putenv('НОВАЯ_ПЕРЕМЕННАЯ = значение');
В случае успеха процедура putenv возвращает нулевое значение. Она изменяет переменную окружения, на которую указывает глобальная переменная envp.
5.10.6. Текущий рабочий каталог
Как было установлено в главе 4, с каждым процессом связан текущий рабочий каталог. Первоначально текущий рабочий каталог наследуется во время создавшего процесс вызова fork или exec. Другими словами, процесс первоначально помещается в тот же каталог, что и родительский процесс.
Важным фактом является то, что текущий рабочий каталог является атрибутом отдельного процесса. Если дочерний процесс меняет каталог при помощи вызова chdir (определение которого приведено в главе 4), то текущий рабочий каталог родительского процесса не меняется. Поэтому стандартная команда cd на самом деле является «встроенной» командой оболочки, а не программой.
5.10.7. Текущий корневой каталог
С каждым процессом также связан корневой каталог, который используется при поиске абсолютного пути. Так же, как и текущий рабочий каталог, корневым каталогом процесса первоначально является корневой каталог его родительского процесса. Для изменения начала иерархии файловой системы для процесса в ОС UNIX существует системный вызов chroot:
Описание
uses stdio;

function chroot(path:pchar):longint;
Переменная path указывает на путь, обозначающий каталог. В случае успешного вызова chroot путь path становится начальной точкой при поиске в путях, начинающихся с символа / (только для вызывающего процесса, корневой каталог системы при этом не меняется). В случае неудачи вызов chroot не меняет корневой каталог и возвращает значение -1. Для изменения корневого каталога вызывающий процесс должен иметь соответствующие права доступа.
Упражнение 5.12. Добавьте к командному интерпретатору smallsh команду cd.
Упражнение 5.13. Напишите собственную версию функции getenv.
5.10.8. Идентификаторы пользователя и группы
С каждым процессом связаны истинные идентификаторы пользователя и группы. Это всегда идентификатор пользователя и текущий идентификатор группы запустившего процесс пользователя.
Действующие идентификаторы пользователя и группы используются для определения возможности доступа процесса к файлу. Чаще всего, эти идентификаторы совпадают с истинными идентификаторами пользователя и группы. Равенство нарушается, если процесс или один из его предков имеет установленные биты доступа set-user-id или set-group-id. Например, если для файла программы установлен бит set-user-id, то при запуске программы на выполнение при помощи вызова ехес действующим идентификатором пользователя становится идентификатор владельца файла, а не запустившего процесс пользователя.
Для получения связанных с процессом идентификаторов пользователя и группы существует несколько системных вызовов. Следующий фрагмент программы демонстрирует их:
uses linux;

var
uid, euid, gid, egid : longint;
begin
(* Получить истинный идентификатор пользователя *)
uid := getuid;
(* Получить действующий идентификатор пользователя *)
euid := geteuid;
(* Получить истинный идентификатор группы *)
gid := getgid;
(* Получить действующий идентификатор группы *)
egid := getegid;
end.
Для задания действующих идентификаторов пользователя и группы процесса также существуют два вызова:
uses stdio;

var
newuid, newgid:longint;
.
.
.
(* Задать действующий идентификатор пользователя *)
status := setuid(newuid);

(* Задать действующий идентификатор группы *)
status := setgid(newgid);
Процесс, запущенный непривилегированным пользователем (то есть любым пользователем, кроме суперпользователя) может менять действующие идентификаторы пользователя и группы только на истинные.1 Суперпользователю, как всегда, предоставляется полная свобода. Обе процедуры возвращают нулевое значение в случае успеха, и -1 – в случае неудачи.
Упражнение 5.14. Напишите процедуру, которая получает истинные идентификаторы пользователя и группы вызывающего процесса, а затем преобразует их в символьную форму и записывает в лог-файл.
5.10.9. Ограничения на размер файла: вызов ulimit
Для каждого процесса существует ограничение на размер файла, который может быть создан при помощи системного вызова fdwrite. Это ограничение распространяется также на ситуацию, когда увеличивается размер существующего файла, имевшего до этого длину, меньшую максимально возможной.
Предельный размер файла можно изменять при помощи системного вызова ulimit.
Описание
uses stdio;

function ulimit(cmd:longint;args:array of const):longint;
Для получения текущего максимального размера файла можно вызвать ulimit, установив значение параметра cmd равным UL_GETFSIZE. Возвращаемое значение равно числу 512-байтных блоков.
Для изменения максимального размера файла можно присвоить переменной cmd значение UL_SETFSIZE и поместить новое значение максимального размера файла, также в 512-байтных блоках, в переменную newlimit, например:
if ulimit(UL_SETFSIZE, newlimit) < 0 then
perror('Ошибка вызова ulimit');
В действительности увеличить максимальный размер файла таким способом может только суперпользователь. Процессы с идентификаторами других пользователей могут только уменьшать этот предел.
5.10.10. Приоритеты процессов
Система приближенно вычисляет долю процессорного времени, отводимую для работы процесса, руководствуясь значением nice (буквально «дружелюбный»). Значения nice находятся в диапазоне от нуля до максимального значения, зависящего от конкретной системы. Чем больше это число, тем ниже приоритет процесса. Процессы, «дружелюбно настроенные», могут понижать свой приоритет, используя системный вызов nice. Этот вызов имеет один аргумент, положительное число, на которое увеличивается текущее значение nice, например:
nice(5);
Процессы суперпользователя (и только суперпользователя) могут увеличивав свой приоритет, используя отрицательное значение параметра вызова nice. Вызов nice может пригодиться, если есть необходимость, например, вычислить число π с точностью до ста миллионов знаков, не изменяя существенно время реакции системы для остальных пользователей. Вызов nice был введен давно. Современные факультативные расширения реального времени стандарта POSIX определяют гораздо более точное управление планированием параллельной работы процессов.
Описание
uses linux;

Function GetPriority(Which,Who:Integer):Integer;
Function SetPriority(Which,Who,Prio:Integer):Integer;
GetPriority возвращает приоритет процесса, определяемых переменными Which и Who. Which может принимать значения Prio_Process, Prio_PGrp и Prio_User для идентификаторов процесса, его группы и владельца соответственно.
SetPriority устанавливает приоритет процесса. Значение Prio может быть в диапазоне от -20 до 20.
Программа, демонстрирующая функции Nice и Get/SetPriority:
Uses linux;

begin
writeln ('Setting priority to 5');
setpriority (prio_process,getpid,5);
writeln ('New priority = ',getpriority (prio_process,getpid));
writeln ('Doing nice 10');
nice (10);
writeln ('New Priority = ',getpriority (prio_process,getpid));
end.

Глава 6. Сигналы и их обработка
6.1. Введение
Часто требуется создавать программные системы, состоящие не из одной монолитной программы, а из нескольких сотрудничающих процессов. Для этого может быть множество причин: одиночная программа может оказаться слишком громоздкой и не поместиться в памяти; часть требуемых функций часто уже реализована в каких-либо существующих программах; задачу может быть удобнее решать при помощи процесса-сервера, взаимодействующего с произвольным числом процессов-клиентов; есть возможность использовать несколько процессоров и т.д.
К счастью, ОС UNIX имеет развитые механизмы межпроцессного взаимодействия. В этой и следующей главах мы обсудим три наиболее популярных средства: сигналы (signals), программные каналы (pipes) и именованные каналы (FIFO). Данные средства, наряду с более сложными средствами, которым будут посвящены главы 8 и 10, предоставляют разработчику программного обеспечения широкий выбор средств построения многопроцессных систем.
Эта глава будет посвящена изучению первого средства – сигналам. Рассмотрим пример запуска команды UNIX, выполнение которой, вероятно, займет много времени:
$ fpc verybigprog.pas
Позже становится ясным, что программа содержит ошибку, и ее компиляция не может завершиться успехом. Тогда, чтобы сэкономить время, следует прекратить выполнение команды нажатием специальной клавиши прерывания задания (interrupt key) терминала; обычно это клавиша Del или клавиатурная комбинация Ctrl+C. Выполнение программы прекратится, и программист вернется к приглашению ввода команды командного интерпретатора.
В действительности при этом происходит следующая последовательность событий: часть ядра, отвечающая за ввод с клавиатуры, распознает символ прерывания задания. Затем ядро посылает сигнал SIGINT всем процессам, для которых текущий терминал является управляющим терминалом. Среди этих процессов будет и экземпляр компилятора cc. Когда процесс компилятора ее получит этот сигнал, он выполнит связанное с сигналом SIGINT действие по умолчанию – завершит работу. Интересно отметить, что сигнал SIGINT посылается и процессу оболочки, тоже связанному с терминалом. Тем не менее процесс оболочки благоразумно игнорирует этот сигнал, поскольку он должен продолжать работу для интерпретации последующих команд. Как будет рассмотрено далее, пользовательские программы также могут выбирать, нужно ли им перехватывать сигнал SIGINT, есть выполнять специальную процедуру реакции на поступление сигнала, и полагаться на действие по умолчанию для данного сигнала.
Сигналы также используются ядром для обработки определенных типов критических ошибок. Например, предположим, что файл программы поврежден и содержит недопустимые машинные инструкции. При выполнении этой программы процессом, ядро определит попытку выполнения недопустимой инструкции и пошлет процессу сигнал SIGILL (здесь ILL является сокращением от illegal, то есть недопустимый) для завершения его работы. Получившийся диалог может выглядеть примерно так:
$ badprog
illegal instruction - core dumped
Смысл термина core dumped (сброс образа памяти) будет объяснен ниже.
Сигналы могут посылаться не только от ядра к процессу, но и между процессами. Проще всего показать это на примере команды kill. Предположим, например, что программист запускает в фоновом режиме длительную команду
$ fpc verybigprog.pas &
[1] 1098
а затем решает завершить ее работу. Тогда, чтобы послать процессу сигнал SIGTERM, можно использовать команду kill. Так же, как и сигнал SIGINT, сигнал SIGTERM завершит процесс, если в процессе не переопределена стандартная реакция на этот сигнал. В качестве аргумента команды kill должен быть задан идентификатор процесса:
$ kill 1098
Terminated
Сигналы обеспечивают простой метод программного прерывания работы процессов UNIX. Образно можно сравнить его с похлопыванием по плечу, отвлекающим от работы. Из-за своей природы сигналы обычно используются для обработки исключительных ситуаций, а не для обмена данными между процессами.
Процесс может выполнять три действия с сигналами, а именно:
• изменять свою реакцию на поступление определенного сигнала (изменять обработку сигналов);
• блокировать сигналы (то есть откладывать их обработку) на время выполнения определенных критических участков кода;
• посылать сигналы другим процессам.
Каждое из этих действий будет рассмотрено далее в этой главе.
6.1.1. Имена сигналов
Сигналы не могут непосредственно переносить информацию, что ограничивает их применимость в качестве общего механизма межпроцессного взаимодействия. Тем не менее каждому типу сигналов присвоено мнемоническое имя (например, SIGINT), которое указывает, для чего обычно используется сигнал этого типа. Имена сигналов определены в модуле linux при помощи директивы const. Как и следовало ожидать, эти имена соответствуют небольшим положительным целым числам. Например, сигнал SIGINT обычно определяется так:
const SIGINT = 2; (* прерывание (rubout) *)
Большинство типов сигналов UNIX предназначены для использования ядром, хотя есть несколько сигналов, которые посылаются от процесса к процессу. Ниже приведен описанный в спецификации XSI полный список стандартных сигналов и их значение. Для удобства список сигналов отсортирован в алфавитном порядке. При первом чтении этот список может быть пропущен.
• SIGABRT – сигнал прерывания процесса (process abort signal). Посылается процессу при вызове им функции abort. В результате сигнала SIGABRT произойдет то, что спецификация XSI описывает как аварийное завершение (abnormal termination), авост. Следствием этого в реализациях UNIX является сброс образа памяти (core dump, иногда переводится как «дамп памяти») с выводом сообщения Quit - core dumped. Образ памяти процесса сохраняется в файле на диске для изучения с помощью отладчика;
• SIGALRM – сигнал таймера (alarm clock). Посылается процессу ядром при срабатывании таймера. Каждый процесс может устанавливать не менее трех таймеров. Первый из них измеряет прошедшее реальное время. Этот таймер устанавливается самим процессом при помощи системного вызова alarm (или установки значения первого параметра в более редко применяющемся вызове setitimer равным ITIMER_REAL). Вызов alarm будет описан в разделе 6.4.2. При необходимости больше узнать о вызове setitimer следует обратиться к справочному руководству системы;
• SIGBUS – сигнал ошибки на шине (bus error). Этот сигнал посылается при возникновении некоторой аппаратной ошибки. Смысл ошибки на шине определяется конкретной реализацией (обычно он генерируется при попытке обращения к допустимому виртуальному адресу, для которого нет физической страницы). Данный сигнал, так же как и сигнал SIGABRT, вызывает аварийное завершение;
• SIGCHLD – сигнал останова или завершения дочернего процесса (child process terminated or stopped). Если дочерний процесс останавливается или завершается, то ядро сообщит об этом родительскому процессу, послав ему сигнал SIGCHLD. По умолчанию родительский процесс игнорирует этот сигнал, поэтому, если в родительском процессе необходимо получать сведения о завершении дочерних процессов, то нужно перехватывать этот сигнал;
• SIGCONT – продолжение работы остановленного процесса (continue executing if stopped). Этот сигнал управления процессом, который продолжит выполнение процесса, если он был остановлен; в противном случае процесс будет игнорировать этот сигнал. Это сигнал обратный сигналу SIGSTOP;
• SIGHUP – сигнал освобождения линии (hangup signal). Посылается ядром всем процессам, подключенным к управляющему терминалу (control terminal) при отключении терминала. (Обычно управляющий терминал группы процесса является терминалом пользователя, хотя это и не всегда так. Это понятие изучается более подробно в главе 9.) Он также посылается всем членам сеанса, если завершает работу лидер сеанса (обычно процесс командного интерпретатора), связанного с управляющим терминалом. Это гарантирует, что если не были предприняты специальные меры, то при выходе пользователя из системы завершаются все фоновые процессы, запущенные им (подробно об этом написано в разделе 5.10);
• SIGILL – недопустимая команда процессора (illegal instruction). Посылается операционной системой, если процесс пытается выполнить недопустимую машинную команду. Иногда этот сигнал может возникнуть из-за того, что программа каким-либо образом повредила свой код, хотя это и маловероятно. Более вероятной представляется попытка выполнения вещественной операции, не поддерживаемой оборудованием. В результате сигнала SIGILL происходит аварийное завершение программы;
• SIGINT – сигнал прерывания программы (interrupt). Посылается ядром всем процессам сеанса, связанного с терминалом, когда пользователь нажимает клавишу прерывания. Это также обычный способ остановки выполняющейся программы;
• SIGKILL – сигнал уничтожения процесса (kill). Это довольно специфически сигнал, который посылается от одного процесса к другому и приводит к немедленному прекращению работы получающего сигнал процесса. Иногда он также посылается системой (например, при завершении работы системы). Сигнал SIGKILL – один из двух сигналов, которые не могут игнорироваться или перехватываться (то есть обрабатываться при помощи определенной пользователем процедуры);
• SIGPIPE – сигнал о попытке записи в канал или сокет, для которых принимающий процесс уже завершил работу (write on a pipe or socket when recipient is terminated). Программные каналы и сокеты являются другими средствами межпроцессного взаимодействия, которые обсудим в следующих главах. Там же будет рассмотрен и сигнал SIGPIPE;
• SIGPOLL – сигнал о возникновении одного из опрашиваемых событий (pollable event). Этот сигнал генерируется ядром, когда некоторый открытый дескриптор файла становится готовым для ввода или вывода. Тем не менее более удобный способ организации слежения за состояниями некоторого множества открытых файловых дескрипторов заключается в использовании системного вызова select, который подробно описан в главе 7;
• SIGPROF – сигнал профилирующего таймера (profiling time expired). Как было уже упомянуто для сигнала SIGALARM, любой процесс может установить не менее трех таймеров. Второй из этих таймеров может использоваться для измерения времени выполнения процесса в пользовательском и системном режимах. Сигнал SIGPROF генерируется, когда истекает время, установленное в этом таймере, и поэтому может быть использован средством профилирования программы. (Таймер устанавливается заданием первого параметра функции setitimer равным ITIMER_PROF.);
• SIGQUIT – сигнал о выходе (quit). Очень похожий на сигнал SIGINT, этот сигнал посылается ядром, когда пользователь нажимает клавишу выхода используемого терминала. Значение клавиши выхода по умолчанию соответствует символу ASCII FS или Ctrl-\. В отличие от SIGINT, этот сигнал приводит к аварийному завершению и сбросу образа памяти;
• SIGSEGV – обращение к некорректному адресу памяти (invalid memory reference). Сокращение SEGV в названии сигнала означает нарушение границ сегментов памяти (segmentation violation). Сигнал генерируется, если процесс пытается обратиться к неверному адресу памяти. Получение сигнала SIGSEGV приводит к аварийному завершению процесса;
• SIGSTOP – сигнал останова (stop executing). Это сигнал управления заданиями, который останавливает процесс. Его, как и сигнал SIGKILL, нельзя проигнорировать или перехватить;
• SIGSYS – некорректный системный вызов (invalid system call). Посылается ядром, если процесс пытается выполнить некорректный системный вызов. Это еще один сигнал, приводящий к аварийному завершению;
• SIGTERM – программный сигнал завершения (software termination signal). По соглашению, используется для завершения процесса (как и следует из его названия). Программист может использовать этот сигнал для того, чтобы дать процессу время для «наведение порядка», прежде чем посылать ему сигнал SIGKILL. Команда kill по умолчанию посылает именно этот сигнал;
• SIGTRAP – сигнал трассировочного прерывания (trace trap). Это особый сигнал, который в сочетании с системным вызовом ptrace используется отладчиками, такими как sdb, adb, и gdb. Поскольку он предназначен для отладки, его рассмотрение в рамках данной книги не требуется. По умолчанию сигнал SIGTRAP приводит к аварийному завершению;
• SIGTSTP – терминальный сигнал остановки (terminal stop signal). Этот сигнал формируется при нажатии специальной клавиши останова (обычно Ctrl+Z). Сигнал SIGTSTP аналогичен сигналу SIGSTOP, но его можно перехватить или игнорировать;
• SIGTTIN – сигнал о попытке ввода с терминала фоновым процессом (background process attempting read). Если процесс выполняется в фоновом режиме и пытается выполнить чтение с управляющего терминала, то ему посылается сигнал SIGTTIN. Действие сигнала по умолчанию – остановка процесса;
• SIGTTOU – сигнал о попытке вывода на терминал фоновым процессом (background process attempting write). Аналогичен сигналу SIGTTIN, но генерируется, если фоновый процесс пытается выполнить запись в управляющий терминал. И снова действие по умолчанию – остановка процесса;
• SIGURG – сигнал о поступлении в буфер сокета срочных данных (high bandwidth data is available at a socket). Этот сигнал сообщает процессу, что по сетевому соединению получены срочные внеочередные данные;
• SIGUSR1 и SIGUSR2 – пользовательские сигналы (user defined signals 1 and 2). Так же, как и сигнал SIGTERM, эти сигналы никогда не посылаются ядром и могут использоваться для любых целей по выбору пользователя;
• SIGVTALRM – сигнал виртуального таймера (virtual timer expired). Как уже упоминалось для сигналов SIGALRM и SIGPROF, каждый процесс может ими не менее трех таймеров. Последний из этих таймеров можно установить так, чтобы он измерял время, которое процесс выполняет в пользовательском режиме. (Таймер устанавливается заданием первого параметра функции setitimer равным ITIMER_VIRTUAL);
• SIGXCPU – сигнал о превышении лимита процессорного времени (CPU time limit exceeded). Этот сигнал посылается процессу, если суммарное процессорное время, занятое его работой, превысило установленный предел. Действие по умолчанию – аварийное завершение;
• SIGXFSZ – сигнал о превышении предела на размер файла (file size limit exceeded). Этот сигнал генерируется, если процесс превысит максимально допустимый размер файла. Действие по умолчанию – аварийное завершение.
Могут встретиться и некоторые другие сигналы, но их наличие зависит от конкретной реализации системы; его не требует спецификация XSI. Большая часть этих сигналов также используется ядром для индикации ошибок, например SIGEMT – прерывание эмулятора (emulator trap) часто обозначает отказ оборудования и зависит от конкретной реализации.1
6.1.2. Нормальное и аварийное завершение
Получение большинства сигналов приводит к нормальному завершению (normal termination) процесса. Действие сигнала при этом похоже на неожиданный вызов процессом функции _exit. Статус завершения, возвращаемый при этом родительскому процессу, сообщит о причине завершения дочернего процесса. В файле stdio определены макросы, которые позволяют родительскому процессу определить причину завершения дочернего процесса (получение сигнала и, собственно, значение сигнала. Следующий фрагмент программы демонстрирует родительский процесс, проверяющий причину завершения дочернего процесса и выводящий соответствующее сообщение:
uses stdio;
.
.
pid:=wait(@status);
if pid=-1 then
begin
perror('ошибка вызова wait');
halt(1);
end;
(* Проверка нормального завершения дочернего процесса *)
if WIFEXITED(status) then
begin
exit_status := WEXITSTATUS(status);
writeln('Статус завершения ', pid, ' был ', exit_status);
end;
(* Проверка, получил ли дочерний процесс сигнал *)
if WIFSIGNALED(status) then
begin
sig_no := WTERMSIG(status);
writeln('Сигнал номер ', sig_no, ' завершил процесс ', pid);
end;
(* Проверка остановки дочернего процесса *)
if WIFSTOPPED(status) then
begin
sig_no := WSTOPSIG(status);
writeln('Сигнал номер ', sig_no, ' остановил процесс ', pid);
end;
Как уже упоминалось, сигналы SIGABRT, SIGBUS, SIGSEGV, SIGQUIT, SIGILL, SIGTRAP, SIGSYS, SIGXCPU, SIGXFSZ и SIGFPE приводят к аварийному завершению и обычно сопровождаются сбросом образа памяти на диск. Образ памяти процесса записывается в файл с именем core в текущем рабочем каталоге процесса (термин core, или сердечник, напоминает о временах, когда оперативная память состояла из матриц ферритовых сердечников). Файл core будет содержать значения всех переменных программы, регистров процессора и необходимую управляющую информацию ядра на момент завершения программы. Статус завершения процесса после аварийного завершения будет тем же, каким он был бы в случае нормального завершения из-за этого сигнала, только в нем будет дополнительно выставлен седьмой бит младшего байта. В большинстве современных систем UNIX определен макрос WCOREDUMP, который возвращает истинное или ложное значение в зависимости от установки этого бита в своем аргументе. Тем не менее следует учесть, что макрос WCOREDUMP не определен спецификацией XSI. С применением этого макроса предыдущий пример можно переписать так:
(* Проверка, получил ли дочерний процесс сигнал *)
if WIFSIGNALED(status) then
begin
sig_no := WTERMSIG(status);
writeln('Сигнал номер ', sig_no, ' завершил процесс ', pid);
if WCOREDUMP(status) then
writeln('... создан файл дампа памяти');
end;
Формат файла core известен отладчикам UNIX, и этот файл можно использовать для изучения состояния процесса в момент сброса образа памяти. Этим можно воспользоваться для определения точки, в которой возникает проблема.
Стоит также упомянуть функцию abort, которая не имеет аргументов:
abort;
Эта функция посылает вызвавшему ее процессу сигнал SIGABRT, вызывая его аварийное завершение, то есть сброс образа памяти. Процедура abort полезна в качестве средства отладки, так как позволяет процессу записать свое текущее состояние, если что-то происходит не так. Она также иллюстрирует тот факт, что процесс может послать сигнал самому себе.
6.2. Обработка сигналов
При получении сигнала процесс может выполнить одно из трех действий:
• выполнить действие по умолчанию. Обычно действие по умолчанию заключается в прекращении выполнения процесса. Для некоторых сигналов, например, для сигналов SIGUSR1 и SIGUSR2, действие по умолчанию заключается в игнорировании сигнала.1 Для других сигналов, например, для сигнала SIGSTOP, действие по умолчанию заключается в остановке процесса;
• игнорировать сигнал и продолжать выполнение. В больших программах неожиданно возникающие сигналы могут привести к проблемам. Например, нет смысла позволять программе останавливаться в результате случайного нажатия на клавишу прерывания, в то время как она производит обновление важной базы данных;
• выполнить определенное пользователем действие. Программист может захотеть выполнить при выходе из программы операции по «наведению порядка» (такие как удаление рабочих файлов), что бы ни являлось причиной этого выхода.
В старых версиях UNIX обработка сигналов была относительно простой. Здесь будут изучены современные процедуры управления механизмом сигналов, и хотя эти процедуры несколько сложнее, их использование дает вполне надежный результат, в отличие от устаревших методов обработки сигналов. Прежде чем перейти к примерам, следует сделать несколько пояснений. Начнем с определена набора сигналов (signal set).
6.2.1. Наборы сигналов
Наборы сигналов являются одним из основных параметров, передаваемых работающим с сигналами системным вызовам. Они просто задают список сигналов, которые необходимо передать системному вызову.
Наборы сигналов определяются при помощи типа sigset_t, который определен в файле linux. Размер типа задан так, чтобы в нем мог поместиться весь набор определенных в системе сигналов. Выбрать определенные сигналы можно, начав либо с полного набора сигналов и удалив ненужные сигналы, либо с пустого набора, включив в него нужные. Инициализация пустого и полного набора сигналов выполняется при помощи процедур sigemptyset и sigfillset соответственно. После инициализации с наборами сигналов можно оперировать при помощи процедур sigaddset и sigdelset, соответственно добавляющих и удаляющих указанные вами сигналы.
Описание
uses stdio;

(* Инициализация *)
function sigemptyset(__set:psigset_t):integer;
function sigfillset(__set:psigset_t):integer;

(* Добавление и удаление сигналов *)
function sigaddset(__set:psigset_t;__signo:integer):integer;
function sigdelset(__set:psigset_t;__signo:integer):integer;
Процедуры sigemptyset и sigfillset имеют единственный параметр – указатель на переменную типа sigset_t. Вызов sigemptyset инициализирует набор __set, исключив из него все сигналы. И наоборот, вызов sigfillset инициализирует набор, на который указывает __set, включив в него все сигналы. Приложения должны вызывать sigemptyset или sigfillset хотя бы один раз для каждой переменной типа sigset_t.
Процедуры sigaddset и sigdelset принимают в качестве параметров указатель на инициализированный набор сигналов и номер сигнала, который должен быть добавлен или удален. Второй параметр, signo, может быть символическим именем константы, таким как SIGINT, или настоящим номером сигнала, но в последнем случае программа окажется системно-зависимой.
В следующем примере создадим два набора сигналов. Первый образуется из пустого набора добавлением сигналов SIGINT и SIGQUIT. Второй набор изначально заполнен, и из него удаляется сигнал SIGCHLD.
uses stdio;

var
mask1, mask2:sigset_t;
.
.
.
(* Создать пустой набор сигналов *)
sigemptyset(@mask1);

(* Добавить сигналы *)
sigaddset(@mask1, SIGINT);
sigaddset(@mask1, SIGQUIT);

(* Создать полный набор сигналов *)
sigfillset(@mask2);

(* Удалить сигнал *)
sigdelset(@mask2, SIGCHLD);
.
.
.
6.2.2. Задание обработчика сигналов: вызов sigaction
После определения списка сигналов можно задать определенный метод обработки сигнала при помощи процедуры sigaction.
Описание
uses linux;

Procedure SigAction(Signo:Integer; Var Act,OAct:PSigActionRec);
Как вскоре увидим, структура sigactionrec, в свою очередь, также содержит набор сигналов. Первый параметр signo задает отдельный сигнал, для которого нужно определить действие. Чтобы это действие выполнялось, процедура sigaction должна быть вызвана до получения сигнала типа signo. Значением переменной signo может быть любое из ранее определенных имен сигналов, за исключений SIGSTOP и SIGKILL, которые предназначены только для остановки или завершения процесса и не могут обрабатываться по-другому.
Второй параметр, act, определяет обработчика сигнала signo. Третий параметр, oact, если не равен nil, указывает на структуру, куда будет помещено описание старого метода обработки сигнала. Рассмотрим структуру sigactionrec, определенную в файле linux:
SigActionRec = packed record
Handler : record
case byte of
0: (Sh: SignalHandler); (* Функция обработчика *)
1: (Sa: TSigAction);
end;
Sa_Mask : SigSet; (* Сигналы, которые блокируются
во время обработки сигнала *)
Sa_Flags : Longint; /* Флаги, влияющие
на поведение сигнала */
Sa_restorer : SignalRestorer; { Obsolete - Don't use }
end;
Эта структура кажется сложной, но давайте рассмотрим ее поля по отдельности. Первое поле, handler, задает обработчик сигнала signo. Это поле может иметь три вида значений:
• SIG_DFL – константа, сообщающая, что нужно восстановить обработку сигнала по умолчанию (для большинства сигналов это завершение процесса);
• SIG_IGN – константа, означающая, что нужно игнорировать данный сигнал (ignore the signal). He может использоваться для сигналов SIGSTOP и SIGKILL;
• адрес функции, принимающей аргумент типа int. Если функция объявлена в тексте программы до заполнения sigaction, то полю handler.sh/handler.sa можно просто присвоить имя функции. Компилятор поймет, что имелся в виду ее адрес. Эта функция будет выполняться при получении сигнала signo, а само значение signo будет передано в качестве аргумента вызываемой функции. Управление будет передано функции, как только процесс получит сигнал, какой бы участок программы при этом не выполнялся. После возврата из функции управление будет снова передано процессу и продолжится с точки, в которой выполнение процесса было прервано. Этот механизм станет понятен из следующего примера.
Функция-обработчик может быть одного из двух типов:
type
TSigAction = procedure(Sig: Longint; SigContext: SigContextRec);cdecl;
SignalHandler = Procedure (Sig: Integer);cdecl;
Второе поле, sa_mask, демонстрирует первое практическое использование набора сигналов. Сигналы, заданные в поле sa_mask, будут блокироваться во время выполнения функции, заданной полем handler. Это не означает, что эти сигналы будут игнорироваться, просто их обработка будет отложена до завершения функции. При входе в функцию перехваченный сигнал также будет неявно добавлен к текущей маске сигналов. Все это делает механизм сигналов более надежным механизмом межпроцессного взаимодействия, чем он был в ранних версиях системы UNIX.1
Тип поля sa_mask, sigset, ограниченно совместим с типом sigset_t. Размер типа sigset_t определяется константой
_SIGSET_NWORDS=1024 div (8 * sizeof (longint));
Это в четыре раза больше размера типа sigset, поэтому при переходе к нему необходимо использовать поле __val[0] типа sigset_t. К примеру,
sa_mask:=mask.__val[0];
Поле sa_flags может использоваться для изменения характера реакции на сигнал signo. Например, после возврата из обработчика можно вернуть обработчик по умолчанию SIG_DFL, установив значение поля sa_flags равным SA_RESETHAND. Если же значение поля sa_flags установлено равным SA_SIGINFO, то обработчику сигнала будет передаваться дополнительная информация.
Все это достаточно трудно усвоить, поэтому рассмотрим несколько примеров. В действительности все намного проще, чем кажется.
Пример 1: перехват сигнала SIGINT
Этот пример демонстрирует, как можно перехватить сигнал, а также проясняет лежащий в его основе механизм сигналов. Программа sigex просто связывает с сигналом SIGINT функцию catchint, а затем выполняет набор операторов sleep и writeln. В данном примере определена структура act типа sigactionrec как глобальная, поэтому при инициализации структуры все поля, и в частности поле sa_flags обнуляются.
(* Программа sigex - демонстрирует работу sigaction *)
uses linux,stdio;

(* Простая функция для обработки сигнала SIGINT *)
procedure catchint (signo:integer);cdecl;
begin
writeln (#$a'сигнал CATCHINT: signo=', signo);
writeln ('сигнал CATCHINT: возврат'#$a);
end;

var
act,oact:sigactionrec;
mask:sigset_t;

begin
(* Определение процедуры обработчика сигнала catchint *)

(* Задание действия при получении сигнала SIGINT *)
act.handler.sh := @catchint;
(* Создать маску, включающую все сигналы *)
sigfillset (@mask);
act.sa_mask:=mask.__val[0];
(* До вызова процедуры sigaction сигнал SIGINT
* приводил к завершению процесса (действие по умолчанию)
*)
sigaction (SIGINT, @act, @oact);
(* При получении сигнала SIGINT управление
* будет передаваться процедуре catchint
*)
writeln ('вызов sleep номер 1');
sleep (1);
writeln ('вызов sleep номер 2');
sleep (1);
writeln ('вызов sleep номер 3');
sleep (1);
writeln ('вызов sleep номер 4');
sleep (1);
writeln ('Выход');
halt(0);
end.
Сеанс обычного запуска sigex будет выглядеть так:
$ sigex
Вызов sleep номер 1
Вызов sleep номер 2
Вызов sleep номер 3
Вызов sleep номер 4
Выход
Пользователь может прервать выполнение программы sigex, нажав клавишу прерывания задания. Если она была нажата до того, как в программе sigex была выполнена процедура sigaction, то процесс просто завершит работу. Если же нажать да клавишу прерывания после вызова sigaction, то управление будет передано функции catchint:
$ sigex
Вызов sleep номер 1
<прерывание> (пользователь нажимает на клавишу прерывания)

Сигнал CATCHINT: signo = 2
Сигнал CATCHINT: возврат

Вызов sleep номер 2
Вызов sleep номер 3
Вызов sleep номер 4
Выход
Обратите внимание на то, как передается управление из тела программы процедуре catchint. После завершения процедуры catchint, управление продолжится с точки, в которой программа была прервана. Можно попробовать прервать программу sigex и в другом месте:
$ sigex
Вызов sleep номер 1
Вызов sleep номер 2
<прерывание> (пользователь нажимает на клавишу прерывания)

Сигнал CATCHINT: signo = 2
Сигнал GATCHINT: возврат

Вызов sleep номер 3
Вызов sleep номер 4
Выход
Пример 2: игнорирование сигнала SIGINT
Для того чтобы процесс игнорировал сигнал прерывания SIGINT, все, что нужно для этого сделать – это заменить следующую строку в программе:
act.handler.sh := @catchint;
на
act.handler.sh := SIG_IGN;
После выполнения этого оператора нажатие клавиши прерывания будет безрезультатным. Снова разрешить прерывание можно так:
act.handler.sh := SIG_DFL;
sigaction (SIGINT, @act, nil);
Можно игнорировать сразу несколько сигналов, например:
act.handler.sh := SIG_IGN;
sigaction(SIGINT, @act, nil);
При этом игнорируются оба сигнала SIGINT и SIGQUIT. Это может быть полезно в программах, которые не должны прерываться с клавиатуры.
Некоторые командные интерпретаторы используют этот подход, чтобы гарантировать, что фоновые процессы не останавливаются при нажатии пользователем клавиши прерывания. Это возможно вследствие того, что игнорируемые процессом сигналы будут игнорироваться и после вызова ехес. Поэтому командный интерпретатор может вызвать sigaction для игнорирования сигналов SIGQUIT и SIGINT, а затем запустить новую программу при помощи вызова ехес.
Пример 3: восстановление прежнего действия
Как упоминалось выше, в структуре sigaction может быть заполнен третий параметр oact. Это позволяет сохранять и восстанавливать прежнее состояние обработчика сигнала, как показано в следующем примере:
uses linux;

var
act, oact: sigactionrec;

(* Сохранить старый обработчик сигнала SIGTERM *)
sigaction(SIGTERM, nil, @oact);

(* Определить новый обработчик сигнала SIGTERM *)
act.handler.sh := SIG_IGN;
sigaction(SIGTERM, @act, nil);

(* Выполнить какие-либо действия *)

(* Восстановить старый обработчик *)
sigaction(SIGTERM, @oact, nil);
Пример 4: аккуратный выход
Предположим, что программа использует временный рабочий файл. Следующая простая процедура удаляет файл:
(* Аккуратный выход из программы *)
uses linux;

procedure g_exit(s:integer);cdecl;
begin
unlink ('tempfile');
writeln (stderr, 'Прерывание - выход из программы');
halt(1);
end;
Можно связать эту процедуру с определенным сигналом:
var
act:sigactionrec;
.
.
act.handler.sh := @g_exit;
sigaction (SIGINT, @act, nil);
Если после этого вызова пользователь нажмет клавишу прерывания, то управление будет автоматически передано процедуре g_exit. Можно дополнить процедуру g_exit другими необходимыми для завершения операциями.
6.2.3. Сигналы и системные вызовы
В большинстве случаев, если процессу посылается сигнал во время выполнения им системного вызова, то обработка сигнала откладывается до завершения вызова. Но некоторые системные вызовы ведут себя по-другому, и их выполнение можно прервать при помощи сигнала. Это относится к вызовам ввода/вывода (fdread, fdwrite, fdopen, и т.д.), вызовам wait или pause (который мы обсудим в свое время). Во всех случаях, если процесс перехватывает вызов, то прерванный системный вызов возвращает значение –1 и помещает в переменную linuxerror значение Sys_EINTR. Такие ситуации можно обрабатывать при помощи следующего кода:
if fdwrite(tfd, buf, size) < 0 then
begin
if linuxerror = Sys_EINTR then
begin
warn('Вызов fdwrite прерван');
.
.
.
end;
end;
В этом случае, если программа хочет вернуться к системному вызову fdwrite, то она должна использовать цикл и оператор continue. Но процедура sigactionrec позволяет автоматически повторять прерванный таким образом системный вызов. Это достигается установкой значения SA_RESTART в поле sa_flags структуры sigactionrec. Если установлен этот флаг, то системный вызов будет выполнен снова, и значение переменной linuxerror не будет установлено.
Важно отметить, что сигналы UNIX обычно не могут накапливаться. Другими словами, в любой момент времени только один сигнал каждого типа может ожидать обработки данным процессом, хотя несколько сигналов разных типов могут ожидать обработки одновременно. Фактически то, что сигналы не могут накапливаться, означает, что они не могут использоваться в качестве полностью надежного метода межпроцессного взаимодействия, так как процесс не может быть уверен, что посланный им сигнал не будет «потерян».
Упражнение 6.1. Измените программу smallsh из предыдущей главы так, чтобы она обрабатывала клавиши прерывания и завершения как настоящий командный интерпретатор. Выполнение фоновых процессов не должно прерываться сигналами SIGINT и SIGQUIT. Некоторые командные интерпретаторы, (а именно C-shell и Korn shell) помещают фоновые процессы в другую группу процессов. В чем преимущества и недостатки этого подхода? (В последних версиях стандарта POSIX введено накопление сигналов, но в качестве необязательного расширения.)
6.2.4. Процедуры sigsetjmp и siglongjmp
Иногда при получении сигнала необходимо вернуться на предыдущую позицию в программе. Например, может потребоваться, чтобы пользователь мог вернуться в основное меню программы при нажатии клавиши прерывания. Это можно выполнить, используя две специальные функции sigsetjmp и siglongjmp. (Существуют альтернативные функции setjmp и longjmp, но их нельзя использовать совместно с обработкой сигналов.) Процедура sigsetjmp «сохраняет» текущие значения счетчика команд, позиции стека, регистров процессора и маски сигналов, а процедура siglongjmp передает управление назад в сохраненное таким образом положение. В этом смысле процедура siglongjmp аналогична оператору goto. Важно понимать, что возврат из процедуры siglongjmp не происходит, так как стек возвращается в сохраненное состояние. Как будет показано ниже, при этом происходит выход из соответствующего вызова sigsetjmp.
Описание
uses stdio;

(* Сохранить текущее положение в программе *)
function sigsetjmp(var env:jmp_buf;savemask:longint):integer;

(* Вернуться в сохраненную позицию *)
procedure siglongjmp(var env:jmp_buf;val:integer);
Текущее состояние программы сохраняется в объекте типа sigjmp_buf, определенном в файле stdio. Если во время вызова sigsetjmp значение аргумента savemask не равно нулю, то вызов sigsetjmp сохранит помимо основного контекста программы также текущую маску сигналов (маску блокированных сигналов и действия, связанные со всеми сигналами). Возвращаемое функцией sigsetjmp значение является важным: если в точку sigsetjmp управление переходит из функции siglongjmp, то она возвращает ненулевое значение – аргумент val вызова siglongjmp. Если же функция sigsetjmp вызывается в обычном порядке исполнения программы, то она возвращает значение 0.
Следующий пример демонстрирует технику использования этих функций:
(* Пример использования процедур sigsetjmp и siglongjmp *)
uses linux,stdio;

var
position:sigjmp_buf;

procedure domenu;
var
choice:integer;
begin
write('Choice menu entry:'#$a' menu 1'#$a' menu 2'#$a' menu 3'#$a'?>');
scanf('%d',[@choice]);
end;

procedure goback(smth:longint);cdecl;
begin
fprintf (stderr, #$a'Прерывание'#$a, []);
(* Вернуться в сохраненную позицию *)
siglongjmp (position, 1);
end;

var
act:sigactionrec;

begin
(*
.
.
. *)
(* Сохранить текущее положение *)
if sigsetjmp(position, 1) = 0 then
begin
act.handler.sh := @goback;
sigaction (SIGINT, @act, nil);
end;
domenu;
(*
.
.
. *)
end.
Если пользователь нажимает на клавишу прерывания задания после вызова sigaction, то управление передается в точку, положение которой было записано при помощи функции sigsetjmp. Поэтому выполнение программы продолжается, как если бы только что завершился соответствующий вызов sigsetjmp. В этом случае возвращаемое функцией sigsetjmp значение будет равно второму параметру процедуры siglongjmp.
6.3. Блокирование сигналов
Если программа выполняет важную задачу, такую как обновление базы данных, то может понадобиться ее защита от прерывания на время выполнения таких критических действий. Как уже упоминалось, вместо игнорирования поступающих сигналов процесс может блокировать сигналы, это будет означать, что их выполнение будет отложено до тех пор, пока процесс не завершит выполнение критического участка.
Блокировать определенные сигналы в процессе позволяет системный вызов sigprocmask, определенный следующим образом:
Описание
uses linux;

Procedure SigProcMask(How:Integer; SSet,OSSet:PSigSet);
Параметр how сообщает вызову sigpromask, какое действие он должен выполнять. Например, этот параметр может иметь значение SIG_MASK, указывающее, что с этого момента будут блокироваться сигналы, заданные во втором параметр sset, то есть будет произведена установка маски блокирования сигналов. Третий параметр просто заполняется текущей маской блокируемых сигналов – если не нужно ее знать, просто присвойте этому параметру значение nil. Поясним это на примере:
var
set1:sigset_t;

.
.
.
(* Создать полный набор сигналов *)
sigfillset (@set1);

(* Установить блокировку *)
sigprocmask (SIG_SETMASK, @set1, nil);

(* Критический участок кода .. *)

(* Отменить блокировку сигналов *)
sigprocmask (SIG_UNBLOCK, @set1, nil);
Обратите внимание на использование для отмены блокирования сигналов параметра SIG_UNBLOCK. Заметим, что если использовать в качестве первого параметра SIG_BLOCK вместо SIG_SETMASK, то это приведет к добавлению заданных в переменной set сигналов к текущему набору сигналов.
Следующий более сложный пример показывает, как сначала выполняется блокирование всех сигналов во время выполнения особенно важного участка программы, а затем, при выполнении менее критического участка, блокируются только сигналы SIGINT и SIGQUIT.
(* Блокировка сигналов - демонстрирует вызов sigprocmask *)
uses linux,stdio;

var
set1, set2:sigset_t;

begin
(* Создать полный набор сигналов *)
sigfillset (@set1);

(* Создать набор сигналов, не включающий
* сигналы SIGINT и SIGQUIT
*)
sigfillset (@set2);
sigdelset (@set2, SIGINT);
sigdelset (@set2, SIGQUIT);
(* Некритический участок кода ... *)
(* Установить блокировку всех сигналов *)
sigprocmask (SIG_SETMASK, @set1, nil);
(* Более критический участок кода ... *)
(* Блокировка меньшего числа сигналов. *)
sigprocmask (SIG_UNBLOCK, @set2, nil);
(* Менее критический участок кода ... *)
(* Отменить блокировку для всех сигналов *)
sigprocmask (SIG_UNBLOCK, @set1, nil);
end.
Упражнение 6.2. Перепишите процедуру g_exit в примере 4 из раздела 6.2.2 так, чтобы во время ее выполнения игнорировались сигналы SIGINT и SIGQUIT.
6.4. Посылка сигналов
6.4.1. Посылка сигналов другим процессам: вызов kill
Процесс вызывает процедуру sigaction для установки реакции на поступление сигнала. Обратную операцию, посылку сигнала, выполняет системный вызов kill, описанный следующим образом:
Описание
uses linux;

Function Kill(Pid:Longint; Sig:Integer):Integer;
Первый параметр pid определяет процесс или процессы, которым посылается сигнал sig. Обычно pid является положительным числом, и в этом случае он рассматривается как идентификатор процесса. Поэтому следующий оператор
kill(7421, SIGTERM);
означает «послать сигнал SIGTERM процессу с идентификатором 7421». Так как процесс, посылающий сигнал kill, должен знать идентификатор процесса, которому предназначен сигнал, то вызов kill чаще всего используется для обмена между тесно связанными процессами, например, родительским и дочерним. Заметим, что процесс может послать сигнал самому себе.
Существуют некоторые ограничения, связанные с правами доступа. Чтобы можно было послать сигнал процессу, действующий или истинный идентификатор пользователя посылающего процесса должен совпадать с действующим или истинным идентификатором пользователя процесса, которому сигнал адресован. Процессы суперпользователя, как обычно, могут посылать сигналы любым другим процессам. Если непривилегированный пользователь пытается послать сигнал процессу, который принадлежит другому пользователю, то вызов kill завершится неудачей, вернет значение –1 и поместит в переменную linuxerror значение EPERM. (Другие возможные значения ошибок в переменной linuxerror после неудачного вызова kill – это значение Sys_ESRCH, указывающее, что процесс с заданным идентификатором не существует, и Sys_EINVAL, если sig содержит некорректный номер сигнала.)
Параметр pid вызова kill может также принимать определенные значения, которые имеют особый смысл:
• если параметр pid равен нулю, то сигнал будет послан всем процессам, принадлежащим к той же группе, что и процесс, пославший сигнал, в том числе и самому процессу;
• если параметр pid равен –1, и действующий идентификатор пользователя является идентификатором суперпользователя, то сигнал посылается всем процессам, истинный идентификатор пользователя которых равен действующему идентификатору пользователя, пославшего сигнал процесса, снова включая и сам процесс, пославший сигнал;
• если параметр pid равен –1 и действующий идентификатор пользовать является идентификатором суперпользователя, то сигнал посылается всем процессам, кроме определенных системных процессов (последнее исключение относится ко всем попыткам послать сигнал группе процессов, но наиболее важно это в данном случае);
• и, наконец, если параметр pid меньше нуля, но не равен –1, то сигнал посылается всем процессам, идентификатор группы которых равен модулю pid, включая пославший сигнал процесс, если для него также выполняется это условие.
Следующий пример – программа synchro создает два процесса, которые будут поочередно печатать сообщения на стандартный вывод. Они синхронизирут свою работу, посылая друг другу сигнал SIGUSR1 при помощи вызова kill.
(* Программа synchro -- пример использования вызова kill *)
uses linux,stdio;

const
ntimes:integer=0;

procedure p_action(sig:integer);cdecl;
begin
inc(ntimes);
writeln ('Родительский процесс получил сигнал ', ntimes, ' раз(а)');
end;

procedure c_action(sig:integer);cdecl;
begin
inc(ntimes);
writeln ('Дочерний процесс получил сигнал ', ntimes, ' раз(а)');
end;

var
pid, ppid:longint;
pact, cact:sigactionrec;
begin
(* Задать обработчик сигнала SIGUSR1 в родительском процессе *)
pact.handler.sh := @p_action;
sigaction (SIGUSR1, @pact, nil);

pid := fork;
case pid of
-1: (* ошибка *)
begin
perror ('synchro');
halt(1);
end;
0: (* дочерний процесс *)
begin
(* Задать обработчик в дочернем процессе *)
cact.handler.sh := @c_action;
sigaction (SIGUSR1, @cact, nil);
(* Получить идентификатор родительского процесса *)
ppid := getppid;
while true do
begin
sleep (1);
kill (ppid, SIGUSR1);
pause;
end;
(* Бесконечный цикл *)
end;
else (* родительский процесс *)
while true do
begin
pause;
sleep (1);
kill (pid, SIGUSR1);
end;
(* Бесконечный цикл *)
end;
end.
Оба процесса выполняют бесконечный цикл, приостанавливая работу до получения сигнала от другого процесса. Они используют для этого системный вызов pause, который просто приостанавливает работу до получения сигнала (см. раздел 6.4.3). Затем каждый из процессов выводит сообщение и, в свою очередь, посылает сигнал при помощи вызова kill. Дочерний процесс начинает вывод сообщений (обратите внимание на порядок операторов в каждом цикле). Оба процесса завершают работу, когда пользователь нажимает на клавишу прерывания. Диалог с программой может выглядеть примерно так:
$ synchro
Родительский процесс получил сигнал #1
Дочерний процесс получил сигнал #1
Родительский процесс получил сигнал #2
Дочерний процесс получил сигнал #2
< прерывание > (пользователь нажал на клавишу прерывания)
$
6.4.2. Посылка сигналов самому процессу: вызовы sigraise и alarm
Функция sigraise просто посылает сигнал выполняющемуся процессу:
Описание
uses linux;

Procedure SigRaise(Sig:integer);
Вызывающему процессу посылается сигнал, определенный параметром sig и в случае успеха функция sigraise возвращает нулевое значение. Например:
uses Linux;

Var
oa,na : PSigActionRec;

Procedure DoSig(sig : Longint);cdecl;

begin
writeln('Receiving signal: ',sig);
end;

begin
new(na);
new(oa);
na^.handler.sh:=@DoSig;
na^.Sa_Mask:=0;
na^.Sa_Flags:=0;
na^.Sa_Restorer:=Nil;
SigAction(SigUsr1,na,oa);
if LinuxError<>0 then
begin
writeln('Error: ',linuxerror,'.');
halt(1);
end;
Writeln('Sending USR1 (',sigusr1,') signal to self.');
SigRaise(sigusr1);
end.
Вызов alarm – это простой и полезный вызов, который устанавливает таймер процесса. При срабатывании таймера процессу посылается сигнал.
Описание
uses linux;

Function Alarm(Secs:longint):Longint;
Переменная secs задает время в секундах, на которое устанавливается таймер. После истечения заданного интервала времени процессу посылается сигнал SIGALRM. Поэтому вызов
alarm(60);
приводит к посылке сигнала SIGALRM через 60 секунд. Обратите внимание, что вызов alarm не приостанавливает выполнение процесса, как вызов sleep, вместо этого сразу же происходит возврат из вызова alarm, и продолжается нормальное выполнение процесса, по крайней мере, до тех пор, пока не будет получен сигнал SIGALRM. Установленный таймер будет продолжать отсчет и после вызова ехec, но вызов fork выключает таймер в дочернем процессе.
Выключить таймер можно при помощи вызова alarm с нулевым параметром:
(* Выключить таймер *)
alarm(0);
Вызовы alarm не накапливаются: другими словами, если вызвать alarm дважды, то второй вызов отменит предыдущий. Но при этом возвращаемое вызовом alarm значение будет равно времени, оставшемуся до срабатывания предыдущего таймера, и его можно при необходимости записать.
Вызов alarm может быть полезен, если нужно ограничить время выполнения какого-либо действия. Основная идея проста: вызывается alarm, и процесс начинает выполнение задачи. Если задача выполняется вовремя, то таймер сбрасывается. Если выполнение задачи отнимает слишком много времени, то процесс прерывается при помощи сигнала SIGTERM и выполняет корректирующие действия.
Следующая функция quickreply использует этот подход для ввода данных от пользователя за заданное время. Она имеет один аргумент, приглашение командной строки, и возвращает указатель на введенную строку, или нулевой указатель, если после пяти попыток ничего не было введено. Обратите внимание, что после каждого напоминания пользователю функция quickreply посылает на терминал символ Ctrl+G. На большинстве терминалов и эмуляторов терминала это приводит к подаче звукового сигнала.
Функция quickreply вызывает процедуру gets из стандартной библиотеки ввода/вывода (Standard I/O Library). Процедура gets помещает очередную строку из стандартного ввода в массив char. Она возвращает либо указатель на массив, либо нулевой указатель в случае достижения конца файла или ошибки. Обратите внимание на то, что сигнал SIGALRM перехватывается процедурой обработчика прерывания catch. Это важно, так как по умолчанию получение сигнала SIGALRM приводит к завершению процесса. Процедура catch устанавливает флаг timed_out. Функция quickreply проверяет этот флаг, определяя таким образом, не истекло ли заданное время.
uses linux,stdio;

const
TIMEOUT=5; (* время в секундах *)
MAXTRIES=5; (* число попыток *)
LINESIZE=100; (* длина строки *)
CTRL_G=#7; (* ASCII символ звукового сигнала *)

var
(* Флаг, определяющий, истекло ли заданное время *)
timed_out:boolean;
(* Переменная, которая будет содержать введенную строку *)
answer:array [0..LINESIZE-1] of char;

(* Выполняется при получении сигнала SIGALRM *)
procedure catch (sig:integer);cdecl;
begin
(* Установить флаг timed_out *)
timed_out := TRUE;
(* Подать звуковой сигнал *)
write(CTRL_G);
end;

function quickreply(prompt:pchar):pchar;
var
ntries:integer;
act, oact:sigactionrec;
begin
(* Перехватить сигнал SIGALRM и сохранить старый обработчик *)
act.handler.sh := @catch;
sigaction (SIGALRM, @act, @oact);
for ntries:=1 to MAXTRIES do
begin
timed_out := FALSE;
writeln;
write(prompt, ' > ');
(* Установить таймер *)
alarm (TIMEOUT);
(* Получить введенную строку *)
gets (answer);
(* Выключить таймер *)
alarm (0);
(* Если флаг timed_out равен TRUE, завершить работу *)
if not timed_out then
break;
end;
(* Восстановить старый обработчик *)
sigaction (SIGALRM, @oact, nil);
(* Вернуть соответствующее значение *)
if ntries = MAXTRIES then
quickreply:=nil
else quickreply:=answer;
end;

begin
writeln;
writeln(quickreply ('Reply'));
end.
6.4.3. Системный вызов pause
ОС UNIX также содержит дополняющий вызов alarm системный вызов pause, который определен следующим образом:
Описание
uses linux;

Procedure Pause;
Вызов pause приостанавливает выполнение вызывающего процесса (так что процесс при этом не занимает процессорного времени) до получения любого сигнала, например, сигнала SIGALRM. Если сигнал вызывает нормальное завершена процесса или игнорируется процессом, то в результате вызова pause будет просто выполнено соответствующее действие (завершение работы или игнорирована сигнала).
Следующая программа tml (сокращение от «tell me later» – напомнить позднее) использует оба вызова alarm и pause для вывода сообщения в течение заданного числа минут. Она вызывается следующим образом:
$ tml число_минут текст_сообщения
Например:
$ tml 10 время идти домой
Перед сообщением выводятся три символа Ctrl+G (звуковые сигналы) для привлечения внимания пользователя. Обратите внимание на создание в программе tml фонового процесса при помощи вызова fork. Фоновый процесс выполняет работу, позволяя пользователю продолжать выполнение других задач.
(* tml - программа для напоминания *)
{$mode objfpc}

uses linux, stdio, sysutils;

const
BELLS=#7#7#7; (* звуковой сигнал ASCII *)
alarm_flag:boolean = FALSE;

(* Обработчик сигнала SIGALRM *)
procedure setflag(sig:integer);cdecl;
begin
alarm_flag := TRUE;
end;

var
nsecs, j:integer;
pid:longint;
act:sigactionrec;
begin
if paramcount < 2 then
begin
writeln (stderr, 'Применение: tml число_минут сообщение');
halt(1);
end;
try
nsecs := strtoint(paramstr(1)) * 60;
except
on e:econverterror do
begin
writeln (stderr, 'Введено нечисловое значение');
halt(2);
end;
end;
if nsecs <= 0 then
begin
writeln (stderr, 'tml: недопустимое время');
halt(3);
end;

(* Вызов fork, создающий фоновый процесс *)
pid := fork;
case pid of
-1: (* ошибка *)
begin
perror ('tml');
halt(1);
end;
0: (* дочерний процесс *)
;
else (* родительский процесс *)
begin
writeln('Процесс tml с идентификатором ', pid);
halt(0);
end;
end;

(* Установить обработчик таймера *)
act.handler.sh := @setflag;
sigaction (SIGALRM, @act, nil);
(* Установить таймер *)
alarm (nsecs);
(* Приостановить выполнение до получения сигнала ... *)
pause;

(* Если был получен сигнал SIGALRM, вывести сообщение *)
if alarm_flag then
begin
write(BELLS);
for j := 2 to paramcount do
write(paramstr(j),' ');
writeln;
end;
halt(0);
end.
Из этого примера можно получить представление о том, как работает подпрограмма sleep, вызывая вначале alarm, а затем pause.
Упражнение 6.3. Напишите свою версию подпрограммы sleep. Она должна сохранить предыдущее состояние таймера и восстанавливать его при выходе. (Посмотрите полное описание процедуры sleep в справочном руководстве системы.)
Упражнение 6.4. Перепишите программу tml, используя свою версию процедуры sleep.
6.4.4. Системные вызовы sigpending и sigsuspend
ОС UNIX также содержит дополняющие вызов SigProcMask системные вызовы SigPending и SigPending, которые определены следующим образом:
Описание
uses linux;

Function SigPending:SigSet;
Procedure SigSuspend(Mask:SigSet);
SigPending позволяет узнать, какие из временно заблокированных сигналов необходимо обработать. Возвращаемое значение – маска отложенных сигналов.
SigSuspend временно заменяет маску сигналов для процесса на Mask, приостанавливая процесс до получения сигнала.
Глава 7. Межпроцессное взаимодействие при помощи программных каналов
Если два или несколько процессов совместно выполняют одну и ту же задачу, то они неизбежно должны использовать общие данные. Хотя сигналы и могут быть полезны для синхронизации процессов или для обработки исключительных ситуаций или ошибок, они совершенно не подходят для передачи данных от одного процесса к другому. Один из возможных путей разрешения этой проблемы заключается в совместном использовании файлов, так как ничто не мешает нескольким процессам одновременно выполнять операции чтения или записи для одного и того же файла. Тем не менее совместный доступ к файлам может оказаться неэффективным и потребует специальных мер предосторожности для избежания конфликтов.
Для решения этих проблем система UNIX обеспечивает конструкцию, которая называется программными каналами (pipe). (В следующих главах будут также изучены некоторые другие средства коммуникации процессов.) Программный канал (или просто канал) служит для установления односторонний связи, соединяющей один процесс с другим, и является еще одним видом обобщенного ввода/вывода системы UNIX. Как увидим далее, процесс может посылать данные в канал при помощи системного вызова fdwrite, а другой процесс может принимать данные из канала при помощи системного вызова fdread.
7.1. Каналы
7.1.1. Каналы на уровне команд
Большинство пользователей UNIX уже сталкивались с конвейерами команд:
$ pr doc | lр
Этот конвейер организует совместную работу команд pr и lр. Символ | в командной строке сообщает командному интерпретатору, что необходимо создать канал, соединяющий стандартный вывод команды pr со стандартным вводом команды lр. В результате этой команды на матричный принтер будет выведена разбитая на страницы версия файла doc.
Разобьем командную строку на составные части. Программа pr слева от символа, обозначающего канал, ничего не знает о том, что ее стандартный вывод посылается в канал. Она выполняет обычную запись в свой стандартный вывод, не предпринимая никаких особых мер. Аналогично программа lр справа выполи чтение точно так же, как если бы она получала свой стандартный ввод с клавиатуры или из обычного файла.1 Результат в целом будет таким же, как при выполнении следующей последовательности команд:
$ pr doc > tmpfile
$ lр < tmpfile
$ rm tmpfile
Управление потоком в канале осуществляется автоматически и прозрачно для процесса. Поэтому, если программа pr будет выводить информацию слишком быстро, то ее выполнение будет приостановлено. После того как программа lр догонит программу pr, и количество данных, находящихся в канале, упадет до приемлемого уровня, выполнение программы pr продолжится.
Каналы являются одной из самых сильных и характерных особенностей ОС UNIX, доступных даже с уровня командного интерпретатора. Они позволяют легко соединять между собой произвольные последовательности команд. Поэтому программы UNIX могут разрабатываться как простые инструменты, осуществляющие чтение из стандартного ввода, запись в стандартный вывод и выполняют одну, четко определенную задачу. При помощи каналов из этих основных блоков могут быть построены более сложные командные строки, например, команда
$ who | wc -l
направляет вывод программы who в программу подсчета числа слов wc, а задание параметра -l в программе wc определяет, что необходимо подсчитывать только число строк. Таким образом, в конечном итоге программа wc выводит число находящихся в системе пользователей (иногда нужно исключить из суммы первую строку-заголовок вывода who).
7.1.2. Использование каналов в программе
Каналы создаются в программе при помощи системного вызова AssignPipe. В случае удачного завершения вызов сообщает два дескриптора файла: один для записи в канал, а другой для чтения из него. Вызов AssignPipe определяется следующим образом:
Описание
uses linux;

Function AssignPipe(var pipe_in,pipe_out:longint):boolean;
Function AssignPipe(var pipe_in,pipe_out:text):boolean;
Function AssignPipe(var pipe_in,pipe_out:file):boolean;
Переменные pipe_in и pipe_out содержат дескрипторы файлов, обозначающие канал. После успешного вызова pipe_in будет открыт для чтения из канала, a pipe_out для записи в канал.
В случае неудачи вызов pipe вернет значение false. Это может произойти, если в момент вызова произойдет превышение максимально возможного числа дескрипторов файлов, которые могут быть одновременно открыты процессами пользователя (в этом случае переменная linuxerror будет содержать значение Sys_EMFILE), или если произойдет переполнение таблицы открытых файлов в ядре (в этом случае переменная linuxerror будет содержать значение Sys_ENFILE).
После создания канала с ним можно работать просто при помощи вызовов fdread и fdwrite. Следующий пример демонстрирует это: он создает канал, записывает в него три сообщения, а затем считывает их из канала:
uses linux,stdio;

(* Первый пример работы с каналами *)
const
MSGSIZE=16;

(* Эти строки заканчиваются нулевым символом *)
msg1:array [0..MSGSIZE-1] of char = 'hello, world #1';
msg2:array [0..MSGSIZE-1] of char = 'hello, world #2';
msg3:array [0..MSGSIZE-1] of char = 'hello, world #3';

var
inbuf:array [0..MSGSIZE-1] of char;
fdr,fdw,j:longint;
begin
(* Открыть канал *)
if not assignpipe(fdr,fdw) then
begin
perror ('Ошибка вызова pipe');
halt (1);
end;
(* Запись в канал *)
fdwrite (fdw, msg1, MSGSIZE);
fdwrite (fdw, msg2, MSGSIZE);
fdwrite (fdw, msg3, MSGSIZE);
(* Чтение из канала *)
for j := 1 to 3 do
begin
fdread (fdr, inbuf, MSGSIZE);
writeln(inbuf);
end;
halt (0);
end.
На выходе программы получим:
hello, world #1
hello, world #2
hello, world #3
Обратите внимание, что сообщения считываются в том же порядке, в каком они были записаны. Каналы обращаются с данными в порядке «первый вошел – первым вышел» (first-in first-out, или сокращенно FIFO). Другими словами, данные, которые помещаются в канал первыми, первыми и считываются на другом конце канала. Этот порядок нельзя изменить, поскольку вызов fdseek не работает с каналами.
Размеры блоков при записи в канал и чтении из него необязательно должны быть одинаковыми, хотя в нашем примере это и было так. Можно, например, писать в канал блоками по 512 байт, а затем считывать из него по одному символу, так же как и в случае обычного файла. Тем не менее, как будет показано в разделе 7.2, использование блоков фиксированного размера дает определенные преимущества.
Процесс

fdwrite()
fdw → →


fdread()
fdr ← ←

Рис. 7.1. Первый пример работы с каналами

Работа примера показана графически на рис. 7.1. Эта диаграмма позволяет более ясно представить, что процесс только посылает данные сам себе, используя канал в качестве некой разновидности механизма o6paтной связи. Это может показаться бессмысленным, поскольку процесс общается только сам с собой.
Настоящее значение каналов проявляется при использовании вместе с системным вызовом fork, тогда можно воспользоваться тем фактом, что файловые дескрипторы остаются открытыми в обоих процессах. Следующий пример демонстрирует это. Он создает канал и вызывает fork, затем дочерний процесс обменивается несколькими сообщениями с родительским:
(* Второй пример работы с каналами *)
uses linux, stdio;

const
MSGSIZE=16;
msg1:array [0..MSGSIZE-1] of char = 'hello, world #1';
msg2:array [0..MSGSIZE-1] of char = 'hello, world #2';
msg3:array [0..MSGSIZE-1] of char = 'hello, world #3';

var
inbuf:array [0..MSGSIZE-1] of char;
fdr,fdw,j,pid:longint;
begin
(* Открыть канал *)
if not assignpipe (fdr,fdw) then
begin
perror ('Ошибка вызова pipe ');
halt (1);
end;
pid := fork;
case pid of
-1:
begin
perror ('Ошибка вызова fork');
halt (2);
end;
0:
begin
(* Это дочерний процесс, выполнить запись в канал *)
fdwrite (fdw, msg1, MSGSIZE);
fdwrite (fdw, msg2, MSGSIZE);
fdwrite (fdw, msg3, MSGSIZE);
end;
else
begin
(* Это родительский процесс, выполнить чтение из канала *)
for j := 1 to 3 do
begin
fdread (fdr, inbuf, MSGSIZE);
writeln (inbuf);
end;
wait(nil);
end;
end;
halt (0);
end.
Дочерний процесс

Родительский процесс

fdwrite()
fdw → →

← ← fdw
fdwrite()



fdread()
fdr ← ←

→ → fdr
fdread()

Рис. 7.2. Второй пример работы с каналами

Этот пример представлен графически на рис. 7.2. На нем показано, как канал соединяет два процесса. Здесь видно, что и в родительском, и в дочернем процессах открыто по два дескриптора файла, позволяя выполнять запись в канал и чтение из него. Поэтому любой из процессов может выполнять запись в файл с дескриптором fdw и чтение из файла с дескриптором fdr. Это создает определенную проблему. Каналы предназначены для использования в качестве однонаправленного средства связи. Если оба процесса будут одновременно выполнять чтение из канала и запись в него, то это приведет к путанице.
Чтобы избежать этого, каждый процесс должен выполнять либо чтение из канала, либо запись в него и закрывать дескриптор файла, как только он стал не нужен. Фактически программа должна выполнять это для того, чтобы избежать неприятностей, если посылающий данные процесс закроет дескриптор файла, открытого на запись, – раздел 7.1.4 объясняет возможные последствия. Приведенные до сих пор примеры работают только потому, что принимающий процесс в точности знает, какое количество данных он может ожидать. Следующий пример представляет собой законченное решение:
(* Третий пример работы с каналами *)
uses linux,stdio;

const
MSGSIZE=16;
msg1:array [0..MSGSIZE-1] of char = 'hello, world #1';
msg2:array [0..MSGSIZE-1] of char = 'hello, world #2';
msg3:array [0..MSGSIZE-1] of char = 'hello, world #3';

var
inbuf:array [0..MSGSIZE-1] of char;
fdr,fdw,j,pid:longint;
begin
(* Открыть канал *)
if not assignpipe (fdr,fdw) then
begin
perror ('Ошибка вызова pipe ');
halt (1);
end;
pid := fork;
case pid of
-1:
begin
perror ('Ошибка вызова fork');
halt (2);
end;
0:
begin
(* Дочерний процесс, закрывает дескриптор файла,
* открытого для чтения и выполняет запись в канал
*)
fdclose (fdr);
fdwrite (fdw, msg1, MSGSIZE);
fdwrite (fdw, msg2, MSGSIZE);
fdwrite (fdw, msg3, MSGSIZE);
end;
else
begin
(* Родительский процесс, закрывает дескриптор файла,
* открытого для записи и выполняет чтение из канала
*)
fdclose (fdw);
for j := 1 to 3 do
begin
fdread (fdr, inbuf, MSGSIZE);
writeln (inbuf);
end;
wait(nil);
end;
end;
halt (0);
end.
В конечном итоге получится однонаправленный поток данных от дочернего процесса к родительскому. Эта упрощенная ситуация показана на рис. 7.3.
Дочерний процесс

Родительский процесс

→ → fdr
fdread()



fdwrite()
fdw → →

Рис. 7.3. Третий пример работы с каналами

Упражнение 7.1. В последнем примере канал использовался для установления связи между родительским и дочерним процессами. Но дескрипторы файлов канала могут передаваться и сквозь через несколько вызовов fork. Это означает, что несколько процессов могут писать в канал и несколько процессов могут читать из него. Для демонстрации этого поведения напишите программу, которая создает три процесса, два из которых выполняют запись в канал, а один – чтение из него. Процесс, выполняющей чтение, должен выводить все получаемые им сообщения на свой стандартный вывод.
Упражнение 7.2. Для установления двусторонней связи между процессами можно создать два канала, работающих в разных направлениях. Придумайте возможный диалог между процессами и реализуйте его при помощи двух каналов.
7.1.3. Размер канала
Пока в примерах передавались только небольшие объемы данных. Важно заметить, что на практике размер буфера канала конечен. Другими словами, только определенное число байтов может находиться в канале, прежде чем следующий вызов fdwrite будет заблокирован. Минимальный размер, определенный стандартом POSIX, равен 512 байтам. В большинстве существующих систем это значение намного больше. При программировании важно знать максимальный размер канала для системы, так как он влияет на оба вызова fdwrite и fdread. Если вызов fdwrite выполняется для канала, в котором есть свободное место, то данные посылаются в канал, и немедленно происходит возврат из вызова. Если же в момент вызова fdwrite происходит переполнение канала, то выполнение процесса обычно приостанавливается до тех пор, пока место не освободится в результате выполнения другим процессом чтения из канала.
Приведенная в качестве примера следующая программа записывает в канал символ за символом до тех пор, пока не произойдет блокировка вызова fdwrite. Программа использует вызов alarm для предотвращения слишком долгого ожидания в случае, если вызов fdread никогда не произойдет. Необходимо обратить внимание на использование процедуры fpathconf, служащей для определения максимального числа байтов, которые могут быть записаны в канал за один прием.
(* Запись в канал до возникновения блокировки записи *)
uses linux,stdio;

var
count:integer;

(* Вызывается при получении сигнала SIGALRM *)
procedure alrm_action(signo:integer);cdecl;
begin
writeln ('Запись блокируется после вывода ',count,' символов');
halt (0);
end;

const
c:char='x';
var
fdin,fdout,pipe_size:longint;
act:sigactionrec;
temp:sigset_t;
begin
(* Задать обработчик сигнала *)
act.handler.sh := @alrm_action;
sigfillset (@temp);
act.sa_mask:=temp.__val[0];
(* Создать канал *)
if not assignpipe (fdin,fdout) then
begin
perror ('Ошибка вызова pipe ');
halt (1);
end;
(* Определить размер канала *)
pipe_size := fpathconf (fdin, _PC_PIPE_BUF);
writeln('Максимальный размер канала: ',pipe_size,' байт');
(* Задать обработчик сигнала *)
sigaction (SIGALRM, @act, nil);
while true do
begin
(* Установить таймер *)
alarm (20);
(* Запись в канал *)
fdwrite (fdout, c, 1);
(* Сбросить таймер *)
alarm (0);
inc(count);
if count mod 1024 = 0 then
writeln (count, ' символов в канале');
end;
end.
Вот результат работы программы на некоторой системе:
Максимальный размер канала: 32768 байт
1024 символов в канале
2048 символов в канале
3072 символов в канале
4096 символов в канале
5120 символов в канале
.
.
.
31744 символов в канале
32768 символов в канале
Запись блокируется после вывода 32768 символов
Обратите внимание, насколько реальный предел больше, чем заданный стандартом POSIX минимальный размер канала.
Ситуация становится более сложной, если процесс пытается записать за один вызов fdwrite больше данных, чем может вместить даже полностью пустой канал. В этом случае ядро вначале попытается записать в канал максимально возможный объем данных, а затем приостанавливает выполнение процесса до тех пор, пока не освободится место под оставшиеся данные. Это важный момент: обычно вызов fdwrite для канала выполняется неделимыми порциями (atomically), и данные передаются ядром за одну непрерываемую операцию. Если делается попытка записать в канал больше данных, чем он может вместить, то вызов fdwrite выполняется поэтапно. Если при этом несколько процессов выполняют запись в канал, то данные могут оказаться беспорядочно перепутанными.
Взаимодействие вызова fdread с каналами является более простым. При выполнении вызова fdread система проверяет, является ли канал пустым. Если он пуст, то вызов fdread будет заблокирован до тех пор, пока другой процесс не запишет в канал какие-либо данные. При наличии в канале данных произойдет возврат из вызова fdread, даже если запрашивается больший объем данных, чем находится в канале.
7.1.4. Закрытие каналов
Что произойдет, если дескриптор файла, соответствующий одному из концов канала, будет закрыт? Возможны два случая:
• закрывается дескриптор файла, открытого только на запись. Если существуют другие процессы, в которых канал открыт на запись, то ничего не произойдет. Если же больше не существует процессов, которые могли бы выполнять запись в канал, и канал при этом пуст, то любой процесс, который попытается выполнить чтение из канала, получит пустой блок данных. Процессы, которые были приостановлены и ожидали чтения из канала, продолжат свою работу, вызовы read вернут нулевое значение. Для процесса, выполняющего чтение, результат будет похож на достижение конца файла;
• закрывается дескриптор файла, открытого только на чтение. Если еще есть процессы, в которых канал открыт на чтение, то снова ничего не произойдет. Если же больше не существует процессов, выполняющих чтение из канала, то ядро посылает всем процессам, ожидающим записи в канал, сигнал SIGPIPE. Если этот сигнал не перехватывается в процессе, то процесс при этом завершит свою работу. Если же сигнал перехватывается, то после завершения процедуры обработчика прерывания вызов fdwrite вернет значение -1 и переменная linuxerror после этого будет содержать значение Sys_EPIPE. Процессам, которые будут пытаться после этого выполнить запись в канал, также будет посылаться сигнал SIGPIPE.
7.1.5. Запись и чтение без блокирования
Как уже было упомянуто, при использовании и вызова fdread, и вызова fdwrite может возникнуть блокирование, которое иногда нежелательно. Может, например, понадобиться, чтобы программа выполняла процедуру обработки ошибок или опрашивала несколько каналов до тех пор, пока не получит данные из одного из них. К счастью, есть простые способы пресечения нежелательных остановов внутри fdread и fdwrite.
Первый метод заключается в использовании для вызова fstat. Поле size в возвращаемой вызовом структуре tstat сообщает текущее число символов, находящихся в канале. Если только один процесс выполняет чтение из канала, такой подход работает прекрасно. Если же несколько процессов выполняют чтение из канала, то за время, прошедшее между вызовами fstat и fdread, ситуация может измениться, если другой процесс успеет выполнить чтение из канала.
Второй метод заключается в использовании вызова fcntl. Помимо других выполняемых им функций этот вызов позволяет процессу устанавливать для дескриптора файла флаг Open_NONBLOCK. Это предотвращает блокировку последующих вызовов fdread или fdwrite. В этом контексте вызов fcntl может использоваться следующим образом:
uses linux;
.
.
.
fcntl(filedes, F_SETFL, Open_NONBLOCK);
if linuxerror <> 0 then
perror('fcntl');
Если дескриптор filedes является открытым только на запись, то следующие вызовы fdwrite не будут блокироваться при заполнении канала. Вместо этого они будут немедленно возвращать значение -1 и присваивать переменной linuxerror значение Sys_EAGAIN. Аналогично, если дескриптор filedes соответствует выходу канала, то процесс немедленно вернет значение -1, если в канале нет данных, а не приостановит работу. Так же, как и в случае вызова fdwrite, переменной linuxerror будет присвоено значение Sys_EAGAIN. (Если установлен другой флаг – Open_NDELAY, то поведение вызова fdread будет другим. Если канал пуст, то вызов вернет нулевое значение. Далее этот случай не будет рассматриваться.)
Следующая программа иллюстрирует применение вызова fcntl. В ней создается канал, для дескриптора чтения из канала устанавливается флаг Open_NONBLOCK, а затем выполняется вызов fork. Дочерний процесс посылает сообщения родительскому, выполняющему бесконечный цикл, опрашивая канал и проверяя, поступили ли данные.
(* Пример использования флага Open_NONBLOCK *)
uses linux,stdio;

const
MSGSIZE=6;
msg1:array [0..MSGSIZE-1] of char = 'hello';
msg2:array [0..MSGSIZE-1] of char = 'bye!!';

function parent(fdin,fdout:integer):integer; (* код родительского процесса *)
var
nread:longint;
buf:array [0..MSGSIZE-1] of char;
begin
fdclose (fdout);
while true do
begin
nread := fdread (fdin, buf, MSGSIZE);
case nread of
-1:
begin
(* Проверить, есть ли данные в канале. *)
if linuxerror = Sys_EAGAIN then
begin
writeln('(канал пуст)');
sleep (1);
end
else
fatal ('Ошибка вызова read');
end;
0:
begin
(* Канал был закрыт. *)
writeln('Конец связи');
halt (0);
end;
else
writeln('MSG=', buf);
end;
end;
end;

function child (fdin,fdout:integer):integer;
var
count:longint;
begin
fdclose (fdin);
for count := 1 to 3 do
begin
fdwrite (fdout, msg1, MSGSIZE);
sleep (3);
end;
(* Послать последнее сообщение *)
fdwrite (fdout, msg2, MSGSIZE);
halt (0);
end;

var
fdin,fdout:longint;
begin
(* Открыть канал *)
if not assignpipe (fdin,fdout) then
fatal ('Ошибка вызова pipe ');
(* Установить флаг Open_NONBLOCK для дескриптора fdin *)
fcntl (fdin, F_SETFL, Open_NONBLOCK);
if (linuxerror=sys_eagain) or (linuxerror=sys_eacces) then
fatal ('ошибка вызова fcntl');
case fork of
-1: (* ошибка *)
fatal ('ошибка вызова fork');
0: (* дочерний процесс *)
child (fdin,fdout);
else (* родительский процесс *)
parent (fdin,fdout);
end;
end.
Этот пример использует для вывода сообщений об ошибках процедуру fatal, описанную в предыдущей главе. Чтобы не возвращаться назад, приведем ее peaлизацию:
(* Вывести сообщение об ошибке и закончить работу *)
function fatal(s:pchar):integer;
begin
perror (s);
halt (1);
end;
Вывод программы не полностью предсказуем, так как число сообщений «канал пуст» может быть различным. На одном из компьютеров был получен следующий вывод:
MSG=hello
(канал пуст)
(канал пуст)
(канал пуст)
MSG=hello
(канал пуст)
(канал пуст)
(канал пуст)
MSG=hello
(канал пуст)
(канал пуст)
(канал пуст)
MSG=bye!!
Конец связи
7.1.6. Использование системного вызова select для работы с несколькими каналами
Для простых приложений применение неблокирующих операций чтения и записи работает прекрасно. Для работы с множеством каналов одновременно существует другое решение, которое заключается в использовании системного вызова select.
Представьте ситуацию, когда родительский процесс выступает в качестве серверного процесса и может иметь произвольное число связанных с ним клиентских (дочерних) процессов, как показано на рис. 7.4.
В конечном итоге получится однонаправленный поток данных от дочернего процесса к родительскому. Эта упрощенная ситуация показана на рис. 7.3.
Дочерний процесс 1

Родительский процесс

fdwrite()
fdw1 → →

→ → fdr1
fdread()

Дочерний процесс 2

fdwrite()
fdw2 → →

→ → fdr2
fdread()

Рис. 7.4. Клиент/сервер с использованием каналов

В этом случае серверный процесс должен как-то справляться с ситуацией, когда одновременно в нескольких каналах может находиться информация, ожидающая обработки. Кроме того, если ни в одном из каналов нет ожидающих данных, то может иметь смысл приостановить работу серверного процесса до их появления, а не опрашивать постоянно каналы. Если информация поступает более чем по одному каналу, то серверный процесс должен знать обо всех таких каналах для того, чтобы работать с ними в правильном порядке (например, согласно их приоритетам).
Это можно сделать при помощи системного вызова select (существует также аналогичный вызов poll). Системный вызов select используется не только для каналов, но и для обычных файлов, терминальных устройств, именованных каналов (которые будут рассмотрены в разделе 7.2) и сокетов (им посвящена глава 10). Системный вызов select показывает, какие дескрипторы файлов из заданных наборов готовы для чтения, записи или ожидают обработки ошибок. Иногда серверный процесс не должен совсем прекращать работу, даже если не происходит никаких co6ытий, поэтому в вызове select также можно задать предельное время ожидания.
Описание
uses linux;

Function Select(Nfds:Longint; var readfds,writefds, errorfds:PFDset;
Var Timeout): Longint;
Первый параметр nfds задает число дескрипторов файлов, которые могут представлять интерес для сервера. Например, если дескрипторы файлов с номерами 0, 1 и 2 присвоены потокам stdin, stdout и stderr соответственно, и открыты еще два файла с дескрипторами 3 и 4, то можно присвоить параметру nfds значение 5. Программист может определять это значение самостоятельно или воспользоваться постоянной FD_SETSIZE, которая определена в файле stdio. Значение постоянной FD_SETSIZE равно максимальному числу дескрипторов файлов, которые могут быть использованы вызовом select.
Второй, третий и четвертый параметры вызова select являются указателями на битовые маски (bit mask), в которых каждый бит соответствует дескриптору файла. Если бит включен, то это обозначает интерес к соответствующему дескриптору файла. Набор readfds определяет дескрипторы, для которых сервер ожидает возможности чтения; набор writefds – дескрипторы, для которых ожидается возможность выполнить запись; набор errorfds определяет дескрипторы, для которых сервер ожидает появление ошибки или исключительной ситуации, например, по сетевому соединению могут поступить внеочередные данные. Так как работа с битами довольно неприятна и приводит к немобильности программ, существует абстрактный тип данных fdset, а также макросы или функции (в зависимости от конкретной реализации системы) для работы с объектами этого типа. Вот эти макросы для работы с битами файловых дескрипторов:
uses linux;

(* Инициализация битовой маски, на которую указывает fds *)
Procedure FD_ZERO(var fds:fdSet);

(* Установка бита fd в маске, на которую указывает fds *)
Procedure FD_Set(fd:longint;var fds:fdSet);

(* Установлен ли бит fd в маске, на которую указывает fds? *)
Function FD_IsSet(fd:longint;var fds:fdSet):boolean;

(* Сбросить бит fd в маске, на которую указывает fds *)
Procedure FD_Clr(fd:longint;var fds:fdSet);
Следующий пример демонстрирует, как отслеживать состояние двух открытых дескрипторов файлов:
uses linux;
.
.
.
var
fd1, fd2:longint;
readset:fdset;

fd1 := fdopen('file1', Open_RDONLY);
fd2 := fdopen('file2', Open_RDONLY);

FD_ZERO(readset);
FD_SET(fd1, readset);
FD_SET(fd2, readset);

case select(5, @readset, nil, nil, nil) of
(* Обработка ввода *)
end;
Пример очевиден, если вспомнить, что переменные fd1 и fd2 представляют собой небольшие целые числа, которые можно использовать в качестве индексов битовой маски. Обратите внимание на то, что аргументам writefds и errorfds в вызове select присвоено значение nil. Это означает, что представляет интерес только чтение из fd1 и fd2.
Пятый параметр вызова select, timeout, является указателем на следующую структуру timeval:
uses linux;

TimeVal = Record
sec, (* Секунды *)
usec : Longint; (* и микросекунды *)
end;
Если указатель является нулевым, как в этом примере, то вызов select будет заблокирован до тех пор, пока не произойдет интересующее процесс событие. Если в структуре timeout задано нулевое время, то вызов select завершится немедленно (без блокирования). И, наконец, если структура timeout содержит ненулевое значение, то возврат из вызова select произойдет через заданное число секунд или микросекунд, если файловые дескрипторы неактивны.
Возвращаемое вызовом select значение равно -1 в случае ошибки, нулю – после истечения временного интервала или целому числу, равному числу «интересующих» программу дескрипторов файлов. Следует сделать предостережение: при возврате из вызова select он переустанавливает битовые маски, на которые указывают переменные readfds, writefds или errorfds, сбрасывая маску и снова задавая в ней дескрипторы файлов, содержащие искомую информацию. Поэтому необходимо сохранять копию исходных масок.1
Приведем более сложный пример, в котором используются три канала, связанные с тремя дочерними процессами. Родительский процесс должен также отслеживать стандартный ввод.
(* Программа server - обслуживает три дочерних процесса *)
uses linux,stdio;

const
MSGSIZE=6;
msg1:array [0..MSGSIZE-1] of char = 'hello';
msg2:array [0..MSGSIZE-1] of char = 'bye!!';

type
tp1=array [0..1] of longint;
tp3=array [0..2] of tp1;

(* Родительский процесс ожидает сигнала в трех каналах *)
procedure parent(p:tp3); (* код родительского процесса *)
var
ch:char;
buf:array [0..MSGSIZE-1] of char;
_set, master:fdset;
i:integer;
begin
(* Закрыть все ненужные дескрипторы, открытые для записи *)
for i:=0 to 2 do
fdclose (p[i][1]);
(* Задать битовые маски для системного вызова select *)
FD_ZERO (master);
FD_SET (0, master);
for i:=0 to 2 do
FD_SET (p[i][0], master);
(* Лимит времени для вызова select не задан, поэтому он
* будет заблокирован, пока не произойдет событие *)
_set := master;
while select (p[2][0] + 1, @_set, nil, nil, nil) > 0 do
begin
(* Нельзя забывать и про стандартный ввод,
* т.е. дескриптор файла fd=0. *)
if FD_ISSET (0, _set) then
begin
write('Из стандартного ввода...');
fdread (0, ch, 1);
writeln(ch);
end;
for i:=0 to 2 do
begin
if FD_ISSET (p[i][0], _set) then
begin
if fdread (p[i][0], buf, MSGSIZE) > 0 then
begin
writeln('Сообщение от потомка', i);
writeln('MSG=', buf);
end;
end;
end;
(* Если все дочерние процессы прекратили работу,
* то сервер вернется в основную программу
*)
if waitpid (-1, nil, WNOHANG) = -1 then
exit;
_set := master;
end;
end;

function child (p:tp1):integer;
var
count:integer;
begin
fdclose (p[0]);
for count:=1 to 2 do
begin
fdwrite (p[1], msg1, MSGSIZE);
(* Пауза в течение случайно выбранного времени *)
sleep (getpid mod 4);
end;
(* Послать последнее сообщение *)
fdwrite (p[1], msg2, MSGSIZE);
halt (0);
end;

var
pip:tp3;
i:integer;
begin
(* Создать три канала связи, и породить три процесса. *)
for i:=0 to 2 do
begin
if not assignpipe (pip[i][0],pip[i][1]) then
fatal ('Ошибка вызова pipe');
case fork of
-1: (* ошибка *)
fatal ('Ошибка вызова fork');
0: (* дочерний процесс *)
child (pip[i]);
end;
end;
parent (pip);
halt (0);
end.
Результат данной программы может быть таким:
Сообщение от потомка 0
MSG=hello
Сообщение от потомка 1
MSG=hello
Сообщение от потомка 2
MSG=hello

d (пользователь нажимает клавишу d, а затем клавишу Return)
Из стандартного ввода d (повторение символа d)
Из стандартного ввода (повторение символа Return)

Сообщение от потомка 0
MSG=hello
Сообщение от потомка 1
MSG=hello
Сообщение от потомка 2
MSG=hello

Сообщение от потомка 0
MSG=bye
Сообщение от потомка 1
MSG=bye
Сообщение от потомка 2
MSG=bye
Обратите внимание, что в этом примере пользователь нажимает клавишу d, а затем символ перевода строки (Enter или Return), и это отслеживается в стандартном вводе в вызове select.
Функция SelectText является модификацией Select, предназначенной для работы с текстовыми файлами:
Описание
uses linux;

Function SelectText(var T:Text; TimeOut:PTime):Longint;
SelectText выполняет системный вызов Select для файлов типа Text. Время ожидания может быть указано в параметре TimeOut. Вызов SelectText самостоятельно определяет необходимость проверки чтения и записи в зависимости от того, в каком режиме был открыт файл. При Reset выполняется проверка на чтение, при Rewrite и Append – на запись.
Пример использования SelectText:
Uses linux;

Var tv : TimeVal;

begin
Writeln ('Press the to continue the program.');
{ Wait until File descriptor 0 (=Input) changes }
SelectText (Input,nil);
{ Get rid of in buffer }
readln;
Writeln ('Press key in less than 2 seconds...');
tv.sec:=2;
tv.usec:=0;
if SelectText (Input,@tv)>0 then
Writeln ('Thank you !')
else
Writeln ('Too late !');
end.
Связать SelectText и Select можно с помощью функции GetFS, позволяющей из любой файловой переменной получить дескриптор файла.
Описание
uses linux;

Function GetFS(Var F:Any File Type):Longint;
Например:
Uses linux;

begin
Writeln ('File descriptor of input ',getfs(input));
Writeln ('File descriptor of output ',getfs(output));
Writeln ('File descriptor of stderr ',getfs(stderr));
end.
Пример использования SelectText:
Uses linux;

Var tv : TimeVal;

begin
Writeln ('Press the to continue the program.');
{ Wait until File descriptor 0 (=Input) changes }
SelectText (Input,nil);
{ Get rid of in buffer }
readln;
Writeln ('Press key in less than 2 seconds...');
tv.sec:=2;
tv.usec:=0;
if SelectText (Input,@tv)>0 then
Writeln ('Thank you !')
else
Writeln ('Too late !');
end.
7.1.7. Каналы и системный вызов ехес
Вспомним, как можно создать канал между двумя программами с помощью командного интерпретатора:
$ ls | wc
Как это происходит? Ответ состоит из двух частей. Во-первых, командный интерпретатор использует тот факт, что открытые дескрипторы файлов остаются открытыми (по умолчанию) после вызова ехес. Это означает, что два файловых дескриптора канала, которые были открыты до выполнения комбинации вызовов fork/ехес, останутся открытыми и когда дочерний процесс начнет выполнение новой программы. Во-вторых, перед вызовом ехес командный интерпретатор соединяет стандартный вывод программы ls с входом канала, а стандартный ввод программы wc – с выходом канала. Это можно сделать при помощи вызова fcntl или dup2, как было показано в упражнении 5.10. Так как значения дескрипторов файлов, соответствующих стандартному вводу, стандартному выводу и стандартному выводу диагностики, равны 0, 1 и 2 соответственно, то можно, например, соединить стандартный вывод с другим дескриптором файла, используя вызов dup2 следующим образом. Обратите внимание, что перед переназначением вызов dup2 закрывает файл, представленный его вторым параметром.
(* Вызов dup2 будет копировать дескриптор файла 1 *)
dup2(filedes, 1);
.
.
.
(* Теперь программа будет записывать свой стандартный *)
(* вывод в файл, заданный дескриптором filedes *)
.
.
.
Следующий пример, программа join, демонстрирует механизм каналов, задействованный в упрощенном командном интерпретаторе. Программа join имеет два параметра, com1 и com2, каждый из которых соответствует выполняемой команде. Оба параметра в действительности являются массивами строк, которые будут переданы вызову execvp.

Родительский процесс

wait()

Потомок дочернего процесса
(com1)

Дочерний процесс
(com2)

→ → fdin
fdread()



(stdin)
fdwrite()
fdout → →

(stdout)

Рис. 7.5. Программа join

Программа join запустит обе программы на выполнение и свяжет стандартный вывод программы com1 со стандартным вводом программы com2. Работа программы join изображена на рис. 7.5 и может быть описана следующей схемой (без учета обработки ошибок):

процесс порождает дочерний процесс и ожидает действий от него
дочерний процесс продолжает работу

дочерний процесс создает канал

затем дочерний процесс порождает еще один дочерний процесс

В потомке дочернего процесса:
стандартный вывод подключается
к входу канала при помощи вызова dup2

ненужные дескрипторы файлов закрываются

при помощи вызова ехес запускается программа,
заданная параметром 'com1'

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

ненужные дескрипторы файлов закрываются

при помощи вызова ехес запускается программа,
заданная параметром 'com2'
Далее следует реализация программы join; она также использует процедуру fatal, представленную в разделе 7.1.5.
(* Программа join - соединяет две программы каналом *)
function join (com1, com2:ppchar):integer;
var
fdin,fdout:longint;
status:integer;
begin
(* Создать дочерний процесс для выполнения команд *)
case fork of
-1: (* ошибка *)
fatal ('Ошибка 1 вызова fork в программе join');
0: (* дочерний процесс *)
;
else (* родительский процесс *)
begin
wait(@status);
join:=status;
exit;
end;
end;
(* Остаток процедуры, выполняемой дочерним процессом *)
(* Создать канал *)
if not assignpipe(fdin,fdout) then
fatal ('Ошибка вызова pipe в программе join');
(* Создать еще один процесс *)
case fork of
-1:
(* ошибка *)
fatal ('Ошибка 2 вызова fork в программе join');
0:
begin
(* процесс, выполняющий запись *)
dup2 (fdout, 1); (* направить ст. вывод в канал *)
fdclose (fdin); (* сохранить дескрипторы файлов *)
fdclose (fdout);
execvp (com1[0], com1, envp);
(* Если execvp возвращает значение, то произошла ошибка *)
fatal ('Ошибка 1 вызова execvp в программе join');
end;
else
begin
(* процесс, выполняющий чтение *)
dup2 (fdin, 0); (* направить ст. ввод из канала *)
fdclose (fdin);
fdclose (fdout);
execvp (com2[0], com2, envp);
fatal ('Ошибка 2 вызова execvp в программе join');
end;
end;
end;
Эту процедуру можно вызвать следующим образом:
uses linux, stdio;

const
one:array [0..3] of pchar = ('ls', '-l', '/usr/lib', nil);
two:array [0..2] of pchar = ('grep', '^d', nil);
var
ret:integer;
begin
ret := join (one, two);
writeln ('Возврат из программы join ', ret);
halt (0);
end.
Упражнение 7.3. Как можно обобщить подход, показанный в программе join, для связи нескольких процессов при помощи каналов?
Упражнение 7.4. Добавьте возможность работы с каналами в командный интерпретатор smallsh, представленный в предыдущей главе.
Упражнение 7.5. Придумайте метод, позволяющий родительскому процессу запускать программу в качестве дочернего процесса, а затем считывать ее стандартный вывод при помощи канала. Стоит отметить, что эта идея лежит в основе процедур popen/pipeopen и pclose/pipeclose, которые входят в стандартную библиотеку ввода/вывода. Процедуры popen/pipeopen и pclose/pipeclose избавляют программиста от большинства утомительных деталей согласования вызовов fork, ехес, fdclose, dup или dup2. Эти процедуры обсуждаются в главе 11.
7.2. Именованные каналы, или FIFO
Каналы являются изящным и мощным механизмом межпроцессного взаимодействия. Тем не менее они имеют ряд недостатков.
Первый, и наиболее серьезный из них, заключается в том, что каналы могут использоваться только для связи процессов, имеющих общее происхождение, таких как родительский процесс и его потомок. Это ограничение становится видным при попытке разработать настоящую «серверную» программу, которая выполняется постоянно, обеспечивая системный сервис. Примерами таких программ являются серверы управления сетью и спулеры печати. В идеале клиентские процессы должны иметь возможность стартовать, подключаться к не связанному с ними серверному процессу при помощи канала, а затем снова отключаться от него. К сожалению, такую модель при помощи обычных каналов реализовать нельзя.
Второй недостаток каналов заключается в том, что они не могут существовать постоянно. Они каждый раз должны создаваться заново, а после завершения обращающегося к ним процесса уничтожаются.
Для восполнения этих недостатков существует разновидность канала, называемая именованным каналом, или файлом типа FIFO (сокращение от first-in first-out, то есть «первый вошел/первым вышел»). В отношении вызовов fdread и fdwrite именованные каналы идентичны обычным. Тем не менее, в отличие от обычных каналов, именованные каналы являются постоянными и им присвоено имя файла системы UNIX. Именованный канал также имеет владельца, размер и связанные с ним права доступа. Он может быть открыт, закрыт и удален, как и любой файл UNIX, но при чтении или записи ведет себя аналогично каналу.
Прежде чем рассматривать применение каналов FIFO на программном уровне, рассмотрим их использование на уровне команд. Для создания именованного канала используется команда mknod:
$ /etc/mknod channel p
Первый аргумент channel является именем канала FIFO (в качестве него можно задать любое допустимое имя UNIX). Параметр р команды mknod указывает, что нужно создать именованный канал. Этот параметр необходим, так как команда mknod также используется для создания файлов устройств.
Некоторые атрибуты вновь созданного канала FIFO можно вывести при помощи команды ls:
$ ls -l channel
prw-rw-r- 1 ben usr 0 Aug 1 21:05 channel
Символ р в первой колонке обозначает, что channel является файлом типа FIFO. Обратите внимание на права доступа к именованному каналу channel (чтение/запись для владельца и группы владельца, только чтение для всех остальных пользователей); владельца и группу владельца (ben, usr); размер (0 байт, то есть в настоящий момент канал пуст) и время создания.
При помощи стандартных команд UNIX можно выполнять чтение из канала FIFO и запись в него, например:
$ cat < channel
Если выполнить эту команду сразу же после создания именованного канала channel, то она «зависнет». Это происходит из-за того, что процесс, открывающий канал FIFO на чтение, по умолчанию будет блокирован до тех пор, пока другой процесс не попытается открыть канал FIFO для записи. Аналогично процесс, пытающийся открыть канал FIFO для записи, будет блокирован до тех пор, пока другой процесс не попытается открыть его для чтения. Это благоразумный подход, так как он экономит системные ресурсы и облегчает координацию работы программы. Вследствие этого, при необходимости создания одновременно как записывающего, так и читающего процессов, потребуется запустить один из них в фоновом режиме (или с другого терминала, или псевдотерминала xterm графического интерфейса), например:
$ cat < channel &
102
$ ls -l > channel; wait
total 17
prw-rw-r- 1 ben usr 0 Aug 1 21:05 channel
-rw-rw-r- 1 ben usr 0 Aug 1 21:06 f
-rw-rw-r- 1 ben usr 937 Jul 27 22:30 fifos
-rw-rw-r- 1 ben usr 7152 Jul 27 22:11 pipes.cont
Проанализируем подробнее этот результат. Содержимое каталога вначале выводится при помощи команды ls, а затем записывается в канал FIFO. Ожидающая команда cat затем считывает данные из канала FIFO и выводит их на экран. После этого процесс, выполняющий команду cat, завершает работу. Это происходит из-за того, что канал FIFO больше не открыт для записи, чтение из него будет безуспешным, как и для обычного канала, что команда cat понимает как достижение конца файла. Команда же wait заставляет командный интерпретатор ждать завершения команды cat перед тем, как снова вывести приглашение командной строки.
7.2.1. Программирование при помощи каналов FIFO
Программирование при помощи каналов FIFO, в основном, идентично программированию с использованием обычных каналов. Единственное существенное различие заключается в их инициализации. Вместо использования вызова assignpipe канал FIFO создается при помощи вызова mkfifo. В старых версиях UNIX может потребоваться использование более общего вызова mknod.
Описание
uses linux;

Function MkFifo(PathName:String; Mode:Longint):Boolean;
Системный вызов mkfifo создает файл FIFO с именем, заданным первым параметром pathname. Канал FIFO будет иметь права доступа, заданные параметром mode и измененные в соответствии со значением umask процесса.
После создания канала FIFO он должен быть открыт при помощи вызова fdореn. Поэтому, например, фрагмент кода
uses linux;
.
.
mkfifo('/tmp/fifo', octal(0666));
.
.
fd := fdopen('/tmp/fifo', Open_WRONLY);
открывает канал FIFO для записи. Вызов fdopen будет заблокирован до тех пор, пока другой процесс не откроет канал FIFO для чтения (конечно же, если канал FIFO уже был открыт для чтения, то возврат из вызова open произойдет немедленно).
Можно выполнить не блокирующий вызов fdopen для канала FIFO. Для этого во время вызова должен быть установлен флаг Open_NONBLOCK (определенный в файле linux) и один из флагов Open_RDONLY или Open_WRONLY, например:
fd := fdopen('/tmp/fifo', Open_WRONLY or Open_NONBLOCK);
if fd = -1 then
perror('Ошибка вызова open для канала FIFO');
Если не существует процесс, в котором канал FIFO открыт для чтения, то этот вызов fdopen вернет значение -1 вместо блокировки выполнения, а переменная linuxerror будет содержать значение Sys_ENXIO. В случае же успешного вызова fdopen последующие вызовы fdwrite для канала FIFO также будут не блокирующими.
Наступило время привести пример. Представим две программы, которые показывают, как можно использовать канал FIFO для реализации системы обмена сообщениями. Эти программы используют тот факт, что вызовы fdread или fdwrite для каналов FIFO, как и для программных каналов, являются неделимыми (для небольших порций данных). Если при помощи канала FIFO пересылаются сообщения фиксированного размера, то отдельные сообщения будут сохраняться, даже если несколько процессов одновременно выполняют запись в канал.
Рассмотрим вначале программу sendmessage, которая посылает отдельные сообщения в канал FIFO с именем fifo. Она вызывается следующим образом:
$ sendmessage 'текст сообщения 1' 'текст сообщения 2'
Обратите внимание на то, что каждое сообщение заключено в кавычки и поэтому считается просто одним длинным аргументом. Если не сделать этого, то каждое слово будет рассматриваться, как отдельное сообщение. Программа sendmessage имеет следующий исходный текст:
(* Программа sendmessage - пересылка сообщений через FIFO *)
uses linux,stdio,strings;

const
MSGSIZ=63;
fifo = 'fifo';

var
fd,j:integer;
nwrite:longint;
msgbuf:array [0..MSGSIZ] of char;
begin
if paramcount=0 then
begin
writeln (stderr, 'Применение: sendmessage сообщение');
halt (1);
end;
(* Открыть канал fifo, установив флаг Open_NONBLOCK *)
fd := fdopen (fifo, Open_WRONLY or Open_NONBLOCK);
if fd < 0 then
fatal ('Ошибка вызова open для fifo');
(* Посылка сообщений *)
for j := 1 to paramcount do
begin
if length(paramstr(j)) > MSGSIZ then
begin
writeln('Слишком длинное сообщение ', paramstr(j));
continue;
end;
strpcopy(msgbuf, paramstr(j));
nwrite := fdwrite (fd, msgbuf, MSGSIZ + 1);
if nwrite = -1 then
fatal ('Ошибка записи сообщения');
end;
halt(0);
end.
И снова для вывода сообщений об ошибках использована процедура fatal. Сообщения посылаются блоками по 64 байта при помощи не блокируемого вызова fdwrite. В действительности текст сообщения ограничен 63 символами, а последний символ является нулевым.
Программа rcvmessage принимает сообщения при помощи чтения из канала FIFO. Она не выполняет никаких полезных действий и служит только демонстрационным примером:
(* Программа rcvmessage - получение сообщений из канала fifo *)
uses linux,stdio;

const
MSGSIZ=63;
fifo = 'fifo';

var
fd:integer;
msgbuf:array [0..MSGSIZ] of char;
begin
(* Создать канал fifo, если он еще не существует *)
if not mkfifo (fifo, octal(0666)) then
if linuxerror <> Sys_EEXIST then
fatal ('Ошибка приемника: вызов mkfifo');

(* Открыть канал fifo для чтения и записи. *)
fd := fdopen (fifo, Open_RDWR);
if fd < 0 then
fatal ('Ошибка при открытии канала fifo');

(* Прием сообщений *)
while true do
begin
if fdread (fd, msgbuf, MSGSIZ + 1) < 0 then
fatal ('Ошибка при чтении сообщения');
(*
* вывести сообщение; в настоящей программе
* вместо этого могут выполняться какие-либо
* полезные действия.
*)

writeln('Получено сообщение: ', msgbuf);
end;
end.
Обратите внимание на то, что канал FIFO открывается одновременно для чтения и записи (при помощи задания флага Open_RDWR). Чтобы понять, для чего это сделано, предположим, что канал FIFO был открыт только для чтения при помощи задания флага Open_RDONLY. Тогда выполнение программы rcvmessage будет сразу заблокировано в момент вызова fdopen. Когда после старта программы sendmessage в канал FIFO будет произведена запись, вызов fdopen будет разблокирован, программа rcvmessage будет читать все посылаемые сообщения. Когда же канал FIFO станет пустым, а процесс sendmessage завершит работу, вызов fdread начнет возвращать нулевое значение, так как канал FIFO уже не будет открыт на запись ни в одном процессе. При этом программа rcvmessage войдет в бесконечный цикл. Использование флага Open_RDWR позволяет гарантировать, что, по крайней мере, в одном процессе, то есть самом процессе программы rcvmessage, канал FIFO будет открыт для записи. В результате вызов open всегда будет блокироваться то тех пор, пока в канал FIFO снова не будут записаны данные.
Следующий диалог показывает, как можно использовать эти две программы. Программа rcvmessage выполняется в фоновом режиме для получения сообщений от разных процессов, выполняющих программу sendmessage.
$ rcvmessage &
40
$ sendmessage 'сообщение 1' 'сообщение 2'
Получено сообщение: сообщение 1
Получено сообщение: сообщение 2
$ sendmessage 'сообщение номер 3'
Получено сообщение: сообщение номер 3
Упражнение 7.6. Программы sendmessage и rcvmessage образуют основу простой системы обмена данными. Сообщения, посылаемые программе rcvmessage, могут, например, быть именами файлов, которые нужно обработать. Проблема заключается в том, что текущие каталоги программ sendmessage и rcvmessage могут быть различными, поэтому относительные пути будут восприняты неправильно. Как можно разрешить эту проблему? Можно ли создать, скажем, спулер печати в большой системе, используя только каналы FIFO?
Упражнение 7.7. Если программу rcvmessage нужно сделать настоящей серверной программой, то потребуется гарантия того, что в произвольный момент времени выполняется только одна копия сервера. Существует несколько способов достичь этого. Один из методов состоит в создании файла блокировки. Рассмотрим следующую процедуру:
uses linux;

const
lck = '/tmp/lockfile';

function makelock:integer;
var
fd:integer;
begin
fd := fdopen (lck, Open_RDWR or Open_CREAT or Open_EXCL, octal(0600));

if fd < 0 then
begin
if linuxerror = SYS_EEXIST then
halt (1) (* файл занят другим процессом *)
else
halt (127); (* неизвестная ошибка *)
end;

(* Файл блокировки создан, выход из процедуры *)
fdclose (fd);
makelock:=0;
end;
Эта процедура использует тот факт, что вызов open осуществляется за один шаг. Поэтому, если несколько процессов пытаются выполнить процедуру makelock, одному из них это удастся первым, и он создаст файл блокировки и «заблокирует» работу остальных. Добавьте эту процедуру к программе sendmessage. При этом, если выполнение программы sendmessage завершается при помощи сигнала SIGHUP или SIGTERM, то она должна удалять файл блокировки перед выходом. Как вы думаете, почему мы использовали в процедуре makelock вызов fdopen, а не fdcreat?
Глава 8. Дополнительные методы межпроцессного взаимодействия
8.1. Введение
Используя средства из глав 6 и 7, можно осуществить основные взаимодействия между процессами. В этой главе рассматриваются усовершенствованные средства межпроцессного взаимодействия, которые позволят использовать более сложные методы программирования.
Первая и наиболее простая тема данной главы – блокировка записей (record locking), которая фактически является не формой прямого межпроцессного взаимодействия, а скорее – методом координирования работы процессов. Блокировка позволяет процессу временно резервировать часть файла для исключительного использования при решении некоторых сложных задач управления базами данных. Здесь стоит сделать предупреждение: спецификация XSI определяет блокировку записей как рекомендательную (advisory), означающую, что она не препятствует непосредственному выполнению операций файлового ввода/вывода, а вся ответственность за проверку установленных блокировок полностью ложится на процесс.1
Другие механизмы межпроцессного взаимодействия, обсуждаемые в этой главе, являются более редкими. В общем случае эти средства описываются как средства IPC (IPC facilities, где сокращение IPC означает inter-process communication – межпроцессное взаимодействие) и включены в одноименный модуль. Этот общий термин подчеркивает общность их применения и структуры, хотя существуют три определенных типа таких средств:
• очереди сообщений (message passing). Они позволяют процессу посылать и принимать сообщения, под которыми понимается произвольная последовательность байтов или символов;
• семафоры (semaphores). По сравнению с очередями сообщений семафоры представляют собой низкоуровневый метод синхронизации процессов, малопригодный для передачи больших объемов данных. Их теория берет начало из работ Дейкстры (Е.W. Dijkstra, 1968);
• разделяемая память (shared memory). Это средство межпроцессного взаимодействия позволяет двум и более процессам совместно использовать данные, содержащиеся в определенных сегментах памяти. Естественно, обычно данные процесса являются недоступными для других процессов. Этот механизм обычно является самым быстрым механизмом межпроцессного взаимодействия.2
8.2. Блокировка записей
8.2.1. Мотивация
На первом этапе стоит рассмотреть простой пример демонстрации того, почему блокировка записей необходима в некоторых ситуациях.
Примером будет служить известная корпорация ACME Airlines, использующая ОС UNIX в своей системе заказа билетов. Она имеет два офиса, А и В, в каждом из которых установлен терминал, подключенный к компьютеру авиакомпании. Служащие компании используют для доступа к базе данных, реализованной в виде обычного файла UNIX, программу acmebook. Эта программа позволяет пользователю выполнять чтение и обновление базы данных. В частности, служащий может уменьшить на единицу число свободных мест при заказе билета на определенный авиарейс.
Предположим теперь, что осталось всего одно свободное место на рейс АСМ501 в Лондон, и миссис Джонс входит в офис А; в то же самое время мистер Смит входит в офис В, и они оба заказывают место на рейс АСМ501. При этом возможна такая последовательность событий:
1. Служащий офиса А запускает программу acmebook. Назовем стартовавший процесс РА.
2. Сразу же после этого служащий в офисе В также запускает программу acmebook. Назовем этот процесс РВ.
3. Процесс РА считывает соответствующую часть базы данных при помощи системного вызова fdread и определяет, что осталось всего одно свободное место.
4. Процесс РВ выполняет чтение из базы данных сразу же после процесса РА и также определяет, что осталось одно свободное место на рейс АСМ501.
5. Процесс РА обнуляет счетчик свободных мест для рейса при помощи системного вызова fdwrite, изменяя соответствующую часть базы данных. Служащий в офисе А вручает билет миссис Джонс.
6. Сразу же вслед за этим процесс РВ также выполняет запись в базу данных, также записывая нулевое значение в счетчик свободных мест. Но на этот раз значение счетчика ошибочно – на самом деле оно должно было бы быть равно -1, то есть хотя процесс РА уже обновил базу данных, процесс РВ не знает об этом и спешит выполнить заказ, как если бы место было свободно. Вследствие этого мистер Смит также получит билет, и на самолет будет продано больше билетов, чем число свободных мест в нем.
Эта проблема возникает из-за того, что несколько процессов могут одновременно обращаться к файлу UNIX. Комплексная операция с данными файла, состоящая из нескольких вызовов fdseek, fdread и fdwrite, может быть выполнена двумя или более процессами одновременно, и это, как показывает наш простой пример, будет иметь непредвиденные последствия.
Одно из решений состоит в том, чтобы разрешить процессу выполнить блокировку (lock) части файла, с которой он работает. Блокировка, которая нисколько не изменяет содержимое файла, показывает другим процессам, что данные, о которых идет речь, уже используются. Это предотвращает вмешательство другого процесса во время последовательности дискретных физических операций, образующих одну комплексную операцию, или транзакцию. Этот механизм часто называют блокировкой записи (record locking), где запись означает просто произвольную часть файла. Для обеспечения корректности сама операция блокировки должна быть атомарной, чтобы она не могла пересечься с параллельной попыткой блокировки в другом процессе.
Для обеспечения нормальной работы блокировка должна выполняться централизованно. Возможно, лучше всего это возложить на ядро, хотя пользовательский процесс, выступающий в качестве агента базы данных, также может служить для этой цели. Блокировка записей на уровне ядра может выполняться при помощи уже известного нам вызова fcntl.
Обратите внимание, что возможен также альтернативный способ блокировки записей – при помощи процедуры lockf. Этот подход все еще встречается во многих системах – за дополнительными сведениями следует обратиться к справочному руководству системы.
8.2.2. Блокировка записей при помощи вызова fcntl
О системном вызове управления файловым вводом/выводом fcntl уже упоминалось ранее. В дополнение к привычным функциям вызов fcntl может также использоваться для выполнения блокировки записей. Он предлагает два типа блокировки:
• блокировка чтения (read locks) – просто предотвращает установку другими процессами блокировки записи при помощи вызова fcntl. Несколько процессов могут одновременно выполнять блокировку чтения для одного и того же участка файла. Блокировка чтения может быть полезной, если, например, требуется предотвратить обновление данных, не скрывая их от просмотра другими пользователями;
• блокировка записи (write locks) – предотвращает установку другими процессами блокировку чтения или записи для файла. Другими словами, для заданного участка файла может существовать только одна блокировка записи одновременно. Блокировка записи может использоваться, например, для скрытия участков файла от просмотра при выполнении обновления.
Следует напомнить, что в соответствии со спецификацией XSI блокировка вызовом fcntl является всего лишь рекомендательной. Поэтому процессам необходимо явно согласовывать свои действия, чтобы блокировка вызовом fcntl была действенной (процессы не должны производить операции ввода/вывода без предварительного блокирования соответствующей области).
Для блокировки записей вызов fcntl используется следующим образом:
Описание
uses linux, stdio;

Procedure Fcntl(filedes:longint; Cmd:longint; ldata: pflockrec);
Как обычно, аргумент filedes должен быть допустимым дескриптором открытого файла. Для блокировки чтения дескриптор filedes должен быть открыт при помощи флагов Open_RDONLY или Open_RDWR, поэтому в качестве него не подойдет дескриптор, возвращаемый вызовом fdcreat. Для блокировки записи дескриптор filedes должен быть открыт при помощи флагов Open_WRONLY или Open_RDWR.
Как уже упоминалось, параметр вызова cmd определяет выполняемое действие, кодируемое одним из значений, определенных в файле linux. Следуют три команды относятся к блокировке записей:
F_GETLK
Получить описание блокировки на основе данных, передаваемых в аргументе ldata. (Возвращаемая информация описывает первую блокировку, которая препятствует наложению блокировки, описанной структурой ldata)
F_SETLK
Попытаться применить блокировку к файлу и немедленно возвратить управление, если это невозможно. Используется также для удаления активной блокировки
F_SETLKW
Попытаться применить блокировку к файлу и приостановить работу, если блокировка уже наложена другим процессом. Ожидание процесса внутри вызова fcntl можно прервать при помощи сигнала
ldata содержит описание блокировки. Структура flockrec определена в файле stdio и включает следующие элементы:
flockrec=record
l_type:word; (*Описывает тип блокировки: F_RDLCK, F_WRLCK, F_UNLCK. *)
l_whence:word; (* Тип смещения, как и в вызове lseek *)
l_start:longint; (* Смещение в байтах *)
l_len:longint; (*Размер сегмента данных; 0 означает до конца файла *)
l_pid:longint; (*Устанавливается командой F_GETLK *)
end;
Три элемента, l_whence, l_start и l_len, определяют участок файла, который будет заблокирован, проверен или разблокирован. Переменная l_whence идентична третьему аргументу вызова lseek. Она принимает одно из трех значений: SEEK_SET, SEEK_CUR или SEEK_END, обозначая, что смещение должно вычисляться от начала файла, от текущей позиции указателя чтения-записи или конца файла. Элемент l_start устанавливает начальное положение участка файла по отношению к точке, заданной элементом l_whence. Элемент l_len является длиной участка в байтах; нулевое значение обозначает участок с заданной начальной позиции до максимально возможного смещения. На рис. 8.1. показано, как это работает для случая, если значение поля l_whence равно SEEK_CUR. Структура tsemid_ds определена в файле linux.

Указатель файла

|
|
|
|


Блокируемый участок

/

\

a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w


|
|
|

l_start
l_len

←–––––––→
←–––––––→

l_whence=
SEEK_CUR

Рис. 8.1. Параметры блокировки

Тип l_type определяет тип блокировки. Он может принимать одно из трех значений, определенных в файле stdio:
F_RDLCK Выполняется блокировка чтения
F_WRLCK Выполняется блокировка записи
F_UNLCK Снимается блокировка заданного участка
Поле l_pid существенно только при выборе команды F_GETLK в вызове fcntl. Если существует блокировка, препятствующая установке блокировки, описанной полями структуры ldata, то значение поля l_pid будет равно значению идентификатора процесса, установившего ее. Другие элементы структуры также будут переустановлены системой. Они будут содержать параметры блокировки, наложенной другим процессом.
Установка блокировки при помощи вызова fcntl
Следующий пример показывает, как можно использовать вызов fcntl для установления блокировки записи.
uses linux,stdio;
.
.
.
var
my_lock:flockrec;

my_lock.l_type := F_WRLCK;
my_lock.l_whence := SEEK_CUR;
my_lock.l_start := 0;
my_lock.l_len := 512;
fcntl (fd, F_SETLKW, longint(@my_lock));
При этом будут заблокированы 512 байт, начиная с текущего положения указателя чтения-записи. Заблокированный участок теперь считается «зарезервированным» для исключительного использования процессом. Информация о блокировке помещается в свободную ячейку системной таблицы блокировок.
Если весь участок файла или какая-то его часть уже были заблокированы другим процессом, то вызывающий процесс будет приостановлен до тех пор, пока не будет доступен весь участок. Приостановка работы процесса может быть прервана при помощи сигнала; в частности, для задания времени ожидания может быть использован вызов alarm. Если ожидание не будет прервано, и, в конце концов, участок файла освободится, то процесс заблокирует его. Если происходит ошибка, например, если вызову fcntl передается неверный дескриптор файла или переполнится системная таблица блокировок, то будет возвращено значение -1.
Следующий пример – программа lockit открывает файл с именем locktest (который должен существовать) и блокирует его первые десять байт при помощи вызова fcntl. Затем она порождает дочерний процесс, пытающийся заблокировать первые пять байт файла; родительский процесс в это время делает паузу на пять секунд, а затем завершает работу. В этот момент система автоматически снимает блокировку, установленную родительским процессом.
(* Программа lockit - блокировка при помощи вызова fcntl *)
uses linux,stdio;

var
fd:integer;
my_lock:flockrec;
begin
(* Установка параметров блокировки записи *)
my_lock.l_type := F_WRLCK;
my_lock.l_whence := SEEK_SET;
my_lock.l_start := 0;
my_lock.l_len := 10;
(* Открыть файл *)
fd := fdopen ('locktest', Open_RDWR);
(* Заблокировать первые десять байт *)
fcntl (fd, F_SETLKW, longint(@my_lock));
if linuxerror > 0 then
begin
perror ('parent: locking');
halt (1);
end;
writeln('Родительский процесс: блокировка установлена');
case fork of
-1: (* ошибка *)
begin
perror ('Ошибка вызова fork');
halt (1);
end;
0:
begin
my_lock.l_len := 5;
fcntl (fd, F_SETLKW, longint(@my_lock));
if linuxerror > 0 then
begin
perror ('Дочерний процесс: установка блокировки');
halt (1);
end;
writeln ('Дочерний процесс: блокировка установлена');
writeln ('Дочерний процесс: выход');
halt (0);
end;
end;
(* родительский процесс *)
sleep (5);
(* Выход, который автоматически снимет блокировку. *)
writeln ('Родительский процесс: выход');
halt (0);
end.
Вывод программы lockit может выглядеть примерно так:
Родительский процесс: блокировка установлена
Родительский процесс: выход
Дочерний процесс: блокировка установлена
Дочерний процесс: выход
Обратите внимание на порядок, в котором выводятся сообщения. Он показывает, что дочерний процесс не может наложить блокировку до тех пор, пока родительский процесс не завершит работу и не снимет тем самым блокировку, в противном случае сообщение Дочерний процесс: блокировка установлена появилось бы вторым, а не третьим. Этот пример показывает, что блокировка, наложенная родительским процессом, также затронула и дочерний, хотя блокируемые участки файла и не совсем совпадали. Другими словами, попытка блокировки завершится неудачей, даже если участок файла лишь частично перекрывает уже заблокированный. Эта программа иллюстрирует несколько интересных моментов. Во-первых, блокировка информации не наследуется при вызове fork; дочерний и родительский процессы в данном примере выполняют блокировку независимо друг от друга. Во-вторых, вызов fcntl не изменяет положение указателя чтения-записи файла – во время выполнения обоих процессов он указывает на начало файла. В-третьих, все связанные с процессом блокировки автоматически снимаются при его завершении.
Снятие блокировки при помощи вызова fcntl
Можно разблокировать участок файла, который был ранее заблокирован, присвоив при вызове переменной l_type значение F_UNLK. Снятие блокировки обычно используется через некоторое время после предыдущего запроса fcntl. Если еще какие-либо процессы собирались заблокировать освободившийся участок, то один из них прекратит ожидание и продолжит свою работу.
Если участок, с которого снимается блокировка, находится в середине большого заблокированного этим же процессом участка файла, то система создаст две блокировки меньшего размера, исключающие освобождаемый участок. Это означает, что при этом занимается еще одна ячейка в системной таблице блокировок. Вследствие этого запрос на снятие блокировки может завершиться неудачей из-за переполнения системной таблицы, хотя поначалу этого не видно.
Например, в предыдущей программе lockit родительский процесс снял блокировку в момент выхода из программы, но вместо этого он мог осуществить эту операцию при помощи следующего кода:
(* Родительский процесс снимает блокировку перед выходом *)
writeln('Родительский процесс: снятие блокировки');
my_lock.l_type := F_UNLCK;
fcntl(fd, F_SETLK, longint(@my_lock));
if linuxerror <> 0 then
begin
perror('ошибка снятия блокировки в родительском процессе');
halt(1);
end;
Задача об авиакомпании ACME Airlines
Теперь удастся разрешить конфликтную ситуацию в примере с авиакомпаний ACME Airlines. Для того, чтобы гарантировать целостность базы данных, нужно построить критический участок кода в программе acmebook следующим образом:
заблокировать соответствующий участок базы данных на запись

обновить участок базы данных

разблокировать участок базы данных
Если ни одна программа не обходит механизм блокировки, то запрос на блокировку гарантирует, что вызывающий процесс имеет исключительный доступ к критической части базы данных. Запрос на снятие блокировки снова делает доступной эту область для общего использования. Выполнение любой конкурирующей копии программы acmebook, которая пытается получить доступ к соответствующей части базы данных, будет приостановлено на стадии попытки наложения блокировки.
Текст критического участка программы можно реализовать следующим образом:
(* Набросок процедуры обновления программы acmebook *)

var
db_lock:flockrec;
.
.
.
(* Установить параметры блокировки *)
db_lock.l_type := F_WRLCK;
db_lock.l_whence := SEEK_SET;
db_lock.l_start := recstart;
db_lock.l_len := RECSIZE;
.
.
.
(* Заблокировать запись в базе, выполнение приостановится *)
(* если запись уже заблокирована *)
fcntl(fd, F_SETLKW, longint(@db_lock));
if linuxerror <> 0 then
fatal('Ошибка блокировки');

(* Код для проверки и обновления данных о заказах *)
.
.
.
(* Освободить запись для использования другими процессами *)
db_lock.l_type := F_UNLCK;
fcntl(fd, F_SETLK, longint(@db_lock));
Проверка блокировки
При неудачной попытке программы установить блокировку, задав параметр F_SETLK в вызове fcntl, вызов установит значение переменной linuxerror равным Sys_EAGAIN или Sys_EACCESS (в спецификации XSI определены оба эти значения). Если блокировка уже существует, то с помощью команды F_GETLK можно определить процесс, установивший эту блокировку:
uses linux, stdio;
.
.
.
fcntl(fd, F_SETLK, longint(@alock));
if linuxerror <> 0 then
begin
if (linuxerror = Sys EACCES) or (linuxerror = Sys_EAGAIN) then
begin
fcntl(fd, F_GETLK, longint(@b_lock));
writeln(stderr, 'Запись заблокирована процессом ', b_lock.l_pid);
end
else
perror('Ошибка блокировки');
end;
Клинч
Предположим, что два процесса, РА и РВ, работают с одним файлом. Допустим, что процесс РА блокирует участок файла SX, а процесс РВ – не пересекающийся с ним участок SY. Пусть далее процесс РА попытается заблокировать участок SY при помощи команды F_SETLKW, а процесс РВ попытается заблокировать участок SX, также используя команду F_SETLKW. Ни одна из этих попыток не будет успешной, так как процесс РА приостановит работу, ожидая, когда процесс РВ освободит участок SY, а процесс РВ также будет приостановлен в ожидании освобождения участка SX процессом РА. Если не произойдет вмешательства извне, то будет казаться, что два процесса обречены вечно находиться в этом «смертельном объятии».
Такая ситуация называется клинчем (deadlock) по очевидным причинам. Однако UNIX иногда предотвращает возникновение клинча. Если выполнение запроса F_SETLK приведет к очевидному возникновению клинча, то вызов завершается неудачей, и возвращается значение -1, а переменная linuxerror принимает значение Sys_EDEADLK. К сожалению, вызов fcntl может определять только клинч между двумя процессами, в то время как можно создать трехсторонний клинч.1 Во избежание такой ситуации сложные приложения, использующие блокировки, должны всегда задавать предельное время ожидания.
Следующий пример поможет пояснить изложенное. В точке /*А*/ программа блокирует с 0 по 9 байты файла locktest. Затем программа порождает дочерний процесс, который в точках, помеченных как /*В*/ и /*С*/, блокирует байты с 10 по 14 и пытается выполнить блокировку байтов с 0 по 9. Из-за того, что родительский процесс уже выполнил последнюю блокировку, работа дочернего будет приостановлена. В это время родительский процесс выполняет вызов sleep в течение 10 секунд. Предполагается, что этого времени достаточно, чтобы дочерний процесс выполнил два вызова, устанавливающие блокировку. После того, как родительский процесс продолжит работу, он пытается заблокировать байты с 10 по 14 в точке /*D*/, которые уже были заблокированы дочерним процессом. В этой точке возникнет опасность клинча, и вызов fcntl завершится неудачей.
(* Программа deadlock - демонстрация клинча *)
uses linux, stdio;

var
fd:longint;
first_lock, second_lock:flockrec;
begin
first_lock.l_type := F_WRLCK;
first_lock.l_whence := SEEK_SET;
first_lock.l_start := 0;
first_lock.l_len := 10;
second_lock.l_type := F_WRLCK;
second_lock.l_whence := SEEK_SET;
second_lock.l_start := 10;
second_lock.l_len := 5;

writeln(sizeof(flockrec));
fd := fdopen ('locktest', Open_RDWR);

fcntl (fd, F_SETLKW, longint(@first_lock));
if linuxerror>0 then (*A *)
fatal ('A');

writeln ('A: успешная блокировка (процесс ',getpid,')');

case fork of
-1:
(* ошибка *)
fatal ('Ошибка вызова fork');
0:
begin
(* дочерний процесс *)
fcntl (fd, F_SETLKW, longint(@second_lock));
if linuxerror>0 then (*B *)
fatal ('B');
writeln ('B: успешная блокировка (процесс ',getpid,')');
fcntl (fd, F_SETLKW, longint(@first_lock));
if linuxerror>0 then (*C *)
fatal ('C');
writeln ('C: успешная блокировка (процесс ',getpid,')');
halt (0);
end;
else
begin
(* родительский процесс *)
writeln ('Приостановка родительского процесса');
sleep (10);
fcntl (fd, F_SETLKW, longint(@second_lock));
if linuxerror>0 then (*D *)
fatal ('D');
writeln ('D: успешная блокировка (процесс ',getpid,')');
end;
end;
end.
Вот пример работы этой программы:
А: успешная блокировка (процесс 1410)
Приостановка родительского процесса
В: успешная блокировка (процесс 1411)
D: Deadlock situation detected/avoided
С: успешная блокировка (процесс 1411)
В данном случае попытка блокировки завершается неудачей в точке /*D*/, и процедура perror выводит соответствующее системное сообщение об ошибке. Обратите внимание, что после того, как родительский процесс завершит работу и его блокировки будут сняты, дочерний процесс сможет выполнить вторую блокировку.
Это пример использует процедуру fatal, которая была применена в предыдущих главах.
Упражнение 8.1. Напишите процедуры, выполняющие те же действия, что и вызовы fdread и fdwrite, но которые завершатся неудачей, если уже установлена блокировка нужного участка файла. Измените аналог вызова fdread так, чтобы он блокировал читаемый участок. Блокировка должна сниматься после завершения вызова fdread.
Упражнение 8.2. Придумайте и реализуйте условную схему блокировок нумерованных логических записей файла. (Совет: можно блокировать участки файла вблизи максимально возможного смещения файла, даже если там нет данных. Блокировки в этом участке файла могут иметь особое значение, например, каждый байт может соответствовать определенной логической записи. Блокировка в этой области может также использоваться для установки различных флагов.)
8.3. Дополнительные средства межпроцессного взаимодействия
8.3.1. Введение и основные понятия
ОС UNIX предлагает множество дополнительных механизмов межпроцессного взаимодействия. Их наличие дает UNIX богатые возможности в области связи между процессами и позволяет разработчику использовать различные подходы при программировании многозадачных систем. Дополнительные средства межпроцессного взаимодействия, которые будут рассмотрены, можно разбить на следующие категории:
• передача сообщений;
• семафоры;
• разделяемая память.
Эти средства широко применяются и ведут свое начало от системы UNIX System V, поэтому их иногда называют IPC System V. Следует заметить, что вышеназванные дополнительные средства были определены в последних версиях стандарта POSIX.1
Ключи средств межпроцессного взаимодействия
Программный интерфейс всех трех средств IPC System V однороден, что отражает схожесть их реализации в ядре. Наиболее важным из общих свойств является ключ (key). Ключи – это числа, обозначающие объект межпроцессного взаимодействия в системе UNIX примерно так же, как имя файла обозначает файл. Другими словами, ключ позволяет ресурсу межпроцессного взаимодействия совместно использоваться несколькими процессами. Обозначаемый ключом объект может быть очередью сообщения, набором семафоров или сегментом разделяемой памяти. Ключ имеет тип TKey, состав которого зависит от реализации и определяется в файле ipc.
Ключи не являются именами файлов и несут меньший смысл. Они должны выбираться осторожно во избежание конфликта между различными программами, в этом помогает применение дополнительной опции – «версии проекта». (Одна известная система управления базами данных использовала для ключа шестнадцатеричное значение типа 0xDB; плохое решение, так как такое же значение мог выбрать и другой разработчик.) В ОС UNIX существует простая библиотечная функция ftok, которая образует ключ по указанному файлу.
Описание
uses ipc;

Function ftok(Path:String; ID:char):TKey;
Эта процедура возвращает номер ключа на основе информации, связанной с файлом path. Параметр id также учитывается и обеспечивает еще один уровень уникальности – «версию проекта»; другими словами, для одного имени path будут получены разные ключи при разных значениях id. Процедура ftok не слишком удобна: например, если удалить файл, а затем создать другой с таким же именем, то возвращаемый после этого ключ будет другим. Она завершится неудачей и вернет значение -1 и в случае, если файл path не существует. Процедуру ftok можно применять в приложениях, использующих функции межпроцессного взаимодействия для работы с определенными файлами или при применении для генерации ключа файла, являющегося постоянной и неотъемлемой частью приложения.
Операция get
Программа применяет ключ для создания объекта межпроцессного взаимодействия или получения доступа к существующему объекту. Обе операции вызываются при помощи операции get. Результатом операции get является его целочисленный идентификатор (facility identifier), который может использоваться при вызовах других процедур межпроцессного взаимодействия. Если продолжить аналогию с именами файлов, то операция get похожа на вызов fdcreat или fdopen, а идентификатор средства межпроцессного взаимодействия ведет себя подобно дескриптору файла. В действительности, в отличие от дескрипторов файла, идентификатор средства межпроцессного взаимодействия является уникальным. Различные процессы будут использовать одно и то же значение идентификатора для объекта межпроцессного взаимодействия.
В качестве примера рассмотрим вызов msgget для создания новой очереди сообщений (что представляет собой очередь сообщений, обсудим позже):
mqid := msgget(octal(0100), octal(0644) or IPC_CREAT or IPC_EXCL);
Первый аргумент вызова, msgget, является ключом очереди сообщений. В случае успеха процедура вернет неотрицательное значение в переменной mqid, которая служит идентификатором очереди сообщений. Соответствующие вызовы для семафоров и объектов разделяемой памяти называются соответственно semget и shmget.
Другие операции
Есть еще два типа операций, которые применимы к средствам межпроцессного взаимодействия. Во-первых, это операции управления, которые используются для опроса и изменения статуса объекта IPC, их функции выполняют вызовы msgctl, semctl и shmctl. Во-вторых, существуют операции, выполняющие основные функции IPC. Для каждого из средств межпроцессного взаимодействия существует набор операций, которые будут обсуждаться ниже в соответствующих пунктах. Например, есть две операции для работы с сообщениями: операция msgsnd помещает сообщение в очередь сообщений, а операция msgrcv считывает из нее сообщение.
Структуры данных статуса
При создании объекта межпроцессного взаимодействия система также создает структуру статуса средства межпроцессного взаимодействия (IPC facility status structure), содержащую всю управляющую информацию, связанную с объектом. Для сообщений, семафоров и разделяемой памяти существуют разные типы структуры статуса. Каждый тип содержит информацию, свойственную этому средству межпроцессного взаимодействия. Тем не менее все три типа структуры статуса содержат общую структуру прав доступа. Структура прав доступа tipc_perm содержит следующие элементы:
TIPC_Perm = record
key : TKey;
uid, (* Действующий идентификатор пользователя *)
gid, (* Действующий идентификатор группы *)
cuid, (* Идентификатор пользователя создателя объекта *)
cgid, (* Идентификатор группы создателя объекта *)
mode, (* Права доступа *)
seq : Word;
end;
Права доступа определяют, может ли пользователь выполнять «чтение» из объекта (получать информацию о нем) или «запись» в объект (работать с ним). Коды прав доступа образуются точно таким же образом, как и для файлов. Поэтому значение 0644 для элемента umode означает, что владелец может выполнить чтение и запись объекта, а другие пользователи – только чтение из него. Обратите внимание, что права доступа, заданные элементом mode, применяются в сочетании с действующими идентификаторами пользователя и группы (записанными в элементах uid и gid).1 Очевидно также, что права на выполнение в данном случае не имеют значения. Как обычно, суперпользователь имеет неограниченные полномочия. В отличие от других конструкций UNIX, значение переменной umask пользователя не действует при создании средства межпроцессного взаимодействия.
8.3.2. Очереди сообщений
Начнем подробное рассмотрение средств межпроцессного взаимодействия с примитивов очередей сообщений.
В сущности, сообщение является просто последовательностью символов или байтов (необязательно заканчивающейся нулевым символом). Сообщения передаются между процессами при помощи очередей сообщений (message queues), которые можно создавать или получать к ним доступ при помощи вызова msgget. После создания очереди процесс может помещать в нее сообщения при помощи вызова msgsnd, если он имеет соответствующие права доступа. Затем другой процесс может считать это сообщение при помощи примитива msgrcv, который извлекает сообщение из очереди. Таким образом, обработка сообщений аналогична обмену данными при помощи вызовов чтения и записи для каналов (рассмотренном в разделе 7.1.2.).
Функция msgget определяется следующим образом:
Описание
uses ipc;

Function msgget(key:TKey; permflags:longint):longint;
Этот вызов лучше всего представить как аналог вызова fdopen или fdcreat. Как уже упоминалось в разделе 8.3.1, параметр key, который, в сущности, является простым числом, идентифицирует очередь сообщений в системе. В случае успешного вызова, после создания новой очереди или доступа к уже существующей, вызов msgget вернет ненулевое целое значение, которое называется идентификатором очереди сообщений (message queue identifier).
Параметр permflags указывает выполняемое вызовом msgget действие, которое задается при помощи двух констант, определенных в файле ipc; они могут использоваться по отдельности или объединяться при помощи операции побитового ИЛИ:
IPC_CREAT При задании этого флага вызов msgget создает новую очередь сообщений для данного значения, если она еще не существует. Если продолжить аналогию с файлами, то при задании этого флага вызов msgget выполняется в соответствии с вызовом creat, хотя очередь сообщений и не будет «перезаписана», если она уже существует. Если же флаг IPC_CREAT не установлен и очередь с этим ключом существует, то вызов msgget вернет идентификатор существующей очереди сообщений
IPC_EXCL Если установлен этот флаг и флаг IPC_CREAT, то вызов предназначается только для создания очереди сообщений. Поэтому, если очередь с ключом key уже существует, то вызов msgget завершится неудачей и вернет значение -1. Переменная linuxerror будет при этом содержать значение Sys_EEXIST
При создании очереди сообщений младшие девять бит переменной permflags используются для задания прав доступа к очереди сообщений аналогично коду доступа к файлу. Они хранятся в структуре tipc_perm, создаваемой одновременно с самой очередью.
Теперь можно вернуться к примеру из раздела 8.3.1.
mqid := msgget(octal(0100), octal(0644) or IPC_CREAT or IPC_EXCL);
Этот вызов предназначен для создания (и только создания) очереди сообщений для значения ключа равного octal(0100). В случае успешного завершения вызова очередь будет иметь код доступа octal(0644). Этот код интерпретируется таким же образом, как и код доступа к файлу, обозначая, что создатель очереди может отправлять и принимать сообщения, а члены его группы и все остальные могут выполнять только чтение. При необходимости для изменения прав доступа или владельца очереди может использоваться вызов msgctl.
Работа с очередью сообщений: примитивы msgsnd и msgrcv
После создания очереди сообщений для работы с ней могут использоваться два следующих примитива:
Описание
uses ipc;

Function msgsnd(mqid:longint; message:PMSGBuf; size:longint;
msg_type:longint; flags:longint): Boolean;
Function msgrcv(mqid:longint; message:PMSGBuf; size:longint;
msg_type:longint; flags:longint): Boolean;
Первый из вызовов, msgsnd, используется для добавления сообщения в очередь, обозначенную идентификатором mqid.
Сообщение содержится в структуре message – шаблоне, определенном пользователем и имеющем следующую форму:
PMSGbuf=^TMSGbuf;
TMSGbuf=record
mtype:longint; (* Тип сообщения *)
mtext:array [0..SOMEVALUE-1] of char; (* Текст сообщения *)
end;
Значение поля mtype может использоваться программистом для разбиения сообщений на категории. При этом значимыми являются только положительные значения; отрицательные или нулевые не могут использоваться (это будет видно из дальнейшего описания операций передачи сообщений). Массив mtext служит для хранения данных сообщения (постоянная SOMEVALUE выбрана совершенно произвольно). Длина посылаемого сообщения задается параметром size вызова msgsnd и может быть в диапазоне от нуля до меньшего из двух значений SOMEVALUE и максимального размера сообщения, определенного в системе.
Параметр flsgs вызова msgsnd может нести только один флаг: IPC_NOWAIT. При неустановленном параметре IPC_NOWAIT вызывающий процесс приостановит работу, если для посылки сообщения недостаточно системных ресурсов. На практике это произойдет, если полная длина сообщений в очереди превысит максимум, заданный для очереди или всей системы. Если флаг IPC_NOWAIT установлен, тогда при невозможности послать сообщение возврат из вызова произойдет немедленно. Возвращаемое значение будет равно -1, и переменная ipcerror будет иметь значение Sys_EAGAIN, означающее необходимость повторения попытки.
Вызов msgsnd также может завершиться неудачей из-за установленных прав доступа. Например, если ни действующий идентификатор пользователя, ни действующий идентификатор группы процесса не связаны с очередью, и установлен код доступа к очереди octal(0660), то вызов msgsnd для этой очереди завершится неудачей. Переменная ipcerror получит значение Sys_EACCES.
Перейдем теперь к чтению сообщений. Для чтения из очереди, заданной идентификатором mqid, используется вызов msgrcv. Чтение разрешено, если процесс имеет права доступа к очереди на чтение. Успешное чтение сообщения приводит к удалению его из очереди.
На этот раз переменная message используется для хранения полученного сообщения, а параметр size задает максимальную длину сообщений, которые могут находиться в этой структуре. Успешный вызов возвращает длину полученного сообщения.
Параметр msg_type определяет тип принимаемого сообщения, он помогает выбрать нужное из находящихся в очереди сообщений. Если параметр msg_type равен нулю, из очереди считывается первое сообщение, то есть то, которое было послано первым. При ненулевом положительном значении параметра msg_type считывается первое сообщение из очереди с заданным типом сообщения. Например, если очередь содержит сообщения со значениями mtype 999, 5 и 1, а параметр msg_type в вызове msgrcv имеет значение 5, то считывается сообщение типа 5. И, наконец, если параметр msg_type имеет ненулевое отрицательное значение, то считывается первое сообщение с наименьшим значением mtype, которое меньше или равно модулю параметра msg_type. Этот алгоритм кажется сложным, но выражает простое правило: если вернуться к нашему предыдущему примеру с тремя сообщениями со значениями mtype 999, 5 и 1, то при значении параметра msg_type в вызове msgrcv равном -999 и троекратном вызове сообщения будут получены в порядке 1, 5, 999.
Последний параметр flags содержит управляющую информацию. В этом параметре могут быть независимо установлены два флага – IPS_NOWAIT и MSG_NOERROR. Флаг IPC_NOWAIT имеет обычный смысл – если он не задан, то процесс будет приостановлен при отсутствии в очереди подходящих сообщений, и возврат из вызова произойдет после поступления сообщения соответствующего типа. Если же этот флаг установлен, то возврат из вызова при любых обстоятельствах произойдет немедленно.
При установленном флаге MSG_NOERROR сообщение будет усечено, если его длина больше, чем size байт, без этого флага попытка чтения длинного сообщения приводит к неудаче вызова msgrcv. К сожалению, узнать о том, что усечение имело место, невозможно.
Этот раздел может показаться сложным: формулировка средств межпроцессного взаимодействия несколько не соответствует по своей сложности и стилю природе ОС UNIX. В действительности же процедуры передачи сообщений просты в применении и имеют множество потенциальных применений, что попробуем продемонстрировать на следующем примере.
Пример передачи сообщений: очередь с приоритетами
В этом разделе разработаем простое приложение для передачи сообщений. Его целью является реализация очереди, в которой для каждого элемента может быть задан приоритет. Серверный процесс будет выбирать элементы из очереди и обрабатывать их каким-либо образом. Например, элементы очереди могут быть именами файлов, а серверный процесс может копировать их на принтер. Этот пример аналогичен примеру использования FIFO из раздела 7.2.1.
Отправной точкой будет следующий заголовочный файл q.inc:
(* q.h - заголовок для примера очереди сообщений *)

const
QKEY:tkey=(1 shl 6) + 5{0105}; (* ключ очереди *)
QPERM=(6 shl 6) + (6 shl 3){0660}; (* права доступа *)
MAXOBN=50; (* макс. длина имени объекта *)
MAXPRIOR=10; (* максимальный приоритет *)

type
q_entry=record
mtype:longint;
mtext:array [0..MAXOBN] of char;
end;
pq_entry=^q_entry;
Определение QKEY задает значение ключа, которое будет обозначать очередь сообщений в системе. Определение QPERM устанавливает связанные с очередью права доступа. Так как код доступа равен octal(0660), то владелец очереди и члены его группы смогут выполнять чтение и запись. Как увидим позже, определения MAXOBN и MAXPRIOR будут налагать ограничения на сообщения, помещаемые в очередь. Последняя часть этого включаемого файла содержит определение структуры q_entry. Структуры этого типа будут использоваться в качестве сообщений, передаваемых и принимаемых следующими процедурами.
Первая рассматриваемая процедура называется enter, она помещает в очередь имя объекта, заканчивающееся нулевым символом, и имеет следующую форму:
{$i q.inc}

(* Процедура enter - поместить объект в очередь *)
function enter (objname:string;priority:longint):boolean;
var
len, s_qid:longint;
s_entry:q_entry; (* структура для хранения сообщений *)
begin
(* Проверка длины имени и уровня приоритета *)
len := length (objname);
if len > MAXOBN then
begin
warn ('слишком длинное имя');
enter:=false;
exit;
end;
if (priority > MAXPRIOR) or (priority < 0) then
begin
warn ('недопустимый уровень приоритета');
enter:=false;
exit;
end;

(* Инициализация очереди сообщений, если это необходимо *)
s_qid := init_queue;
if s_qid = -1 then
begin
enter:=false;
exit;
end;
(* Инициализация структуры переменной s_entry *)
s_entry.mtype := priority;
strlcopy (s_entry.mtext, @objname[1], MAXOBN);
(* Посылаем сообщение, выполнив ожидание, если это необходимо *)
if not msgsnd (s_qid, @s_entry, len, 0) then
begin
perror ('Ошибка вызова msgsnd');
enter:=false;
exit;
end
else
enter:=true;
end;
Первое действие, выполняемое процедурой enter, заключается в проверке длины имени объекта и уровня приоритета. Обратите внимание на то, что минимальное значение переменной приоритета priority равно 1, так как нулевое значение приведет к неудачному завершению вызова msgsnd. Затем процедура enter «открывает» очередь, вызывая процедуру init_queue, реализацию которой приведем позже.
После завершения этих действий процедура формирует сообщение и пытается послать его при помощи вызова msgsnd. Здесь для хранения сообщения использована структура s_entry типа q_entry, и последний параметр вызова msgsnd равен нулю. Это означает, что система приостановит выполнение текущего процесса, если очередь заполнена (так как не задан флаг IPC_NOWAIT).
Процедура enter сообщает о возникших проблемах при помощи функции warn или библиотечной функции perror. Для простоты функция warn реализована следующим образом:
procedure warn (s:pchar);
begin
writeln(stderr, 'Предупреждение: ', s);
end;
В реальных системах функция warn должна записывать сообщения в специальный файл протокола.
Назначение функции init_queue очевидно. Она инициализирует идентификатор очереди сообщений или возвращает идентификатор очереди сообщений, который с ней уже связан.
{$i q.inc}

(* Инициализация очереди - получить идентификатор очереди *)
function init_queue:longint;
var
queue_id:longint;
begin
(* Попытка создания или открытия очереди сообщений *)
queue_id := msgget (QKEY, IPC_CREAT or QPERM);
if queue_id = -1 then
perror ('Ошибка вызова msgget');
init_queue:=queue_id;
end;
Следующая процедура, serve, используется серверным процессом для получения сообщений из очереди и противоположна процедуре enter.
{$i q.inc}

(* Процедура serve - принимает и обрабатывает сообщение обслуживает
* объект очереди с наивысшим приоритетом
*)
function serve:integer;
var
r_qid:longint;
r_entry:q_entry;
begin
(* Инициализация очереди сообщений, если это необходимо *)
r_qid := init_queue;
if r_qid = -1 then
begin
serve:=-1;
exit;
end;

(* Получить и обработать следующее сообщение *)
while true do
begin
if not msgrcv(r_qid, @r_entry, MAXOBN, -1*MAXPRIOR, MSG_NOERROR) then
begin
perror ('Ошибка вызова msgrcv');
serve:=-1;
exit;
end
else
begin
(* Обработать имя объекта *)
proc_obj (@r_entry);
end;
end;
end;
Обратите внимание на вызов msgrcv. Так как в качестве параметра типа задано отрицательное значение (-1 * MAXPRIOR), то система вначале проверяет очередь на наличие сообщений со значением mtype равным 1, затем равным 2 и так далее, до значения MAXPRIOR включительно. Другими словами, сообщения с наименьшим номером будут иметь наивысший приоритет. Процедура proc_obj работает с объектом. Для системы печати она может просто копировать файл на принтер.
Две следующих простых программы демонстрируют взаимодействие этих процедур: программа etest помещает элемент в очередь, а программа stest обрабатывает его (в действительности она всего лишь выводит содержимое и тип сообщения).
Программа etest
(* Программа etest - ввод имен объектов в очередь. *)
{$mode objfpc}
uses ipc,linux,stdio,sysutils;
{$i q.inc}

var
priority:longint;
begin
if paramcount <> 2 then
begin
writeln(stderr, 'Применение: ',paramstr(0),' имя приоритет');
halt (1);
end;
try
priority:=strtoint(paramstr(2));
except
on e:econverterror do
begin
warn ('Нечисловой приоритет');
halt (2);
end;
end;
if (priority <= 0) or (priority > MAXPRIOR) then
begin
warn ('Недопустимый приоритет');
halt (2);
end;
if not enter (paramstr(1), priority) then
begin
warn ('Ошибка в процедуре enter');
halt (3);
end;
halt (0);
end.
Программа stest
(* Программа stest - простой сервер для очереди *)
uses ipc,linux,stdio,sysutils;
{$i q.inc}

function proc_obj (msg:pq_entry):integer;
begin
writeln(#$a'Приоритет: ',msg^.mtype,' имя: ',msg^.mtext);
end;

var
pid:longint;
begin
pid := fork;
case pid of
0: (* дочерний процесс *)
serve;
-1: (* сервер не существует *)
warn ('Не удалось запустить сервер');
else
writeln('Серверный процесс с идентификатором ', pid);
end;
if pid <> -1 then
halt (0)
else
halt (1);
end.
Ниже следует пример использования этих двух простых программ. Перед запуском программы stest в очередь вводятся четыре простых сообщения при помощи программы etest. Обратите внимание на порядок, в котором выводятся сообщения:
$ etest objname1 3
$ etest objname2 4
$ etest objname3 1
$ etest objname4 9
$ stest
Серверный процесс с идентификатором 2545
$
Приоритет 1 имя objname3

Приоритет 3 имя objname1

Приоритет 4 имя objname2

Приоритет 9 имя objname4
Упражнение 8.3. Измените процедуры enter и serve так, чтобы можно было посылать серверу управляющие сообщения. Зарезервируйте для таких сообщений единичный тип сообщения (как это повлияет на расстановку приоритетов?). Реализуйте следующие возможности:
1. Остановка сервера.
2. Стирание всех сообщений из очереди.
3. Стирание сообщений с заданным уровнем приоритета.
Системный вызов msgctl
Процедура msgctl служит трем целям: она позволяет процессу получать информацию о статусе очереди сообщений, изменять некоторые из связанных с очередью ограничений или удалять очередь из системы.
Описание
uses ipc;

Function msgctl(mqid:longint; cmd:longint; msg_stat:PMSQid_ds): Boolean;
Переменная mqid должна быть допустимым идентификатором очереди. Пропуская пока параметр cmd, обратимся к третьему параметру msg_stat, который содержит адрес структуры TMSQid_ds. Эта структура определяется в файле ipc и содержит следующие элементы:
PMSQid_ds = ^TMSQid_ds;
TMSQid_ds = record
msg_perm : TIPC_perm; (* Владелец/права доступа *)
msg_first : PMsg;
msg_last : PMsg;
msg_stime : Longint; (* Время посл. вызова msgsnd *)
msg_rtime : Longint; (* Время посл. вызова msgrcv *)
msg_ctime : Longint; (* Время посл. изменения *)
wwait : Pointer;
rwait : pointer;
msg_cbytes : word;
msg_qnum : word; (* Число сообщений в очереди *)
msg_qbytes : word; (* Макс. число байтов в очереди *)
msg_lspid : word; (* Идентификатор процесса,
последним вызвавшего msgsnd *)
msg_lrpid : word; (* Идентификатор процесса,
последним вызвавшего msgrcv *)
end;
Структура TIPC_perm, с которой уже встречались ранее, содержит связанную с очередью информацию о владельце и правах доступа. Переменные msg_stime, msg_rtime, msg_ctime содержат число секунд, прошедшее с 00:00 по гринвичскому времени 1 января 1970 г. (Следующий пример покажет, как можно преобразовать такие значения в удобочитаемый формат.)
Параметр cmd в вызове msgctl сообщает системе, какую операцию она должна выполнить. Существуют три возможных значения этого параметра, каждое из которых может быть применено к одному из трех средств межпроцессного взаимодействия. Они обозначаются следующими константами, определенными в файле ipc.
IPC_STAT
Сообщает системе, что нужно поместить информацию о статусе объекта в структуру msg_stat
IPC_SET
Используется для задания значений управляющих параметров очереди сообщений, содержащихся в структуре msg_stat. При этом могут быть изменены только следующие поля:
msq_stat.msg_perm.uid
msq_stat.msg_perm.gid
msq_stat.msg_perm.mode
msq_stat.msg_qbytes
Операция IPC_SET завершится успехом только в случае ее выполнения суперпользователем или текущим владельцем очереди, заданным параметром msq_stat.msg_perm.uid. Кроме того, только суперпользователь может увеличивать значение msg_qbytes – максимальное количество байтов, которое может находиться в очереди
IPC_RMID
Эта операция удаляет очередь сообщений из системы. Она также может быть выполнена только суперпользователем или владельцем очереди. Если параметр command принимает значение IPC_RMID, to параметр msg_stat задается равным nil
Следующий пример, программа show_msg, выводит часть информации о статусе объекта очереди сообщений. Программа должна вызываться так:
$ show_msg значение_ключа
Программа show_msg использует библиотечную процедуру ctime для преобразования значений структуры time_t в привычную запись. (Процедура ctime и другие функции для работы с временными значениями будут обсуждаться в главе 12.) Текст программы show_msg:
(* Программа showmsg - выводит данные об очереди сообщений *)
{$mode objfpc}
uses ipc,stdio,sysutils;

procedure mqstat_print(mkey:tkey; msq_id:longint; mstat:pmsqid_ds);
begin
writeln (#$a'Ключ ', mkey, ', msg_qid ', msq_id, #$a);
writeln(mstat^.msg_qnum, ' сообщений в очереди'#$a);

writeln('Последнее сообщение послано процессом ', mstat^.msg_lspid, ' в ',
ctime(mstat^.msg_stime));
writeln('Последнее сообщение принято процессом ', mstat^.msg_lrpid, ' в ',
ctime(mstat^.msg_rtime));
end;

var
mkey:tkey;
msq_id:longint;
msq_status:tmsqid_ds;
begin
if paramcount<>1 then
begin
writeln(stderr, 'Применение: showmsg значение_ключа');
halt(1);
end;

(* Получаем идентификатор очереди сообщений *)
try
mkey:=tkey(strtoint(paramstr(1)));
except
on e:econverterror do
begin
writeln(stderr, 'Нечисловой идентификатор очереди сообщений');
halt (2);
end;
end;

msq_id := msgget(mkey, 0);
if msq_id = -1 then
begin
perror('Ошибка вызова msgget');
halt(2);
end;

(* Получаем информацию о статусе *)
if not msgctl(msq_id, IPC_STAT, @msq_status) then
begin
perror('Ошибка вызова msgctl');
halt(3);
end;

(* Выводим информацию о статусе *)
mqstat_print(mkey, msq_id, @msq_status);
halt(0);
end.
Упражнение 8.4. Измените процедуру show_msg так, чтобы она выводила информацию о владельце и правах доступа очереди сообщений.
Упражнение 8.5. Взяв за основу программу chmod, напишите программу msg_chmod, которая изменяет связанные с очередью права доступа. Очередь сообщений также должна указываться значением ее ключа.
8.3.3. Семафоры
Семафор как теоретическая конструкция
В информатике понятие семафор (semaphore) был впервые введено голландским теоретиком Э.В. Дейкстрой (E.W. Dijkstra) для решения задач синхронизации процессов. Семафор sem может рассматриваться как целочисленная переменная, для которой определены следующие операции:
р(sem) или wait (sem)

if sem<>0 then
уменьшить sem на единицу
else
ждать, пока sem не станет ненулевым, затем вычесть единицу

v(sem) или signal(sem)

увеличить sem на единицу
if очередь ожидающих процессов не пуста then
продолжить выполнение первого процесса в очереди ожидания
Обратите внимание, что обозначения р и v происходят от голландских терминов для понятий ожидания (wait) и сигнализации (signal), причем последнее понятие не следует путать с обычными сигналами UNIX.
Действия проверки и установки в обеих операциях должны составлять одно атомарное действие, чтобы только один процесс мог изменять семафор sem в каждый момент времени.
Формально удобство семафоров заключается в том, что утверждение
(начальное значение семафора
+ число операций v
- число завершившихся операций р) >= 0
всегда истинно. Это – инвариант семафора (semaphore invariant). Теоретикам нравятся такие инварианты, так как они делают возможным систематическое и строгое доказательство правильности программ.
Семафоры могут использоваться несколькими способами. Наиболее простой из них заключается в обеспечении взаимного исключения (mutual exclusion), когда только один процесс может выполнять определенный участок кода одновременно. Рассмотрим схему следующей программы:
p(sem);

какие-либо действия

v(sem);
Предположим далее, что начальное значение семафора sem равно единице. Из инварианта семафора можно увидеть, что:
(число завершенных операций р -
число завершенных операций v) <= начального значения семафора
или:
(число завершенных операций р -
число завершенных операций v) <= 1
Другими словами, в каждый момент времени только один процесс может выполнять группу операторов, заключенных между определенными операциями р и v. Такая область программы часто называется критическим участком (critical section).
Реализация семафоров в ОС UNIX основана на этой теоретической идее, хотя в действительности предлагаемые средства являются более общими (и, возможно, чрезмерно сложными). Вначале рассмотрим процедуры semget и semctl.
Системный вызов semget
Описание
uses ipc;

Function semget(key:Tkey; nsems:longint; permflags:longint):longint;
Вызов semget аналогичен вызову msgget. Дополнительный параметр nsems задает требуемое число семафоров в наборе семафоров; это важный момент – семафорные операции в System V IPC приспособлены для работы с наборами семафоров, а не с отдельными объектами семафоров. На рис. 8.2 показан набор семафоров. Ниже увидим, что использование целого набора семафоров усложняет интерфейс процедур работы с семафорами.

Индекс 0
Индекс 1
Индекс 2
Индекс 3
semid
semval = 2
semval = 4
semval = 1
semval = 3

nsems=4

Рис.8.2. Набор семафоров

Значение, возвращаемое в результате успешного вызова semget, является идентификатором набора семафоров (semaphore set identifier), который ведет себя почти так же, как идентификатор очереди сообщений. Идентификатор набора семафоров обозначен на рис. 8.2 как semid. Следуя обычной практике, индекс семафора в наборе может принимать значения от 0 до nsems-1.
С каждым семафором в наборе связаны следующие значения:
semval Значение семафора, положительное целое число. Устанавливается при помощи системных вызовов работы с семафорами, то есть к значениям семафоров нельзя получить прямой доступ из программы, как к другим объектам данных
sempid Идентификатор процесса, который последним работал с семафором
semcnt Число процессов, ожидающих увеличения значения семафора
semzcnt Число процессов, ожидающих обнуления значения семафора
Системный вызов semctl
Описание
uses ipc;

Function semctl(semid:longint; sem_num:longint; command:longint;
var ctl_arg:tsemun):longint;
Из определения видно, что функция semctl намного сложнее, чем msgctl. Параметр semid должен быть допустимым идентификатором семафора, возвращенным вызовом semget. Параметр command имеет тот же смысл, что и в вызове msgctl, – задает требуемую команду. Команды распадаются на три категории: стандартные команды управления средством межпроцессного взаимодействия (такие как IPC_STAT); команды, которые воздействуют только на один семафор; и команды, действующие на весь набор семафоров. Все доступные команды приведены в табл. 8.1.
Таблица 8.1. Коды функций вызова semctl
Стандартные функции межпроцессного взаимодействия
IPC_STAT Поместить информацию о статусе в поле ctl_arg.stat
IPC_SET Установить данные о владельце/правах доступа
IPC_RMID Удалить набор семафоров из системы
Операции над одиночными семафорами
(относятся к семафору sem_num, значение возвращается вызовом semctl)
GETVAL Вернуть значение семафора (то есть setval)
SETVAL Установить значение семафора равным ctl_arg.val
GETPID Вернуть значение sempid
GETNCNT Вернуть semncnt (см. выше)
GETZCNT Вернуть semzcnt (см. выше)
Операции над всеми семафорами
GETALL Поместить все значения setval в массив ctl_arg.array
SETALL Установить все значения setval из массива ctl_arg.array
Параметр sem_num используется со второй группой возможных операций вызова semctl для задания определенного семафора. Последний параметр ctl_arg является объединением (записью с вариантами), определенным следующим образом:
PSEMun = ^TSEMun;
TSEMun = record
case longint of
0 : (val : longint);
1 : (buf : PSEMid_ds);
2 : (arr : PWord);
3 : (padbuf : PSeminfo);
4 : (padpad : pointer);
end;
Каждый элемент объединения представляет некоторый тип значения, передаваемого вызову semctl при выполнении определенной команды. Например, если значение command равно SETVAL, то будет использоваться элемент ctl_arg.val.
Одно из важных применений функции setval заключается в установке начальных значений семафоров, так как вызов semget не позволяет процессу сделать это. Приведенная в качестве примера функция initsem может использоваться для создания одиночного семафора и получения связанного с ним идентификатора набора семафоров. После создания семафора (если семафор еще не существовал) функция semctl присваивает ему начальное значение, равное единице.
{$i pv.inc}

(* Функция initsem - инициализация семафора *)
function initsem(semkey:tkey):longint;
var
status, semid:longint;
arg:tsemun;
begin
status := 0;
semid := semget (semkey, 1,
SEMPERM or IPC_CREAT or IPC_EXCL);
if semid = -1 then
begin
if ipcerror = Sys_EEXIST then
semid := semget (semkey, 1, 0);
end
else
(* если семафор создается ... *)
begin
arg.val := 1;
status := semctl (semid, 0, SETVAL, arg);
end;
if (semid = -1) or (status = -1) then
begin
perror ('ошибка вызова initsem');
initsem:=-1;
exit;
end;
(* Все в порядке *)
initsem:=semid;
end;
Включаемый файл pv.inc содержит следующие определения:
(* Заголовочный файл для примера работы с семафорами *)
const
SEMPERM=6 shl 6{0600};
Функция initsem будет использована в примере следующего раздела.
Операции над семафорами: вызов semop
Вызов semop выполняет основные операции над семафорами.
Описание
uses ipc;

Function semop(semid:longint;op_array:pointer;num_ops:cardinal):Boolean;
Переменная semid является идентификатором набора семафоров, полученным с помощью вызова semget. Параметр op_array является массивом структур TSEMbuf, определенных в файле ipc. Каждая структура TSEMbuf содержит описание операций, выполняемых над семафором.
И снова основной акцент делается на операции с наборами семафоров, при этом функция semop позволяет выполнять группу операций как атомарную операцию. Это означает, что пока не появится возможность одновременного выполнения всех операций с отдельными семафорами набора, не будет выполнена ни одна из этих операций. Если не указано обратного, процесс приостановит работу до тех пор, пока он не сможет выполнить все операции сразу.
Рассмотрим структуру TSEMbuf. Она включает в себя следующие элементы:
TSEMbuf=record
sem_num : word;
sem_op : integer;
sem_flg : integer;
end;
Поле sem_num содержит индекс семафора в наборе. Если, например, набор содержит всего один элемент, то значение sem_num должно быть равно нулю. Поле sem_op содержит целое число со знаком, значение которого сообщает функции semop, что необходимо сделать. При этом возможны три случая:
Случай 1: отрицательное значение sem_op
Это обобщенная форма команды для работы с семафорами р(), которая обсуждалась ранее. Действие функции semop можно описать при помощи псевдокода следующим образом (обратите внимание, что ABS() обозначает модуль переменной):
if semval >= ABS(sem_op) then
begin
semval := semval - ABS(sem_op)
end
else
begin
if (sem_flg and IPC_NOWAIT) <> 0 then
немедленно вернуть -1
else
behin
ждать, пока semval не станет больше или равно ABS(sem_op)
затем, как и выше, вычесть ABS(sem_op)
end;
end;
Основная идея заключается в том, что функция semop вначале проверяет значение semval, связанное с семафором sem_num. Если значение semval достаточно велико, то оно сразу уменьшается на указанную величину. В противном случае процесс будет ждать, пока значение semval не станет достаточно большим. Тем не менее, если в переменной sem_flg установлен флаг IPC_NOWAIT, то возврат из вызова sem_op произойдет немедленно, и переменная ipcerror будет содержать код ошибки Sys_EAGAIN.
Случай 2: положительное значение sem_op
Это соответствует традиционной операции v(). Значение переменной sem_op просто прибавляется к соответствующему значению semval. Если есть процессы, ожидающие изменения значения этого семафора, то они могут продолжить выполнение, если новое значение семафора удовлетворит их условия.
Случай 3: нулевое значение sem_op
В этом случае вызов sem_op будет ждать, пока значение семафора не станет равным нулю; значение semval этим вызовом не будет изменяться. Если в переменной sem_flg установлен флаг IPC_NOWAIT, а значение semval еще не равно нулю, то функция semop сразу же вернет сообщение об ошибке.
Флаг SEM_UNDO
Это еще один флаг, который может быть установлен в элементе sem_flg структуры sembuf. Он сообщает системе, что нужно автоматически «отменить» эту операцию после завершения процесса. Для отслеживания всей последовательности таких операций система поддерживает для семафора целочисленную переменную semadj. Важно понимать, что переменная semadj связана с процессами, и для разных процессов один и тот же семафор будет иметь различные значения semadj. Если при выполнении операции semop установлен флаг SEM_UNDO, то значение переменной sem_num просто вычитается из значения semadj. При этом важен знак переменной sem_num: значение semadj уменьшается, если значение sem_num положительное, и увеличивается, если оно отрицательное. После выхода из процесса система прибавляет все значения semadj к соответствующим семафорам и, таким образом, сводит на нет эффект от всех вызовов semop. В общем случае флаг SEM_UNDO должен быть всегда установлен, кроме тех случаев, когда значения, устанавливаемые процессом, должны сохраняться после завершения процесса.
Пример работы с семафорами
Теперь продолжим пример, который начали с процедуры initsem. Он содержит две процедуры р() и v(), реализующие традиционные операции над семафорами. Сначала рассмотрим р():
{$i pv.inc}

(* Процедура p.pas - операция p для семафора *)
function p (semid:longint):longint;
var
p_buf:tsembuf;
begin
p_buf.sem_num := 0;
p_buf.sem_op := -1;
p_buf.sem_flg := SEM_UNDO;
if not semop (semid, @p_buf, 1) then
begin
perror ('ошибка операции p(semid)');
halt (1);
end;
p:=0;
end;
Обратите внимание на то, что здесь использован флаг SEM_UNDO. Теперь рассмотрим текст процедуры v().
{$i pv.inc}

(* Процедура v.pas - операция v для семафора *)
function v (semid:longint):longint;
var
v_buf:tsembuf;
begin
v_buf.sem_num := 0;
v_buf.sem_op := 1;
v_buf.sem_flg := SEM_UNDO;
if not semop (semid, @v_buf, 1) then
begin
perror ('Ошибка операции v(semid)');
halt (1);
end;
v:=0;
end;
Можно продемонстрировать использование этих довольно простых процедур для реализации взаимного исключения. Рассмотрим следующую программу:
(* Программа testsem - проверка процедур работы с семафорами *)
uses ipc,stdio,linux;
{$i pv.inc}

procedure handlesem (skey:tkey);
var
semid, pid:longint;
begin
pid := getpid;

semid := initsem (skey);
if semid < 0 then
halt (1);

writeln (#$a'Процесс ',pid,' перед критическим участком');
p (semid);
writeln ('Процесс ',pid,' выполняет критический участок');

(* В реальной программе здесь выполняется нечто осмысленное *)
sleep (10);

writeln ('Процесс ',pid,' покинул критический участок');
v (semid);
writeln ('Процесс ',pid,' завершает работу');

halt (0);
end;

const
semkey:tkey = $200;
var
i:integer;
begin
for i := 1 to 3 do
if fork = 0 then
handlesem (semkey);
end.
Программа testsem порождает три дочерних процесса, которые используют вызовы р() и v() для того, чтобы в каждый момент времени только один из них выполнял критический участок. Запуск программы testsem может дать следующий результат:
Процесс 799 перед критическим участком
Процесс 799 выполняет критический участок
Процесс 800 перед критическим участком
Процесс 801 перед критическим участком
Процесс 799 покинул критический участок
Процесс 801 выполняет критический участок
Процесс 799 завершает работу
Процесс 801 покинул критический участок
Процесс 801 завершает работу
Процесс 800 выполняет критический участок
Процесс 800 покинул критический участок
Процесс 800 завершает работу
8.3.4. Разделяемая память
Операции с разделяемой памятью позволяют двум и более процессам совместно использовать область физической памяти (общеизвестно, что обычно области данных любых двух программ совершенно отделены друг от друга). Чаще всего разделяемая память является наиболее производительным механизмом межпроцессного взаимодействия.
Для того, чтобы сегмент памяти мог использоваться совместно, он должен быть сначала создан при помощи системного вызова shmget. После создания сегмента разделяемой памяти процесс может подключаться к нему при помощи вызова shmat и затем использовать его для своих частных целей. Когда этот сегмент памяти больше не нужен, процесс может отключиться от него при помощи вызова shmdt.
Системный вызов shmget
Сегменты разделяемой памяти создаются при помощи вызова shmget.
Описание
uses ipc;

Function shmget(key:Tkey; Size:longint; permflags:longint):longint;
Этот вызов аналогичен вызовам msgget и semget. Наиболее интересным параметром вызова является size, который задает требуемый минимальный размер (в байтах) сегмента памяти. Параметр key является значением ключа сегмента памяти, параметр permflags задает права доступа к сегменту памяти и, кроме того, может содержать флаги IPC_CREAT и IPC_EXCL.
Операции с разделяемой памятью: вызовы shmat и shmdt
Сегмент памяти, созданный вызовом shmget, является участком физической памяти и не находится в логическом пространстве данных процесса. Для использования разделяемой памяти текущий процесс, а также все другие процессы, взаимодействующие с этим сегментом, должны явно подключать этот участок памяти к логическому адресному пространству при помощи вызова shmat:
Описание
uses ipc;

Function shmat(shmid:longint; daddr:pchar; shmflags:longint):pchar;
Вызов shmat связывает участок памяти, обозначенный идентификатором shmid (который был получен в результате вызова shmget) с некоторым допустимым адресом логического адресного пространства вызывающего процесса. Этот адрес является значением, возвращаемым вызовом shmat.
Параметр daddr позволяет программисту до некоторой степени управлять выбором этого адреса. Если этот параметр равен nil, то участок подключается к первому доступному адресу, выбранному системой. Это наиболее простой случай использования вызова shmat. Если параметр daddr не равен nil, то участок будет подключен к содержащемуся в нем адресу или адресу в ближайшей окрестности в зависимости от флагов, заданных в аргументе shmflags. Этот вариант сложнее, так как при этом необходимо знать расположение программы в памяти.
Аргумент shmflag может содержать два флага, SHM_RDONLY и SHM_RND, определенные в заголовочном файле ipc. При задании флага SHM_RDONLY участок памяти подключается только для чтения. Флаг SHM_RND определяет, если это возможно, способ обработки в вызове shmat ненулевого значения daddr.
В случае ошибки вызов shmat вернет значение:
(pchar) -1
Вызов shmdt противоположен вызову shmat и отключает участок разделяемой памяти от логического адресного пространства процесса (это означает, что процесс больше не может использовать его). Он вызывается очень просто:
retval := shmdt(memptr);
Возвращаемое значение retval является логическим значением и равно true в случае успеха и false – в случае ошибки.
Системный вызов shmctl
Описание
uses ipc;

Function shmctl(shmid:longint; command:longint; shm_stat: pshmid_ds):
Boolean;
Этот вызов в точности соответствует вызову msgctl, и параметр command может, наряду с другими, принимать значения IPC_STAT, IPC_SET и IPC_RMID. В следующем примере этот вызов будет использован с аргументом command равным IPC_RMID.
Пример работы с разделяемой памятью: программа shmcopy
В этом разделе создадим простую программу shmcopy для демонстрации практического использования разделяемой памяти. Программа shmcopy просто копирует данные со своего стандартного ввода на стандартный вывод, но позволяет избежать лишних простоев в вызовах fdread и fdwrite. При запуске программы shmcopy создаются два процесса, один из которых выполняет чтение, а другой – запись, и которые совместно используют два буфера, реализованных в виде сегментов разделяемой памяти. Когда первый процесс считывает данные в первый буфер, второй записывает содержимое второго буфера, и наоборот. Так как чтение и запись выполняются одновременно, пропускная способность возрастает. Этот подход используется, например, в программах, которые выводят информацию на ленточный накопитель.
Для согласования двух процессов (чтобы записывающий процесс не писал в буфер до тех пор, пока считывающий процесс его не заполнит) будем использовать два семафора. Почти во всех программах, использующих разделяемую память, требуется дополнительная синхронизация, так как механизм разделяемой памяти не содержит собственных средств синхронизации.
Программа shmcopy использует следующий заголовочный файл share_ex.inc:
(* Заголовочный файл для примера работы с разделяемой памятью *)

const
SHMKEY1:tkey=$10; (* ключ разделяемой памяти *)
SHMKEY2:tkey=$15; (* ключ разделяемой памяти *)
SEMKEY :tkey=$20; (* ключ семафора *)

(* Размер буфера для чтения и записи *)
BUFSIZ=8192;
SIZ=5*BUFSIZ;

(* В этой структуре будут находиться данные и счетчик чтения *)
type
databuf=record
d_nread:integer;
d_buf:array [0..SIZ-1] of char;
end;
pdatabuf=^databuf;
Напомним, что постоянная BUFSIZ определена в файле stdio и задает оптимальный размер порций данных при работе с файловой системой. Шаблон databuf показывает структуру, которая связывается с каждым сегментом разделяемой памяти. В частности, элемент d_nread позволит процессу, выполняющему чтение, передавать другому, осуществляющему запись, через участок разделяемой памяти число считанных символов.
Следующий файл содержит процедуры для инициализации двух участков разделяемой памяти и набора семафоров. Он также содержит процедуру remobj, которая удаляет различные объекты межпроцессного взаимодействия в конце работы программы. Обратите внимание на способ вызова shmat для подключения участков разделяемой памяти к адресному пространству процесса.
(* Процедуры инициализации *)

{$i share_ex.inc}

const
IFLAGS=IPC_CREAT or IPC_EXCL;
ERR:pdatabuf=pdatabuf(-1);

var
shmid1, shmid2, semid : longint;

procedure getseg (var p1,p2:pdatabuf);
begin
(* Создать участок разделяемой памяти *)
shmid1 := shmget (SHMKEY1, sizeof (databuf), octal(0600) or IFLAGS);
if shmid1 = -1 then
fatal ('shmget');

shmid2 := shmget (SHMKEY2, sizeof (databuf), octal(0600) or IFLAGS);
if shmid2 = -1 then
fatal ('shmget');

(* Подключить участки разделяемой памяти. *)
p1 := pdatabuf( shmat (shmid1, 0, 0));
if p1 = ERR then
fatal ('shmat');

p2 := pdatabuf( shmat (shmid2, 0, 0));
if p2 = ERR then
fatal ('shmat');
end;

function getsem:longint; (* Получить набор семафоров *)
var
x:tsemun;
begin
x.val := 0;

(* Создать два набора семафоров *)
semid := semget (SEMKEY, 2, octal(0600) or IFLAGS);
if semid = -1 then
fatal ('semget');

(* Задать начальные значения *)

if semctl (semid, 0, SETVAL, x) = -1 then
fatal ('semctl');

if semctl (semid, 1, SETVAL, x) = -1 then
fatal ('semctl');

getsem:=semid;
end;

(* Удаляет идентификаторы разделяемой памяти
* и идентификатор набора семафоров
*)
procedure remobj;
var
x:tsemun;
begin
if not shmctl (shmid1, IPC_RMID, nil) then
fatal ('shmctl');

if not shmctl (shmid2, IPC_RMID, nil) then
fatal ('shmctl');

if semctl (semid, 0, IPC_RMID, x) = -1 then
fatal ('semctl');
end;
Ошибки в этих процедурах обрабатываются при помощи процедуры fatal, которая использовалась в предыдущих примерах. Она просто вызывает perror, а затем halt.
Ниже следует главная функция для программы shmcopy. Она вызывает процедуры инициализации, а затем создает процесс для чтения (родительский) и для записи (дочерний). Обратите внимание на то, что именно выполняющий запись процесс вызывает процедуру remobj при завершении программы.
(* Программа shmcopy - главная функция *)
uses ipc,stdio,linux;
{$i share_ex.inc}

var
pid : longint;
buf1, buf2 : pdatabuf;
begin
(* Инициализация набора семафоров. *)
semid := getsem;

(* Создать и подключить участки разделяемой памяти. *)
getseg (buf1, buf2);

pid := fork;
case pid of
-1:
fatal ('fork');
0: (* дочерний процесс *)
begin
writer (semid, buf1, buf2);
remobj;
end;
else (* родительский процесс *)
reader (semid, buf1, buf2);
end;

halt (0);
end.
Программа создает объекты межпроцессного взаимодействия до вызова fork. Обратите внимание на то, что адреса, определяющие сегменты разделяемой памяти (которые находятся в переменных buf1 и buf2), будут заданы в обоих процессах.
Процедура reader принимает данные со стандартного ввода, то есть из дескриптора файла 0, и является первой функцией, представляющей интерес. Ей передается идентификатор набора семафоров в параметре semid и адреса двух участков разделяемой памяти в переменных buf1 и buf2.
{$i share_ex.inc}

(* Определения процедур p() и v() для двух семафоров *)
const
p1:tsembuf=(sem_num:0;sem_op:-1;sem_flg:0);
p2:tsembuf=(sem_num:1;sem_op:-1;sem_flg:0);
v1:tsembuf=(sem_num:0;sem_op:1;sem_flg:0);
v2:tsembuf=(sem_num:1;sem_op:1;sem_flg:0);

(* Процедура reader - выполняет чтение из файла *)
procedure reader(semid:longint;buf1,buf2:pdatabuf);
begin
while true do
begin
(* Считать в буфер buf1 *)
buf1^.d_nread := fdread (0, buf1^.d_buf, SIZ);

(* Точка синхронизации *)
semop (semid, @v1, 1);
semop (semid, @p2, 1);

(* Чтобы процедура writer не была приостановлена. *)
if buf1^.d_nread <= 0 then
exit;

buf2^.d_nread := fdread (0, buf2^.d_buf, SIZ);

semop (semid, @v2, 1);
semop (semid, @p1, 1);

if buf2^.d_nread <= 0 then
exit;
end;
end;
Структуры sembuf просто определяют операции р() и v() для набора из двух семафоров. Но на этот раз они используются не для блокировки критических участков кода, а для синхронизации процедур, выполняющих чтение и запись. Процедура reader использует операцию v2 для сообщения о том, что она завершила чтение и ожидает, вызвав semop с параметром р1, пока процедура writer сообщит о завершении записи. Это станет более очевидным при описании процедуры writer. Возможны другие подходы, включающие или четыре бинарных семафора, или семафоры, имеющие более двух значений.
Последней процедурой, вызываемой программой shmcopy, является процедура writer:
{$i share_ex.inc}

(* Процедура writer - выполняет запись *)
procedure writer(semid:longint;buf1,buf2:pdatabuf);
begin
while true do
begin
semop (semid, @p1, 1);
semop (semid, @v2, 1);

if buf1^.d_nread <= 0 then
exit;

fdwrite (1, buf1^.d_buf, buf1^.d_nread);
semop (semid, @p2, 1);
semop (semid, @v1, 1);

if buf2^.d_nread <= 0 then
exit;

fdwrite (1, buf2^.d_buf, buf2^.d_nread);
end;
end;
И снова следует обратить внимание на использование набора семафоров для согласования работы процедур reader и writer. На этот раз процедура writer использует операцию v2 для сигнализации и ждет р1. Важно также отметить, что значения buf1^.d_nread и buf2^.d_nread устанавливаются процессом, выполняющим чтение.
После компиляции можно использовать программу shmcopy при помощи подобной команды:
$ shmcopy < big > /tmp/big
Упражнение 8.6. Усовершенствуйте обработку ошибок и вывод сообщений в программе shmcopy (в особенности для вызовов fdread и fdwrite). Сделайте так, чтобы программа shmcopy принимала в качестве аргументов имена файлов в форме команды cat. Какие последствия возникнут при прерывании программы shmcopy? Можете ли вы улучшить поведение программы?
Упражнение 8.7. Придумайте систему передачи сообщений, использующую разделяемую память. Измерьте ее производительность и сравните с производительностью стандартных процедур передачи сообщений.
8.3.5. Команды ipcs и ipcrm
Существуют две команды оболочки для работы со средствами межпроцессного взаимодействия. Первая из них – команда ipcs, которая выводит информацию о текущем статусе средства межпроцессного взаимодействия. Вот простой пример ее применения:
$ ipcs

IPC status from /dev/kmem as of Wed Feb 26 18:31:31 1998
Т ID KEY MODE OWNER GROUP
Message Queues:
Shared Memory:
Semaphores
s 10 0х00000200 --ra------- keith users
Другая команда ipcrm используется для удаления средства межпроцессного взаимодействия из системы (если пользователь является его владельцем или суперпользователем), например, команда
$ ipcrm -s 0
удаляет семафор, связанный с идентификатором 0, а команда
$ ipcrm -S 200
удаляет семафор со значением ключа равным 200.
За дополнительной информацией о возможных параметрах этих команд следует обратиться к справочному руководству системы.
Глава 9. Терминал
9.1. Введение
Когда пользователь взаимодействует с программой при помощи терминала, происходит намного больше действий, чем может показаться на первый взгляд. Например, если программа выводит строку на терминальное устройство, то она вначале обрабатывается в разделе ядра, которое будем называть драйвером терминала (terminal driver). В зависимости от значения определенных флагов состояния системы строка может передаваться буквально или как-то изменяться драйвером. Одно из обычных изменений заключается в замене символов line-feed (перевод строки) или newline (новая строка) на последовательность из двух символов carriage-return (возврат каретки) и newline. Это гарантирует, что каждая строка всегда будет начинаться с левого края экрана терминала или открытого окна.
Аналогично драйвер терминала обычно позволяет пользователю редактировать ошибки в строке ввода при помощи текущих символов erase (стереть) и kill (уничтожить). Символ erase удаляет последний напечатанный символ, а символ kill – все символы до начала строки. Только после того, как вид строки устроит пользователя и он нажмет на клавишу Return (ввод), драйвер терминам передаст строку программе.
Но это еще не все. После того, как выводимая строка достигает терминала, аппаратура терминала может либо просто вывести ее на экран, либо интерпретировать ее как escape-последовательность (escape-sequence), посланную для управления экраном. В результате, например, может произойти не вывод сообщения, а очистка экрана.
На рис. 9.1 более ясно показаны различные компоненты связи между компьютером и терминалом.

Рис. 9.1. Связь между процессом UNIX и терминалом

Эта связь включает четыре элемента:
• программы (А). Программа генерирует выходные последовательности волов и интерпретирует входные. Она может взаимодействовать с терминалом при помощи системных вызовов (fdread или fdwrite), стандартной библиотеки ввода/вывода или специального библиотечного пакета, разработанного для управления экраном. Разумеется, в конечном счете весь ввод/вывод будет осуществляться при помощи вызовов fdread и fdwrite, так как высокоуровневые библиотеки могут вызывать только эти основные примитивы;
• драйвер терминала (В). Основная функция драйвера терминала заключается в передаче данных от программы к периферийному устройству и наоборот. В самом ядре UNIX терминал обычно состоит из двух основных программных компонентов – драйвера устройства (device driver) и дисциплины линии связи (line discipline). Драйвер устройства является низкоуровневым программным обеспечением, написанным для связи с определенным аппаратным обеспечением, которое позволяет компьютеру взаимодействовать с терминалом. На самом деле чаще всего драйверы устройств нужны для того, чтобы работать с разными типами аппаратного обеспечения. Над этим нижним слоем надстроены средства, которые полагаются на то, что основные свойства, поддерживаемые драйвером устройства, являются общими независимо от аппаратного обеспечения. Кроме этой основной функции передачи данных, драйвер терминала будет также выполнять некоторую логическую обработку входных и выходных данных, преобразуя одну последовательность символов в другую. Это осуществляется дисциплиной линии связи. Она также может обеспечивать множество функций для помощи конечному пользователю, таких как редактирование строки ввода. Точная обработка и преобразование данных зависят от флагов состояния, которые хранятся в дисциплине линии связи для каждого порта терминала. Они могут устанавливаться при помощи группы системных вызовов, которая будут рассмотрена в следующих разделах;
• клавиатура и экран (С и D). Эти два элемента представляют сам терминал и подчеркивают его двойственную природу. Узел (С) означает клавиатуру терминала и служит источником ввода. Узел (D) представляет экран терминала и выступает в качестве назначения вывода. Программа может получить доступ к терминалу и как к устройству ввода, и как к устройству вывода при помощи общего имени терминала, и, в конечном счете, единственного дескриптора файла. Для того чтобы это было возможно, дисциплина лини связи имеет раздельные очереди ввода и вывода для каждого терминала. Эта схема показана на рис. 9.2.

Рис. 9.2. Реализация терминала

До сих пор предполагалось, что подключенное к терминалу периферийное устройство является стандартным дисплеем. Вместе с тем периферийное устройство может быть принтером, плоттером, сетевым адаптером или даже другим компьютером. Тем не менее, независимо от природы периферийного устройства, оно может служить и в качестве источника, и в качестве назначения для входного и выходного потоков данных соответственно.
Эта глава будет в основном посвящена узлам (А) и (В) схемы. Другими словами, будет рассмотрено взаимодействие между программой и драйвером устройства на уровне системных вызовов. Не будем касаться совершенно отдельного вопроса работы с экраном, поскольку драйвер терминала не принимает участия в создании соответствующих escape-последовательностей, управляющих экраном.
Перед тем как продолжить дальше, следует сделать два предостережения. Во-первых, будут рассматриваться только «обычные» терминалы, а не графические, построенные на оконных системах Х Window System или MS Windows. Для них характерны свои проблемы, которых касаться не будем. Во-вторых, работа с терминалом в UNIX является областью, печально известной своей несовместимостью. Тем не менее спецификация XSI обеспечивает стандартный набор системных вызовов. Именно на них и сфокусируем внимание.
9.2. Терминал UNIX
Как уже упоминалось в главе 4, терминалы обозначаются файлами устройств (из-за природы терминалов они рассматриваются как символьные устройства). Вследствие этого доступ к терминалам, а точнее к портам терминалов, обычно можно получить при помощи имен файлов в каталоге dev. Типичные имена терминалов могут быть такими:
/dev/console
/dev/tty01
/dev/tty02
/dev/tty03
...
Обозначение tty является широко используемым в UNIX синонимом терминала.
Из-за универсальности понятия файла UNIX к терминалам можно получить доступ при помощи стандартных примитивов доступа к файлам, таких как fdread или fdwrite. Права доступа к файлам сохраняют свои обычные значения и поэтому управляют доступом к терминалам в системе. Чтобы эта схема работала, владелец терминала меняется при входе пользователя в систему, при этом все пользователи являются владельцами терминала, за которым они работают.
Обычно процессу не нужно явно открывать файл терминала для взаимодействия с пользователем. Это происходит из-за того, что его стандартный ввод и вывод, если они не переопределены, будут по умолчанию связаны с терминалом пользователя. Поэтому, если предположить, что стандартный вывод не назначен в файл, то следующий фрагмент кода приведет к выводу данных на экран терминала:
const FD_STDOUT=1;
.
.
.
fdwrite(FD_STDOUT, mybuffer, somesize);
В традиционном окружении UNIX терминалы, обеспечивающие вход в систему, обычно первоначально открываются при старте системы программой управления процессами init. Дескрипторы файла терминала передаются потомка программы init, и, в конечном итоге, каждый процесс пользовательской оболочки будет наследовать три дескриптора файла, связанные с терминалом пользователя. Эти дескрипторы будут представлять стандартный ввод, стандартный вывод и стандартный вывод диагностики оболочки. Они в свою очередь передаются всем запущенным из оболочки программам.
9.2.1. Управляющий терминал
При обычных обстоятельствах терминал, связанный с процессом при помощи его стандартных дескрипторов файлов, является управляющим терминалом (control terminal) этого процесса и его сеанса. Управляющий терминал является важным атрибутом процесса, который определяет обработку генерируемых с клавиатуры прерываний. Например, если пользователь нажимает текущую клавишу прерывания, то все процессы, которые считают терминал своим управляющим терминалом, получат сигнал SIGINT. Управляющие терминалы, как и другие атрибуты процесса, наследуются при вызове fork. (Более конкретно, терминал становится управляющим терминалом для сеанса, когда его открывает лидер сеанса, при условии, что терминал еще не связан с сеансом и лидер сеанса еще не имеет управляющего терминала. Вследствие этого процесс может разорвать свою связь с управляющим терминалом, изменив свой сеанс при помощи вызова setsid. Этот аспект был рассмотрен в главе 5, хотя, возможно, там это было несколько преждевременно. Теперь же следует получить понимание того, как процесс init инициализирует систему при старте.)
Если процесс должен получить доступ к своему управляющему терминалу независимо от состояния его стандартных дескрипторов файлов, то можно использовать имя файла
/dev/tty
которое всегда интерпретируется как определяющее текущий управляющий терминал процесса. Следовательно, терминал, обозначаемый в действительности этим файлом, различается для разных процессов.
9.2.2. Передача данных
Основной задачей драйвера терминала является передача данных между процессом и его терминальным устройством. На самом деле это достаточно сложное требование, так как пользователь может печатать символы в любое время, даже во время вывода. Чтобы лучше понять эту ситуацию, вернемся к рис. 9.1 и представим, что по путям от (С) к (В) и от (В) к (D) одновременно передаются данные. Напомним, что программа, представленная на схеме узлом (А), может выполнять только последовательные вызовы fdread или fdwrite.
Поскольку в каждый момент времени программа может обрабатывать только один поток данных терминала, то для одновременного управления двумя потоками дисциплина линии связи запоминает входные и выходные данные во внутренних буферах. Входные данные передаются программе пользователя, когда он выполняет вызов fdread. Входные символы могут быть потеряны при переполнении поддерживаемых ядром буферов или если число символов, поступивших на терминал, превысит ограничение MAX_INPUT, определенное в файле stdio. Это предельное значение обычно равно 255, что достаточно велико для того, чтобы потери данных при обычном использовании были достаточно редки. Тем не менее не существует способа определить, что данные были потеряны, так как система просто молча отбрасывает лишние символы.
Ситуация с выводом несколько проще. Каждый вызов fdwrite для терминала помещает символы в очередь вывода. Если очередь переполняется, то следующий вызов fdwrite будет «заблокирован» (то есть процесс будет приостановлен) до тех пор, пока очередь вывода не уменьшится до нужного уровня.
9.2.3. Эхо-отображение вводимых символов и опережающий ввод с клавиатуры
Поскольку терминалы используются для взаимодействия между людьми и компьютерными программами, драйвер терминала UNIX поддерживает множество дополнительных средств, облегчающих жизнь пользователям.
Возможно, самым элементарным из этих дополнительных средств является «эхо», то есть отображение вводимых с клавиатуры символов на экране. Оно позволяет увидеть на экране символ «А», когда вы печатаете «А» на клавиатуре. Подключенные к системам UNIX терминалы обычно работают в полнодуплексном (full-duplex) режиме; это означает, что за эхо-отображение символов на экране отвечает система UNIX, а не терминал. Следовательно, при наборе символа он вначале передается терминалом системе UNIX. После его получения дисциплина линии связи сразу же помещает его копию в очередь вывода терминала. Затем символ выводится на экран терминала. Если вернуться к рис. 9.1, то увидим, что символ при этом пересылается по пути от (С) к (В), а затем сразу же направляется по пути от (В) к (D). Все может произойти до того, как программа (А) успеет считать символ. Это приводит к интересному явлению, когда символы, вводимые с клавиатуры в то время, когда программа осуществляет вывод на экран, отображаются в середине вывода. В операционных системах некоторых семейств (не UNIX) отображение вводимых символов на экране может подавляться до тех пор, пока программа не сможет прочитать их.
9.2.4. Канонический режим, редактирование строки и специальные символы
Терминал может быть настроен на самые разные режимы в соответствии с типом работающих с ним программ; работа этих режимов поддерживается соответствующей дисциплиной линии связи. Например, экранному редактору может понадобиться максимальная свобода действий, поэтому он может перевести терминал в режим прямого доступа (raw mode), при котором дисциплина линии связи просто передает символы программе по мере их поступления, без всякой обработки.
Вместе с тем ОС UNIX не была бы универсальной программной средой, если бы всем программам приходилось бы иметь дело с деталями управления терминалом. Поэтому стандартная дисциплина линии связи обеспечивает также режим работы, специально приспособленный для простого интерактивного использования на основе построчного ввода. Этот режим называется каноническим режим терминала (canonical mode) и используется командным интерпретатором, редактором ed и аналогичными программами.
В каноническом режиме драйвер терминала выполняет специальные действия при нажатии специальных клавиш. Многие из этих действий относятся к редактированию строки. Вследствие этого, если терминал находится в каноническом режиме, то ввод в программе осуществляется целыми строками (подробнее об этом ниже).
Наиболее часто используемой в каноническом режиме клавишей редактирования является клавиша erase, нажатие которой приводит к стиранию предыдущего символа в строке. Например, следующий клавиатурный ввод
$ whpo
за которым следует перевод строки, приведет к тому, что драйвер терминала передаст командному интерпретатору строку who. Если терминал настроен правильно, то символ р должен быть стерт с экрана.
В качестве символа erase пользователем может быть задано любое значение ASCII. Наиболее часто для этих целей используется символ ASCII backspace (возврат).
На уровне оболочки проще всего поменять этот символ при помощи команды stty, например
$ stty erase "^h"
задает в качестве символа erase комбинацию Ctrl+H, которая является еще одним именем для символа возврата. Обратите внимание, что зачастую можно набрать строку "^h", а не нажимать саму комбинацию клавиш Ctrl+H.
Ниже следует систематическое описание других символов, определенных в спецификации XSI и имеющих особое значение для оболочки в каноническом режиме. Если не указано иначе, их точные значения могут быть установлены пользователем или администратором системы.
kill
Приводит к стиранию всех символов до начала строки. Поэтому входная последовательность
$ echo < kill > who
за которой следует новая строка, приводит к выполнению команды who. По умолчанию значение символа kill равно Ctrl+?, часто также используются комбинации клавиш Ctrl+X и Ctrl+U. Можно поменять символ kill при помощи команды:
$ stty kill <новый_символ>
intr
Символ прерывания. Если пользователь набирает его, то программе, выполняющей чтение с терминала, а также всем другим процессам, которые считают терминал своим управляющим терминалом, посылается сигнал SIGINT. Такие программы, как командный интерпретатор, перехватывают этот сигнал, поскольку по умолчанию сигнал SIGINT приводит к завершению программы. Одно из значений для символа intr по умолчанию равно символу ASCII delete (удалить), который часто называется DEL. Вместо него может также использоваться символ Ctrl+C. Для изменения текущего значения символа intr на уровне оболочки используйте команду:
$ stty intr <новый_символ>
quit
Обратитесь к главе 6 за описанием обработки сигналов quit.
Если пользователь набирает этот символ, то группе процессов, связанной с терминалом, посылается сигнал SIGQUIT. Командный интерпретатор перехватывает этот сигнал, остальные пользовательские процессы также имеют возможность перехватить и обработать этот сигнал. Как уже описывалось в главе 6, действием этого сигнала по умолчанию является сброс образа памяти процесса на диск и аварийное завершение, сопровождаемое выводом сообщения «Quit – core dumped». Обычно символ quit – это символ ASCII FS, или Ctrl+\. Его можно изменить, выполнив команду:
$ stty quit <новый_символ>
eof
Этот символ используется для обозначения окончания входного потока с терминала (для этого он должен быть единственным символом в начале новой строки). Стандартным начальным значением для него является символ ASCII eof, известный также как Ctrl+D. Его можно изменить, выполнив команду:
$ stty eof <новый_символ>
nl
Это обычный разделитель строк. Он всегда имеет значение ASCII символа line-feed (перевод строки), который соответствует символу newline (новая строка) в языке С. Он не может быть изменен или установлен пользователем. На терминалах, которые посылают вместо символа line-feed символ carriage-return (возврат каретки), дисциплина линии связи может быть настроена так, чтобы преобразовывать возврат каретки в перевод строки
еоl
Еще один разделитель строк, который действует так же, как и nl. Обычно не используется и поэтому имеет по умолчанию значение символа ASCII NULL
stop
Обычно имеет значение Ctrl+S и в некоторых реализациях может быть изменен пользователем. Используется для временной приостановки записи вывода на терминал. Этот символ особенно полезен при применении старомодных терминалов, так как он может использоваться для того, чтобы приостановить вывод прежде, чем он исчезнет за границей экрана терминала
start
Обычно имеет значение Ctrl+Q. Может ли он быть изменен, также зависит от конкретной реализации. Он используется для продолжения вывода, приостановленного при помощи комбинации клавиш Ctrl+S. Если комбинация Ctrl+S не была нажата, то Ctrl+Q игнорируется
susp
Если пользователь набирает этот символ, то группе процессов, связанной с терминалом, посылается сигнал SIGTSTP. При этом выполнение группы приоритетных процессов останавливается, и она переводится в фоновый режим. Обычное значение символа suspend равно Ctrl+Z. Его также можно изменить, выполнив команду:
$ stty susp <новый_символ>
Специальное значение символов erase, kill, и eof можно отменить, набрав перед ними символ обратной косой черты (\). При этом связанная с символом функция не выполняется, и символ посылается программе без изменений. Например, строка
aa\ b с
приведет к тому, что программе, выполняющей чтение с терминала, будет передана строка аа\ с.
9.3. Взгляд с точки зрения программы
До сих пор изучались функции драйвера терминала, относящиеся к пользовательскому интерфейсу. Теперь же мы рассмотрим их с точки зрения программы, использующей терминал.
9.3.1. Системный вызов fdopen
Вызов open может использоваться для открытия дисциплины линии связи терминала так же, как и для открытия обычного дискового файла, например:
fd := fdopen('/dev/tty0a', Open_RDWR);
Однако при попытке открыть терминал возврата из вызова не произойдет до тех пор, пока не будет установлено соединение. Для терминалов с модемным управлением это означает, что возврат из вызова не произойдет до тех пор, пока не будут установлены сигналы управления модемом и не получен сигнал «детектирования несущей», что может потребовать значительного времени либо вообще не произойти.
Следующая процедура использует вызов alarm (представленный в главе 6) для задания интервала ожидания, если возврат из вызова fdopen не произойдет за заданное время:
(* Процедура ttyopen - вызов fdopen с интервалом ожидания *)
uses stdio,linux;

const
TIMEOUT=10;
timeout_flag:boolean=FALSE;
termname:pchar='';

procedure settimeout(value:longint);cdecl;
begin
writeln(stderr, 'Превышено время ожидания ', termname);
timeout_flag := TRUE;
end;

function ttyopen(filename:pchar; flags:longint):longint;
var
fd:longint;
act, oact:sigactionrec;
mask:sigset_t;
begin
fd := -1;

termname := filename;

(* Установить флаг таймаута *)
timeout_flag := FALSE;

(* Установить обработчик сигнала SIGALRM *)
act.handler.sh := @settimeout;
sigfillset(@mask);
act.sa_mask:=mask.__val[0];
sigaction(SIGALRM, @act, @oact);

alarm(TIMEOUT);

fd := fdopen(filename, flags);

(* Сброс установок *)
alarm(0);
sigaction(SIGALRM, @oact, @act);
if timeout_flag then
ttyopen:=-1
else
ttyopen:=0;
end;
9.3.2. Системный вызов fdread
При использовании файла терминального устройства вместо обычного дискового файла изменения больше всего затрагивают работу системного вызова read. Это особенно проявляется, если терминал находится в каноническом режиме, устанавливаемом по умолчанию для обычного интерактивного использования. В этом режиме основной единицей ввода становится строка. Следовательно, программа не может считать символы из строки, пока пользователь не нажмет на клавишу Return, которая интерпретируется системой как переход на новую строку. Важно также, что после ввода новой строки всегда происходит возврат из вызова fdread, даже если число символов в строке меньше, чем число символов, запрошенное вызовом fdread. Если вводится только Return и системе посылается пустая строка, то соответствующий вызов fdread вернет значение 1, так как сам символ новой строки тоже доступен программе. Поэтому нулевое значение, как и обычно, может использоваться для определения конца файла (то есть ввода символа еof).
Использование вызова fdread для чтения из терминала в программе io уже рассматривалось в главе 2. Тем не менее эта тема нуждается в более подробном объяснении, поэтому рассмотрим следующий вызов:
nread := fdread(0, buffer, 256);
При извлечении стандартного ввода процесса из обычного файла вызов интерпретируется просто: если в файле более 256 символов, то вызов fdread вернет в точности 256 символов в массиве buffer. Чтение из терминала происходит в несколько этапов – чтобы продемонстрировать это, обратимся к рис. 9.3, который показывает взаимодействие между программой и пользователем в процессе вызова fdread.
Эта схема иллюстрирует возможную последовательность действий, инициированных чтением с терминала с помощью вызова read. Для каждого шага изображены два прямоугольника. Верхний из них показывает текущее состояние строки ввода на уровне драйвера терминала; нижний, обозначенный как буфер чтения, показывает данные, доступные для чтения процессом в настоящий момент. Необходимо подчеркнуть, что схема показывает только логику работы с точки зрения пользовательского процесса. Кроме того, обычная реализация драйвера терминала использует не один, а два буфера (очереди), что, впрочем, не намного усложняет представленную схему.
1
Строка ввода →
echo
(Вызов read)

Буфер чтения →

2

echo q

3

echo hello

4

echo hello
↓ (Данные перемещаются в буфер чтения)

5

(Возврат из вызова read)

echo hello

6

(Теперь буфер пуст)

Рис. 9.3. Этапы чтения из терминала в каноническом режиме

Шаг 1 представляет ситуацию в момент выполнения программой вызова read. В этот момент пользователь уже напечатал строку echo, но поскольку строка еще не завершена символом перевода строки, то в буфере чтения нет данных, и выполнение процесса приостановлено.
На шаге 2 пользователь напечатал q, затем передумал и набрал символ erase для удаления символа из строки ввода. Эта часть схемы подчеркивает, что редактирование строки ввода может выполняться, не затрагивая программу, выполняющую вызов read.
На шаге 3 строка ввода завершена, только еще не содержит символ перехода на новую строку. Часть схемы, обозначенная как шаг 4, показывает состояние в момент ввода символа перехода на новую строку, при этом драйвер терминала передает строку ввода, включая символ перевода строки, в буфер чтения. Это приводит к шагу 5, на котором вся строка ввода становится доступной для чтения. В процессе, выполнившем вызов read, происходит возврат из этого вызова, при этом число введенных символов nread равно 11. Шаг 6 показывает ситуацию сразу же после такого завершения вызова, при этом и строка ввода, и буфер чтения снова временно пусты.
Следующий пример подкрепляет эти рассуждения. Он основан на простой программе read_demo, которая имеет лишь одну особенность: она использует небольшой размер буфера для приема байтов из стандартного ввода.
(* Программа read_demo - вызов fdread для терминала *)
uses linux;
const
SMALLSZ=10;

var
nread:longint;
smallbuf:array [0..SMALLSZ] of char;
begin
nread := fdread (0, smallbuf, SMALLSZ);
while nread > 0 do
begin
smallbuf[nread] := #0;
writeln('nread: ',nread,' ', smallbuf);
nread := fdread (0, smallbuf, SMALLSZ);
end;
end.
Если подать на вход программы следующий терминальный ввод
1
1234
Это более длинная строка

то получится такой диалог:
1
nread: 2 1
1234
nread: 5 1234
Это более длинная строка
nread: 10 Это более
nread: 10 длинная с
nread: 6 трока
Обратите внимание, что для чтения самой длинной строки требуется несколько последовательных операций чтения. Заметим также, что значения счетчика nread включают также символ перехода на новую строку. Это не показано для простоты изложения.
Что происходит, если терминал не находится в каноническом режиме? В таком случае для полного управления процессом ввода программа должна устанавливать дополнительные параметры состояния терминала. Это осуществляется при помощи семейства системных вызовов, которые будут описаны позже.
Упражнение 9.1. Попробуйте запустить программу read_demo, перенаправив ее ввод на чтение файла.
9.3.3. Системный вызов fdwrite
Этот вызов намного проще в отношении взаимодействия с терминалом. Единственный важный момент заключается в том, что вызов fdwrite будет блокироваться при переполнении очереди вывода терминала. Программа продолжит работу, только когда число символов в очереди станет меньше некоторого заданного порогового уровня.
9.3.4. Функции ttyname и isatty
Теперь представим две полезных функции, которые будем использовать в следующих примерах. Функция ttyname возвращает имя терминального устройства, связанного с дескриптором открытого файла, а функция isatty возвращает значение true (то есть истинно в терминах языка Паскаль), если дескриптор файла описывает терминальное устройство, и false (ложно) – в противном случае.
Описание
uses linux;

Function TTYName(var f):String;

Function IsATTY(var f):Boolean;
В обоих случаях параметр f является дескриптором открытого файла либо файловой переменной. Если f не соответствует терминалу, то функция ttyname вернет пустую строку.
Следующий пример – процедура what_tty выводит имя терминала, связанного с дескриптором файла, если это возможно:
(* Процедура what_tty - выводит имя терминала *)

procedure what_tty(fd:longint);
begin
if isatty(fd) then
writeln('fd ',fd,' =>> ', ttyname(fd));
else
writeln ('fd ',fd, ' не является терминалом!');
end;
Упражнение 9.2. Измените процедуру ttyopen предыдущего раздела так, чтобы она возвращала дескриптор файла только для терминалов, а не для дисковых файлов или других типов файлов. Для выполнения проверки используйте функцию isatty. Существуют ли еще какие-либо способы сделать это?
9.3.5. Изменение свойств терминала: структура termios
На уровне оболочки пользователь может вызвать команду stty для изменения свойств дисциплины линии связи терминала. Программа может сделать практически то же самое, используя структуру termios вместе с соответствующими функциями. Обратите внимание, что в более старых системах для этого использовался системный вызов ioctl (сокращение от I/O control – управление вводом/выводом), его применение было описано в первом издании этой книги. Вызов ioctl предназначен для более общих целей и теперь разделен на несколько конкретных вызовов. Совокупность этих вызовов обеспечивает общий программный интерфейс ко всем асинхронным портам связи, независимо от свойств их оборудования.
Структуру termios можно представлять себе как объект, способный описать общее состояние терминала в соответствии с набором флагов, поддерживаемым системой для любого терминального устройства. Точное определение структуры termios будет вскоре рассмотрено. Структуры termios могут заполняться текущими установками терминала при помощи вызова tcgetattr, определенного следующим образом:
Описание
uses linux;

Function TCGetAttr(ttyfd:longint; var tsaved:TermIOS):Boolean;
Эта функция сохраняет текущее состояние терминала, связанного с дескриптором файла ttyfd в структуре tsaved типа termios. Параметр ttyfd должен быть дескриптором файла, описывающим терминал.
Описание
uses linux;

Function TCSetAttr(ttyfd:longint; actions:longint; var tnew:TermIOS):
Boolean;
Вызов tcsetattr установит новое состояние дисциплины связи, заданное структурой tnew. Второй параметр вызова tcsetattr, переменная actions, определяет, как и когда будут установлены новые атрибуты терминала. Существует три возможных варианта, определенных в файле linux:
ТСSANOW
Немедленное выполнение изменений, что может вызвать проблемы, если в момент изменения флагов драйвер терминала выполняет вывод на терминал
TCSADRAIN
Выполняет ту же функцию, что и TCSANOW, но перед установкой новых параметров ждет опустошения очереди вывода
TCSAFLUSH
Аналогично TCSADRAIN ждет, пока очередь вывода не опустеет, а затем также очищает и очередь ввода перед установкой для параметров дисциплины линии связи значений, заданных в структуре tnew
Следующие две функции используют описанные вызовы. Функция tsave сохраняет текущие параметры, связанные с управляющим терминалом процесса, а функция tback восстанавливает последний набор сохраненных параметров. Флаг saved используется для предотвращения восстановления установок функцией tback, если перед этим не была использована функция tsave.
(* Структура tsaved будет содержать параметры терминала *)
var
tsaved:termios;

(* Равно TRUE если параметры сохранены *)
const
saved:boolean=false;

function tsave:boolean;
begin
if isatty(0) and tcgetattr(0,tsaved) then
begin
saved := true;
tsave := true;
exit;
end;
tsave := false;
end;

function tback:boolean; (* Восстанавливает состояние терминала *)
begin
if not isatty(0) or not saved then
tback:=false
else
tback:=tcsetattr(0, TCSAFLUSH, tsaved);
end;
Между этими двумя процедурами может быть заключен участок кода, который временно изменяет состояние терминала, например:
uses linux;

begin
if not tsave then
begin
writeln(stderr, 'Невозможно сохранить параметры терминала');
halt(1);
end;
(* Интересующий нас участок *)

tback;
halt(0);
end.
Определение структуры termios
Теперь изучим состав структуры termios. Определение структуры termios находится в файле linux и содержит следующие элементы:
termios = record
c_iflag, (* Режимы ввода *)
c_oflag, (* Режимы вывода *)
c_cflag, (* Управляющие режимы *)
c_lflag : Cardinal; (* Режимы дисциплины линии связи *)
c_line : char; (* Дисциплина линии связи *)
c_cc : array [0..NCCS-1] of char; (* Управляющие символы *)
end;
Проще всего рассматривать структуру, начав с ее последнего элемента с_сс.
Массив с_сс
Символы редактирования строки, которые рассматривались в разделе 9.2.4, находятся в массиве с_сс. Их позиции в этом массиве задаются константами, определенными в файле stdio. Все определенные в спецификации ХSI значения приведены в табл. 9.1. Размер массива определяется константой NCCS, определенной в файле linux.
Следующий фрагмент программы показывает, как можно изменить значение символа quit для терминала, связанного со стандартным вводом (дескриптор файла со значением 0):
var
tdes:termios;

(* Получить исходные настройки терминала *)
tcgetattr(0, tdes);
tdes.c_cc[VQUIT] := char(octal(031)); (* CTRL-Y *)
(* Изменить установки терминала *)
tcsetattr(0, TCSAFLUSH, tdes);
Таблица 9.1. Коды управляющих символов
Константа
Значение
VINTR
Клавиша прерывания (Interrupt key)
VQUIT
Клавиша завершения (Quit key)
VERASE
Символ стирания (Erase character)
VKILL
Символ удаления строки (Kill character)
VEOF
Символ конца файла (EOF character)
VEOL
Символ конца строки (End of line marker – необязательный)
VSTART
Символ продолжения передачи данных (Start character)
VSTOP
Символ остановки передачи данных (Stop character)
VSUSP
Символ временной приостановки выполнения (Suspend character)
Этот пример иллюстрирует наиболее безопасный способ изменения состояния терминала. Сначала нужно получить текущее состояние терминала. Далее следует изменить только нужные параметры, не трогая остальные. И, наконец, изменить состояние терминала при помощи модифицированной структуры termios. Как уже было упомянуто, сохранять исходные значения полезно также для восстановления состояния терминала перед завершением программы, иначе нестандартное состояние терминала может оказаться сюрпризом для остальных программ.
Поле c_cflag
Поле c_cflag содержит параметры, управляющие состоянием порта терминала. Обычно процессу не нужно изменять значения поля c_cflag своего управляющего терминала. Изменения этого поля могут понадобиться специальным коммуникационным приложениям, или программам, открывающим коммуникационные линии для дополнительного оборудования, например, для принтера. Значения поля c_cflag образуются объединением при помощи операции ИЛИ битовых констант, определенных в файле stdio. В общем случае каждая константа представляет бит поля c_cflag, который может быть установлен или сброшен. Не будем объяснять назначение всех битов этого поля (за полным описанием обратитесь к справочному руководству системы). Тем не менее есть четыре функции, которые позволяют опрашивать и устанавливать скорость ввода и вывода, закодированную в этом поле, не беспокоясь о правилах манипулирования битами.
Описание
uses linux;

(* Установить скорость ввода *)
Procedure CFSetISpeed(var tdes:TermIOS; Speed:Longint);

(* Установить скорость вывода *)
Procedure CFSetOSpeed(var tdes:TermIOS; Speed:Longint);

uses stdio;

(* Получить скорость ввода *)
function cfgetispeed(var tdes:TermIOS):longint;

(* Получить скорость вывода *)
function cfgetospeed(var tdes:TermIOS):longint;
Следующий пример устанавливает в структуре tdes значение скорости терминала равное 9600 бод. Постоянная В9600 определена в файле stdio.
var
tdes:termios;

(* Получает исходные настройки терминала * )
tcgetattr(0, tdes);

(* Изменяет скорость ввода и вывода *)
cfsetispeed(tdes, В9600);
cfsetospeed(tdes, В9600);
Конечно, эти изменения не будут иметь эффекта, пока не будет выполнен вызов tcsetattr:
tcsetattr(0, TCSAFLUSH, tdes);
Следующий пример устанавливает режим контроля четности, напрямую устанавливая необходимые биты:
tdes.c_cflag := tdes.c_cflag or PARENB or PARODD;
tcsetattr(0, TCSAFLUSH, tdes);
В этом примере установка флага PARENB включает проверку четности. Установленный флаг PARODD сообщает, что ожидаемый контроль – контроль нечетности. Если флаг PARODD сброшен и установлен флаг PARENB, то предполагается, что используется контроль по четности. (Термин четность, parity, относится к использованию битов проверки при передаче данных. Для каждого символа задается один такой бит. Это возможно благодаря тому, что набор символов ASCII занимает только семь бит из восьми, используемых для хранения символа на большинстве компьютеров. Значение бита проверки может использоваться для того, чтобы полное число битов в байте было либо четным, либо нечетным. Программист также может полностью выключить проверку четности.)
Поле c_iflag
Поле c_iflag в структуре termios служит для общего управления вводом с терминала. Не будем рассматривать все возможные установки, а выберем из них только наиболее общие.
Три из связанных с этим полем флага связаны с обработкой символа возврата каретки. Они могут быть полезны в терминалах, которые посылают для обозначения конца строки последовательность, включающую символ возврата каретки CR. ОС UNIX, конечно же, ожидает в качестве символа конца строки символ LF (line feed) или символ перевода строки ASCII, символ NL (new line). Следующие флаги могут исправить ситуацию:
INLCR Преобразовывать символ новой строки в возврат каретки
IGNCR Игнорировать возврат каретки
ICRNL Преобразовывать возврат каретки в символ новой строки
Три других поля c_iflag связаны с управлением потоком данных:
IXON Разрешить старт/стопное управление выводом
IXANY Продолжать вывод при нажатии любого символа
IXOFF Разрешить старт/стопное управление вводом
Флаг IXON дает пользователю возможность управления выводом. Если этот флаг установлен, то пользователь может прервать вывод, нажав комбинацию клавиш Ctrl+S. Вывод продолжится после нажатия комбинации Ctrl+Q. Если также установлен параметр IXANY, то для возобновления приостановленного вывода достаточно нажатия любой клавиши, хотя для остановки вывода должна быть нажата именно комбинация клавиш Ctrl+S. Если установлен флаг IXOFF, то система сама пошлет терминалу символ остановки (как обычно, Ctrl+S), когда буфер ввода будет почти заполнен. После того как система будет снова готова к приему данных, для продолжения ввода будет послана комбинация символов Ctrl+Q.
Поле c_oflag
Поле с_оflag позволяет управлять режимом вывода. Наиболее важным флатом в этом поле является флаг OPOST. Если он не установлен, то выводимые символы передаются без изменений. В противном случае символы подвергаются обработке, заданной остальными флагами, устанавливаемыми в поле c_oflag. Некоторые из них вызывают подстановку символа возврата каретки (CR) при выводе на терминал:
ONLCR
Преобразовать символ возврата каретки (CR) в символ возврата каретки (CR) и символ перевода строки (NL)
OCRNL
Преобразовать символ возврата каретки (CR) в символ перевода строки (NL)
ONOCR
Не выводить символ возврата каретки (CR) в нулевом столбце
ONLRET
Символ перевода строки (NL) выполняет функцию символа возврата каретки (CR)
Если установлен флаг ONLCR, то символы перевода строки NL преобразуются в последовательность CR+NL (символ возврата каретки и символ перевода строки). Это гарантирует, что каждая строка будет начинаться с левого края экрана. И наоборот, если установлен флаг OCRNL, то символ возврат каретки будет преобразовываться в символ перевода строки. Установка флага ONLRET сообщает драйверу терминала, что для используемого терминала символы перевода строки будут автоматически выполнять и возврат каретки. Если установлен флаг ONOCR, то символ возврата каретки не будет посылаться при выводе строки нулевой длины.
Большинство остальных флагов поля c_oflag относятся к задержкам в передаче, связанным с интерпретацией специальных символов, таких как перевод строки, табуляция, перевод страницы и др. Эти задержки учитывают время позиционирования указателя знакоместа, где должен быть выведен следующий символ на экране или принтере. Подробное описание этих флагов должно содержать справочное руководство системы.
Поле с_lflag
Возможно, наиболее интересным элементом структуры termios для программиста является поле c_lflag. Оно используется текущей дисциплиной линии связи для управления функциями терминала. Это поле содержит следующие флаги:
ICANON
Канонический построчный ввод
ISIG
Разрешить обработку прерываний
IEXTEN
Разрешить дополнительную (зависящую от реализации) обработку вводимых символов
ECHO
Разрешить отображение вводимых символов на экране
ЕСНОЕ
Отображать символ удаления как возврат-пробел-возврат
ЕСНОК
Отображать новую строку после удаления строки
ECHONL
Отображать перевод строки
NOFLSH
Отменить очистку буфера ввода после прерывания
TOSTOP
Посылать сигнал SIGTTOU при попытке вывода фонового процесса
Если установлен флаг ICANON, то включается канонический режим работы терминала. Как уже было видно выше, это позволяет использовать символы редактирования строки в процессе построчного ввода. Если флаг ICANON не установлен, то терминал находится в режиме прямого доступа (raw mode), который чаще всего используется полноэкранными программами и коммуникационными пакетами. Вызовы read будут при этом получать данные непосредственно из очереди ввода. Другими словами, основной единицей ввода будет одиночный символ, а не логическая строка. Программа при этом может считывать данные по одному символу (что обязательно для экранных редакторов) или большими блоками фиксированного размера (что удобно для коммуникационных программ). Но для того чтобы полностью управлять поведением вызова fdread, программист должен задать еще два дополнительных параметра. Это параметр VMIN, наименьшее число символов, которые должны быть приняты до возврата из вызова read, и параметр VTIME, максимальное время ожидания для вызова fdread. Оба параметра записываются в массиве с_сс. Это важная тема, которая будет подробно изучена в следующем разделе. А пока просто обратим внимание на то, как в следующем примере сбрасывается флаг ICANON.
uses stdio, linux;

var
tdes:termios;
.
.
.
tcgetattr(0, tdes);

tdes.c_lflag := tdes.c_lflag and (not ICANON);

tcsetattr(0, TCSAFLUSH, tdes);
Если установлен флаг ISIG, то разрешается обработка клавиш прерывания (intr) и аварийного завершения (quit). Обычно это позволяет пользователю завершить выполнение программы. Если флаг ISIG не установлен, то проверка не выполняется, и символы intr и quit передаются программе без изменений.
Если установлен флаг ECHO, то символы будут отображаться на экране по мере их набора. Сброс этого флага полезен для процедур проверки паролей и программ, которые используют клавиатуру для особых функций, например, для перемещения курсора или команд экранного редактора.
Если одновременно установлены флаги ЕСНОЕ и ECHO, то символ удаления будет отображаться как последовательность символов backspace-space-backspace (возврат–пробел–возврат). При этом последний символ на терминале немедленно стирается с экрана, и пользователь видит, что символ действительно был удален. Если флаг ЕСНОЕ установлен, а флаг ECHO нет, то символ удаления будет отображаться как space-backspace, тогда при его вводе будет удаляться символ в позиции курсора алфавитно-цифрового терминала.
Если установлен флаг ECHONL, то перевод строки будет всегда отображать на экране, даже если отображение символов отключено, что может быть полезным при выполнении самим терминалом локального отображения вводима символов. Такой режим часто называется полудуплексным режимом (half-duplex mode).
Последним флагом, заслуживающим внимания в этой группе флагов, является флаг NOFLSH, который подавляет обычную очистку очередей ввода и вывод при нажатии клавиш intr и quit и очистку очереди ввода при нажатии клавиши susp.
Альтернативой TCGetAttr может быть вызов IOCtl:
uses Linux;

var
tios : Termios;
begin
IOCtl(1,TCGETS,@tios);
WriteLn('Input Flags : $',hexstr(tios.c_iflag,8));
WriteLn('Output Flags : $',hexstr(tios.c_oflag,8));
WriteLn('Line Flags : $',hexstr(tios.c_lflag,8));
WriteLn('Control Flags: $',hexstr(tios.c_cflag,8));
end.
Для удобства изменения параметров терминала в файле linux определена функция CFMakeRaw:
Описание
uses linux;

Procedure CFMakeRaw(var Tios:TermIOS);
CFMakeRaw устанавливает флаги в структуре Termios в состояние, соответствующее переводу терминала в неканонический режим. Пример:
uses Linux;

procedure ShowTermios(var tios:Termios);
begin
WriteLn('Input Flags : $',hexstr(tios.c_iflag,8)+#13);
WriteLn('Output Flags : $',hexstr(tios.c_oflag,8));
WriteLn('Line Flags : $',hexstr(tios.c_lflag,8));
WriteLn('Control Flags: $',hexstr(tios.c_cflag,8));
end;

var
oldios,
tios : Termios;
begin
WriteLn('Old attributes:');
TCGetAttr(1,tios);
ShowTermios(tios);
oldios:=tios;
Writeln('Setting raw terminal mode');
CFMakeRaw(tios);
TCSetAttr(1,TCSANOW,tios);
WriteLn('Current attributes:');
TCGetAttr(1,tios);
ShowTermios(tios);
TCSetAttr(1,TCSANOW,oldios);
end.
Упражнение 9.3. Напишите программу ttystate, которая выводит текущее состояние терминала, связанного со стандартным вводом. Эта программа должна использовать имена констант, описанные в этом разделе (ICANON, ЕСНОЕ, и т.д.). Найдите в справочном руководстве системы полный список этих имен.
Упражнение 9.4. Напишите программу ttyset, которая распознает выходной формат программы ttystate и настраивает терминал, связанный с ее стандартным выводом в соответствии с описанным состоянием. Есть ли какая-то польза от программ ttystate и ttyset, вместе или по отдельности?
9.3.6. Параметры MIN и TIME
Параметры MIN и TIME имеют значение только при выключенном флаге ICANON. Они предназначены для тонкой настройки управления вводом данных. Параметр MIN задает минимальное число символов, которое должен получить драйвер терминала для возврата из вызова fdread. Параметр TIME задает значение максимального интервала ожидания; этот параметр обеспечивает еще один уровень управления вводом с терминала. Время ожидания измеряется десятыми долями секунды.
Значения параметров MIN и МАХ находятся в массиве с_сс структуры termios, описывающей состояние терминала. Их индексы в массиве определяются постоянными VMIN и VTIME из файла stdio. Следующий фрагмент программы показывает, как можно задать их значения:
uses stdio, linux;

var
tdes:termios;
ttyfd:longint;

(* Получает текущее состояние *)
tcgetattr(ttyfd, tdes);

tdes.c_lflag := tdes.c_lflag and (not ICANON); (* Отключает канонический режим *)
tdes.c_cc[VMIN] := 64; (* В символах *)
tdes.c_cc[VTIME] := 2; (* В десятых долях секунды *)

tcsetattr(0, TCSAFLUSH, &tdes);
Константы VMIN и VTIME обычно имеют те же самые значения, что и постоянные VEOF и VEOL. Это означает, что параметры MIN и TIME занимают то же положение, что и символы eof и еоl. Следовательно, при переключении из канонического в неканонический режим нужно обязательно задавать значения параметров MIN и TIME, иначе может наблюдаться странное поведение терминала. (В частности, если символу eof соответствует комбинация клавиш Ctrl+D, то программа будет читать ввод блоками по четыре символа.) Аналогичная опасность возникает при возврате в канонический режим.
Существуют четыре возможных комбинации параметров MIN и TIME:
• оба параметра MIN и TIME равны нулю. При этом возврат из вызова fdread обычно происходит немедленно. Если в очереди ввода терминала присутствуют символы (напомним, что попытка ввода может быть осуществлена в любой момент времени), то они будут помещены в буфер процесса. Поэтому, если программа переводит свой управляющий терминал в режим прямого доступа при помощи сброса флага ICANON и оба параметра MIN и TIME равны нулю, то вызов
nread := fdread(0, buffer, SOMESZ);
вернет произвольное число символов от нуля до SOMESZ в зависимости от того, сколько символов находится в очереди в момент выполнения вызова;
• параметр MIN больше нуля, а параметр TIME равен нулю. В этом случае таймер не используется. Вызов fdread завершится только после того, как будут считаны MIN символов. Это происходит даже в том случае, если вызов read запрашивал меньше, чем MIN символов.
В самом простом варианте параметр MIN равен единице, а параметр TIME – нулю, что приводит к возврату из вызова fdread после получения каждого символа из линии терминала. Это может быть полезно при чтении с клавиатуры терминала, хотя могут возникнуть проблемы с клавишами, посылающими последовательности из нескольких символов;
• параметр MIN равен нулю, а параметр TIME больше нуля. В этом случае параметр MIN не используется. Таймер запускается сразу же после выполнения вызова fdread. Возврат из вызова read происходит, как только вводится первый символ. Если заданный интервал времени истекает (то есть проход время, заданное в параметре TIME в десятых долях секунды), то вызов read возвращает нулевые символы;
• оба параметра MIN и TIME больше нуля. Это, возможно, наиболее полезный и гибкий вариант. Таймер запускается после получения первого символа, а не при входе в вызов fdread. Если MIN символов будут получены до истечения заданного интервала времени, то происходит возврат из вызова fdread. Если таймер срабатывает раньше, то в программу пользователя возвращаются только символы, находящиеся при этом в очереди ввода. Этот режим работы полезен при поступлении ввода пакетами, которые посылаются в течение коротких интервалов времени. При этом упрощается программирование и уменьшается число необходимых системных вызовов. Такой режим также полезен, например, при работе с функциональными клавишами, которые посылают при нажатии на них сразу несколько символов.
9.3.7. Другие системные вызовы для работы с терминалом
Есть несколько дополнительных системных вызовов для работы с терминалом, позволяющих программисту до некоторой степени управлять очередями ввода и вывода, поддерживаемыми драйвером терминала. Эти вызовы определены следующим образом.
Описание
uses linux;

Function TCFlush(ttyfd, queue:longint):Boolean;

Function TCDrain(ttyfd:longint):Boolean;

Function TCFlow(ttyfd, actions:longint):Boolean;

Function TCSendBreak(ttyfd, duration:longint):longint;

Function TCGetPGrp(Fd:longint;var Id:longint):boolean;

Function TCSetPGrp(Fd,Id:longint):boolean;
Вызов tcflush очищает заданную очередь. Если параметр queue имеет значение TCIFLUSH (определенное в файле stdio), то очищается очередь ввода. Это означает, что все символы в очереди ввода сбрасываются. Если параметр queue имеет значение TCOFLUSH, то очищается очередь вывода. При значении TCIOFLUSH параметра queue очищаются и очередь ввода, и очередь вывода.
Вызов tcdrain приводит к приостановке работы процесса до тех пор, пока текущий вывод не будет записан в терминал ttyfd.
Вызов tсflow обеспечивает старт/стопное управление драйвером терминала. Если параметр actions равен TCOOFF, то вывод приостанавливается. Он может быть возобновлен при помощи еще одного вызова tсflow со значением параметра actions равным TCOON. Вызов tcflow также может использоваться для посылки драйверу терминала специальных символов START и STOP, это происходит при задании значения параметра actions равного TCIOFF или TCION соответственно. Специальные символы START и STOP служат для приостановки и возобновления ввода с терминала.
Вызов TCSendBreak используется для посылки сигнала прерывания сеанса связи, которому соответствует посылка нулевых битов в течение времени, заданного параметром duration. Если параметр duration равен 0, то биты посылаются в течение не менее четверти секунды и не более полсекунды. Если параметр duration не равен нулю, то биты будут посылаться в течение некоторого промежутка времени, длительность которого зависит от значения параметра duration и конкретной реализации.
TCSetPGrp устанавливает, а TCGetPGrp – получает идентификатор группы фоновых процессов, сохраняя его в Id.
9.3.8. Сигнал разрыва соединения
В главе 6 упоминалось, что сигнал разрыва соединения SIGHUP посылается членам сеанса в момент завершения работы лидера сеанса (при условии, что он имеет управляющий терминал). Этот сигнал также имеет другое применение в средах, в которых соединение между компьютером и терминалом может быть разорвано (тогда пропадает сигнал несущей в линии терминала). Это может происходить, например, если терминалы подключены через телефонную сеть или с помощью некоторых типов локальных сетей. В этих обстоятельствах драйвер терминала должен послать сигнал SIGHUP всем процессам, которые считают данный терминал своим управляющим терминалом. Если этот сигнал не перехватывается, то он приводит к завершению работы программы. (В отличие от сигнала SIGINT, сигнал SIGHUP обычно завершает и процесс оболочки. В результате пользователь автоматически отключается от системы при нарушении его связи с системой – такой подход необходим для обеспечения безопасности.)
Обычно программисту не нужно обрабатывать сигнал SIGHUP, так как он служит нужным целям. Тем не менее может потребоваться перехватывать его для выполнения некоторых операций по «наведению порядка» перед выходом из программы; вот как это можно сделать:
uses linux;

procedure hup_action(sig:integer);cdecl;forward;

var
act:sigactionrec;
.
.
.
act.handler.sh:=@hup_action;
sigaction(SIGHUP, @act, nil);
Этот подход используется некоторыми редакторами, сохраняющими редактируемый файл и отсылающими пользователю сообщение перед выходом. Если сигнал SIGHUP полностью игнорируется (установкой значения act.handler.sh равного SIG_IGN) и терминал разрывает соединение, то следующие попытки чтения из терминала будут возвращать 0 для обозначения «конца файла».
9.4. Псевдотерминалы
Еще одно применение модуля дисциплины линии связи заключается в поддержке работы так называемого псевдотерминала (pseudo terminal), применяемого для организации дистанционного доступа через сеть. Псевдотерминал предназначен для обеспечения соединения терминала одного компьютера с командным интерпретатором другого компьютера. Пример такого соединения приведен на рис. 9.4. На нем пользователь подключен к компьютеру А (клиенту), но использует командный интерпретатор на компьютере В (сервере). Стрелки на схеме показывают направление пересылки вводимых с клавиатуры символов. Здесь схема несколько упрощена за счет исключения стеков сетевых протоколов на клиентской и серверной системе.
Когда пользователь подключается к командному интерпретатору на другом компьютере (обычно при помощи команды rlogin), то локальное терминальное соединение должно быть изменено. По мере того как данные считываются с локального терминала, они должны без изменений передаваться через модуль дисциплины линии связи клиентскому процессу rlogin, выполняющемуся на локальном компьютере. Поэтому дисциплина линии связи на локальном компьютере должна работать в режиме прямого доступа. Следующий фрагмент должен напомнить, как включается такой режим:
uses stdio, linux;

var
attr:termios;
.
.
.
(* Получает текущую дисциплину линии связи *)
tcgetattr(0, attr);

(* Возврат из вызова fdread разрешен только после считывания одного символа *)
attr.c_cc[VMIN] := 1;
attr.c_cc(VTIME] := 0;
attr.c_lflag := attr.c_lflag and not (ISIG or ECHO or ICANON);

(* Устанавливает новую дисциплину линии связи *)
tcsetattr(0, TCSAFLUSH, attr);

Рис. 9.4. Удаленный вход в систему в ОС UNIX

Теперь процесс клиента rlogin может передавать данные по сети в исходном виде.
Когда сервер (В) получает первоначальный запрос на вход в систему, он выполняет вызовы fork и ехес, порождая новый командный интерпретатор. С новым командным интерпретатором не связан управляющий терминал, поэтому создается псевдотерминал, который имитирует обычный драйвер устройства терминала. Псевдотерминал (pseudo tty) действует подобно двунаправленному каналу и просто позволяет двум процессам передавать данные. В рассматриваемом примере псевдотерминал связывает командный интерпретатор с соответствующим сетевым процессом. Псевдотерминал является парой устройств, которые называются ведущим и ведомым устройствами, или портами псевдотерминала. Сетевой процесс открывает ведущий порт устройства псевдотерминала, а затем выполняет запись и чтение. Процесс командного интерпретатора открывает ведомый порт, а затем работает с ним (через дисциплину линии связи). Данные, записываемые в ведущий порт, попадают на вход ведомого порта, и наоборот. В результате пользователь на клиентском компьютере (А) как бы напрямую обращается к командному интерпретатору, который на самом деле выполняется на сервере (В). Аналогично, по мере того как данные выводятся командным интерпретатором на сервере, они обрабатываются дисциплиной линии связи на сервере (которая работает в каноническом режиме) и затем передаются без изменений клиентскому терминалу, не подвергаясь модификации со стороны дисциплины линии связи клиента.
Хотя средства, при помощи которых выполняется инициализация псевдотерминалов, были улучшены в новых версиях ОС UNIX и спецификации XSI, они все еще остаются довольно громоздкими. Система UNIX обеспечивает конечное число псевдотерминалов, и процесс командного интерпретатора должен открыть следующий доступный псевдотерминал. В системе SVR4 это выполняется и помощи открытия устройства /dev/ptmx, которое определяет и открывает первое неиспользуемое ведущее устройство псевдотерминала. С каждым ведущим устройством связано ведомое устройство. Для того, чтобы предотвратить открытие ведомого устройства другим процессом, открытие устройства /dev/ptmx также блокирует соответствующее ведомое устройство.
uses linux;
var
mfd:longint;
.
.
.
(* Открыть псевдотерминал -
* получить дескриптор файла главного устройства *)
masterfd := fdopen ('/dev/ptmx', Open_RDWR);
if masterfd = -1 then
begin
perror('Ошибка при открытии главного устройства');
halt(1);
end;
Перед тем как открыть и «разблокировать» ведомое устройство, необходимо убедиться, что только один процесс с соответствующими правами доступа сможет выполнять чтение из устройства и запись в него. Функция grantpt изменяет режим доступа и идентификатор владельца ведомого устройства в соответствии с параметрами связанного с ним главного устройства. Функция unlockpt снимает флаг, блокирующий ведомое устройство (то есть делает его доступным). Далее нужно открыть ведомое устройство. Но его имя пока еще не известно. Функция ptsname возвращает имя ведомого устройства, связанного с заданным ведущим устройством, которое обычно имеет вид /dev/pts/pttyXX. Следующий фрагмент демонстрирует последовательность необходимых действий:
uses stdio, linux;

var
mfd, sfd:longint;
slavenm:pchar;
.
.
.
(* Открываем ведущее устройство, как и раньше *)
mfd := fdopen ('/dev/ptmx', Open_RDWR);
if mfd = -1 then
begin
perror('Ошибка при открытии ведущего устройства');
halt(1);
end;
(* Изменяем права доступа ведомого устройства *)
if grantpt (mfd) = -1 then
begin
perror('Невозможно разрешить доступ к ведомому устройству');
halt(1);
end;
(* Разблокируем ведомое устройство, связанное с mfd *)
if unlockpt(mfd) = -1 then
begin
perror('Невозможно разблокировать ведомое устройство');
halt(1);
end;
(* Получаем имя ведомого устройства и затем пытаемся открыть его *)
slavenm := ptsname (mfd);
if slavenm = nil then
begin
perror('Невозможно получить имя ведомого устройства');
halt(1);
end;
sfd := fdopen (slavenm, Open_RDWR);
if slavefd = -1 then
begin
perror('Ошибка при открытии ведомого устройства');
halt(1);
end;
Теперь, когда получен доступ к драйверу устройства псевдотерминала, нужно установить для него дисциплину линии связи. До сих пор дисциплина линии связи рассматривалась как единое целое, тогда как в действительности она состоит из набора внутренних модулей ядра, известных как модули STREAM. Стандартная дисциплина линии связи псевдотерминала состоит из трех модулей: ldterm (модуль дисциплины линии связи терминала), ptem (модуль эмуляции псевдотерминала) и ведомой части псевдотерминала. Вместе они работают как настоящий терминал. Эта конфигурация показана на рис. 9.5.

ldterm

pterm

Дисциплина
линии связи

Ведомый порт
псевдотерминала

Рис. 9.5. Дисциплина линии связи в виде модулей STREAM для устройства псевдотерминала

Для создания дисциплины линии связи нужно «вставить» дополнительные модули STREAM в ведомое устройство. Это достигается при помощи многоцелевой функции ioctl, например:
/*
* Заголовочный файл stdio содержит интерфейс STREAMS
* и определяет макрокоманду I_PUSH, используемую в качестве
* второго аргумента функции ioctl().
*/

uses stdio;
.
.
.
(* Открываем ведущее и ведомое устройства, как и раньше *)
(* Вставляем два модуля в ведомое устройство *)
ioctl(sfd, I_PUSH, 'ptem');
ioctl(sfd, I_PUSH, 'ldterm');
Обратимся теперь к главному примеру, программе tscript, которая использует псевдотерминал в пределах одного компьютера для перехвата вывода командного интерпретатора в процессе интерактивного сеанса, не влияя на ход этого сеанса. (Эта программа аналогична команде UNIX script.) Данный пример можно расширить и для дистанционного входа через сеть.
9.5. Пример управления терминалом: программа tscript
Программа tscript устроена следующим образом: при старте она выполняет вызовы fork и ехес для запуска пользовательской оболочки. Далее все данные, записываемые на терминал оболочкой, сохраняются в файле, при этом оболочка ничего об этом не знает и продолжает вести себя так, как будто она полностью управляет дисциплиной линии связи и, следовательно, терминалом. Логическая структура программы tscript показана на рис. 9.6.

Рис. 9.6. Использование псевдотерминала в программе tscript

Основные элементы схемы:
tscript
Первый запускаемый процесс. После инициализации псевдотерминала и дисциплины линии связи этот процесс использует вызовы fork и ехес для создания оболочки shell. Теперь программа tscript играет две роли. Первая состоит в чтении из настоящего терминала и записи всех данных в порт ведущего устройства псевдотерминала. (Все данные, записываемые в ведущее устройство псевдотерминала, непосредственно передаются на ведомое устройство псевдотерминала.) Вторая роль состоит в чтении вывода программы оболочки shell при помощи псевдотерминала и копировании этих данных на настоящий терминал и в выходной файл
shell
Пользовательская оболочка. Перед запуском процесса shell модули дисциплины линии связи STREAM вставляются в ведомое устройство. Стандартный ввод, стандартный вывод и стандартный вывод диагностики оболочки перенаправляются в ведомое устройство псевдотерминала
Первая задача программы tscript состоит в установке обработчика сигнала SIGCHLD и в открытии псевдотерминала. Затем программа создает процесс shell. И, наконец, вызывается процедура script. Эта процедура отслеживает два потока данных: ввод с клавиатуры (стандартный ввод), который она передает ведущему устройству псевдотерминала, и ввод с ведущего устройства псевдотерминала, передаваемый на стандартный вывод и записываемый в выходной файл.
(* Программа tscript управление терминалом *)
(* Хотя в Linux этот пример и не работает... *)
uses linux,stdio;

var
dattr:termios;

var
act:sigactionrec;
mfd, sfd:longint;
err:integer;
buf:array [0..511] of char;
mask:sigset_t;
begin
(* Сохранить текущие установки терминала *)
tcgetattr (0, dattr);
(* Открыть псевдотерминал *)
err := pttyopen (mfd, sfd);
if err <> 1 then
begin
writeln (stderr, 'pttyopen: ', err);
perror ('Ошибка при открытии псевдотерминала');
halt (1);
end;
(* Установить обработчик сигнала SIGCHLD *)
act.handler.sh := @catch_child;
sigfillset (@mask);
act.sa_mask:=mask.__val[0];
sigaction (SIGCHLD, @act, nil);
(* Создать процесс оболочки *)
case fork of
-1: (* ошибка *)
begin
perror ('Ошибка вызова оболочки');
halt (2);
end;
0: (* дочерний процесс *)
begin
fdclose (mfd);
runshell (sfd);
end;
else (* родительский процесс *)
begin
fdclose (sfd);
script (mfd);
end;
end;
end.
Основная программа использует четыре процедуры. Первая из них называется catch_child. Это обработчик сигнала SIGCHLD. При получении сигнала SIGCHLD процедура catch_child восстанавливает атрибуты терминала и завершает работу.
procedure catch_child (signo:integer);cdecl;
begin
tcsetattr (0, TCSAFLUSH, dattr);
halt (0);
end;
Вторая процедура, pttyopen, открывает псевдотерминал.
function pttyopen (var masterfd, slavefd:longint):integer;
var
slavenm:pchar;
begin
(* Открыть псевдотерминал -
* получить дескриптор файла главного устройства *)
masterfd := fdopen ('/dev/ptmx', Open_RDWR);
if masterfd = -1 then
begin
pttyopen:=-1;
exit;
end;
(* Изменить права доступа для ведомого устройства *)
if grantpt (masterfd) = -1 then
begin
fdclose (masterfd);
pttyopen:=-2;
exit;
end;
(* Разблокировать ведомое устройство, связанное с mfd *)
if unlockpt (masterfd) = -1 then
begin
fdclose (masterfd);
pttyopen:=-3;
exit;
end;
(* Получить имя ведомого устройства и затем открыть его *)
slavenm := ptsname (masterfd);
if slavenm = nil then
begin
fdclose (masterfd);
pttyopen:=-4;
exit;
end;
slavefd := fdopen (slavenm, Open_RDWR);
if slavefd = -1 then
begin
fdclose (masterfd);
pttyopen:=-5;
exit;
end;
(* Создать дисциплину линии связи *)
ioctl (slavefd, I_PUSH, pchar('ptem'));
if linuxerror>0 then
begin
fdclose (masterfd);
fdclose (slavefd);
pttyopen:=-6;
exit;
end;
ioctl (slavefd, I_PUSH, pchar('ldterm'));
if linuxerror>0 then
begin
fdclose (masterfd);
fdclose (slavefd);
pttyopen:=-7;
exit;
end;
pttyopen:=1;
end;
Третья процедура – процедура runshell. Она выполняет следующие задачи:
• вызывает setpgrp, чтобы оболочка выполнялась в своей группе процессов. Это позволяет оболочке полностью управлять обработкой сигналов, в особенности в отношении управления заданиями;
• вызывает системный вызов dup2 для перенаправления дескрипторов stdin, stdout и stderr на дескриптор файла ведомого устройства. Это особенно важный шаг;
• запускает оболочку при помощи вызова ехес, которая выполняется до тех пор, пока не будет прервана пользователем.
procedure runshell (sfd:longint);
begin
setpgrp;
dup2 (sfd, 0);
dup2 (sfd, 1);
dup2 (sfd, 2);
execl ('/bin/sh -i');
end;
Теперь рассмотрим саму процедуру script. Первым действием процедуры script является изменение дисциплины линии связи так, чтобы она работала в режиме прямого доступа. Это достигается получением текущих атрибутов терминала и изменением их при помощи вызова tcsetattr. Затем процедура script открывает файл output и использует системный вызов select (обсуждавшийся в главе 7) для обеспечения одновременного ввода со своего стандартного ввода и ведущего устройства псевдотерминала. Если данные поступают со стандартного ввода, то процедура script передает их без изменений ведущему устройству псевдотерминала. При поступлении же данных с ведущего устройства псевдотерминала процедура script записывает эти данные в терминал пользователя и в файл output.
procedure script(mfd:longint);
var
nread, ofile:longint;
_set, master:fdset;
attr:termios;
buf:array [0..511] of char;
begin
(* Перевести дисциплину линии связи в режим прямого доступа *)
tcgetattr (0, attr);
attr.c_cc[VMIN] := 1;
attr.c_cc[VTIME] := 0;
attr.c_lflag := attr.c_lflag and not (ISIG or ECHO or ICANON);
tcsetattr (0, TCSAFLUSH, attr);

(* Открыть выходной файл *)
ofile := fdopen ('output', Open_CREAT or Open_WRONLY or Open_TRUNC, octal(0666));

(* Задать битовые маски для системного вызова select *)
FD_ZERO (master);
FD_SET (0, master);
FD_SET (mfd, master);

(* Вызов select осуществляется без таймаута,
* и будет заблокирован до наступления события. *)
_set := master;
while select (mfd + 1, @_set, nil, nil, nil) > 0 do
begin
(* Проверить стандартный ввод *)
if FD_ISSET (0, _set) then
begin
nread := fdread (0, buf, 512);
fdwrite (mfd, buf, nread);
end;
(* Проверить главное устройство *)
if FD_ISSET (mfd, _set) then
begin
nread := fdread (mfd, buf, 512);
write (ofile, buf, nread);
write (1, buf, nread);
end;
_set := master;
end;
end;
Следующий сеанс демонстрирует работу программы tscript. Комментарии, обозначенные символом #, показывают, какая из оболочек выполняется в данный момент.
$ ./tscript

$ ls -l tscript # работает новая оболочка
-rwxr-xr-x 1 spate fcf 6984 Jan 22 21:57 tscript

$ head -2 /etc/passwd # выполняется в новой оболочке
root:х:0:1:0000-Admin(0000):/:/bin/ksh
daemon:x:1:1:0000-Admin(0000):/:

$ exit # выход из новой оболочки

$ cat output # работает исходная оболочка
-rwxr-xr-x 1 spate fcf 6984 Jan 22 21:57 tscript
root:х:0:1:0000-Admin(0000):/:/bin/ksh
daemon:x:1:1:0000-Admin(0000):/:
Упражнение 9.5. Добавьте к программе обработку ошибок и возможность задания в качестве параметра имени выходного файла. Если имя не задано, используйте по умолчанию имя output.
Упражнение 9.6. Эквивалентная стандартная программа UNIX script позволяет задать параметр -а, который указывает на необходимость дополнения файла output (содержимое файла не уничтожается). Реализуйте аналогичную возможность в программе tscript.
Глава 10.Сокеты
10.1. Введение
В предыдущих главах был изучен ряд механизмов межпроцессного взаимодействия системы UNIX. В настоящее время пользователями и разработчиками часто используются и сетевые среды, предоставляющие возможности технологи клиент/сервер. Такой подход позволяет совместно использовать данные, дисковое пространство, периферийные устройства, процессорное время и другие ресурсы. Работа в сетевой среде по технологии клиент/сервер предполагает взаимодействие процессов, находящихся на клиентских и серверных системах, разделенных средой передачи данных.
Исторически сетевые средства UNIX развивались двумя путями. Разработчики Berkeley UNIX создали в начале 80-х годов известный и широко применяемый интерфейс сокетов, а разработчики System V выпустили в 1986 г. Transport Level Interface (интерфейс транспортного уровня, сокращенно TLI). Раздел сетевого программирования в документации по стандарту Х/Open часто называют спецификацией XTI. Спецификация XTI включает и интерфейс сокетов, и интерфейс TLI. Основные понятия являются общими для обеих реализации, но интерфейс TLI использует намного больше структур данных, и его реализация гораздо сложнее, чем реализация интерфейса сокетов. Поэтому в этой главе будет рассмотрен хорошо известный и испытанный интерфейс сокетов. Они обеспечивают простой программный интерфейс, применимый как для связи процессов на одном компьютере, так и для связи процессов через сети. Целью сокетов является обеспечение средства межпроцессного взаимодействия для двунаправленного обмена сообщениями между двумя процессами независимо от того, находятся ли они на одном или на разных компьютерах.
Глава будет посвящена краткому ознакомлению с основными понятиями и средствами работы с сокетами. При необходимости продолжить их изучение следует обратиться к более подробному руководству (например, книге «Advanced Programming in the UNIX Environment» У.Р. Стивенса) или к специалисту по сетевому программированию.
Кроме этого, необходимо отметить, что во время компоновки программ может понадобиться подключение сетевых библиотек – за рекомендациями обратитесь к справочному руководству системы.
10.2. Типы соединения
Если процессам нужно передать данные по сети, они могут выбрать для этого один из двух способов связи. Процесс, которому нужно посылать неформатированный, непрерывный поток символов одному и тому же абоненту, например, процессу удаленного входа в систему, может использовать модель соединения (connection oriented model) или виртуальное соединение (virtual circuit). В других же случаях (например, если серверу нужно разослать сообщение клиентам, не проверяя его доставку) процесс может использовать модель дейтаграмм (connectionless oriented model). При этом процесс может посылать сообщения (дейтаграммы) по произвольным адресам через один и тот же сокет без предварительного установления связи с этими адресами. Термин дейтаграмма (datagram) обозначает пакет пользовательского сообщения, посылаемый через сеть. Для облегчения понимания приведем аналогию: модель виртуальных соединений напоминает телефонную сеть, а модель дейтаграмм – пересылку писем по почте. Поэтому в последнем случае нельзя быть абсолютно уверенным, что сообщение дошло до адресата, а если необходимо получить ответ на него, то нужно указать свой обратный адрес на конверте. Модель соединений будет более подходящей при необходимости получения тесного взаимодействия между системами, когда обмен сообщениями и подтверждениями происходит в определенном порядке. Модель без соединений является более эффективной и лучше подходит в таких случаях, как рассылка широковещательных сообщений большому числу компьютеров.
Для того чтобы взаимодействие между процессами на разных компьютерах стало возможным, они должны быть связаны между собой как на аппаратном уровне при помощи сетевого оборудования – кабелей, сетевых карт и различных устройств маршрутизации, так и на программном уровне при помощи стандартного набора сетевых протоколов. Протокол представляет собой просто набор правил, в случае сетевого протокола – набор правил обмена сообщениями между компьютерами. Поэтому в системе UNIX должны существовать наборы правил для обеих моделей – как для модели соединений, так и для модели дейтаграмм. Для модели соединений используется протокол управления передачей (Transmission Control Protocol, сокращенно TCP), а для модели дейтаграмм – протокол пользовательских дейтаграмм (User Datagram Protocol, сокращенно UDP).1
10.3. Адресация
Чтобы процессы могли связаться по сети, должен существовать механизм определения сетевого адреса (network address) компьютера, на котором находится другой процесс. В конечном счете адрес определяет физическое положение компьютера в сети. Обычно адреса состоят из нескольких частей, соответствующих различным уровням сети. Далее будут затронуты только те вопросы, без ответов на которые не обойтись при программировании с использованием сокетов.
10.3.1. Адресация Internet
Сейчас почти во всех глобальных сетях применима адресация IP (сокращение от Internet Protocol – межсетевой протокол, протокол сети Интернет).
Адрес IP состоит из четырех десятичных чисел, разделенных точками, например:
197.124.10.1
Эти четыре числа содержат достаточную информацию для определения сети назначения, а также компьютера в этой сети; собственно, термин Internet и означает «сеть сетей».
Сетевые вызовы UNIX не могут работать с IP адресами в таком формате. На программном уровне IP адреса хранятся в структуре типа in_addr_t. Обычно программистам не нужно знать внутреннее представление этого типа, так как для преобразования IP адреса в структуру типа in_addr_t предназначена процедура inet_addr.
Описание
uses stdio;

function inet_addr(ip_address:pchar):in_addr_t;
Процедура inet_addr принимает IP адрес в форме строки вида 1.2.3.4 и возвращает адрес в виде структуры соответствующего типа. Если вызов процедуры завершается неудачей из-за неверного формата IP адреса, то возвращаемое значение будет равно in_addr_t(-1), например:
var
server:in_addr_t;

server := inet_addr('197.124.10.1');
Для того чтобы процесс мог ссылаться на адрес своего компьютера, в заголовочном файле stdio определена постоянная INADDR_ANY, содержащая локальный адрес компьютера в формате in_addr_t.
10.3.2. Порты
Кроме адреса компьютера, клиентская программа должна иметь возможность подключения к нужному серверному процессу. Серверный процесс ждет подключения к заданному номеру порта (port number). Поэтому клиентский процесс должен выполнить запрос на подключение к определенному порту на заданном компьютере. Если продолжить аналогию с пересылкой писем по почте, то это равносильно дополнению адреса номером комнаты или квартиры.
Некоторые номера портов по соглашению считаются отведенными для стандартных сервисов, таких как ftp или rlogin. Эти номера портов записаны в файле /etc/services. В общем случае порты с номерами, меньшими 1024, считаются зарезервированными для системных процессов UNIX. Все остальные порты доступны для пользовательских процессов.
10.4. Интерфейс сокетов
Для хранения информации об адресе и порте адресата (абонента) существуют стандартные структуры. Обобщенная структура адреса сокета определяется в модуле sockets следующим образом:
TSockAddr=packed Record
family:word; (* Семейство адресов *)
data :array [0..13] of char; (* Адрес сокета *)
end;
Эта структура называется обобщенным сокетом (generic socket), так как в действительности применяются различные типы сокетов в зависимости от того, используются ли они в качестве средства межпроцессного взаимодействия на одном и том же компьютере или для связи процессов через сеть. Сокеты для связи через сеть имеют следующую форму:
uses sockets;

TInetSockAddr = packed Record
family : Word; (* Семейство адресов *)
port : Word; (* Номер порта *)
addr : Cardinal; (* IP-адрес *)
pad : array [1..8] of byte; (* Поле выравнивания *)
end;
10.4.1. Создание сокета
При любых моделях связи клиент и сервер должны создать абонентские точки (transport end points,), или сокеты, которые являются дескрипторами, используемыми для установки связи между процессами в сети. Они создаются при помощи системного вызова socket.
Описание
uses sockets;

Function Socket(Domain,SocketType,Protocol:Longint):Longint;
Параметр domain определяет коммуникационный домен, в котором будет использоваться сокет. Например, значение AF_INET определяет, что будет использоваться домен Internet. Интерес может представлять также другой домен, AF_UNIX, который используется, если процессы находятся на одном и том же компьютере.
Параметр SocketType определяет тип создаваемого сокета. Значение SOCK_STREAM указывается при создании сокета для работы в режиме виртуальных соединений, а значение SOCK_DGRAM – для работы в режиме пересылок дейтаграмм. Последний параметр protocol определяет используемый протокол. Этот параметр обычно задается равным нулю, при этом по умолчанию сокет типа SOCK_STREAM будет использовать протокол TCP, а сокет типа SOCK_DGRAM – протокол UDP. Оба данных протокола являются стандартными протоколами UNIX. Поэтому виртуальное соединение часто называют TCP-соединением, а пересылку дейтаграмм – работой с UDP-сокетами.
Системный вызов socket обычно возвращает неотрицательное целое число, которое является дескриптором файла сокета, что позволяет считать механизм сокетов разновидностью обобщенного файлового ввода/вывода UNIX.
10.5. Программирование в режиме TCP-соединения
Для того чтобы продемонстрировать основные системные вызовы для работы с сокетами, рассмотрим пример, в котором клиент посылает серверу поток строчных символов через TCP-соединение. Сервер преобразует строчные символы в прописные и посылает их обратно клиенту. В следующих разделах этой главы приведем тот же самый пример, но использующий сокеты UDP-протокола.
Сначала составим план реализации серверного процесса:
(* Серверный процесс *)

(* Включает нужные заголовочные файлы *)
uses sockets,stdio,linux;

var
sockfd:longint;
begin
(* Установить абонентскую точку сокета *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* 'Связывание' адреса сервера с сокетом

Ожидание подключения

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

(* Включает нужные заголовочные файлы *)

var
sockfd:longint;
begin
(* Создает сокет *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* Соединяет сокет с адресом серверного процесса *)
(* В цикле посылает данные серверу и принимает от него ответы *)
end.
Далее будем постепенно превращать эти шаблоны в настоящие программы, начиная с реализации сервера.
10.5.1. Связывание
Системный вызов bind связывает сетевой адрес компьютера с идентификатором сокета.
Описание
uses sockets;

Function Bind(sockfd:Longint; Var address; add_len:Longint):Boolean;
Function Bind(sockfd:longint; const address:string):boolean;
Первый параметр, sockfd, является дескриптором файла сокета, созданным с помощью вызова socket, а второй – указателем на обобщенную структуру адреса сокета или адрес в форме строки. В рассматриваемом примере данные пересылаются по сети, поэтому в действительности в качестве этого параметра будет задан адрес структуры TInetSockAddr, содержащей информацию об адресе нашего сервера. Последний параметр содержит размер указанной структуры адреса сокета. В случае успешного завершения вызова bind он возвращает значение 0. В случае ошибки, например, если сокет для этого адреса уже существует, вызов bind возвращает значение -1. Переменная linuxerror будет иметь при этом значение Sys_EADDRINUSE.
10.5.2. Включение приема TCP-соединений
После выполнения связывания с адресом и перед тем, как какой-либо клиент сможет подключиться к созданному сокету, сервер должен включить прием соединений. Для этого служит вызов listen.
Описание
uses sockets;

Function Listen(sockfd, queue_size:Longint):Boolean;
Параметр sockfd имеет то же значение, что и в предыдущем вызове. В очереди сервера может находиться не более queue_size запросов на соединение. (Спецификация XSI определяет минимальное ограничение сверху на длину очереди равное пяти.)
10.5.3. Прием запроса на установку TCP-соединения
Когда сервер получает от клиента запрос на соединение, он должен создать новый сокет для работы с новым соединением. Первый же сокет используется только для установки соединения. Дополнительный сокет создается при помощи вызова accept, принимающего очередное соединение.
Описание
uses sockets;

Function Accept(sockfd:Longint;Var address;Var add_len:Longint):Longint;
Function Accept(sockfd:longint;var address:string;
var SockIn,SockOut:text):Boolean;
Function Accept(sockfd:longint;var address:string;
var SockIn,SockOut:File):Boolean;
Function Accept(sockfd:longint;var address:TInetSockAddr;
var SockIn,SockOut:File):Boolean;
Системному вызову accept передается дескриптор сокета, для которого ведется прием соединений. Возвращаемое значение соответствует идентификатору нового сокета, который будет использоваться для связи. Параметр address заполняется информацией о клиенте. Так как связь использует соединение, адрес клиента знать не обязательно, поэтому можно присвоить параметру address значение nil. Если значение address не равно nil, то переменная, на которую указывает параметр add_len, первоначально должна содержать размер структуры адреса, заданной параметром address. После возврата из вызова accept переменная add_len будет содержать реальный размер записанной структуры.
Вторая, третья и четвертая формы вызова accept эквивалентны вызову первой с последующим использованием функции Sock2Text, преобразующей сокет sockfd в две файловые переменные типа Text, одна из которых отвечает за чтение из сокета (SockIn), а другая – за запись в сокет (SockOut).
После подстановки вызовов bind, listen и accept текст программы сервера примет вид:
(* Серверный процесс *)
uses sockets,stdio,linux;

const
SIZE=sizeof(tinetsockaddr);
(* Инициализация сокета Internet с номером порта 7000
* и локальным адресом, заданным в постоянной INADDR_ANY *)
server:tinetsockaddr = (family:AF_INET; port:7000; addr:INADDR_ANY);

var
newsockfd:longint;
sockfd:longint;

begin
(* Создает сокет *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* Связавает адрес с сокетом *)
if not bind (sockfd, server, SIZE) then
begin
perror ('Ошибка вызова bind');
halt (1);
end;

(* Включает прием соединений *)
if not listen (sockfd, 5) then
begin
perror ('ошибка вызова listen');
halt (1);
end;

while true do
begin
(* Принимает очередной запрос на соединение *)
newsockfd := accept (sockfd, client, clientaddrlen);
if newsockfd = -1 then
begin
perror ('Ошибка вызова accept');
continue;
end;
(*
Создает дочерний процесс для работы с соединением.
Если это дочерний процесс,
то в цикле принимает данные от клиента
и посылает ему ответы.
*)
end;
end.
Обратите внимание на то, что сервер использует константу INADDR_ANY, coответствующую адресу локального компьютера.
Теперь имеется серверный процесс, способный переходить в режим приёма соединений и принимать запросы на установку соединений. Рассмотрим, как клиент должен обращаться к серверу.
10.5.4. Подключение клиента
Для выполнения запроса на подключение к серверному процессу клиент использует системный вызов connect.
Описание
uses sockets;

Function Connect(csockfd:Longint; Var address; add_len:Longint): Longint;
Function Connect(csockfd:longint; const address:string;
var SockIn,SockOut:text):Boolean;
Function Connect(csockfd:longint; const address:string;
var SockIn,SockOut:file):Boolean;
Function Connect(csockfd:longint; const address:TInetSockAddr;
var SockIn,SockOut:file):Boolean;
Первый параметр csockfd является дескриптором сокета клиента и не имеет отношения к дескриптору сокета на сервере. Параметр address указывает на структуру, содержащую адрес сервера, либо на адрес в формате строки. Параметр add_len определяет размер используемой структуры адреса.
Вторая, третья и четвертая формы вызова connect эквивалентны вызову первой с последующим использованием функции Sock2Text, преобразующей сокет sockfd в две файловые переменные типа Text, одна из которых отвечает за чтение из сокета (SockIn), а другая – за запись в сокет (SockOut).
Продолжая составление рассматриваемого примера, запишем следующий вариант текста программы клиента:
(* Клиентский процесс *)
uses sockets,stdio,linux;

const
SIZE=sizeof(tinetsockaddr);
server:tinetsockaddr=(family:AF_INET; port:7000);

var
sockfd:longint;

begin
(* Преобразовать и сохранить IP address сервера *)
server.addr := inet_addr ('127.0.0.1');

(* Создать сокет *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* Соединяет сокет с сервером *)
if not connect (sockfd, server, SIZE) then
begin
perror ('Ошибка вызова connect');
halt (1);
end;

(* Обмен данными с сервером *)
end.
Адрес сервера преобразуется в нужный формат при помощи вызова inet_addr. Адреса известных компьютеров локальной сети обычно можно найти в файле /etc/hosts.
10.5.5. Пересылка данных
Теперь уже освоена процедура установления соединения между клиентом и сервером. Для сокетов типа SOCK_STREAM и клиент, и сервер получают дескрипторы файлов, которые могут использоваться для чтения или записи. В большинстве случаев для этого годятся обычные вызовы fdread и fdwrite. Если же необходимо задавать дополнительные параметры пересылки данных по сети, то можно использовать два новых системных вызова – send и recv. Эти вызовы имеют схожий интерфейс и ведут себя точно так же, как вызовы fdread и fdwrite, если их четвертый аргумент равен нулю.
Описание
uses sockets;

Function Recv(sockfd:Longint; Var buffer; length,Flags:Longint):Longint;
Function Send(sockfd:Longint; Var buffer; length,Flags:Longint):Longint;
Вызов recv имеет четыре параметра: дескриптор файла filedes, из которого читаются данные, буфер buffer, в который они помещаются, размер буфера length и поле флагов flags.
Параметр flags указывает дополнительные опции получения данных. Его возможные значения определяются комбинациями следующих констант:
MSG_PEEK
Процесс может просматривать данные, не «получая» их
MSG_OOB
Обычные данные пропускаются. Процесс принимает только срочные данные, например, сигнал прерывания
MSG_WAITALL
Возврат из вызова recv произойдет только после получения всех данных
При аргументе flags равном нулю вызов send работает точно так же, как и вызов write, пересылая массив данных буфера buffer в сокет sockfd. Параметр length задает размер массива данных. Аналогично вызову recv параметр flags определяет опции передачи данных. Его возможные значения определяются комбинациями следующих констант:
MSG_OOB
Передать срочные (out of band) данные
MSG_DONTROUTE
При передаче сообщения игнорируются условия маршрутизации протокола более низкого уровня. Обычно это означает, что сообщение посылается по прямому, а не по самому быстрому маршруту (самый быстрый маршрут не обязательно прямой и может зависеть от текущего распределения нагрузки сети)
Теперь с помощью этих вызовов можно реализовать обработку данных на серверной стороне:
(* Серверный процесс *)

var
c:char;
client:tinetsockaddr;
clientaddrlen:longint;

begin
(* Приведенная выше инициализация сокета *)
.
.
.

while true do
begin
(* Принимает запрос на установку соединения *)
newsockfd := accept (sockfd, client, clientaddrlen);
if newsockfd = -1 then
begin
perror ('Ошибка вызова accept');
continue;
end;

(* Создает дочерний процесс для работы с соединением *)
if fork = 0 then
begin
(* Принимает данные *)
while recv (newsockfd, c, 1, 0) > 0 do
begin
(* Преобразовывает строчный символ в прописной *)
c := upcase (c);
(* Пересылает символ обратно *)
send (newsockfd, c, 1, 0);
end;
end;
end;
end.
Напомним, что использование вызова fork позволяет серверу обслуживать несколько клиентов. Цикл работы клиентского процесса может быть реализован так:
(* Клиентский процесс *)

var
sockfd:longint;
c,rc:char;

begin
(* Приведенная выше инициализация сокета и запрос
* на установку соединения *)

(* Обмен данными с сервером *)
rc := #$a;
while true do
begin
if rc = #$a then
writeln ('Введите строчный символ');
c:=char(getchar);
send (sockfd, c, 1, 0);
recv (sockfd, rc, 1, 0);
write (rc)
end;
end.
10.5.6. Закрытие TCP-соединения
При работе с сокетами важно корректно реагировать на завершение работы абонентского процесса. Так как сокет является двусторонним механизмом связи, то нельзя предсказать заранее, когда произойдет разрыв соединения – во время чтения или записи. Поэтому нужно учитывать оба возможных варианта.
Если процесс пытается записать данные в оборванный сокет при помощи вызова fdwrite или send, то он получит сигнал SIGPIPE, который может быть перехвачен соответствующим обработчиком сигнала. При чтении обрыв диагностируется проще.
В случае разорванной связи вызов fdread или recv возвращает нулевое значение. Поэтому для вызовов fdread и recv необходимо всегда проверять возвращаемое значение, чтобы не зациклиться при приеме данных.
Закрываются сокеты так же, как и обычные дескрипторы файлового ввода/вывода, – при помощи системного вызова fdclose. Для сокета типа SOCK_STREAM ядро гарантирует, что все записанные в сокет данные будут переданы принимающему процессу. Это может вызвать блокирование операции закрытия сокета до тех пор, пока данные не будут доставлены. (Если сокет имеет тип SOCK_DGRAM, то сокет закрывается немедленно.)
Теперь можно привести полный текст примера клиента и сервера, добавив в серверный процесс обработку сигналов и вызов fdclose в обе программы. В данном случае эти меры могут показаться излишними, но в реальном клиент/серверном приложении обязательна надежная обработка всех исключительных ситуаций. Приведем окончательный текст программы сервера:
(* Серверный процесс *)
uses sockets,stdio,linux;

const
SIZE=sizeof(tinetsockaddr);
server:tinetsockaddr = (family:AF_INET; port:7000; addr:INADDR_ANY);

var
newsockfd:longint;

procedure catcher (sig:integer);cdecl;
begin
fdclose (newsockfd);
halt (0);
end;

var
sockfd:longint;
c:char;
act:sigactionrec;
mask:sigset_t;
client:tinetsockaddr;
clientaddrlen:longint;
begin
act.handler.sh := @catcher;
sigfillset (@mask);
act.sa_mask:=mask.__val[0];
sigaction (SIGPIPE, @act, nil);

(* Установить абонентскую точку сокета *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* Связать адрес с абонентской точкой *)
if not bind (sockfd, server, SIZE) then
begin
perror ('Ошибка вызова bind');
halt (1);
end;

(* Включить прием соединений *)
if not listen (sockfd, 5) then
begin
perror ('ошибка вызова listen');
halt (1);
end;

while true do
begin
(* Прием запроса на соединение *)
newsockfd := accept (sockfd, client, clientaddrlen);
if newsockfd = -1 then
begin
perror ('Ошибка вызова accept');
continue;
end;

(* Создать дочерний процесс для работы с соединением *)
if fork = 0 then
begin
while recv (newsockfd, c, 1, 0) > 0 do
begin
c := upcase (c);
send (newsockfd, c, 1, 0);
end;

(* После того, как клиент прекратит передачу данных,
* сокет может быть закрыт и дочерний процесс
* завершает работу *)
fdclose (newsockfd);
halt (0);
end;

(* В родительском процессе newsockfd не нужен *)
fdclose (newsockfd);
end;
end.
И клиента:
(* Клиентский процесс *)
uses sockets,stdio,linux;

const
SIZE=sizeof(tinetsockaddr);
server:tinetsockaddr=(family:AF_INET; port:7000);

var
sockfd:longint;
c,rc:char;

begin
(* Преобразовать и сохранить IP address сервера *)
server.addr := inet_addr ('127.0.0.1');

(* Установить абонентскую точку сокета *)
sockfd := socket (AF_INET, SOCK_STREAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* Подключить сокет к адресу сервера *)
if not connect (sockfd, server, SIZE) then
begin
perror ('Ошибка вызова connect');
halt (1);
end;

(* Обмен данными с сервером *)
rc := #$a;
while true do
begin
if rc = #$a then
writeln ('Введите строчный символ');
c:=char(getchar);
send (sockfd, c, 1, 0);
if recv (sockfd, rc, 1, 0) > 0 then
write (rc)
else
begin
writeln ('Сервер не отвечает');
fdclose (sockfd);
halt (1);
end;
end;
end.
Упражнение 10.1. Запустите приведенную программу сервера и несколько клиентских процессов. Что произойдет после того, как все клиентские процессы завершат работу?
Упражнение 10.2. Измените код программ так, чтобы после того, как все клиентские процессы завершат свою работу, сервер также завершал работу после заданного промежутка времени, если не поступят новые запросы на соединение.
Упражнение 10.3. Измените код программ так, чтобы два взаимодействующих процесса выполнялись на одном и том же компьютере. В этом случае сокет должен иметь коммуникационный домен AF_UNIX.
10.6. Программирование в режиме пересылок UDP-дейтаграмм
Перепишем теперь пример, используя модель дейтаграмм. Основное отличие будет заключаться в том, что дейтаграммы (UDP-пакеты), передаваемые между клиентом и сервером, могут достигать точки назначения в произвольном порядке. К тому же, как уже упоминалось, протокол UDP не гарантирует доставку пакетов. При работе с UDP-сокетами процесс клиента должен также сначала создать сокет и связать с ним свой локальный адрес при помощи вызова bind. После этого процесс сможет использовать этот сокет для посылки и приема UDP-пакетов. Чтобы послать сообщение, процесс должен знать адрес назначения, который может быть как конкретным адресом, так и шаблоном, называемым «широковещательным адресом» и обозначающим сразу несколько компьютеров.
10.6.1. Прием и передача UDP-сообщений
Для сокетов UDP есть два новых системных вызова – sendto и recvfrom. Параметр sockfd в обоих вызовах задает связанный с локальным адресом сокет, через который принимаются и передаются пакеты.
Описание
uses stdio;

function recvfrom(sockfd:longint; var message; length, flags:longint;
var send_addr:tsockaddr; var add_len:longint):longint;

function sendto(sockfd:longint; var message; length, flags:longint;
var dest_addr:tsockaddr; dest_len:longint):longint;
Если параметр send_addr равен nil, то вызов recvfrom работает точно так же, как и вызов recv. Параметр message указывает на буфер, в который помещается принимаемая дейтаграмма, а параметр length задает число байтов, которые должны быть считаны в буфер. Параметр flags принимает те же самые значения, что и в вызове recv. Два последних параметра помогают установить двустороннюю связь с помощью UDP-сокета. В структуру send_addr будет помещена информация об адресе и порте, откуда пришел прочитанный пакет. Это позволяет принимающему процессу направить ответ пославшему пакет процессу. Последний параметр является указателем на целочисленную переменную типа longint, в которую помещается длина записанного в структуру send_addr адреса.
Вызов sendto противоположен вызову recvfrom. В этом вызове параметр dest_addr задает адрес узла сети и порт, куда должно быть передано сообщение, а параметр dest_len определяет длину адреса.
Адаптируем пример для модели дейтаграммных посылок.
(* Сервер *)
uses sockets,linux,stdio;

const
SIZE=sizeof(tinetsockaddr);
(* Локальный серверный порт *)
server:tinetsockaddr = (family:AF_INET; port:7000; addr:INADDR_ANY);
client_len:longint=SIZE;

var
sockfd:longint;
c:char;
(* Структура, которая будет содержать адрес процесса 2 *)
client:tinetsockaddr;

begin
(* Установить абонентскую точку сокета *)
sockfd := socket (AF_INET, SOCK_DGRAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* Связать локальный адрес с абонентской точкой *)
if not bind (sockfd, server, SIZE) then
begin
perror ('Ошибка вызова bind');
halt (1);
end;

(* Бесконечный цикл ожидания сообщений *)
while true do
begin
(* Принимает сообщение и записывает адрес клиента *)
if recvfrom (sockfd, c, 1, 0, tsockaddr(client), client_len) = -1 then
begin
perror ('Сервер: ошибка при приеме');
continue;
end;
c := upcase (c);
(* Посылает сообщение обратно *)
if sendto (sockfd, c, 1, 0, tsockaddr(client), client_len) = -1 then
begin
perror ('Сервер: ошибка при передаче');
continue;
end;
end;
end.
Новый текст клиента:
(* Клиентский процесс *)
uses sockets,stdio,linux;

const
SIZE=sizeof(tinetsockaddr);

(* Локальный порт на клиенте *)
client:tinetsockaddr = (family:AF_INET; port:INADDR_ANY; addr:INADDR_ANY);

(* Адрес удаленного сервера *)
server:tinetsockaddr = (family:AF_INET; port:7000);

var
sockfd:longint;
c:char;
begin
(* Преобразовать и записать IP адрес *)
server.addr := inet_addr ('127.0.0.1');

(* Установить абонентскую точку сокета *)
sockfd := socket (AF_INET, SOCK_DGRAM, 0);
if sockfd = -1 then
begin
perror ('Ошибка вызова socket');
halt (1);
end;

(* Связать локальный адрес с абонентской точкой сокета. *)
if not bind (sockfd, client, SIZE) then
begin
perror ('Ошибка вызова bind');
halt (1);
end;

(* Считать символ с клавиатуры *)
while fdread (0, c, 1) <> 0 do
begin
(* Передать символ серверу *)
if sendto (sockfd, c, 1, 0, tsockaddr(server), SIZE) = -1 then
begin
perror ('Клиент: ошибка передачи');
continue;
end;

(* Принять вернувшееся сообщение *)
if recv (sockfd, c, 1, 0) = -1 then
begin
perror ('Клиент: ошибка приема');
continue;
end;

fdwrite (1, c, 1);
end;
end.
Упражнение 10.4. Запустите сервер и несколько клиентов. Как сервер определяет, от какого клиента он принимает сообщение?
10.7. Различия между двумя моделями
Обсудим различия между двумя реализациями рассматриваемого примера с точки зрения техники программирования.
В обеих моделях сервер должен создать сокет и связать свой локальный адрес с этим сокетом. В модели TCP-соединений серверу следует после этого включить прием соединений. В модели UDP-сокетов этот шаг не нужен, зато на клиента возлагается больше обязанностей.
С точки зрения клиента в модели TCP-соединений достаточно простого подключения к серверу. В модели UDP-сокетов клиент должен создать сокет и связать свой локальный адрес с этим сокетом.
И, наконец, для передачи данных обычно используются различные системные вызовы. Системные вызовы sendto и recvfrom могут использоваться в обеих моделях, но все же они обычно используются в UDP-модели, чтобы сервер мог получить информацию об отправителе и отправить обратно ответ.
Глава 11. Стандартная библиотека ввода/вывода
11.1. Введение
В последних главах книги рассмотрим некоторые из стандартных библиотек процедур системы UNIX (а также большинства сред поддержки языка С в других операционных системах).
Начнем с изучения очень важной стандартной библиотеки ввода/вывода, образующей основную часть стандартной библиотеки С, поставляемой со всеми системами UNIX. Интерфейс этой библиотекой составляет основную часть приведенного в приложении модуля stdio.
Читатели кратко ознакомились со стандартным вводом/выводом во второй главе и уже встречались с некоторыми из входящих в его состав процедур, например, процедурами getchar и printf. Основная цель стандартной библиотеки ввода/вывода состоит в предоставлении эффективных, развитых и переносимых средств доступа к файлам. Эффективность процедур, образующих библиотеку, достигается за счет обеспечения механизма автоматической буферизации, который невидим для пользователя и минимизирует число действительных обращений к файлам и число выполняемых низкоуровневых системных вызовов. Библиотека предлагает широкий выбор функций, таких как форматированный вывод и преобразование данных. Процедуры стандартного ввода/вывода являются переносимыми, так как они не привязаны к особым свойствам системы UNIX и на самом деле являются частью независимого от UNIX стандарта ANSI языка С. Любой полноценный компилятор языка С предоставляет доступ к стандартной библиотеке ввода/вывода независимо от используемой операционной системы. Компилятор Free Pascal позволяет нам использовать эту библиотеку, как и многие другие, простым экспортом её функций.
11.2. Структура TFILE
Процедуры буферизованного ввода/вывода идентифицируют открытые файлы (каналы, сокеты, устройства и другие объекты) при помощи указателя на структуру типа FILE. Процедуры этого семейства также называют процедурами стандартного ввода/вывода, так как они содержатся в стандартной библиотеке языка С. Указатель на объект FILE часто называется также потоком ввода/вывода и является аналогом файловых дескрипторов базового ввода/вывода.
Определение структуры TFILE находится в заголовочном файле stdio. Следует отметить, что программисту нет необходимости знать устройство структуры TFILE, более того, ее определение различно в разных системах.
Все данные, считываемые из файла или записываемые в файл, передаются через буфер структуры TFILE. Например, стандартная процедура вывода сначала будет лишь заполнять символ за символом буфер. Только после заполнения буфета очередной вызов библиотечной процедуры вывода автоматически запишет его содержимое в файл вызовом fdwrite. Эти действия невидимы для пользовательской программы. Размер буфера составляет BUFSIZ байтов. Постоянная BUFSIZ определена в файле stdio и, как уже описывалось во второй главе, обычно задает размер блоков на диске. Как правило, ее значение равно 512 или 1024 байта.
Аналогично процедура ввода извлекает данные из буфера, связанного со структурой TFILE. Как только буфер опустеет, для его заполнения автоматически считывается еще один фрагмент файла. Эти действия также не видимы для пользовательской программы.
Механизм буферизации стандартной библиотеки ввода/вывода гарантирует, что данные всегда считываются и записываются блоками стандартного размера. В результате число обращений к файлам и число внутренних системных вызовов поддерживаются на оптимальном уровне. Но поскольку вся буферизация скрыта внутри процедур ввода/вывода, программист может их использовать для чтения или записи произвольных порций данных, даже по одному байту. Поэтому программа может быть составлена исходя из требований простоты и наглядности, а проблему общей эффективности ввода/вывода решат стандартные библиотеки. Далее увидим, что стандартные библиотечные процедуры также обеспечиваю простые в использовании средства форматирования. Поэтому для большинства приложений стандартный ввод/вывод является предпочтительным методом доступа к файлам.
11.3. Открытие и закрытие потоков: процедуры fopen и fclose
Описание
uses stdio;

function fopen(filename:pchar; _type:pchar):pfile;

function fclose(_stream:pfile):integer;
Библиотечные процедуры fopen и fclose являются эквивалентами вызовов fdopen и fdclose. Процедура fopen открывает файл, заданный параметром filename, и связывает с ним структуру TFILE. В случае успешного завершения процедура fopen возвращает указатель на структуру TFILE, идентифицирующую открытый файл, объект PFILE также часто называют открытым потоком ввода/вывода (эта структура FILE является элементом внутренней таблицы). Процедура fclose закрывает файл, заданный параметром _stream, и, если этот файл использовался для вывода, также сбрасывает на диск все данные из внутреннего буфера.
В случае неудачи процедура fopen возвращает нулевой указатель nil, определенный в файле system. В этом случае, так же как и для вызова fdopen, переменная linuxerror будет содержать код ошибки, указывающий на ее причину.
Второй параметр процедуры fopen указывает на строку, определяющую режим доступа. Она может принимать следующие основные значения:
r
Открыть файл filename только для чтения. (Если файл не существует, то вызов завершится неудачей и процедура fopen вернет нулевой указатель nil)
w
Создать файл filename и открыть его только для записи. (Если файл уже существует, то он будет усечен до нулевой длины)
а
Открыть файл filename только для записи. Все данные будут добавляться в конец файла. Если файл не существует, он создается
Файл может быть также открыт для обновления, то есть программа может выполнять чтение из файла и запись в него. Другими словами, программа может одновременно выполнять для файла и операции ввода, и операции вывода без необходимости открывать его заново. В то же время из-за механизма буферизации такой ввод/вывод будет более ограниченным, чем режим чтения/записи, поддерживаемый вызовами fdread и fdwrite. В частности, после вывода нельзя осуществить ввод без вызова одной из стандартных процедур ввода/вывода fseek или rewind. Эти процедуры изменяют положение внутреннего указателя чтения/ записи и обсуждаются ниже. Аналогично нельзя выполнить вывод после ввода без вызова процедур fseek или rewind или процедуры ввода, которая перемещает указатель в конец файла. Режим обновления обозначается символом + в конце аргумента, передаваемого процедуре fopen. Вышеприведенные режимы можно дополнить следующим образом:
r+
Открыть файл filename для чтения и записи. Если файл не существует, то вызов снова завершится неудачей
w+
Создать файл filename и открыть его для чтения и записи. (Если файл уже существует, то он будет усечен до нулевой длины)
а+
Открыть файл filename для чтения и записи. При записи данные будут добавляться в конец файла. Если файл не существует, то он создается
В некоторых системах для доступа к двоичным, а не текстовым файлам, к строке также нужно добавлять символ b, например, rb.
Если файл создается при помощи процедуры fopen, для него обычно устанавливается код доступа octal(0666). Это позволяет всем пользователям выполнять чтение из файла и запись в него. Эти права доступа по умолчанию могут быть изменены установкой ненулевого значения атрибута процесса umask. (Системный вызов umask был изучен в главе 3.)
Следующий пример программы показывает использование процедуры fopen и ее связь с процедурой fclose. При этом, если файл indata существует, то он открывается для чтения, а файл outdata создается (или усекается до нулевой длины, если он существует). Процедура fatal предназначена для вывода сообщения об ошибке, ее описание было представлено в предыдущих главах. Она просто передает свой аргумент процедуре perror, а затем вызывает halt для завершения работы программы.
uses stdio;

const
inname:pchar = 'indata';
outname:pchar = 'outdata';

function fatal(s:pchar):integer;
begin
perror (s);
halt (1);
end;

var
inf,outf:pfile;
begin
inf := fopen (inname, 'r');
if inf = nil then
fatal ('Невозможно открыть входной файл');
outf := fopen (outname, 'w');
if outf = nil then
fatal ('Невозможно открыть выходной файл');
(* Выполняются какие-либо действия ... *)
fclose (inf);
fclose (outf);
halt (0);
end.
На самом деле, в данном случае оба вызова fсlose не нужны. Дескрипторы, связанные с файлами inf и outf, будут автоматически закрыты при завершении работы процесса, и вызов halt автоматически сбросит данные из буфера указателя outf на диск, записав их в файл outdata.
С процедурой fclose тесно связана процедура fflush:
Описание
uses stdio;

function fflush(_stream:pfile):integer;
Выполнение этой процедуры приводит к сбросу на диск содержимого буфера вывода, связанного с потоком _stream. Другими словами, данные из буфера записываются в файл немедленно, независимо от того, заполнен буфер или нет. Это гарантирует, что содержимое файла на диске будет соответствовать тому, как он выглядит с точки зрения процесса. (Процесс считает, что данные записаны в файл с того момента, как они оказываются в буфере, поскольку механизм буферизации прозрачен.) Любые данные из буфера ввода этим вызовом предусмотрительно отбрасываются.
Поток _stream остается открытым после завершения процедуры fflush. Как и процедура fclose, процедура fflush возвращает постоянную EOF в случае ошибки и нулевое значение – в случае успеха. (Значение постоянной EOF задано в файле stdio равным –1. Оно обозначает конец файла, но может также использоваться для обозначения ошибок.)
11.4. Посимвольный ввод/вывод: процедуры getc и putc
Описание
uses stdio;

function getc(inf:pfile):integer;

function putc(c:integer; outf:pfile):integer;
Наиболее простыми из процедур стандартной библиотеки ввода/вывода являются процедуры getc и putc. Процедура getc возвращает очередной символ из входного потока inf. Процедура putc помещает символ, обозначенный параметром с, в выходной поток outf.
В обеих процедурах символ с имеет тип integer, а не char, что позволяет процедурам использовать наборы 16-битовых «широких» символов. Это также позволяет процедуре getc возвращать значение –1, находящееся вне диапазона возможных значений типа char. Постоянная EOF используется процедурой getc для обозначения того, что либо достигнут конец файла, либо произошла ошибка. Процедура putc также может возвращать значение EOF в случае ошибки.
Следующий пример является новой версией процедуры copyfile, представленной в главе 2; в данном случае вместо использования вызовов fdread и fdwrite используются процедуры getc и putc:
uses stdio;

(* Скопировать файл f1 в файл f2
* при помощи стандартных процедур ввода/вывода
*)
function copyfile(const f1, f2:pchar):integer;
var
inf, outf:pfile;
c:longint;
begin
inf := fopen (f1, 'r');
if inf = nil then
begin
copyfile:=-1;
exit;
end;
outf := fopen (f2, 'w');
if outf = nil then
begin
fclose (inf);
copyfile:=-2;
exit;
end;
c := getc (inf);
while c <> EOF do
begin
putc (c, outf);
c := getc (inf);
end;
fclose (inf);
fclose (outf);
copyfile:=0;
end;
Копирование выполняет внутренний цикл while. Снова обратите внимание на то, что переменная с имеет тип longint, а не char.
Упражнение 11.1. В упражнениях 2.4 и 2.5 мы описали программу count, которая выводит число символов, слов и строк во входном файле. (Напомним, что слово определялось, как любая последовательность алфавитно-цифровых символов или одиночный пробельный символ.) Перепишите программу count, используя процедуру getc.
Упражнение 11.2. Используя процедуру getс, напишите программу, выводящую статистику распределения символов в файле, то есть число раз, которое встречается в файле каждый символ. Один из способов сделать это состоит в использовании массива целых чисел типа long, который будет содержать счетчики числа символов, а затем рассматривать значение каждого символа в качестве индекса увеличиваемого счетчика массива. Программа также должна рисовать простую гистограмму полученного распределения при помощи процедур printf и putc.
11.5. Возврат символов в поток: процедура ungetc
Описание
uses stdio;

function ungetc(c:integer; _stream:pfile):integer;
Процедура ungetc возвращает символ с в поток _stream. Это всего лишь логическая операция. Входной файл не будет при этом изменяться. В случае успешного завершения процедуры ungetc символ с будет следующим символом, который будет считан процедурой getc. Гарантируется возврат только одного символа. В случае неудачной попытки вернуть символ с процедура ungetc возвращает значение EOF. Попытка вернуть сам символ EOF должна всегда завершаться неудачей. Но это обычно не представляет проблемы, так как все последующие вызовы процедуры getc после достижения конца файла приведут к возврату символа EOF.
Обычно процедура ungetc используется для восстановления исходного состояния входного потока после чтения лишнего символа для проверки условия. Следующая процедура getword применяет это простой подход для ввода строки, которая содержит либо непрерывную последовательность алфавитно-цифровых символов, либо одиночный нетекстовый символ. Конец файла кодируется возвращенным значением nil. Процедура getword принимает в качестве аргумента указатель на структуру TFILE. Она использует для проверки два макроса, определенные в файле stdio. Первый из них, isspace, определяет, является ли символ пробельным символом, таким как символ пробела, табуляции или перевода строки. Второй, isalnum, проверяет, является ли символ алфавитно-цифровым, то есть цифрой или буквой.
(* В этом файле определены isspace и isalnum *)
uses stdio;

const
MAXTOK=256;

var
inbuf:array [0..MAXTOK] of char;

function getword (inf:pfile):pchar;
var
c,count:integer;
begin
count:=0;
(* Удалить пробельные символы *)
repeat
c := getc (inf);
until not isspace (c);
if c = EOF then
begin
getword:=nil;
exit;
end;
if not isalnum (c) then (* символ не является алфавитно-цифровым *)
begin
inbuf[count] := char(c);
inc(count);
end
else
begin
(* Сборка "слова" *)
repeat
if count < MAXTOK then
begin
inbuf[count] := char(c);
inc(count);
end;
c := getc (inf);
until not isalnum (c);
ungetc (c, inf); (* вернуть символ *)
end;
inbuf[count] := #0;
(* нулевой символ в конце строки *)
getword:=inbuf;
end;

var
word:pchar;
begin
while true do
begin
word := getword (stdin);
if word <> nil then
puts (word)
else
break;
end;
end.
Если подать на вход программы следующий ввод
Это данные
на входе
программы!!!
то процедура getword вернет следующую последовательность строк:
Это
данные
на
входе
программы
!
!
!
Упражнение 11.3. Измените процедуру getword так, чтобы она распознавала также числа, которые могут начинаться со знака минус или плюс и могут содержать десятичную точку.
11.6. Стандартный ввод, стандартный вывод и стандартный вывод диагностики
Стандартная библиотека ввода/вывода обеспечивает две структуры TFILE, связанные со стандартным вводом и стандартным выводом, и переменная типа TEXT, связанная со стандартным выводом диагностики. (Еще раз напомним, что не следует путать эти потоки с одноименными дескрипторами ввода/вывода 0, 1 и 2.) Эти стандартные структуры не требуют открытия и задаются предопределенными указателями:
stdin Соответствует стандартному вводу
stdout Соответствует стандартному выводу
stderr Соответствует стандартному выводу диагностики
Следующий вызов получает очередной символ из структуры stdin, которая так же, как и дескриптор файла со значением 0, по умолчанию соответствует клавиатуре:
inchar := getc (stdin);
Так как ввод и вывод через потоки stdin и stdout используются очень часто, для удобства определены еще две процедуры – getchar и putchar. Процедура getchar возвращает очередной символ из stdin, а процедура putchar выводит символ в stdout. Они аналогичны процедурам getc и putc, но не имеют аргументов.
Следующая программа io2 использует процедуры getchar и putchar для копирования стандартного ввода в стандартный вывод:
(* Программа io2 - копирует stdin в stdout *)
uses stdio;

var
c:integer;
begin
c := getchar;
while c <> EOF do
begin
putchar (c);
c := getchar;
end;
end.
Программа io2 ведет себя почти аналогично приведенному ранее примеру – программе io из главы 2.
Так же, как getc и putc, getchar и putchar могут быть макросами. Фактически getchar часто просто определяется как getс(stdin), a putchar – как putc(stdout).
stderr обычно предназначена для вывода сообщений об ошибках, поэтому вывод в stderr обычно не буферизуется. Другими словами, символ, который посылается в stderr, будет немедленно записан в файл или устройство, соединенное со стандартным выводом диагностики. При включении отладочной печати в код для тестирования рекомендуется выполнять вывод в stderr. Вывод в stdout буферизуется и может появиться через несколько шагов после того, как он в действительности произойдет. (Вместо этого можно использовать процедуру fflush(stdout) после каждого вывода для записи всех сообщений из буфера stdout.)1
Упражнение 11.4. При помощи стандартной команды time сравните производительность программы io2 и программы io, разработанной в главе 2. Измените исходную версию программы io так, чтобы она использовала вызовы fdread и fdwrite для посимвольного ввода и вывода. Снова сравните производительность полученной программы и программы io2.
Упражнение 11.5. Перепишите программу io2 так, чтобы она более соответствовала команде cat. В частности, сделайте так, чтобы она выводила на экран содержимое файлов, заданных в качестве аргументов командной строки. При отсутствии аргументов она должна принимать ввод из stdin.
11.7. Стандартные процедуры опроса состояния
Для опроса состояния структуры TFILE существует ряд простых процедур. Они, например, позволяют программе определять, вернула ли процедура ввода, такая как getc, символ EOF из-за того, что достигнут конец файла или в результате возникновения ошибки. Эти процедуры описаны ниже:
Описание
uses stdio;

function ferror(_stream:pfile):integer;

function feof(_stream:pfile):integer;

procedure clearerr(_stream:pfile);

function fileno(_stream:pfile):longint;
Функция ferror является предикатом, который возвращает ненулевое значение, если в потоке _stream возникла ошибка во время последнего запроса на ввод или вывод. Ошибка может возникать в результате вызова примитивов доступа к файлам (fdread, fdwrite и др.) внутри процедуры стандартного ввода/вывода. Если же функция ferror возвращает нулевое значение, значит, ошибок не было. Функция ferror может использоваться следующим образом:
if ferror(_stream) <> 0 then
begin
(* Обработка ошибок *)
end
else
begin
(* Ошибок нет *)
end;
Функция feof является предикатом, возвращающим ненулевое значение, если для потока _stream достигнут конец файла. Возврат нулевого значения просто означает, что этого еще не произошло.
Функция clearerr используется для сброса индикаторов ошибки и флага достижения конца файла для потока _stream. При этом гарантируется, что последующие вызовы функций ferror и feof для этого файла вернут нулевое значение, если за это время не произошло что-нибудь еще. Очевидно, что функция clearerr бывает необходима редко.
Функция fileno является вспомогательной и не связана с обработкой ошибок. Она возвращает целочисленный дескриптор файла, содержащийся в структуре TFILE, на которую указывает параметр _stream. Это может быть полезно, если нужно передать какой-либо процедуре дескриптор файла, а не идентификатора потока TFILE. Однако не следует использовать процедуру fileno для смешивания вызовов буферизованного и небуферизованного ввода/вывода. Это почти неизбежно приведет к хаосу.
Следующий пример – процедура egetc использует функцию ferror, чтобы отличить ошибку от достижения конца файла при возврате процедурой getc значения ЕОF.
(* Процедура egetc - getc с проверкой ошибок *)
uses stdio;

function egetc (stream:pfile):longint;
var
c:longint;
begin
c := getc (stream);
if c = EOF then
begin
if ferror (stream) <> 0 then
begin
writeln (stderr, 'Фатальная ошибка: ошибка ввода');
halt (1);
end
else
writeln (stderr, 'Предупреждение: EOF');
end;
egetc:=c;
end;
11.8. Построчный ввод и вывод
Существует также набор простых процедур для ввода и вывода строк (под которыми понимается последовательность символов, завершаемая символом перевода строки). Эти процедуры удобно использовать в интерактивных программах, которые выполняют чтение с клавиатуры и вывод на экран терминала. Основные процедуры для ввода строк называются gets и fgets.
Описание
uses stdio;

function gets(buf:pchar):pchar;

function fgets(buf:pchar; nsize:integer; inf:pfile):pchar;
Процедура gets считывает последовательность символов из потока стандартного ввода (stdin), помещая все символы в буфер, на который указывает аргумент buf. Символы считываются до тех пор, пока не встретится символ перевода строки или конца файла. Символ перевода строки newline отбрасывается, и вместо него в буфер buf помещается нулевой символ, образуя завершенную строку. В случае возникновения ошибки или при достижении конца файла возвращается значение nil.
Процедура fgets является обобщенной версией процедуры gets. Она считывает символы из потока inf в буфер buf до тех пор, пока не будет считано nsize-1 символов или не встретится раньше символ перевода строки newline или не будет достигнут конец файла. В процедуре fgets символы перевода строки newline не отбрасываются, а помещаются в конец буфера (это позволяет вызывающей функции определить, в результате чего произошел возврат из процедуры fgets). Как и процедура gets, процедура fgets возвращает указатель на буфер buf в случае успеха и nil – в противном случае.
Процедура gets является довольно примитивной. Так как она не знает размер передаваемого буфера, то слишком длинная строка может привести к возникновению внутренней ошибки в процедуре. Чтобы избежать этого, можно использовать процедуру fgets (для стандартного ввода stdin).
Следующая процедура yesno использует процедуру fgets для получения положительного или отрицательного ответа от пользователя; она также вызывает макрос isspace для пропуска пробельных символов в строке ответа:
(* Процедура yesno - получить ответ от пользователя *)
uses stdio;

const
YES=1;
NO=0;
ANSWSZ=80;
pdefault:pchar = 'Наберите "y" (YES), или "n" (NO)';
error:pchar = 'Неопределенный ответ';

function yesno (prompt:pchar):integer;
var
buf:array [0..ANSWSZ-1] of char;
p_use, p:pchar;
begin
(* Вывести приглашение, если он не равно nil.
* Иначе использовать приглашение по умолчанию
* pdefault *)
if prompt <> nil then
p_use := prompt
else
p_use := pdefault;
(* Бесконечный цикл до получения правильного ответа. *)
while true do
begin
(* Вывести приглашение *)
printf ('%s > ', [p_use]);
if fgets (buf, ANSWSZ, stdin) = nil then
begin
yesno:=EOF;
exit;
end;
(* Удалить пробельные символы *)
p := buf;
while isspace (byte(p^)) do
inc(p);
case p^ of
'Y','y':
begin
yesno:=YES;
exit;
end;
'N','n':
begin
yesno:=NO;
exit;
end;
else
printf (#$a'%s'#$a, [error]);
end;
end;
end;

var
ans:integer;
begin
ans := yesno (nil);
printf ('Получен ответ: ',[]);
if ans = YES then
printf ('Да'#$a,[])
else
printf ('Нет'#$a,[]);
end.
В этом примере предполагается, что stdin связан с терминалом. Как можно сделать эту процедуру более безопасной?
Обратными процедурами для gets и fgets будут соответственно процедуры puts и fputs.
Описание
uses stdio;

function puts(str:pchar):integer;

function fputs(str:pchar; outf:pfile):integer;
Процедура puts записывает все символы (кроме завершающего нулевого символа) из строки str на стандартный вывод (stdout). Процедура fputs записывает строку str в поток outf. Для обеспечения совместимости со старыми версиями системы процедура puts добавляет в конце символ перевода строки, процедура же fputs не делает этого. Обе функции возвращают в случае ошибки значение EOF.
Следующий вызов процедуры puts приводит к выводу сообщения Hello, world на стандартный вывод, при этом автоматически добавляется символ перевода строки newline:
puts('Hello, world');
11.9. Ввод и вывод бинарных данных: процедуры fread и fwrite
Описание
uses stdio;

function fread(buffer:pointer; size, nitems:longint; inf:pfile):longint;

function fwrite(buffer:pointer; size,nitems:longint; outf:pfile):longint;
Эти две полезные процедуры обеспечивают ввод и вывод произвольных нетекстовых данных. Процедура fread считывает nitems объектов данных из входного файла, соответствующего потоку inf. Считанные байты будут помещены в массив buffer. Каждый считанный объект представляется последовательностью байтов длины size. Возвращаемое значение дает число успешно считанных объектов.
Процедура fwrite является точной противоположностью процедуры fread. Она записывает данные из массива buffer в поток outf. Массив buffer содержит nitems объектов, размер которых равен size. Возвращаемое процедурой значение дает число успешно записанных объектов.
Эти процедуры обычно используются для чтения и записи содержимого произвольных структур данных языка Паскаль. При этом параметр size часто содержит конструкцию sizeof, которая возвращает размер структуры в байтах.
Следующий пример показывает, как все это работает. В нем используется шаблон структуры dict_elem. Экземпляр этой структуры может представлять собой часть записи простой базы данных. Используя терминологию баз данных, структура dict_elem представляет собой запись, или атрибут, базы данных. Мы поместили определение структуры dict_elem в заголовочный файл dict.inc, который выглядит следующим образом:
(* dict.inc - заголовочный файл для writedict и readdict *)
uses stdio;

(* Структура dict_elem элемент данных *)
(* (соответствует полю базы данных) *)
type dict_elem=record
d_name:array [0..14] of char; (* имя элемента словаря *)
d_start:integer; (* начальное положение записи *)
d_length:integer; (* длина поля *)
d_type:integer; (* обозначает тип данных *)
end;
pdict_elem=^dict_elem;

const
ERROR=-1;
SUCCESS=0;
He вдаваясь в смысл элементов структуры, введем две процедуры writedict и readdict, которые соответственно выполняют запись и чтение массива структур dict_elem. Файлы, создаваемые при помощи этих двух процедур, можно рассматривать как простые словари данных для записей в базе данных.
Процедура writedict имеет два параметра, имя входного файла и адрес массива структур dict_elem. Предполагается, что этот список заканчивается первой структурой массива, в которой элемент d_length равен нулю.
{$i dict.inc}

function writedict (const dictname:pchar; elist:pdict_elem):integer;
var
j:integer;
outf:pfile;
begin
(* Открыть входной файл *)
outf := fopen (dictname, 'w');
if outf = nil then
begin
writedict:=ERROR;
exit;
end;